diff --git a/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md b/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md index d034b20377..a0fd94b943 100644 --- a/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md +++ b/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md @@ -33,7 +33,7 @@ The format is: ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/.github/skills/new-java-e2e-test-yaml-and-test/examples.md b/.github/skills/new-java-e2e-test-yaml-and-test/examples.md index af82ef4dba..12971244a2 100644 --- a/.github/skills/new-java-e2e-test-yaml-and-test/examples.md +++ b/.github/skills/new-java-e2e-test-yaml-and-test/examples.md @@ -8,7 +8,7 @@ File: `test/snapshots/system_message_sections/should_use_replaced_identity_secti ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -73,7 +73,7 @@ File: `test/snapshots/system_message_transform/should_invoke_transform_callbacks ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: # First exchange: model decides to call tools - messages: diff --git a/.github/workflows/dotnet-sdk-tests.yml b/.github/workflows/dotnet-sdk-tests.yml index 4695cae7f2..0acf807f0a 100644 --- a/.github/workflows/dotnet-sdk-tests.yml +++ b/.github/workflows/dotnet-sdk-tests.yml @@ -1,9 +1,6 @@ name: ".NET SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -11,6 +8,40 @@ permissions: contents: read jobs: + validate: + name: ".NET SDK Build and Format" + if: github.event.repository.fork == false + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./dotnet + steps: + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.0.x" + - uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Restore .NET dependencies + run: dotnet restore + + - name: Run dotnet format check + run: | + if ! dotnet format --no-restore --verify-no-changes; then + echo "❌ dotnet format produced changes. Please run 'dotnet format' in dotnet" + exit 1 + fi + echo "✅ dotnet format produced no changes" + + # Build the whole solution once to validate every SDK target framework. + # Matrix cells below only need to build frameworks consumed by the tests. + - name: Build SDK + run: dotnet build --no-restore + test: name: ".NET SDK Tests (${{ matrix.os }}, ${{ matrix.transport }}, ${{ matrix.backend }}, ${{ matrix.shard }})" if: github.event.repository.fork == false @@ -73,7 +104,43 @@ jobs: - os: macos-latest transport: default backend: capi - shard: "2b" + shard: "2b1a-permission-provider" + - os: macos-latest + transport: default + backend: capi + shard: "2b1a-rewind" + - os: macos-latest + transport: default + backend: capi + shard: "2b1a-rpc-a" + - os: macos-latest + transport: default + backend: capi + shard: "2b1a-rpc-e" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-mcp-and-skills" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-mcp-config" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-mcp-lifecycle" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-queue" + - os: macos-latest + transport: default + backend: capi + shard: "2b1b-remote" + - os: macos-latest + transport: default + backend: capi + shard: "2b2" - os: macos-latest transport: default backend: capi @@ -97,7 +164,7 @@ jobs: # A hung test used to run until the runner died (~50 min) and the dying # runner never uploaded its logs, so the failures were undiagnosable. # Every healthy cell finishes well under 15 min. - timeout-minutes: 30 + timeout-minutes: 20 defaults: run: shell: bash @@ -111,27 +178,16 @@ jobs: with: node-version: "22" cache: "npm" - cache-dependency-path: "./nodejs/package-lock.json" + cache-dependency-path: | + ./nodejs/package-lock.json + ./test/harness/package-lock.json - - name: Install Node.js dependencies (for CLI version extraction) + - name: Install Node.js dependencies working-directory: ./nodejs run: npm ci --ignore-scripts - name: Restore .NET dependencies - run: dotnet restore - - - name: Run dotnet format check - if: runner.os == 'Linux' - run: | - dotnet format --verify-no-changes - if [ $? -ne 0 ]; then - echo "❌ dotnet format produced changes. Please run 'dotnet format' in dotnet" - exit 1 - fi - echo "✅ dotnet format produced no changes" - - - name: Build SDK - run: dotnet build --no-restore + run: dotnet restore test/GitHub.Copilot.SDK.Test.csproj - name: Install test harness dependencies working-directory: ./test/harness @@ -153,26 +209,85 @@ jobs: # --blame-hang names the offending test instead of letting it wedge # the runner. No single test legitimately runs for 10 minutes; the # whole suite normally finishes in about five. - args=(--no-build -v n --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type none) + # The validation job performs the full analyzer-enabled SDK build. + # Build only test-consumed frameworks here and do not repeat analyzers. + args=( + --no-restore + -v n + --blame-hang + --blame-hang-timeout 10m + --blame-hang-dump-type none + --logger "trx;LogFilePrefix=test-results" + --results-directory "$GITHUB_WORKSPACE/dotnet/TestResults" + -p:RunAnalyzers=false + ) filter="$DOTNET_TEST_FILTER" + individual_filters=() if [[ "$DOTNET_TEST_SHARD" != "full" ]]; then case "$DOTNET_TEST_SHARD" in 1) - initials=(A C D H I J K L N Q S U W Y) + # M moved here after recent runs showed shard 2 was about two + # minutes slower; this keeps Windows and macOS balanced. + initials=(A C D H I J K L M N Q S U W Y) shard_filter="FullyQualifiedName~GitHub.Copilot.Test.ConnectionToken" ;; 2) - initials=(B E F G M O P R T V X Z) + initials=(B E F G O P R T V X Z) shard_filter="" ;; 2a) initials=(B E F G) shard_filter="" ;; - 2b) - initials=(M O P R) + 2b1a-permission-provider) + initials=() shard_filter="" + individual_filters=( + "FullyQualifiedName~GitHub.Copilot.Test.E2E.PendingWorkResumeE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.E2E.PerSessionAuthE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.E2E.PermissionE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.E2E.PreMcpToolCallHookE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.E2E.ProviderEndpointE2ETests" + "FullyQualifiedName~GitHub.Copilot.Test.Unit.PermissionHandlerTests" + "FullyQualifiedName~GitHub.Copilot.Test.Unit.PublicDtoTests" + ) + ;; + 2b1a-rewind) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.Rewind" + ;; + 2b1a-rpc-a) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcA" + ;; + 2b1a-rpc-e) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcE" + ;; + 2b1b-mcp-and-skills) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpAndSkillsE2ETests" + ;; + 2b1b-mcp-config) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpConfigE2ETests" + ;; + 2b1b-mcp-lifecycle) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpLifecycleE2ETests" + ;; + 2b1b-queue) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcQ" + ;; + 2b1b-remote) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcR" + ;; + 2b2) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcS|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcT|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcU|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcW|FullyQualifiedName~GitHub.Copilot.Test.Unit.R" ;; 2c) initials=(T V X Z) @@ -180,6 +295,14 @@ jobs: ;; esac + if (( ${#individual_filters[@]} > 0 )); then + for individual_filter in "${individual_filters[@]}"; do + combined_filter="${filter:+(${filter})&}(${individual_filter})" + dotnet test test/GitHub.Copilot.SDK.Test.csproj "${args[@]}" --filter "$combined_filter" + done + exit 0 + fi + for namespace in E2E Unit; do for initial in "${initials[@]}"; do clause="FullyQualifiedName~GitHub.Copilot.Test.${namespace}.${initial}" @@ -192,4 +315,13 @@ jobs: if [[ -n "$filter" ]]; then args+=(--filter "$filter") fi - dotnet test "${args[@]}" + dotnet test test/GitHub.Copilot.SDK.Test.csproj "${args[@]}" + + - name: Upload .NET test diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: dotnet-test-diagnostics-${{ matrix.os }}-${{ matrix.transport }}-${{ matrix.backend }}-${{ matrix.shard }}-${{ github.run_attempt }} + path: dotnet/TestResults/ + if-no-files-found: warn + retention-days: 7 diff --git a/.github/workflows/go-sdk-tests.yml b/.github/workflows/go-sdk-tests.yml index 61d74d257e..4fb6fd0184 100644 --- a/.github/workflows/go-sdk-tests.yml +++ b/.github/workflows/go-sdk-tests.yml @@ -1,9 +1,6 @@ name: "Go SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -22,6 +19,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index 36310d26b5..88185deb14 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -1,21 +1,17 @@ name: "Java SDK Tests" on: - push: - branches: - - main - paths: - - "java/**" - - "test/**" - - ".github/workflows/java-sdk-tests.yml" - - ".github/actions/setup-copilot/**" - - ".github/actions/java-test-report/**" workflow_dispatch: workflow_call: permissions: contents: read +env: + MAVEN_OPTS: >- + -Daether.connector.http.retryHandler.count=3 + -Daether.connector.http.retryHandler.serviceUnavailable=429,502,503 + jobs: java-sdk-inprocess: name: "Java SDK InProcess Tests (${{ matrix.classifier }})" @@ -63,7 +59,7 @@ jobs: run: mvn clean verify -Pinprocess ${{ matrix.maven-args }} - name: Generate Test Report Summary - if: always() + if: failure() uses: ./.github/actions/java-test-report with: title: "Copilot Java SDK :: Test Results InProcess" @@ -130,11 +126,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-linux-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linux-arm64.jar java/copilot-native/target/linux-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-windows: @@ -187,11 +184,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-x64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-x64.jar java/copilot-native/target/win32-x64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-windows-arm64: @@ -244,11 +242,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-win32-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-arm64.jar java/copilot-native/target/win32-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-darwin: @@ -302,11 +301,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-darwin-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-darwin-arm64.jar java/copilot-native/target/darwin-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-assembly: @@ -342,22 +342,22 @@ jobs: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-linux-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/linux-arm64 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-x64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/windows - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-win32-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/windows-arm64 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-darwin-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/darwin - name: Verify native inputs and deploy the complete local release @@ -481,9 +481,8 @@ jobs: if: matrix.test-jdk == '25' env: CI: "true" - run: | - node copilot-native/scripts/validate-native-host.mjs linux-x64 - mvn verify -Dskip.test.harness=true -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=false + # Native artifacts are built and exercised by every classifier job. + run: mvn -pl sdk verify -Dskip.test.harness=true - name: Switch to JDK 17 if: matrix.test-jdk == '17' @@ -502,7 +501,7 @@ jobs: mvn -pl sdk jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test-jdk-banner surefire:test failsafe:integration-test failsafe:verify jacoco:report@build-coverage-report-from-tests -Denforcer.skip=true - name: Upload test results for site generation - if: success() && github.ref == 'refs/heads/main' && matrix.test-jdk == '25' + if: success() && github.event_name == 'merge_group' && matrix.test-jdk == '25' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-for-site diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml index 4c31f79cc4..1738828e6b 100644 --- a/.github/workflows/nodejs-sdk-tests.yml +++ b/.github/workflows/nodejs-sdk-tests.yml @@ -4,9 +4,6 @@ env: HUSKY: 0 on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -56,6 +53,11 @@ jobs: working-directory: ./test/harness run: npm ci --ignore-scripts + - name: Run test harness tests + if: runner.os == 'Linux' && matrix.transport == 'default' + working-directory: ./test/harness + run: npm test + - name: Warm up PowerShell if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" diff --git a/.github/workflows/python-sdk-tests.yml b/.github/workflows/python-sdk-tests.yml index 1ea9739756..aa6e803372 100644 --- a/.github/workflows/python-sdk-tests.yml +++ b/.github/workflows/python-sdk-tests.yml @@ -4,9 +4,6 @@ env: PYTHONUTF8: 1 on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -27,6 +24,7 @@ jobs: python-version: ["3.11"] transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -52,7 +50,7 @@ jobs: - name: Install Node.js dependencies (for CLI in tests) working-directory: ./nodejs - run: npm ci --ignore-scripts + run: npm ci --ignore-scripts --fetch-retries=4 --fetch-retry-mintimeout=10000 --fetch-retry-maxtimeout=60000 - name: Run ruff format check run: uv run ruff format --check . @@ -65,7 +63,7 @@ jobs: - name: Install test harness dependencies working-directory: ./test/harness - run: npm ci --ignore-scripts + run: npm ci --ignore-scripts --fetch-retries=4 --fetch-retry-mintimeout=10000 --fetch-retry-maxtimeout=60000 - name: Warm up PowerShell if: runner.os == 'Windows' diff --git a/.github/workflows/required-checks.yml b/.github/workflows/required-checks.yml index dbd79fd4cf..f4083dd307 100644 --- a/.github/workflows/required-checks.yml +++ b/.github/workflows/required-checks.yml @@ -10,6 +10,10 @@ permissions: contents: read pull-requests: read +concurrency: + group: sdk-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: changes: name: Select SDK workflows diff --git a/.github/workflows/rust-sdk-tests.yml b/.github/workflows/rust-sdk-tests.yml index 7fdac3b818..da030ff999 100644 --- a/.github/workflows/rust-sdk-tests.yml +++ b/.github/workflows/rust-sdk-tests.yml @@ -1,9 +1,6 @@ name: "Rust SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -23,6 +20,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -38,17 +36,6 @@ jobs: uses: dtolnay/rust-toolchain@stable with: toolchain: "1.94.0" - components: rustfmt, clippy - - # Nightly rustfmt for unstable format options (group_imports, - # imports_granularity, reorder_impl_items) — pinned in - # `.rustfmt.nightly.toml`. - - name: Install nightly rustfmt - if: runner.os == 'Linux' - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2026-04-14 - components: rustfmt - uses: Swatinem/rust-cache@v2 with: @@ -73,23 +60,6 @@ jobs: path: ./rust/.bundled-cli-cache key: bundled-cli-${{ matrix.os }}-${{ steps.cli-version.outputs.version }} - - name: cargo fmt --check (nightly) - if: runner.os == 'Linux' - run: cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check - - - name: cargo clippy - if: runner.os == 'Linux' - env: - BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo clippy --all-targets --features test-support,bundled-in-process -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type - - - name: cargo doc - if: runner.os == 'Linux' - env: - RUSTDOCFLAGS: "-D warnings" - BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo doc --no-deps --all-features - - name: Install test harness dependencies working-directory: ./test/harness run: npm ci --ignore-scripts @@ -112,6 +82,112 @@ jobs: # The dedicated `bundle` job below exercises the embed pipeline. run: cargo test --no-default-features --features test-support -- --test-threads=4 --nocapture + clippy: + name: "Rust SDK Format and Clippy" + if: github.event.repository.fork == false + env: + POWERSHELL_UPDATECHECK: Off + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./rust + steps: + - uses: actions/checkout@v6.0.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: "1.94.0" + components: clippy + + # Nightly rustfmt for unstable format options (group_imports, + # imports_granularity, reorder_impl_items) — pinned in + # `.rustfmt.nightly.toml`. + - name: Install nightly rustfmt + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: nightly-2026-04-14 + components: rustfmt + + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 + with: + workspaces: "rust" + prefix-key: v1-rust-no-bin + cache-bin: false + + - name: Read pinned @github/copilot CLI version + id: cli-version + working-directory: ./nodejs + run: | + version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Pinned CLI version: $version" + + - name: Cache bundled CLI archives + uses: actions/cache@v4 + with: + path: ./rust/.bundled-cli-cache + key: bundled-cli-ubuntu-latest-${{ steps.cli-version.outputs.version }} + + - name: cargo fmt --check (nightly) + run: cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check + + - name: cargo clippy + env: + BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache + run: cargo clippy --all-targets --features test-support,bundled-in-process -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type + + doc: + name: "Rust SDK Docs" + if: github.event.repository.fork == false + env: + POWERSHELL_UPDATECHECK: Off + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./rust + steps: + - uses: actions/checkout@v6.0.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: "1.94.0" + + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 + with: + workspaces: "rust" + prefix-key: v1-rust-no-bin + cache-bin: false + + - name: Read pinned @github/copilot CLI version + id: cli-version + working-directory: ./nodejs + run: | + version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Pinned CLI version: $version" + + - name: Cache bundled CLI archives + uses: actions/cache@v4 + with: + path: ./rust/.bundled-cli-cache + key: bundled-cli-ubuntu-latest-${{ steps.cli-version.outputs.version }} + + - name: cargo doc + env: + RUSTDOCFLAGS: "-D warnings" + BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache + run: cargo doc --no-deps --all-features + # Exercises the in-process FFI transport (`Transport::InProcess`, the Rust # analogue of the .NET `RuntimeConnection.ForInProcess()`), mirroring the # `inprocess` transport cell in dotnet-sdk-tests.yml. Sets @@ -133,6 +209,7 @@ jobs: # TODO: Re-enable Windows after fixing the napi-oop peer shutdown crash. os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -207,6 +284,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash diff --git a/.github/workflows/sdk-consistency-review.lock.yml b/.github/workflows/sdk-consistency-review.lock.yml index bc33be9ad1..4480d40559 100644 --- a/.github/workflows/sdk-consistency-review.lock.yml +++ b/.github/workflows/sdk-consistency-review.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fb73d13f101fc375308576a64180f63934cc9e8306cb6ef6303f1b9788d9df28","body_hash":"cc60c817de34cdb662ae4c091203c67a5ef240ca0165b0cd26a842f03b22614f","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"79b4a25722897db4bbdb7fa862f579bd3c37e12d0ce147e46d618ab79bce67c4","body_hash":"cc60c817de34cdb662ae4c091203c67a5ef240ca0165b0cd26a842f03b22614f","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","detection_agent_id":"copilot","detection_agent_model":"${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","engine_versions":{"copilot":"1.0.73"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}],"has_pull_request":true} # This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -134,7 +134,7 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" GH_AW_INFO_VERSION: "1.0.73" GH_AW_INFO_AGENT_VERSION: "1.0.73" GH_AW_INFO_CLI_VERSION: "v0.83.1" @@ -863,10 +863,11 @@ jobs: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} @@ -1467,10 +1468,11 @@ jobs: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 @@ -1564,7 +1566,7 @@ jobs: GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" - GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} diff --git a/.github/workflows/sdk-consistency-review.md b/.github/workflows/sdk-consistency-review.md index 550d9349d0..4d1c7e4511 100644 --- a/.github/workflows/sdk-consistency-review.md +++ b/.github/workflows/sdk-consistency-review.md @@ -1,6 +1,7 @@ --- description: Reviews PRs to ensure features are implemented consistently across all SDK language implementations tracker-id: sdk-consistency-review +model: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} on: roles: all pull_request: @@ -35,6 +36,10 @@ safe-outputs: max: 1 hide-older-comments: true allowed-reasons: [outdated] + threat-detection: + engine: + id: copilot + model: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} timeout-minutes: 15 --- diff --git a/CHANGELOG.md b/CHANGELOG.md index d695ed6fb6..f7e753beda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu ## [Unreleased] +### Feature: declare application identity with client info + +Client options now accept optional client info (application name and version, integration name and version) across all six SDKs, exposed idiomatically per language (`clientInfo` in Node.js, `client_info` in Python and Rust, `ClientInfo` in Go and .NET, `setClientInfo` in Java). When set, the SDK forwards it on the `server.connect` handshake so the telemetry the runtime emits on the connection is attributed to the application and its Copilot integration instead of the runtime's own build. All fields are optional, and leaving client info unset keeps the runtime's default attribution. See [Client info](./docs/features/client-info.md). + +### Feature: Node Agent Factories pagination and run notifications + +The experimental Node.js Agent Factories convenience API now supports paginated run history. Existing `session.factory.listRuns()` calls still return the runs array, while calls with `afterSeq`, `beforeSeq`, or `limit` return the full page with cursor and truncation metadata. + +Factory `run` and `resume` options now accept `notifyOnComplete` and `logPhaseNames`. The SDK forwards these options to the Copilot CLI for new and resumed runs. + +### Feature: selectable `ask_user` session behavior + +Session create and cold resume now accept a language-specific `askUserVariant` option with `legacy` and `elicitation` values. SDK sessions retain the legacy question-and-answer tool by default. Select `elicitation` and provide an elicitation handler to expose the structured form-based `ask_user` tool. + ### Feature: rotating session-scoped GitHub credentials All six SDKs can now acquire short-lived GitHub credentials through a session-scoped callback. The SDK registers the callback before session create or resume, maps `initial` and `refresh` requests to the owning session, and removes registrations on rollback, replacement, session close, and client close. Static per-session `gitHubToken` credentials remain supported and are mutually exclusive with the callback. diff --git a/docs/features/README.md b/docs/features/README.md index f97140b784..5ea070a5aa 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -20,6 +20,7 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [Image Input](./image-input.md) | Send images to sessions as attachments | | [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) | | [Usage and Billing](./usage-and-billing.md) | Read token counts, context-window utilization, AI credit cost, and account quota | +| [Client info](./client-info.md) | Declare application and integration identity for runtime telemetry attribution | | [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing | | [Context Clearing](./context-management.md) | Replace conversation context safely with terminal tools | | [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage | diff --git a/docs/features/client-info.md b/docs/features/client-info.md new file mode 100644 index 0000000000..f72780e41b --- /dev/null +++ b/docs/features/client-info.md @@ -0,0 +1,241 @@ +# Client info + +Client info identifies the application using the Copilot SDK and, when applicable, a specific integration within it. An integration is an identifiable sub-part of the application through which the SDK is used, such as an extension or plugin. Set the optional `clientInfo` client option to attribute runtime telemetry for that connection to your application instead of the runtime's own build. + +## When to set client info + +Set client info when your SDK application represents a distinct product, service, or integration whose runtime activity should be attributed consistently. + +Leave client info unset for scripts, one-off tools, and jobs that do not represent a distinct application. The runtime then keeps its default attribution. + +Client info has four optional string fields. Set the fields you know and omit the rest. The SDK includes client info in the `server.connect` handshake only when at least one field has a non-empty value. + +| Field | Example | Meaning | +|---|---|---| +| `applicationName` | `"vscode"` | Name of the application using the SDK | +| `applicationVersion` | `"1.124.2"` | Version of the application using the SDK | +| `integrationName` | `"copilot-chat"` | Name of the extension, plugin, or other application sub-part using the SDK | +| `integrationVersion` | `"0.54.0"` | Version of that extension, plugin, or application sub-part | + +For a standalone application without a distinct integration, set only the application fields. For example, a developer portal could set `applicationName` to `"acme-developer-portal"` and `applicationVersion` to `"2.4.0"`, leaving both integration fields unset. + +The SDK sends client info once when it establishes the connection. The identity applies for the lifetime of that connection. + +## Configure client info + +Pass client info when you create the client: + +
+TypeScript + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +async function main() { + const client = new CopilotClient({ + clientInfo: { + applicationName: "vscode", + applicationVersion: "1.124.2", + integrationName: "copilot-chat", + integrationVersion: "0.54.0", + }, + }); + + await client.start(); +} + +main(); +``` + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + clientInfo: { + applicationName: "vscode", + applicationVersion: "1.124.2", + integrationName: "copilot-chat", + integrationVersion: "0.54.0", + }, +}); + +await client.start(); +``` + +
+ +
+Python + + +```python +from copilot import CopilotClient + +client = CopilotClient( + client_info={ + "application_name": "vscode", + "application_version": "1.124.2", + "integration_name": "copilot-chat", + "integration_version": "0.54.0", + }, +) +await client.start() +``` + +
+ +
+Go + + +```go +package main + +import ( + "context" + + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(&copilot.ClientOptions{ + ClientInfo: &copilot.ClientInfo{ + ApplicationName: "vscode", + ApplicationVersion: "1.124.2", + IntegrationName: "copilot-chat", + IntegrationVersion: "0.54.0", + }, + }) + if err := client.Start(ctx); err != nil { + return + } +} +``` + + +```go +client := copilot.NewClient(&copilot.ClientOptions{ + ClientInfo: &copilot.ClientInfo{ + ApplicationName: "vscode", + ApplicationVersion: "1.124.2", + IntegrationName: "copilot-chat", + IntegrationVersion: "0.54.0", + }, +}) +if err := client.Start(ctx); err != nil { + return err +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot; + +await using var client = new CopilotClient(new CopilotClientOptions +{ + ClientInfo = new CopilotClientInfo + { + ApplicationName = "vscode", + ApplicationVersion = "1.124.2", + IntegrationName = "copilot-chat", + IntegrationVersion = "0.54.0", + }, +}); + +await client.StartAsync(); +``` + +
+ +
+Java + + +```java +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.ClientInfo; +import com.github.copilot.rpc.CopilotClientOptions; + +public class ClientInfoExample { + public static void main(String[] args) throws Exception { + var options = new CopilotClientOptions() + .setClientInfo(new ClientInfo() + .setApplicationName("vscode") + .setApplicationVersion("1.124.2") + .setIntegrationName("copilot-chat") + .setIntegrationVersion("0.54.0")); + + var client = new CopilotClient(options); + client.start().get(); + } +} +``` + + +```java +var options = new CopilotClientOptions() + .setClientInfo(new ClientInfo() + .setApplicationName("vscode") + .setApplicationVersion("1.124.2") + .setIntegrationName("copilot-chat") + .setIntegrationVersion("0.54.0")); + +var client = new CopilotClient(options); +client.start().get(); +``` + +
+ +
+Rust + + +```rust +use github_copilot_sdk::{Client, ClientInfo, ClientOptions}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let _client = Client::start( + ClientOptions::new().with_client_info( + ClientInfo::new() + .with_application_name("vscode") + .with_application_version("1.124.2") + .with_integration_name("copilot-chat") + .with_integration_version("0.54.0"), + ), + ) + .await?; + Ok(()) +} +``` + + +```rust +use github_copilot_sdk::{Client, ClientInfo, ClientOptions}; + +let client = Client::start( + ClientOptions::new().with_client_info( + ClientInfo::new() + .with_application_name("vscode") + .with_application_version("1.124.2") + .with_integration_name("copilot-chat") + .with_integration_version("0.54.0"), + ), +) +.await?; +``` + +
+ +## Notes + +* Client info is advisory. The runtime can ignore values that do not match the expected format, such as an invalid version string. +* Setting client info changes how the runtime attributes its telemetry. It does not change what the runtime records. +* If every field is unset or empty, the SDK omits client info from the handshake and the runtime keeps its default attribution. diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 3bfff10d0f..9b0aa9434c 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -242,6 +242,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `availableTools` | Restrict which tools are available | | `excludedTools` | Disable specific tools | | `provider` | Re-provide BYOK credentials (required for BYOK sessions) | +| `capi.autoTier` | Override the persisted Auto routing preference on cold resume only | | `reasoningEffort` | Adjust reasoning effort level | | `streaming` | Enable/disable streaming responses | | `workingDirectory` | Change the working directory | @@ -253,6 +254,21 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `disabledSkills` | Skills to disable | | `infiniteSessions` | Configure infinite session behavior | +### Auto tier persistence + +With `model: "auto"`, the optional `capi.autoTier` setting selects an Auto routing preference: `efficiency`, `balance`, or `intelligence`. In Python, use `capi={"auto_tier": "balance"}`. This requires Copilot CLI `1.0.82-1` or later with V2 Auto routing; V1 Auto requests are unchanged. + +The runtime persists the selected tier, so applications do not need to resend it on every resume: + +* Omitting the tier when creating a session uses the runtime's default routing behavior. +* A cold resume restores the persisted tier. Supplying an explicit tier overrides the restored value for the new activation. +* When resuming a session already resident in the runtime, omitting the tier preserves the current selection, supplying the same tier is a no-op, and supplying a different tier is rejected. +* Older sessions without a persisted tier retain default routing behavior. + +Tier selection is not a live model-switch operation. The SDK forwards the preference; the runtime owns persistence and validation. + +The `session.start` and `session.resume` events expose the selected tier in their optional `data.autoTier` field (`data.auto_tier` in Python). When no tier is selected, the field is omitted. + ### Example: changing model on resume ```typescript diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index 7f1da36e82..b7b288bb8f 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -540,7 +540,7 @@ setInterval(() => cleanupSessions(24 * 60 * 60 * 1000), 60 * 60 * 1000); | **Single CLI server = single point of failure** | See [Scaling guide](./scaling.md) for HA patterns | | **No built-in auth between SDK and CLI** | Secure the network path (same host, VPC, etc.) | | **Session state on local disk** | Mount persistent storage for container restarts | -| **30-minute idle timeout** | Sessions without activity are auto-cleaned | +| **No idle timeout by default** | Pass `--session-idle-timeout ` to the CLI server to automatically clean up inactive sessions | ## When to move on diff --git a/docs/setup/scaling.md b/docs/setup/scaling.md index c4a7a0953f..acf82bdb26 100644 --- a/docs/setup/scaling.md +++ b/docs/setup/scaling.md @@ -626,7 +626,7 @@ flowchart TB | **No built-in session locking** | Implement application-level locking for concurrent access | | **No built-in load balancing** | Use external LB or service mesh | | **Session state is file-based** | Requires shared filesystem for multi-server setups | -| **30-minute idle timeout** | Sessions without activity are auto-cleaned by the CLI | +| **No idle timeout by default** | Pass `--session-idle-timeout ` to the CLI server to automatically clean up inactive sessions | | **CLI is single-process** | Scale by adding more CLI server instances, not threads | ## Next steps diff --git a/dotnet/README.md b/dotnet/README.md index 461ff0cf94..23a78030b4 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -101,6 +101,11 @@ new CopilotClient(CopilotClientOptions? options = null) - `RuntimeConnection.ForTcp(port = 0, connectionToken?, path?, args?)` — spawns the runtime as a child process listening on a TCP port. `port = 0` auto-allocates; if a non-zero port is already in use, startup fails (no fallback). Use `CopilotClient.RuntimePort` after `StartAsync` to read the assigned port. `connectionToken` is required if other clients will connect via `RuntimeConnection.ForUri(...)`. - `RuntimeConnection.ForUri(url, connectionToken?)` — connects to an already-running runtime at `url` (e.g., `"localhost:8080"`). Does not spawn a process. +Managed stdio and TCP connections use the bundled `copilot-runtime[.exe]` and +adjacent `runtime.node` by default. An explicit connection path or +`COPILOT_CLI_PATH` overrides the bundled runtime. +Managed launch fails if the bundled wrapper pair is unavailable. + #### Methods ##### `StartAsync(): Task` @@ -135,7 +140,8 @@ Create a new conversation session. - `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`. - `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 user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) 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`. - `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. ##### `ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null): Task` @@ -146,6 +152,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`. +- `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume. ```csharp await using var session = await client.CreateSessionAsync(new SessionConfig @@ -871,7 +878,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `SkipPe ## User Input Requests -Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler: +Enable the legacy question-and-answer `ask_user` tool by providing an `OnUserInputRequest` handler: ```csharp var session = await client.CreateSessionAsync(new SessionConfig @@ -1004,6 +1011,7 @@ var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5", OnPermissionRequest = PermissionHandler.ApproveAll, + AskUserVariant = AskUserVariant.Elicitation, OnElicitationRequest = async (context) => { // context.SessionId - Session that triggered the request diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index f2da0a48f7..92650ee60c 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -55,6 +55,7 @@ namespace GitHub.Copilot; /// public sealed partial class CopilotClient : IDisposable, IAsyncDisposable { + private const string ExplicitBundledCliMarker = ".copilot-explicit-cli"; /// /// Minimum protocol version this SDK can communicate with. /// @@ -416,9 +417,19 @@ async Task StartCoreAsync(CancellationToken ct) ffiArgs.Add("--remote"); } + var explicitCliPath = System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); + if (string.IsNullOrEmpty(explicitCliPath)) + { + explicitCliPath = null; + } + var ffiRuntimePath = explicitCliPath is null + ? GetBundledNativePath(FfiRuntimeHost.GetRuntimeLibraryFileName(), out var searchedRuntime) + ?? throw new InvalidOperationException( + $"In-process FFI runtime library not found at '{searchedRuntime}'.") + : ResolveRuntimePathForExplicitCli(explicitCliPath); var ffiHost = FfiRuntimeHost.Create( - ResolveCliPathForFfi(), - GetNapiPrebuildsFolderOrThrow(), + ffiRuntimePath, + explicitCliPath, ffiEnvironment, ffiArgs, _logger); @@ -493,6 +504,20 @@ await InvokeRpcAsync( await CleanupCliProcessAsync(cliProcess, stderrPump, errors: null, _logger); } + if (ex is IOException + && cliProcess is not null + && stderrPump is not null + && !ex.Message.Contains("stderr:", StringComparison.OrdinalIgnoreCase)) + { + var stderrOutput = GetStderrOutput(stderrPump.Buffer); + if (!string.IsNullOrEmpty(stderrOutput)) + { + throw new IOException( + FormatCliExitedMessage("CLI process exited unexpectedly.", stderrOutput), + ex); + } + } + throw; } } @@ -673,7 +698,7 @@ or IOException private static async Task CleanupCliProcessAsync(Process childProcess, ProcessStderrPump? stderrPump, List? errors, ILogger? logger) { - stderrPump?.Cancel(); + var processExited = false; try { @@ -706,12 +731,19 @@ private static async Task CleanupCliProcessAsync(Process childProcess, ProcessSt AddCleanupError(errors, ex, logger); } } + + processExited = childProcess.HasExited; } catch (Exception ex) { AddCleanupError(errors, ex, logger); } + if (!processExited) + { + stderrPump?.Cancel(); + } + if (stderrPump is not null) { var stderrPumpWaitTimestamp = Stopwatch.GetTimestamp(); @@ -721,6 +753,7 @@ private static async Task CleanupCliProcessAsync(Process childProcess, ProcessSt } catch (TimeoutException ex) { + stderrPump.Cancel(); if (logger is not null) { LoggingHelpers.LogTiming(logger, LogLevel.Debug, ex, @@ -1190,6 +1223,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.EnableCitations, config.EnableFileChangeTracking, wireSystemMessage, + config.AskUserVariant, toolFilter.AvailableTools, toolFilter.ExcludedTools, config.ExcludedBuiltInAgents, @@ -1251,6 +1285,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance Providers: config.Providers, Models: config.Models, ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, + FeatureFlags: config.FeatureFlags, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, GitHubMcpToolConfig: config.GitHubMcpToolConfig, @@ -1428,6 +1463,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.EnableCitations, config.EnableFileChangeTracking, wireSystemMessage, + config.AskUserVariant, toolFilter.AvailableTools, toolFilter.ExcludedTools, config.ExcludedBuiltInAgents, @@ -1491,6 +1527,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes Providers: config.Providers, Models: config.Models, ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, + FeatureFlags: config.FeatureFlags, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, GitHubMcpToolConfig: config.GitHubMcpToolConfig, @@ -1958,13 +1995,15 @@ private static string FormatCliExitedMessage(string message, string stderrOutput private static IOException CreateCliExitedException(string message, StringBuilder stderrBuffer) { - string stderrOutput; + return new IOException(FormatCliExitedMessage(message, GetStderrOutput(stderrBuffer))); + } + + private static string GetStderrOutput(StringBuilder stderrBuffer) + { lock (stderrBuffer) { - stderrOutput = stderrBuffer.ToString().Trim(); + return stderrBuffer.ToString().Trim(); } - - return new IOException(FormatCliExitedMessage(message, stderrOutput)); } private Task EnsureConnectedAsync(CancellationToken cancellationToken) @@ -2143,7 +2182,11 @@ [new ConnectHandshakeRequest( // handler is registered (mirrors the runtime, which reads this flag on the // `connect` handshake so the first session's un-replayable `session.start` // event is forwarded). Also sent on session.create/resume for older CLIs. - _options.OnGitHubTelemetry != null ? true : null)], + _options.OnGitHubTelemetry != null ? true : null, + // Declare the integrating application's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead + // of its own build. Null when the app didn't supply it. + ConnectHandshakeClientInfo.From(_options.ClientInfo))], connection.StderrBuffer, cancellationToken); serverVersion = (int)connectResponse.ProtocolVersion; @@ -2215,17 +2258,19 @@ private static void ApplyTelemetryEnvironment(IDictionary envir var tcpConnection = _connection as TcpRuntimeConnection; var useStdio = _connection is StdioRuntimeConnection; - // Use explicit path, COPILOT_CLI_PATH env var (from the connection's - // Environment, options.Environment, or process env), or bundled runtime - no PATH fallback - var envCliPath = - (childProcessConnection.Environment is not null && childProcessConnection.Environment.TryGetValue("COPILOT_CLI_PATH", out var connEnvValue) ? connEnvValue : null) - ?? (options.Environment is not null && options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue) ? envValue : null) - ?? System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); - var cliPath = childProcessConnection.Path - ?? envCliPath - ?? GetBundledCliPath(out var searchedPath) - ?? throw new InvalidOperationException($"Copilot runtime not found at '{searchedPath}'. Ensure the SDK NuGet package was restored correctly or provide an explicit RuntimeConnection.ForStdio(path: ...) / RuntimeConnection.ForTcp(path: ...)."); - var cliPathSource = childProcessConnection.Path is not null ? "Options" : envCliPath is not null ? "Environment" : "Bundled"; + // Explicit CLI paths preserve the legacy launch contract. Otherwise use + // the bundled native runtime pair. + var configuredEnvironment = childProcessConnection.Environment ?? options.Environment; + var envCliPath = configuredEnvironment is not null + ? configuredEnvironment.TryGetValue("COPILOT_CLI_PATH", out var configuredCliPath) ? configuredCliPath : null + : System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); + var launch = childProcessConnection.Path is not null + ? new RuntimeLaunch(childProcessConnection.Path, "Options") + : envCliPath is not null + ? new RuntimeLaunch(envCliPath, "Environment") + : GetBundledRuntimeLaunch(); + var cliPath = launch.Executable; + var cliPathSource = launch.Source; var args = new List(); if (childProcessConnection.Args != null) @@ -2407,7 +2452,11 @@ private static void ApplyTelemetryEnvironment(IDictionary envir private static string? GetBundledCliPath(out string searchedPath) { - var binaryName = OperatingSystem.IsWindows() ? "copilot.exe" : "copilot"; + return GetBundledNativePath(OperatingSystem.IsWindows() ? "copilot.exe" : "copilot", out searchedPath); + } + + private static string? GetBundledNativePath(string binaryName, out string searchedPath) + { // Always use portable RID (e.g., linux-x64) to match the build-time placement, // since distro-specific RIDs (e.g., ubuntu.24.04-x64) are normalized at build time. var rid = GetPortableRid() @@ -2416,6 +2465,57 @@ private static void ApplyTelemetryEnvironment(IDictionary envir return File.Exists(searchedPath) ? searchedPath : null; } + private static RuntimeLaunch GetBundledRuntimeLaunch() + { + _ = GetBundledNativePath( + OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime", + out var searchedWrapper); + var directory = Path.GetDirectoryName(searchedWrapper)!; + var runtimeNode = Path.Combine(directory, "runtime.node"); + var explicitCliMarker = Path.Combine(directory, ExplicitBundledCliMarker); + if (!File.Exists(searchedWrapper) + && !File.Exists(runtimeNode) + && File.Exists(explicitCliMarker) + && GetBundledCliPath(out _) is { } explicitCli) + { + return new RuntimeLaunch(explicitCli, "Bundled explicit CLI"); + } + return ValidateRuntimePair(searchedWrapper, "Bundled runtime"); + } + + private static RuntimeLaunch ValidateRuntimePair(string wrapper, string source) + { + var runtimeNode = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(wrapper))!, "runtime.node"); + if (!File.Exists(wrapper)) + { + throw new InvalidOperationException($"Copilot runtime wrapper not found at '{wrapper}'."); + } + if (!File.Exists(runtimeNode)) + { + throw new InvalidOperationException( + $"Copilot runtime wrapper at '{wrapper}' is missing its adjacent runtime.node at '{runtimeNode}'."); + } + if (new FileInfo(wrapper).Length == 0 || new FileInfo(runtimeNode).Length == 0) + { + throw new InvalidOperationException("Copilot runtime wrapper and adjacent runtime.node must both be non-empty."); + } +#if NET8_0_OR_GREATER + if (!OperatingSystem.IsWindows()) + { + var mode = File.GetUnixFileMode(wrapper); + const UnixFileMode executeBits = + UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; + if ((mode & executeBits) == 0) + { + File.SetUnixFileMode(wrapper, mode | executeBits); + } + } +#endif + return new RuntimeLaunch(wrapper, source); + } + + private sealed record RuntimeLaunch(string Executable, string Source); + private static string? GetPortableRid() { string os; @@ -2439,26 +2539,22 @@ private static void ApplyTelemetryEnvironment(IDictionary envir return arch != null ? $"{os}-{arch}" : null; } - private string ResolveCliPathForFfi() + private static string ResolveRuntimePathForExplicitCli(string cliPath) { - var envCliPath = _options.Environment is not null && _options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue) - ? envValue - : System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); - if (!string.IsNullOrEmpty(envCliPath)) + var fullEntrypoint = Path.GetFullPath(cliPath); + var directory = Path.GetDirectoryName(fullEntrypoint) + ?? throw new InvalidOperationException($"Could not determine directory for '{cliPath}'."); + var flatLibraryPath = Path.Combine(directory, FfiRuntimeHost.GetRuntimeLibraryFileName()); + if (File.Exists(flatLibraryPath)) { - return envCliPath; + return flatLibraryPath; } - - // Fall back to the bundled single-file CLI the same way stdio discovers it. - // It embeds its own Node and is spawned directly as `copilot --embedded-host`, - // with the sibling cdylib loaded in-process (FfiRuntimeHost.Create prefers the - // flat `libcopilot_runtime.so`/`copilot_runtime.dll` next to the CLI, falling - // back to the dev `prebuilds//runtime.node` layout). - var bundled = GetBundledCliPath(out var searchedPath); - return bundled - ?? throw new InvalidOperationException( - "In-process FFI hosting requires the Copilot CLI. Set the COPILOT_CLI_PATH " - + $"environment variable, or ensure the bundled CLI is present (looked in '{searchedPath}')."); + var prebuildsLibraryPath = Path.Combine( + directory, "prebuilds", GetNapiPrebuildsFolderOrThrow(), "runtime.node"); + return File.Exists(prebuildsLibraryPath) + ? prebuildsLibraryPath + : throw new InvalidOperationException( + $"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'."); } /// @@ -2868,6 +2964,7 @@ internal record CreateSessionRequest( bool? EnableCitations, bool? EnableFileChangeTracking, SystemMessageConfig? SystemMessage, + AskUserVariant? AskUserVariant, IList? AvailableTools, IList? ExcludedTools, [property: JsonPropertyName("excludedBuiltinAgents")] IList? ExcludedBuiltInAgents, @@ -2930,6 +3027,7 @@ internal record CreateSessionRequest( IList? Providers = null, IList? Models = null, OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, + [property: JsonPropertyName("featureFlags")] IDictionary? FeatureFlags = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, @@ -2983,6 +3081,7 @@ internal record ResumeSessionRequest( bool? EnableCitations, bool? EnableFileChangeTracking, SystemMessageConfig? SystemMessage, + AskUserVariant? AskUserVariant, IList? AvailableTools, IList? ExcludedTools, [property: JsonPropertyName("excludedBuiltinAgents")] IList? ExcludedBuiltInAgents, @@ -3047,6 +3146,7 @@ internal record ResumeSessionRequest( IList? Providers = null, IList? Models = null, OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, + [property: JsonPropertyName("featureFlags")] IDictionary? FeatureFlags = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, @@ -3091,7 +3191,42 @@ internal record GetSessionMetadataResponse( internal record ConnectHandshakeRequest( string? Token, - [property: JsonPropertyName("enableGitHubTelemetryForwarding")] bool? EnableGitHubTelemetryForwarding = null); + [property: JsonPropertyName("enableGitHubTelemetryForwarding")] bool? EnableGitHubTelemetryForwarding = null, + [property: JsonPropertyName("clientInfo")] ConnectHandshakeClientInfo? ClientInfo = null); + + internal record ConnectHandshakeClientInfo( + [property: JsonPropertyName("editorName")] string? EditorName = null, + [property: JsonPropertyName("editorVersion")] string? EditorVersion = null, + [property: JsonPropertyName("extensionName")] string? ExtensionName = null, + [property: JsonPropertyName("extensionVersion")] string? ExtensionVersion = null) + { + /// + /// Maps the public onto the connect wire + /// shape, dropping empty fields. Returns when no + /// identity was supplied so the handshake omits clientInfo and the + /// runtime keeps its default attribution. + /// + public static ConnectHandshakeClientInfo? From(CopilotClientInfo? info) + { + if (info is null) + { + return null; + } + + var editorName = NullIfEmpty(info.ApplicationName); + var editorVersion = NullIfEmpty(info.ApplicationVersion); + var extensionName = NullIfEmpty(info.IntegrationName); + var extensionVersion = NullIfEmpty(info.IntegrationVersion); + if (editorName is null && editorVersion is null && extensionName is null && extensionVersion is null) + { + return null; + } + + return new ConnectHandshakeClientInfo(editorName, editorVersion, extensionName, extensionVersion); + } + + private static string? NullIfEmpty(string? value) => string.IsNullOrEmpty(value) ? null : value; + } internal record BuiltinPluginDirectoriesRequest( string[] Paths); @@ -3131,6 +3266,7 @@ internal record HooksInvokeResponse( [JsonSerializable(typeof(GetSessionMetadataRequest))] [JsonSerializable(typeof(GetSessionMetadataResponse))] [JsonSerializable(typeof(ConnectHandshakeRequest))] + [JsonSerializable(typeof(ConnectHandshakeClientInfo))] [JsonSerializable(typeof(BuiltinPluginDirectoriesRequest))] [JsonSerializable(typeof(McpOAuthTokenStorageMode))] [JsonSerializable(typeof(EmbeddingCacheStorageMode))] diff --git a/dotnet/src/FfiRuntimeHost.cs b/dotnet/src/FfiRuntimeHost.cs index a838b9fd17..cc3bccae92 100644 --- a/dotnet/src/FfiRuntimeHost.cs +++ b/dotnet/src/FfiRuntimeHost.cs @@ -17,11 +17,9 @@ namespace GitHub.Copilot; /// and communicating over stdio/TCP. /// /// -/// The Rust host_start export spawns the residual TypeScript worker itself — -/// typically the packaged single-file CLI (copilot --embedded-host, which embeds -/// its own Node) or, for dev, node dist-cli/index.js --embedded-host — so the .NET -/// host never launches Node directly. JSON-RPC frames are pumped across the ABI: writes go -/// to connection_write; inbound frames arrive on a native callback that feeds +/// The Rust host_start export constructs the server synchronously in this +/// process. JSON-RPC frames are pumped across the ABI: writes go to +/// connection_write; inbound frames arrive on a native callback that feeds /// . /// /// The native interop layer has two implementations selected by target framework. On @@ -41,7 +39,7 @@ internal sealed partial class FfiRuntimeHost : IDisposable private const string LibraryName = "copilot_runtime"; private readonly ILogger _logger; - private readonly string _cliEntrypoint; + private readonly string? _cliEntrypoint; private readonly string _libraryPath; private readonly IReadOnlyDictionary? _environment; private readonly IReadOnlyList _args; @@ -53,7 +51,7 @@ internal sealed partial class FfiRuntimeHost : IDisposable private uint _connectionId; private bool _disposed; - private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) + private FfiRuntimeHost(string libraryPath, string? cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) { _libraryPath = libraryPath; _cliEntrypoint = cliEntrypoint; @@ -70,35 +68,22 @@ private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictio ?? throw new InvalidOperationException("FfiRuntimeHost has not been started."); /// - /// Loads the cdylib next to the given CLI entrypoint and prepares the FFI host. - /// The entrypoint is either the packaged single-file CLI binary (e.g. - /// runtimes/<rid>/native/copilot) or, for dev, a .js file (e.g. - /// dist-cli/index.js) launched via node. The cdylib is resolved - /// relative to the entrypoint directory, preferring the flat, natural - /// shared-library name the .NET build emits (e.g. libcopilot_runtime.so) - /// and falling back to the dev tarball layout - /// prebuilds/<prebuildsFolder>/runtime.node, where - /// is the napi-rs - /// <node-platform>-<arch> folder name (e.g. win32-x64). + /// Loads the runtime cdylib and prepares the FFI host. /// - public static FfiRuntimeHost Create(string cliEntrypoint, string prebuildsFolder, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) + public static FfiRuntimeHost Create(string libraryPath, string? cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) { - var fullEntrypoint = Path.GetFullPath(cliEntrypoint); - var distDir = Path.GetDirectoryName(fullEntrypoint) - ?? throw new InvalidOperationException($"Could not determine directory for '{cliEntrypoint}'."); - - // Bundled .NET layout: flat, natural shared-library name next to the CLI. - var flatLibraryPath = Path.Combine(distDir, GetRuntimeLibraryFileName()); - // Dev/tarball layout: dist-cli/prebuilds/-/runtime.node. - var prebuildsLibraryPath = Path.Combine(distDir, "prebuilds", prebuildsFolder, "runtime.node"); - - var libraryPath = File.Exists(flatLibraryPath) ? flatLibraryPath - : File.Exists(prebuildsLibraryPath) ? prebuildsLibraryPath - : throw new InvalidOperationException( - $"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'."); - - PrepareNativeLibrary(libraryPath); - return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args, logger); + var fullLibraryPath = Path.GetFullPath(libraryPath); + if (!File.Exists(fullLibraryPath)) + { + throw new InvalidOperationException($"FFI runtime library not found at '{fullLibraryPath}'."); + } + PrepareNativeLibrary(fullLibraryPath); + return new FfiRuntimeHost( + fullLibraryPath, + cliEntrypoint is null ? null : Path.GetFullPath(cliEntrypoint), + environment, + args, + logger); } /// @@ -106,7 +91,7 @@ public static FfiRuntimeHost Create(string cliEntrypoint, string prebuildsFolder /// emitted by the .NET build (the .node file renamed to what the Rust cdylib /// would be called on this OS). /// - private static string GetRuntimeLibraryFileName() + internal static string GetRuntimeLibraryFileName() { if (OperatingSystem.IsWindows()) return "copilot_runtime.dll"; if (OperatingSystem.IsMacOS()) return "libcopilot_runtime.dylib"; @@ -114,14 +99,11 @@ private static string GetRuntimeLibraryFileName() } /// - /// Starts the in-process runtime: spawns the CLI worker via the Rust host, - /// waits for readiness, and opens the FFI JSON-RPC connection. + /// Starts the in-process Rust runtime and opens the FFI JSON-RPC connection. /// public async Task StartAsync(CancellationToken cancellationToken) { - // host_start blocks until the worker connects back and signals readiness - // (up to ~30s), and connection_open must run outside any async runtime, so - // perform the blocking FFI handshake on a background thread. + // Keep synchronous native startup off the caller's async context. await Task.Run(() => { var argvJson = BuildArgvJson(_cliEntrypoint, _args); @@ -131,7 +113,7 @@ await Task.Run(() => if (_serverId == 0) { throw new InvalidOperationException( - $"copilot_runtime_host_start failed (library '{_libraryPath}', entrypoint '{_cliEntrypoint}')."); + $"copilot_runtime_host_start failed (library '{_libraryPath}')."); } _connectionId = NativeOpenConnection(_serverId); @@ -154,24 +136,22 @@ await Task.Run(() => } } - private static byte[] BuildArgvJson(string cliEntrypoint, IReadOnlyList args) + private static byte[] BuildArgvJson(string? cliEntrypoint, IReadOnlyList args) { - // A .js entrypoint (dev / dist-cli) is launched via node; the packaged - // single-file CLI binary embeds its own Node and is invoked directly. - var isJsFile = cliEntrypoint.EndsWith(".js", StringComparison.OrdinalIgnoreCase); using var stream = new MemoryStream(); using (var writer = new Utf8JsonWriter(stream)) { writer.WriteStartArray(); - if (isJsFile) + if (cliEntrypoint is not null) { - writer.WriteStringValue("node"); + if (cliEntrypoint.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) + { + writer.WriteStringValue("node"); + } + writer.WriteStringValue(cliEntrypoint); + writer.WriteStringValue("--embedded-host"); + writer.WriteStringValue("--no-auto-update"); } - writer.WriteStringValue(cliEntrypoint); - writer.WriteStringValue("--embedded-host"); - // Pin the worker to the bundled pkg matching the loaded cdylib, instead of - // drifting to a newer version under the user's ~/.copilot/pkg (ABI skew). - writer.WriteStringValue("--no-auto-update"); foreach (var arg in args) { writer.WriteStringValue(arg); diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index f3c1a57e6e..c4428b46f8 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -99,6 +99,69 @@ internal sealed class ConnectRequest public string? Token { get; set; } } +/// One server-discovered hook action from user, repository, plugin, or managed-policy configuration. +[Experimental(Diagnostics.Experimental)] +public sealed class DiscoveredHook +{ + /// Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + [JsonPropertyName("disableKey")] + public string? DisableKey { get; set; } + + /// Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Hook event that invokes this action. + [JsonPropertyName("hookType")] + public HookType HookType { get; set; } + + /// Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Configuration tier that contributed this hook action. + [JsonPropertyName("origin")] + public HookOrigin Origin { get; set; } + + /// Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + [JsonPropertyName("projectPath")] + public string? ProjectPath { get; set; } + + /// Human-readable source label, such as a hook file path, settings source, or plugin name. + [JsonPropertyName("source")] + public string? Source { get; set; } +} + +/// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. +[Experimental(Diagnostics.Experimental)] +public sealed class HooksDiscoverResult +{ + /// Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } + + /// All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + [JsonPropertyName("hooks")] + public IList Hooks { get => field ??= []; set; } + + /// Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } +} + +/// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +[Experimental(Diagnostics.Experimental)] +internal sealed class HooksDiscoverRequest +{ + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + [JsonPropertyName("excludeHostHooks")] + public bool? ExcludeHostHooks { get; set; } + + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } +} + /// Active server-driven promotion for a model, including its discount and optional expiry. [Experimental(Diagnostics.Experimental)] public sealed class ModelBillingPromo @@ -118,6 +181,10 @@ public sealed class ModelBillingPromo /// Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. [JsonPropertyName("message")] public string? Message { get; set; } + + /// Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + [JsonPropertyName("showBanner")] + public bool? ShowBanner { get; set; } } /// Long context tier pricing (available for models with extended context windows). @@ -1841,6 +1908,11 @@ public partial class McpPlanInstallResultNetworkFailure : McpPlanInstallResult [JsonPropertyName("reason")] public required CatalogNetworkFailureReason Reason { get; set; } + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("retryAfterSeconds")] + public int? RetryAfterSeconds { get; set; } + /// HTTP status code, when the failure was a rejected response. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("statusCode")] @@ -2655,6 +2727,11 @@ public partial class CatalogSearchResultNetworkFailure : CatalogSearchResult [JsonPropertyName("reason")] public required CatalogNetworkFailureReason Reason { get; set; } + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("retryAfterSeconds")] + public int? RetryAfterSeconds { get; set; } + /// HTTP status code, when the failure was a rejected response. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("statusCode")] @@ -2762,7 +2839,7 @@ internal sealed class CatalogSearchRequest [JsonPropertyName("limit")] public int? Limit { get; set; } - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. [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)] @@ -3227,7 +3304,7 @@ internal sealed class SkillsConfigSetSkillDisabledRequest public string Name { get; set; } = string.Empty; } -/// Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. +/// Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. [Experimental(Diagnostics.Experimental)] public sealed class AgentInfo { @@ -3252,6 +3329,14 @@ public sealed class AgentInfo [JsonPropertyName("model")] public string? Model { get; set; } + /// Whether authored models are preferences or required constraints. + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } + + /// Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + [JsonPropertyName("models")] + public IList? Models { get; set; } + /// Name of the agent. Use `id` as the stable selection identifier. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; @@ -4216,6 +4301,48 @@ internal sealed class SessionsGetMetadataRequest public string SessionId { get; set; } = string.Empty; } +/// Batch of session events returned by a read, with cursor and continuation metadata. +[Experimental(Diagnostics.Experimental)] +public sealed class EventsReadResult +{ + /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). + [JsonPropertyName("cursor")] + public string Cursor { get; set; } = string.Empty; + + /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. + [JsonPropertyName("cursorStatus")] + public EventsCursorStatus CursorStatus { get; set; } + + /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. + [JsonPropertyName("events")] + public IList Events { get => field ??= []; set; } + + /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. + [JsonPropertyName("hasMore")] + public bool HasMore { get; set; } +} + +/// Pagination options for reading an inactive or active local session's persisted event journal. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionsReadPersistedEventsRequest +{ + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } + + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + [JsonPropertyName("direction")] + public EventsReadDirection? Direction { get; set; } + + /// Maximum number of events to return in this batch (1–1000, default 200). + [JsonPropertyName("max")] + public long? Max { get; set; } + + /// Session ID whose persisted event journal should be read. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Recent local session IDs that contain user-visible history. [Experimental(Diagnostics.Experimental)] internal sealed class SessionsListNonEmptySessionIdsResult @@ -5393,6 +5520,32 @@ internal sealed class LogRequest public string? Url { get; set; } } +/// Managed sandbox enforcement state for a session. +[Experimental(Diagnostics.Experimental)] +public sealed class SandboxEnforcementStatus +{ + /// Whether an enforcement failure has permanently blocked the session. + [JsonPropertyName("blocked")] + public bool Blocked { get; set; } + + /// The first sandbox enforcement failure that blocked the session. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Whether the effective managed policy requires an available sandbox backend. + [JsonPropertyName("required")] + public bool Required { get; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionSandboxGetEnforcementStatusRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Authentication status and account metadata for the session. [Experimental(Diagnostics.Experimental)] public sealed class SessionAuthStatus @@ -6218,6 +6371,7 @@ internal sealed class CanvasProviderUnregisterRequest [JsonDerivedType(typeof(FactoryRunFailureFactoryResumeDeclined), "factory_resume_declined")] [JsonDerivedType(typeof(FactoryRunFailureFactoryDurableFailure), "factory_durable_failure")] [JsonDerivedType(typeof(FactoryRunFailureFactoryAccountingIncomplete), "factory_accounting_incomplete")] +[JsonDerivedType(typeof(FactoryRunFailureFactoryProviderDisconnected), "factory_provider_disconnected")] public partial class FactoryRunFailure { /// The type discriminator. @@ -6303,15 +6457,33 @@ public partial class FactoryRunFailureFactoryAccountingIncomplete : FactoryRunFa public required string RunId { get; set; } } +/// The extension that owns the factory disconnected while the run was executing, so the host halted it. The run's journaled subagent results are preserved so a resume can reuse them. +/// The factory_provider_disconnected variant of . +[Experimental(Diagnostics.Experimental)] +public partial class FactoryRunFailureFactoryProviderDisconnected : FactoryRunFailure +{ + /// + [JsonIgnore] + public override string Type => "factory_provider_disconnected"; + + /// Factory run identifier. + [JsonPropertyName("runId")] + public required string RunId { get; set; } +} + /// Complete current or terminal factory run envelope. [Experimental(Diagnostics.Experimental)] public sealed class FactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + [JsonPropertyName("attempt")] + public long? Attempt { get; set; } + /// Error message for an errored run. [JsonPropertyName("error")] public string? Error { get; set; } - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. [JsonPropertyName("failure")] public FactoryRunFailure? Failure { get; set; } @@ -7428,7 +7600,7 @@ internal sealed class ModelSwitchToRequest [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// 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; } @@ -7453,6 +7625,10 @@ internal sealed class ModelApplyStartupOverlayRequest [JsonPropertyName("deviceManagedModel")] public string? DeviceManagedModel { get; set; } + /// 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. + [JsonPropertyName("policyHelperModel")] + public string? PolicyHelperModel { get; set; } + /// Context tier selected by repository settings, when configured. [JsonPropertyName("repoContextTier")] public string? RepoContextTier { get; set; } @@ -9091,6 +9267,10 @@ public sealed class SkillsInvokedSkill [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; + /// Whether model invocation was disabled when this skill was invoked. + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } + /// Turn number when the skill was invoked. [JsonPropertyName("invokedAtTurn")] public long InvokedAtTurn { get; set; } @@ -9099,7 +9279,7 @@ public sealed class SkillsInvokedSkill [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Path to the SKILL.md file. + /// Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity. [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; } @@ -10933,6 +11113,10 @@ public sealed class OptionsUpdateAdditionalContentExclusionPolicy [Experimental(Diagnostics.Experimental)] public sealed class CapiSessionOptions { + /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. [JsonPropertyName("enableWebSocketResponses")] public bool? EnableWebSocketResponses { get; set; } @@ -11108,7 +11292,7 @@ public sealed class SandboxConfigUserPolicyNetworkProxy [JsonPropertyName("password")] public string? Password { get; set; } - /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; @@ -11129,7 +11313,7 @@ public sealed class SandboxConfigUserPolicyNetwork [JsonPropertyName("allowOutbound")] public bool? AllowOutbound { get; set; } - /// HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. + /// HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. [JsonPropertyName("proxy")] public SandboxConfigUserPolicyNetworkProxy? Proxy { get; set; } } @@ -11369,7 +11553,7 @@ internal sealed class SessionUpdateOptionsParams [JsonPropertyName("enableSessionStore")] public bool? EnableSessionStore { get; set; } - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. [JsonPropertyName("enableSkills")] public bool? EnableSkills { get; set; } @@ -12899,6 +13083,10 @@ public sealed class SubagentSettingsEntry /// Model override for matching subagents. [JsonPropertyName("model")] public string? Model { get; set; } + + /// Whether the configured model strategy is preferred or required. + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } } /// Configured per-agent subagent overrides. @@ -13070,6 +13258,11 @@ public partial class SlashCommandInvocationResultCompleted : SlashCommandInvocat [JsonPropertyName("message")] public string? Message { get; set; } + /// Optional target session mode applied without submitting an agent prompt. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mode")] + public SessionMode? Mode { get; set; } + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("runtimeSettingsChanged")] @@ -13395,6 +13588,10 @@ internal sealed class EnqueueCommandParams [JsonPropertyName("command")] public string Command { get; set; } = string.Empty; + /// Optional user-facing text for the queue row. The command string is shown when omitted. + [JsonPropertyName("displayText")] + public string? DisplayText { get; set; } + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -16607,6 +16804,10 @@ public sealed class QueuePendingItems /// Whether this item is a queued user message or a queued slash command / model change. [JsonPropertyName("kind")] public QueuePendingItemsKind Kind { get; set; } + + /// Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + [JsonPropertyName("messageId")] + public string? MessageId { get; set; } } /// Snapshot of the session's pending queued items and immediate-steering messages. @@ -17029,27 +17230,6 @@ internal sealed class SessionQueueProcessRequest public string SessionId { get; set; } = string.Empty; } -/// Batch of session events returned by a read, with cursor and continuation metadata. -[Experimental(Diagnostics.Experimental)] -public sealed class EventsReadResult -{ - /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). - [JsonPropertyName("cursor")] - public string Cursor { get; set; } = string.Empty; - - /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. - [JsonPropertyName("cursorStatus")] - public EventsCursorStatus CursorStatus { get; set; } - - /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. - [JsonPropertyName("events")] - public IList Events { get => field ??= []; set; } - - /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. - [JsonPropertyName("hasMore")] - public bool HasMore { get; set; } -} - /// Cursor, batch size, and optional long-poll/filter parameters for reading session events. [Experimental(Diagnostics.Experimental)] internal sealed class EventLogReadRequest @@ -18761,6 +18941,183 @@ public sealed class GitHubTokenAcquireRequest public string? SessionId { get; set; } } +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runs before a tool is invoked. + public static HookType PreToolUse { get; } = new("preToolUse"); + + /// Runs before an MCP tool is invoked. + public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); + + /// Runs after a tool completes successfully. + public static HookType PostToolUse { get; } = new("postToolUse"); + + /// Runs after a tool fails. + public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); + + /// Runs after the user submits a prompt. + public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); + + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + public static HookType UserPromptTransformed { get; } = new("userPromptTransformed"); + + /// Runs when a session starts. + public static HookType SessionStart { get; } = new("sessionStart"); + + /// Runs when a session ends. + public static HookType SessionEnd { get; } = new("sessionEnd"); + + /// Runs after an agent result is produced. + public static HookType PostResult { get; } = new("postResult"); + + /// Runs before a pull request description is generated. + public static HookType PrePRDescription { get; } = new("prePRDescription"); + + /// Runs when the agent encounters an error. + public static HookType ErrorOccurred { get; } = new("errorOccurred"); + + /// Runs when the agent stops. + public static HookType AgentStop { get; } = new("agentStop"); + + /// Runs when a subagent starts. + public static HookType SubagentStart { get; } = new("subagentStart"); + + /// Runs when a subagent stops. + public static HookType SubagentStop { get; } = new("subagentStop"); + + /// Runs before conversation context is compacted. + public static HookType PreCompact { get; } = new("preCompact"); + + /// Runs when the agent requests permission. + public static HookType PermissionRequest { get; } = new("permissionRequest"); + + /// Runs when the agent emits a notification. + public static HookType Notification { get; } = new("notification"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookType left, HookType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookType left, HookType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookType other && Equals(other); + + /// + public bool Equals(HookType 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 HookType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + } + } +} + + +/// Configuration tier that contributed a discovered hook action. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookOrigin : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookOrigin(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Hook loaded from user settings or the user's hook directory. + public static HookOrigin User { get; } = new("user"); + + /// Hook loaded from repository settings or the repository hook directory. + public static HookOrigin Repository { get; } = new("repository"); + + /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. + public static HookOrigin Plugin { get; } = new("plugin"); + + /// Hook enforced by centrally managed policy. + public static HookOrigin Policy { get; } = new("policy"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookOrigin left, HookOrigin right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookOrigin left, HookOrigin right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookOrigin other && Equals(other); + + /// + public bool Equals(HookOrigin 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 HookOrigin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookOrigin value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookOrigin)); + } + } +} + + /// Resolved Anthropic adaptive-thinking capability for a model. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -20251,7 +20608,16 @@ public CatalogNetworkFailureReason(string value) /// The connection was refused or reset. public static CatalogNetworkFailureReason ConnectionRefused { get; } = new("connection-refused"); - /// The authority returned a status the runtime treats as a failure. + /// The configured proxy returned 407 and requires authentication. + public static CatalogNetworkFailureReason ProxyAuthenticationRequired { get; } = new("proxy-authentication-required"); + + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + public static CatalogNetworkFailureReason RateLimited { get; } = new("rate-limited"); + + /// The authority returned a transient 5xx response. + public static CatalogNetworkFailureReason ServiceUnavailable { get; } = new("service-unavailable"); + + /// The authority returned another status the runtime treats as a failure. public static CatalogNetworkFailureReason HttpStatus { get; } = new("http-status"); /// The response exceeded the permitted size. @@ -22244,6 +22610,132 @@ public override void Write(Utf8JsonWriter writer, SessionSource value, JsonSeria } +/// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history (the beginning for a forward read, the tail for a backward read). The fallback page is a fresh boundary snapshot, not a continuation of the requested cursor, so it may overlap already-rendered events; on 'expired' a consumer should reset/rebase its pagination state (or deduplicate by event id) before continuing from the returned cursor. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EventsCursorStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public EventsCursorStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The cursor was applied successfully. + public static EventsCursorStatus Ok { get; } = new("ok"); + + /// The cursor referred to history that is no longer available. + public static EventsCursorStatus Expired { get; } = new("expired"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); + + /// + public bool Equals(EventsCursorStatus 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 EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, EventsCursorStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); + } + } +} + + +/// Direction to page through the session's persisted event history. 'forward' pages from the cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EventsReadDirection : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public EventsReadDirection(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Page from the cursor toward newer events (default). + public static EventsReadDirection Forward { get; } = new("forward"); + + /// Tail-first: return the newest events and page toward older events. + public static EventsReadDirection Backward { get; } = new("backward"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EventsReadDirection left, EventsReadDirection right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EventsReadDirection left, EventsReadDirection right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); + + /// + public bool Equals(EventsReadDirection 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 EventsReadDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, EventsReadDirection value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); + } + } +} + + /// Kind of attention required when status === "attention". Meaningful only when status === "attention". [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -28534,69 +29026,6 @@ public override void Write(Utf8JsonWriter writer, QueuePendingItemsKind value, J } -/// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history (the beginning for a forward read, the tail for a backward read). The fallback page is a fresh boundary snapshot, not a continuation of the requested cursor, so it may overlap already-rendered events; on 'expired' a consumer should reset/rebase its pagination state (or deduplicate by event id) before continuing from the returned cursor. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct EventsCursorStatus : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public EventsCursorStatus(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// The cursor was applied successfully. - public static EventsCursorStatus Ok { get; } = new("ok"); - - /// The cursor referred to history that is no longer available. - public static EventsCursorStatus Expired { get; } = new("expired"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); - - /// - public bool Equals(EventsCursorStatus 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 EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, EventsCursorStatus value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); - } - } -} - - /// Agent-scope filter: 'primary' returns only main-agent events plus events whose type starts with 'subagent.' (matching the typed-subscription default behavior); 'all' returns events from all agents (matching wildcard-subscription behavior). Default is 'all' to preserve wildcard semantics for catch-up callers. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -28660,69 +29089,6 @@ public override void Write(Utf8JsonWriter writer, EventsAgentScope value, JsonSe } -/// Direction to page through the session's persisted event history. 'forward' pages from the cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct EventsReadDirection : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public EventsReadDirection(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Page from the cursor toward newer events (default). - public static EventsReadDirection Forward { get; } = new("forward"); - - /// Tail-first: return the newest events and page toward older events. - public static EventsReadDirection Backward { get; } = new("backward"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsReadDirection left, EventsReadDirection right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsReadDirection left, EventsReadDirection right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); - - /// - public bool Equals(EventsReadDirection 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 EventsReadDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, EventsReadDirection value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); - } - } -} - - /// Client population used for the prediction baseline. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -29531,7 +29897,7 @@ internal async Task ConnectAsync(bool? enableGitHubTelemetryForwa return await CopilotClient.InvokeRpcAsync(_rpc, "connect", [request], cancellationToken); } - /// Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility. + /// 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. /// The to monitor for cancellation requests. The default is . [Experimental(Diagnostics.Experimental)] public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancellationToken = default) @@ -29539,6 +29905,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell await CopilotClient.InvokeRpcAsync(_rpc, "registerExtensionLaunchProvider", [], cancellationToken); } + /// Hooks APIs. + public ServerHooksApi Hooks => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// Models APIs. public ServerModelsApi Models => field ?? @@ -29654,6 +30026,29 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell field; } +/// Provides server-scoped Hooks APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class ServerHooksApi +{ + private readonly JsonRpc _rpc; + + internal ServerHooksApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + /// The to monitor for cancellation requests. The default is . + /// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + public async Task DiscoverAsync(IList? projectPaths = null, bool? excludeHostHooks = null, CancellationToken cancellationToken = default) + { + var request = new HooksDiscoverRequest { ProjectPaths = projectPaths, ExcludeHostHooks = excludeHostHooks }; + return await CopilotClient.InvokeRpcAsync(_rpc, "hooks.discover", [request], cancellationToken); + } +} + /// Provides server-scoped Models APIs. [Experimental(Diagnostics.Experimental)] public sealed class ServerModelsApi @@ -29980,7 +30375,7 @@ internal ServerCatalogApi(JsonRpc rpc) /// 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. /// Protocol version and capabilities the caller requires. - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. /// Maximum number of candidates to return. Defaults to 10 when omitted. /// Restrict results to these candidate kinds. When omitted, every kind the runtime supports is searched. /// The to monitor for cancellation requests. The default is . @@ -30597,6 +30992,21 @@ internal async Task GetMetadataAsync(string sessionId return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.getMetadata", [request], cancellationToken); } + /// Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + /// Session ID whose persisted event journal should be read. + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + /// Maximum number of events to return in this batch (1–1000, default 200). + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + /// The to monitor for cancellation requests. The default is . + /// Batch of session events returned by a read, with cursor and continuation metadata. + public async Task ReadPersistedEventsAsync(string sessionId, string? cursor = null, long? max = null, EventsReadDirection? direction = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionId); + + var request = new SessionsReadPersistedEventsRequest { SessionId = sessionId, Cursor = cursor, Max = max, Direction = direction }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.readPersistedEvents", [request], cancellationToken); + } + /// Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. /// Maximum number of session IDs to return. /// The to monitor for cancellation requests. The default is . @@ -30941,6 +31351,12 @@ internal SessionRpc(CopilotSession session) internal CopilotSession Session => _session; + /// Sandbox APIs. + public SandboxApi Sandbox => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// GitHubAuth APIs. public GitHubAuthApi GitHubAuth => field ?? @@ -31313,6 +31729,29 @@ public async Task LogAsync(string message, SessionLogLevel? level = n } } +/// Provides session-scoped Sandbox APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class SandboxApi +{ + private readonly CopilotSession _session; + + internal SandboxApi(CopilotSession session) + { + _session = session; + } + + /// Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. + /// The to monitor for cancellation requests. The default is . + /// Managed sandbox enforcement state for a session. + public async Task GetEnforcementStatusAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionSandboxGetEnforcementStatusRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.sandbox.getEnforcementStatus", [request], cancellationToken); + } +} + /// Provides session-scoped GitHubAuth APIs. [Experimental(Diagnostics.Experimental)] public sealed class GitHubAuthApi @@ -31882,7 +32321,7 @@ public async Task GetCurrentAsync(CancellationToken cancellationTo /// Output verbosity level to request for supported models. /// Override individual model capabilities resolved by the runtime. /// 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. - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// 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. /// 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). /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. /// When true, evaluate context-window compaction policy before applying the switch. @@ -31904,6 +32343,7 @@ public async Task SwitchToAsync(string modelId, string? rea /// Resolves and applies organization-managed and repository model overlays. /// Model required by device-managed policy, when configured. /// Model required by server-managed policy, when configured. + /// 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. /// Model selected by repository settings, when configured. /// Reasoning effort selected by repository settings, when configured. /// Context tier selected by repository settings, when configured. @@ -31911,11 +32351,11 @@ public async Task SwitchToAsync(string modelId, string? rea /// Whether the overlay is being applied while resuming a deferred session. /// 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? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? cliModel = null, bool? deferredResume = null, CancellationToken cancellationToken = default) + internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? policyHelperModel = null, string? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? cliModel = null, bool? deferredResume = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, CliModel = cliModel, DeferredResume = deferredResume }; + var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, PolicyHelperModel = policyHelperModel, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, CliModel = cliModel, DeferredResume = deferredResume }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.applyStartupOverlay", [request], cancellationToken); } @@ -33372,7 +33812,7 @@ internal OptionsApi(CopilotSession session) /// Whether to enable loading of `.github/hooks/` filesystem hooks. Separate from the SDK callback hook mechanism. /// Whether to enable host git operations (context resolution, child repo scanning, git info in system prompt). /// Whether to enable cross-session store writes and reads. - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. /// Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. /// Optional session limits. Pass null to clear the session limits. /// The to monitor for cancellation requests. The default is . @@ -33687,14 +34127,15 @@ public async Task ExecuteAsync(string commandName, string /// Enqueues a slash command for FIFO processing on the local session. /// Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. + /// Optional user-facing text for the queue row. The command string is shown when omitted. /// The to monitor for cancellation requests. The default is . /// Indicates whether the command was accepted into the local execution queue. - public async Task EnqueueAsync(string command, CancellationToken cancellationToken = default) + public async Task EnqueueAsync(string command, string? displayText = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); _session.ThrowIfDisposed(); - var request = new EnqueueCommandParams { SessionId = _session.SessionId, Command = command }; + var request = new EnqueueCommandParams { SessionId = _session.SessionId, Command = command, DisplayText = displayText }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.commands.enqueue", [request], cancellationToken); } @@ -35576,6 +36017,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedCancelPhase), TypeInfoPropertyName = "SessionEventsAgentInterruptedCancelPhase")] [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.AssistantFusionPhaseActivityEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseActivityEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseCompletedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseFailedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseFailedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseStartedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseStartedEvent")] @@ -35592,6 +36035,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageStartData), TypeInfoPropertyName = "SessionEventsAssistantMessageStartData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageStartEvent), TypeInfoPropertyName = "SessionEventsAssistantMessageStartEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequest), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequest")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestCaller), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestCaller")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestCallerType), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestCallerType")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageToolRequestType), TypeInfoPropertyName = "SessionEventsAssistantMessageToolRequestType")] [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningData), TypeInfoPropertyName = "SessionEventsAssistantReasoningData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantReasoningDeltaData), TypeInfoPropertyName = "SessionEventsAssistantReasoningDeltaData")] @@ -35646,6 +36091,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedData), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedData")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedEvent), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchResponse), TypeInfoPropertyName = "SessionEventsAutoModeSwitchResponse")] +[JsonSerializable(typeof(GitHub.Copilot.AutoTier), TypeInfoPropertyName = "SessionEventsAutoTier")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedOperation), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedOperation")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedStatus), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedStatus")] [JsonSerializable(typeof(GitHub.Copilot.BinaryAssetReference), TypeInfoPropertyName = "SessionEventsBinaryAssetReference")] @@ -35678,6 +36124,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.CompactionCompleteCompactionTokensUsed), TypeInfoPropertyName = "SessionEventsCompactionCompleteCompactionTokensUsed")] [JsonSerializable(typeof(GitHub.Copilot.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail), TypeInfoPropertyName = "SessionEventsCompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail")] [JsonSerializable(typeof(GitHub.Copilot.CompactionTrigger), TypeInfoPropertyName = "SessionEventsCompactionTrigger")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptEventRange), TypeInfoPropertyName = "SessionEventsCompletionReceiptEventRange")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptFinalTool), TypeInfoPropertyName = "SessionEventsCompletionReceiptFinalTool")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptStopReason), TypeInfoPropertyName = "SessionEventsCompletionReceiptStopReason")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptToolStatus), TypeInfoPropertyName = "SessionEventsCompletionReceiptToolStatus")] [JsonSerializable(typeof(GitHub.Copilot.ContextTier), TypeInfoPropertyName = "SessionEventsContextTier")] [JsonSerializable(typeof(GitHub.Copilot.CustomAgentsUpdatedAgent), TypeInfoPropertyName = "SessionEventsCustomAgentsUpdatedAgent")] [JsonSerializable(typeof(GitHub.Copilot.ElicitationCompletedAction), TypeInfoPropertyName = "SessionEventsElicitationCompletedAction")] @@ -35715,7 +36165,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpAction), TypeInfoPropertyName = "SessionEventsFusionFollowUpAction")] [JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpRecommendation), TypeInfoPropertyName = "SessionEventsFusionFollowUpRecommendation")] [JsonSerializable(typeof(GitHub.Copilot.FusionPattern), TypeInfoPropertyName = "SessionEventsFusionPattern")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhaseActivityKind), TypeInfoPropertyName = "SessionEventsFusionPhaseActivityKind")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseKind), TypeInfoPropertyName = "SessionEventsFusionPhaseKind")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhasePlanStep), TypeInfoPropertyName = "SessionEventsFusionPhasePlanStep")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseStatus), TypeInfoPropertyName = "SessionEventsFusionPhaseStatus")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseUsage), TypeInfoPropertyName = "SessionEventsFusionPhaseUsage")] [JsonSerializable(typeof(GitHub.Copilot.FusionProjectionMode), TypeInfoPropertyName = "SessionEventsFusionProjectionMode")] @@ -36062,6 +36514,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(DiscoveredExtensions))] [JsonSerializable(typeof(DiscoveredExtensionsDisableRequest))] [JsonSerializable(typeof(DiscoveredExtensionsEnableRequest))] +[JsonSerializable(typeof(DiscoveredHook))] [JsonSerializable(typeof(DiscoveredMcpServer))] [JsonSerializable(typeof(EnqueueCommandParams))] [JsonSerializable(typeof(EnqueueCommandResult))] @@ -36146,6 +36599,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(HistorySummarizeForHandoffResult))] [JsonSerializable(typeof(HistoryTruncateRequest))] [JsonSerializable(typeof(HistoryTruncateResult))] +[JsonSerializable(typeof(HooksDiscoverRequest))] +[JsonSerializable(typeof(HooksDiscoverResult))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] @@ -36484,6 +36939,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SandboxConfigUserPolicyNetwork))] [JsonSerializable(typeof(SandboxConfigUserPolicyNetworkProxy))] [JsonSerializable(typeof(SandboxConfigUserPolicySeatbelt))] +[JsonSerializable(typeof(SandboxEnforcementStatus))] [JsonSerializable(typeof(ScheduleAddAtRequest))] [JsonSerializable(typeof(ScheduleAddCronRequest))] [JsonSerializable(typeof(ScheduleAddRequest))] @@ -36620,6 +37076,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionQueueRemoveMostRecentRequest))] [JsonSerializable(typeof(SessionQueueSnapshotRequest))] [JsonSerializable(typeof(SessionRemoteDisableRequest))] +[JsonSerializable(typeof(SessionSandboxGetEnforcementStatusRequest))] [JsonSerializable(typeof(SessionScheduleHasSelfPacedRequest))] [JsonSerializable(typeof(SessionScheduleHydrateRequest))] [JsonSerializable(typeof(SessionScheduleListRequest))] @@ -36691,6 +37148,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsLoadDeferredRepoHooksRequest))] [JsonSerializable(typeof(SessionsOpenProgress))] [JsonSerializable(typeof(SessionsPruneOldRequest))] +[JsonSerializable(typeof(SessionsReadPersistedEventsRequest))] [JsonSerializable(typeof(SessionsRegisterExtensionToolsOnSessionOptions))] [JsonSerializable(typeof(SessionsReleaseLockRequest))] [JsonSerializable(typeof(SessionsReleaseLockResult))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index b4351c8f2e..147e08e458 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -26,6 +26,7 @@ namespace GitHub.Copilot; IgnoreUnrecognizedTypeDiscriminators = true)] [JsonDerivedType(typeof(AbortEvent), "abort")] [JsonDerivedType(typeof(AgentInterruptedEvent), "agent.interrupted")] +[JsonDerivedType(typeof(AssistantFusionPhaseActivityEvent), "assistant.fusion_phase_activity")] [JsonDerivedType(typeof(AssistantFusionPhaseCompletedEvent), "assistant.fusion_phase_completed")] [JsonDerivedType(typeof(AssistantFusionPhaseFailedEvent), "assistant.fusion_phase_failed")] [JsonDerivedType(typeof(AssistantFusionPhaseStartedEvent), "assistant.fusion_phase_started")] @@ -94,6 +95,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionCanvasUnavailableEvent), "session.canvas.unavailable")] [JsonDerivedType(typeof(SessionCompactionCompleteEvent), "session.compaction_complete")] [JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")] +[JsonDerivedType(typeof(SessionCompletionReceiptEvent), "session.completion_receipt")] [JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")] [JsonDerivedType(typeof(SessionContextClearedEvent), "session.context_cleared")] [JsonDerivedType(typeof(SessionCustomAgentsUpdatedEvent), "session.custom_agents_updated")] @@ -113,6 +115,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionMcpServerStatusChangedEvent), "session.mcp_server_status_changed")] [JsonDerivedType(typeof(SessionMcpServersLoadedEvent), "session.mcp_servers_loaded")] [JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")] +[JsonDerivedType(typeof(SessionModeNoticeDeliveredEvent), "session.mode_notice_delivered")] [JsonDerivedType(typeof(SessionModelChangeEvent), "session.model_change")] [JsonDerivedType(typeof(SessionPermissionsChangedEvent), "session.permissions_changed")] [JsonDerivedType(typeof(SessionPlanChangedEvent), "session.plan_changed")] @@ -378,6 +381,19 @@ public sealed partial class SessionModeChangedEvent : SessionEvent public required SessionModeChangedData Data { get; set; } } +/// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +/// Represents the session.mode_notice_delivered event. +public sealed partial class SessionModeNoticeDeliveredEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mode_notice_delivered"; + + /// The session.mode_notice_delivered event payload. + [JsonPropertyName("data")] + public required SessionModeNoticeDeliveredData Data { get; set; } +} + /// Session limits update details. Null clears the limits. /// Represents the session.session_limits_changed event. public sealed partial class SessionSessionLimitsChangedEvent : SessionEvent @@ -587,6 +603,20 @@ public sealed partial class SessionTaskCompleteEvent : SessionEvent public required SessionTaskCompleteData Data { get; set; } } +/// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +/// Represents the session.completion_receipt event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionCompletionReceiptEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.completion_receipt"; + + /// The session.completion_receipt event payload. + [JsonPropertyName("data")] + public required SessionCompletionReceiptData Data { get; set; } +} + /// Experimental transient signal that HydraFusion routing has started for an eligible turn. /// Represents the session.fusion_route_started event. [Experimental(Diagnostics.Experimental)] @@ -735,6 +765,20 @@ public sealed partial class AssistantFusionPhaseStartedEvent : SessionEvent public required AssistantFusionPhaseStartedData Data { get; set; } } +/// Experimental content-safe activity signal for a running HydraFusion phase. +/// Represents the assistant.fusion_phase_activity event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseActivityEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.fusion_phase_activity"; + + /// The assistant.fusion_phase_activity event payload. + [JsonPropertyName("data")] + public required AssistantFusionPhaseActivityData Data { get; set; } +} + /// Experimental durable HydraFusion phase output and lossless replay checkpoint. /// Represents the assistant.fusion_phase_completed event. [Experimental(Diagnostics.Experimental)] @@ -1546,7 +1590,7 @@ public sealed partial class SessionAutoModeResolvedEvent : SessionEvent public required SessionAutoModeResolvedData Data { get; set; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. /// Represents the session.managed_settings_resolved event. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsResolvedEvent : SessionEvent @@ -1916,6 +1960,11 @@ public sealed partial class SessionStartData [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } + /// Auto routing preference selected at session creation time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Working directory and git context at session start. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] @@ -1995,6 +2044,11 @@ public sealed partial class SessionResumeData [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } + /// Auto routing preference active at resume time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Updated working directory and git context at resume time. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] @@ -2336,6 +2390,19 @@ public sealed partial class SessionModeChangedData public required SessionMode PreviousMode { get; set; } } +/// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +public sealed partial class SessionModeNoticeDeliveredData +{ + /// Model-visible transition notice persisted for a mid-turn delivery. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Mode established by the delivered transition notice. + [JsonPropertyName("mode")] + public required SessionMode Mode { get; set; } +} + /// Session limits update details. Null clears the limits. public sealed partial class SessionSessionLimitsChangedData { @@ -2856,6 +2923,44 @@ public sealed partial class SessionTaskCompleteData public string? Summary { get; set; } } +/// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionCompletionReceiptData +{ + /// One-based accepted completion receipt ordinal in the durable session history. + [JsonPropertyName("attempt")] + public required long Attempt { get; set; } + + /// Inclusive durable event range summarized by this receipt. + [JsonPropertyName("eventRange")] + public required CompletionReceiptEventRange EventRange { get; set; } + + /// Number of failed structured tool completions in the covered range. + [JsonPropertyName("failedToolCount")] + public required long FailedToolCount { get; set; } + + /// Final structured tool completion in the covered range, when one exists. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("finalTool")] + public CompletionReceiptFinalTool? FinalTool { get; set; } + + /// Version of the completion receipt payload. + [JsonPropertyName("schemaVersion")] + public required long SchemaVersion { get; set; } + + /// Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + [JsonPropertyName("sourceEventId")] + public required string SourceEventId { get; set; } + + /// Runtime reason the completion decision was accepted. + [JsonPropertyName("stopReason")] + public required CompletionReceiptStopReason StopReason { get; set; } + + /// Number of successful structured tool completions in the covered range. + [JsonPropertyName("successfulToolCount")] + public required long SuccessfulToolCount { get; set; } +} + /// Experimental transient signal that HydraFusion routing has started for an eligible turn. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionFusionRouteStartedData @@ -2948,6 +3053,12 @@ public sealed partial class SessionFusionResolvedData [JsonPropertyName("pattern")] public required FusionPattern Pattern { get; set; } + /// Presentation-neutral phase plan for clients that render workflow progress. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phasePlan")] + public FusionPhasePlanStep[]? PhasePlan { get; set; } + /// Version of the validated execution-plan format. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("planVersion")] @@ -3119,6 +3230,11 @@ public sealed partial class UserMessageData [JsonPropertyName("isAutopilotContinuation")] public bool? IsAutopilotContinuation { get; set; } + /// Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("messageId")] + public string? MessageId { get; set; } + /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("nativeDocumentPathFallbackPaths")] @@ -3300,6 +3416,49 @@ public sealed partial class AssistantFusionPhaseStartedData public required string Role { get; set; } } +/// Experimental content-safe activity signal for a running HydraFusion phase. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseActivityData +{ + /// Kind of real activity observed. + [JsonPropertyName("activity")] + public required FusionPhaseActivityKind Activity { get; set; } + + /// Conversation scope in which the phase executes. + [JsonPropertyName("conversationScope")] + public required FusionConversationScope ConversationScope { get; set; } + + /// Identifier of the HydraFusion turn containing the phase. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// HydraFusion orchestration pattern containing the phase. + [JsonPropertyName("pattern")] + public required FusionPattern Pattern { get; set; } + + /// Stable identifier for the concrete phase. + [JsonPropertyName("phaseId")] + public required string PhaseId { get; set; } + + /// Kind of phase currently executing. + [JsonPropertyName("phaseKind")] + public required FusionPhaseKind PhaseKind { get; set; } + + /// Semantic role assigned to the phase. + [JsonPropertyName("role")] + public required string Role { get; set; } + + /// Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + /// Cumulative private response bytes observed for this model call. The event never includes response text. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("totalResponseSizeBytes")] + public long? TotalResponseSizeBytes { get; set; } +} + /// Experimental durable HydraFusion phase output and lossless replay checkpoint. [Experimental(Diagnostics.Experimental)] public sealed partial class AssistantFusionPhaseCompletedData @@ -4457,6 +4616,11 @@ public sealed partial class SkillInvokedData [JsonPropertyName("description")] public string? Description { get; set; } + /// Whether model invocation is disabled for this skill. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } + /// Model identifier active when the skill was invoked, when known. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("model")] @@ -4466,7 +4630,7 @@ public sealed partial class SkillInvokedData [JsonPropertyName("name")] public required string Name { get; set; } - /// File path to the SKILL.md definition. + /// File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity. [JsonPropertyName("path")] public required string Path { get; set; } @@ -4480,7 +4644,7 @@ public sealed partial class SkillInvokedData [JsonPropertyName("pluginVersion")] public string? PluginVersion { get; set; } - /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill). + /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] public string? Source { get; set; } @@ -4618,6 +4782,11 @@ public sealed partial class SubagentCompletedData [JsonPropertyName("model")] public string? Model { get; set; } + /// Why an explicit task-call model did not become the effective model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelOverrideReason")] + public string? ModelOverrideReason { get; set; } + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -4684,6 +4853,11 @@ public sealed partial class SubagentFailedData [JsonPropertyName("model")] public string? Model { get; set; } + /// Why an explicit task-call model did not become the effective model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelOverrideReason")] + public string? ModelOverrideReason { get; set; } + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -4731,7 +4905,7 @@ public sealed partial class HookStartData [JsonPropertyName("hookType")] public required string HookType { get; set; } - /// Input data passed to the hook. + /// Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("input")] public JsonElement? Input { get; set; } @@ -4863,6 +5037,11 @@ public sealed partial class SystemNotificationData /// Permission request notification requiring client approval with request details. public sealed partial class PermissionRequestedData { + /// Agent mode captured from the owning turn when permission evaluation began. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentMode")] + public SessionMode? AgentMode { get; set; } + /// Details of the permission being requested. [JsonPropertyName("permissionRequest")] public required PermissionRequest PermissionRequest { get; set; } @@ -5400,7 +5579,7 @@ public sealed partial class SessionAutoModeResolvedData public bool? StickyOverride { get; set; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsResolvedData { @@ -5430,6 +5609,11 @@ public sealed partial class SessionManagedSettingsResolvedData [JsonPropertyName("permissionsAllowIntersected")] public bool? PermissionsAllowIntersected { get; set; } + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("policyHelperManaged")] + public bool? PolicyHelperManaged { get; set; } + /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("sandboxEnabledByUndeterminedPolicy")] @@ -5444,7 +5628,7 @@ public sealed partial class SessionManagedSettingsResolvedData [JsonPropertyName("settings")] public JsonElement? Settings { get; set; } - /// Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. [JsonPropertyName("source")] public required ManagedSettingsResolvedSource Source { get; set; } } @@ -6196,6 +6380,42 @@ public sealed partial class CompactionCompleteCompactionTokensUsed public long? OutputTokens { get; set; } } +/// Inclusive durable event range summarized by a completion receipt. +/// Nested data type for CompletionReceiptEventRange. +public sealed partial class CompletionReceiptEventRange +{ + /// Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + [JsonPropertyName("endEventId")] + public required string EndEventId { get; set; } + + /// Identifier of the user message that starts the covered exchange. + [JsonPropertyName("startEventId")] + public required string StartEventId { get; set; } +} + +/// Final structured tool completion in the covered event range. +/// Nested data type for CompletionReceiptFinalTool. +public sealed partial class CompletionReceiptFinalTool +{ + /// Process exit code from a structured shell result, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("exitCode")] + public long? ExitCode { get; set; } + + /// Structured success or failure status from the tool completion event. + [JsonPropertyName("status")] + public required CompletionReceiptToolStatus Status { get; set; } + + /// Unique identifier of the completed tool call. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } + + /// Tool name from the matching tool execution start event, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + /// Durable server recommendation for subsequent HydraFusion turns. /// Nested data type for FusionFollowUpRecommendation. [Experimental(Diagnostics.Experimental)] @@ -6210,6 +6430,28 @@ public sealed partial class FusionFollowUpRecommendation public required FusionFollowUpAction UserTurn { get; set; } } +/// Presentation-neutral phase planned for a HydraFusion turn. +/// Nested data type for FusionPhasePlanStep. +[Experimental(Diagnostics.Experimental)] +public sealed partial class FusionPhasePlanStep +{ + /// Whether the phase executes only when an earlier phase requests it. + [JsonPropertyName("conditional")] + public required bool Conditional { get; set; } + + /// Kind of phase that may execute. + [JsonPropertyName("kind")] + public required FusionPhaseKind Kind { get; set; } + + /// Semantic role assigned to the phase. + [JsonPropertyName("role")] + public required string Role { get; set; } + + /// Conversation scope in which the phase executes. + [JsonPropertyName("scope")] + public required FusionConversationScope Scope { get; set; } +} + /// Validated HydraFusion routing capability scores. /// Nested data type for FusionScores. [Experimental(Diagnostics.Experimental)] @@ -7058,7 +7300,7 @@ public sealed partial class FusionAttribution [Experimental(Diagnostics.Experimental)] public sealed partial class AssistantMessageReasoningBlocks { - /// Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + /// Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("blocks")] public JsonElement[]? Blocks { get; set; } @@ -7098,6 +7340,19 @@ public sealed partial class AssistantMessageServerTools public JsonElement[]? RawContentBlocks { get; set; } } +/// Hosted program that requested this client tool call. +/// Nested data type for AssistantMessageToolRequestCaller. +public sealed partial class AssistantMessageToolRequestCaller +{ + /// Provider-assigned identifier for the hosted caller. + [JsonPropertyName("callerId")] + public required string CallerId { get; set; } + + /// Kind of hosted caller that requested the client tool call. + [JsonPropertyName("type")] + public required AssistantMessageToolRequestCallerType Type { get; set; } +} + /// A tool invocation request from the assistant. /// Nested data type for AssistantMessageToolRequest. public sealed partial class AssistantMessageToolRequest @@ -7107,6 +7362,11 @@ public sealed partial class AssistantMessageToolRequest [JsonPropertyName("arguments")] public JsonElement? Arguments { get; set; } + /// Hosted program that requested this client tool call. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("caller")] + public AssistantMessageToolRequestCaller? Caller { get; set; } + /// Resolved intention summary describing what this specific call does. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("intentionSummary")] @@ -10101,7 +10361,7 @@ public sealed partial class SkillsLoadedSkill [JsonPropertyName("path")] public string? Path { get; set; } - /// Source location type (e.g., project, personal-copilot, plugin, builtin). + /// Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk). [JsonPropertyName("source")] public required SkillSource Source { get; set; } @@ -10110,7 +10370,7 @@ public sealed partial class SkillsLoadedSkill public required bool UserInvocable { get; set; } } -/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. /// Nested data type for CustomAgentsUpdatedAgent. public sealed partial class CustomAgentsUpdatedAgent { @@ -10131,6 +10391,16 @@ public sealed partial class CustomAgentsUpdatedAgent [JsonPropertyName("model")] public string? Model { get; set; } + /// Whether authored models are preferences or required constraints. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } + + /// Authored model ids in priority order, if configured. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("models")] + public string[]? Models { get; set; } + /// Internal name of the agent. [JsonPropertyName("name")] public required string Name { get; set; } @@ -10305,6 +10575,70 @@ public sealed partial class McpAppToolCallCompleteToolMeta public McpAppToolCallCompleteToolMetaUI? Ui { get; set; } } +/// Routing preference used when the session model is `auto`. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AutoTier : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AutoTier(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Optimize for efficiency. + public static AutoTier Efficiency { get; } = new("efficiency"); + + /// Balance efficiency and intelligence. + public static AutoTier Balance { get; } = new("balance"); + + /// Optimize for intelligence. + public static AutoTier Intelligence { get; } = new("intelligence"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutoTier left, AutoTier right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutoTier left, AutoTier right) => !(left == right); + + /// + 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 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 AutoTier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AutoTier value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutoTier)); + } + } +} + /// Hosting platform type of the repository (github or ado). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -11345,43 +11679,51 @@ public override void Write(Utf8JsonWriter writer, TaskCompletionOutcome value, J } } -/// Kind of turn for which HydraFusion routing is running. -[Experimental(Diagnostics.Experimental)] +/// Structured terminal status from a tool completion event. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FusionTurnKind : IEquatable +public readonly struct CompletionReceiptToolStatus : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FusionTurnKind(string value) + public CompletionReceiptToolStatus(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// A user-message turn. - public static FusionTurnKind User { get; } = new("user"); + /// The tool completed successfully. + public static CompletionReceiptToolStatus Success { get; } = new("success"); - /// A conversation-compaction turn. - public static FusionTurnKind Compaction { get; } = new("compaction"); + /// The tool failed without a more specific structured status. + public static CompletionReceiptToolStatus Failure { get; } = new("failure"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FusionTurnKind left, FusionTurnKind right) => left.Equals(right); + /// The tool exceeded its time budget. + public static CompletionReceiptToolStatus Timeout { get; } = new("timeout"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FusionTurnKind left, FusionTurnKind right) => !(left == right); + /// The user rejected the tool call. + public static CompletionReceiptToolStatus Rejected { get; } = new("rejected"); + + /// The permissions service denied the tool call. + public static CompletionReceiptToolStatus Denied { get; } = new("denied"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CompletionReceiptToolStatus left, CompletionReceiptToolStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CompletionReceiptToolStatus left, CompletionReceiptToolStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FusionTurnKind other && Equals(other); + public override bool Equals(object? obj) => obj is CompletionReceiptToolStatus other && Equals(other); /// - public bool Equals(FusionTurnKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CompletionReceiptToolStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -11389,61 +11731,63 @@ public FusionTurnKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FusionTurnKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CompletionReceiptToolStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FusionTurnKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CompletionReceiptToolStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionTurnKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CompletionReceiptToolStatus)); } } } -/// Server-recommended routing behavior for a later HydraFusion turn. -[Experimental(Diagnostics.Experimental)] +/// Runtime reason the completion decision was accepted. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FusionFollowUpAction : IEquatable +public readonly struct CompletionReceiptStopReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FusionFollowUpAction(string value) + public CompletionReceiptStopReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Reuse the durable primary model without routing. - public static FusionFollowUpAction ReusePrimary { get; } = new("reuse_primary"); + /// The model reached a natural terminal response. + public static CompletionReceiptStopReason Natural { get; } = new("natural"); - /// Request a new routing decision. - public static FusionFollowUpAction Reroute { get; } = new("reroute"); + /// A terminal tool ended the interaction. + public static CompletionReceiptStopReason TerminalTool { get; } = new("terminal_tool"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FusionFollowUpAction left, FusionFollowUpAction right) => left.Equals(right); + /// The configured agentStop continuation limit was reached. + public static CompletionReceiptStopReason AgentStopBlockLimit { get; } = new("agent_stop_block_limit"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FusionFollowUpAction left, FusionFollowUpAction right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CompletionReceiptStopReason left, CompletionReceiptStopReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CompletionReceiptStopReason left, CompletionReceiptStopReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FusionFollowUpAction other && Equals(other); + public override bool Equals(object? obj) => obj is CompletionReceiptStopReason other && Equals(other); /// - public bool Equals(FusionFollowUpAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CompletionReceiptStopReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -11451,52 +11795,176 @@ public FusionFollowUpAction(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FusionFollowUpAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override CompletionReceiptStopReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FusionFollowUpAction value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, CompletionReceiptStopReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionFollowUpAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CompletionReceiptStopReason)); } } } -/// Validated HydraFusion execution pattern. +/// Kind of turn for which HydraFusion routing is running. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FusionPattern : IEquatable +public readonly struct FusionTurnKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FusionPattern(string value) + public FusionTurnKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Run one primary solver phase. - public static FusionPattern Single { get; } = new("single"); + /// A user-message turn. + public static FusionTurnKind User { get; } = new("user"); - /// Run a primary phase, a judge, and an optional repair. - public static FusionPattern Cascade { get; } = new("cascade"); + /// A conversation-compaction turn. + public static FusionTurnKind Compaction { get; } = new("compaction"); - /// Run a primary draft, a read-only critique, and a revision. - public static FusionPattern Critique { get; } = new("critique"); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionTurnKind left, FusionTurnKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionTurnKind left, FusionTurnKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionTurnKind other && Equals(other); + + /// + public bool Equals(FusionTurnKind 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 FusionTurnKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionTurnKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionTurnKind)); + } + } +} + +/// Server-recommended routing behavior for a later HydraFusion turn. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionFollowUpAction : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionFollowUpAction(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Reuse the durable primary model without routing. + public static FusionFollowUpAction ReusePrimary { get; } = new("reuse_primary"); + + /// Request a new routing decision. + public static FusionFollowUpAction Reroute { get; } = new("reroute"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionFollowUpAction left, FusionFollowUpAction right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionFollowUpAction left, FusionFollowUpAction right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionFollowUpAction other && Equals(other); + + /// + public bool Equals(FusionFollowUpAction 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 FusionFollowUpAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionFollowUpAction value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionFollowUpAction)); + } + } +} + +/// Validated HydraFusion execution pattern. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionPattern : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionPattern(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Run one primary solver phase. + public static FusionPattern Single { get; } = new("single"); + + /// Run a primary phase, a judge, and an optional repair. + public static FusionPattern Cascade { get; } = new("cascade"); + + /// Run a primary draft, a read-only critique, and a revision. + public static FusionPattern Critique { get; } = new("critique"); /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(FusionPattern left, FusionPattern right) => left.Equals(right); @@ -11534,6 +12002,145 @@ public override void Write(Utf8JsonWriter writer, FusionPattern value, JsonSeria } } +/// HydraFusion phase kind. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionPhaseKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionPhaseKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Primary solver phase. + public static FusionPhaseKind Primary { get; } = new("primary"); + + /// Read-only cascade judge phase. + public static FusionPhaseKind Judge { get; } = new("judge"); + + /// Cascade repair phase. + public static FusionPhaseKind Repair { get; } = new("repair"); + + /// Initial critique-pattern draft phase. + public static FusionPhaseKind Draft { get; } = new("draft"); + + /// Read-only critique phase. + public static FusionPhaseKind Critic { get; } = new("critic"); + + /// Critique-pattern revision phase. + public static FusionPhaseKind Revision { get; } = new("revision"); + + /// Follow-up phase continuing from the resolved model. + public static FusionPhaseKind FollowUp { get; } = new("follow_up"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPhaseKind left, FusionPhaseKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPhaseKind left, FusionPhaseKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionPhaseKind other && Equals(other); + + /// + public bool Equals(FusionPhaseKind 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 FusionPhaseKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionPhaseKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseKind)); + } + } +} + +/// Conversation scope in which a HydraFusion phase executes. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionConversationScope : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionConversationScope(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Canonical root conversation history. + public static FusionConversationScope Root { get; } = new("root"); + + /// Isolated read-only review history that does not enter the root conversation. + public static FusionConversationScope Review { get; } = new("review"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionConversationScope left, FusionConversationScope right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionConversationScope left, FusionConversationScope right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionConversationScope other && Equals(other); + + /// + public bool Equals(FusionConversationScope 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 FusionConversationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionConversationScope value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionConversationScope)); + } + } +} + /// The agent mode that was active when this message was sent. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -11979,120 +12586,46 @@ public override void Write(Utf8JsonWriter writer, ModelCallFailureTransport valu } } -/// Conversation scope in which a HydraFusion phase executes. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct FusionConversationScope : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public FusionConversationScope(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Canonical root conversation history. - public static FusionConversationScope Root { get; } = new("root"); - - /// Isolated read-only review history that does not enter the root conversation. - public static FusionConversationScope Review { get; } = new("review"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FusionConversationScope left, FusionConversationScope right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FusionConversationScope left, FusionConversationScope right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is FusionConversationScope other && Equals(other); - - /// - public bool Equals(FusionConversationScope 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 FusionConversationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, FusionConversationScope value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionConversationScope)); - } - } -} - -/// HydraFusion phase kind. +/// Content-safe activity observed while a HydraFusion phase is running. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FusionPhaseKind : IEquatable +public readonly struct FusionPhaseActivityKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FusionPhaseKind(string value) + public FusionPhaseActivityKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Primary solver phase. - public static FusionPhaseKind Primary { get; } = new("primary"); - - /// Read-only cascade judge phase. - public static FusionPhaseKind Judge { get; } = new("judge"); - - /// Cascade repair phase. - public static FusionPhaseKind Repair { get; } = new("repair"); - - /// Initial critique-pattern draft phase. - public static FusionPhaseKind Draft { get; } = new("draft"); - - /// Read-only critique phase. - public static FusionPhaseKind Critic { get; } = new("critic"); + /// The provider produced additional private output bytes. + public static FusionPhaseActivityKind ModelOutput { get; } = new("model_output"); - /// Critique-pattern revision phase. - public static FusionPhaseKind Revision { get; } = new("revision"); + /// A tool began executing inside the phase. + public static FusionPhaseActivityKind ToolStarted { get; } = new("tool_started"); - /// Follow-up phase continuing from the resolved model. - public static FusionPhaseKind FollowUp { get; } = new("follow_up"); + /// A tool finished executing inside the phase. + public static FusionPhaseActivityKind ToolCompleted { get; } = new("tool_completed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FusionPhaseKind left, FusionPhaseKind right) => left.Equals(right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPhaseActivityKind left, FusionPhaseActivityKind right) => left.Equals(right); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FusionPhaseKind left, FusionPhaseKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPhaseActivityKind left, FusionPhaseActivityKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FusionPhaseKind other && Equals(other); + public override bool Equals(object? obj) => obj is FusionPhaseActivityKind other && Equals(other); /// - public bool Equals(FusionPhaseKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(FusionPhaseActivityKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -12100,20 +12633,20 @@ public FusionPhaseKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FusionPhaseKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override FusionPhaseActivityKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FusionPhaseKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, FusionPhaseActivityKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseActivityKind)); } } } @@ -12374,6 +12907,64 @@ public override void Write(Utf8JsonWriter writer, CitationProvider value, JsonSe } } +/// Hosted program caller type. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AssistantMessageToolRequestCallerType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AssistantMessageToolRequestCallerType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the program value. + public static AssistantMessageToolRequestCallerType Program { get; } = new("program"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AssistantMessageToolRequestCallerType left, AssistantMessageToolRequestCallerType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AssistantMessageToolRequestCallerType left, AssistantMessageToolRequestCallerType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AssistantMessageToolRequestCallerType other && Equals(other); + + /// + public bool Equals(AssistantMessageToolRequestCallerType 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 AssistantMessageToolRequestCallerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AssistantMessageToolRequestCallerType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AssistantMessageToolRequestCallerType)); + } + } +} + /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -14683,7 +15274,10 @@ public ManagedSettingsResolvedSource(string value) /// Only session-local SDK-host injection contributed. public static ManagedSettingsResolvedSource Client { get; } = new("client"); - /// More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + /// A policy helper registered by device or server policy contributed. Device registration takes priority when present. + public static ManagedSettingsResolvedSource PolicyHelper { get; } = new("policyHelper"); + + /// More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. public static ManagedSettingsResolvedSource Mixed { get; } = new("mixed"); /// No managed policy is in force (no channel contributed). @@ -14990,7 +15584,7 @@ public override void Write(Utf8JsonWriter writer, FactoryRunSettledStatus value, } } -/// Source location type (e.g., project, personal-copilot, plugin, builtin). +/// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct SkillSource : IEquatable @@ -15030,6 +15624,9 @@ public SkillSource(string value) /// Skill bundled with the runtime. public static SkillSource Builtin { get; } = new("builtin"); + /// Pathless skill supplied lazily by an SDK skill provider. + public static SkillSource Sdk { get; } = new("sdk"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(SkillSource left, SkillSource right) => left.Equals(right); @@ -15066,6 +15663,67 @@ public override void Write(Utf8JsonWriter writer, SkillSource value, JsonSeriali } } +/// Whether configured models are advisory preferences or required constraints. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AgentModelPolicy : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AgentModelPolicy(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Treat the authored models as advisory preferences that callers may override. + public static AgentModelPolicy Preferred { get; } = new("preferred"); + + /// Require subagent execution to use one of the authored models. + public static AgentModelPolicy Required { get; } = new("required"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentModelPolicy left, AgentModelPolicy right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentModelPolicy left, AgentModelPolicy right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AgentModelPolicy other && Equals(other); + + /// + public bool Equals(AgentModelPolicy 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 AgentModelPolicy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AgentModelPolicy value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentModelPolicy)); + } + } +} + /// Configuration source: user, workspace, plugin, or builtin. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -15419,6 +16077,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(AbortEvent))] [JsonSerializable(typeof(AgentInterruptedData))] [JsonSerializable(typeof(AgentInterruptedEvent))] +[JsonSerializable(typeof(AssistantFusionPhaseActivityData))] +[JsonSerializable(typeof(AssistantFusionPhaseActivityEvent))] [JsonSerializable(typeof(AssistantFusionPhaseCompletedData))] [JsonSerializable(typeof(AssistantFusionPhaseCompletedEvent))] [JsonSerializable(typeof(AssistantFusionPhaseFailedData))] @@ -15438,6 +16098,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(AssistantMessageStartData))] [JsonSerializable(typeof(AssistantMessageStartEvent))] [JsonSerializable(typeof(AssistantMessageToolRequest))] +[JsonSerializable(typeof(AssistantMessageToolRequestCaller))] [JsonSerializable(typeof(AssistantReasoningData))] [JsonSerializable(typeof(AssistantReasoningDeltaData))] [JsonSerializable(typeof(AssistantReasoningDeltaEvent))] @@ -15512,6 +16173,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsed))] [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsedCopilotUsage))] [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail))] +[JsonSerializable(typeof(CompletionReceiptEventRange))] +[JsonSerializable(typeof(CompletionReceiptFinalTool))] [JsonSerializable(typeof(CustomAgentsUpdatedAgent))] [JsonSerializable(typeof(ElicitationCompletedData))] [JsonSerializable(typeof(ElicitationCompletedEvent))] @@ -15538,6 +16201,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(FactoryRunUpdatedEvent))] [JsonSerializable(typeof(FusionAttribution))] [JsonSerializable(typeof(FusionFollowUpRecommendation))] +[JsonSerializable(typeof(FusionPhasePlanStep))] [JsonSerializable(typeof(FusionPhaseUsage))] [JsonSerializable(typeof(FusionScores))] [JsonSerializable(typeof(FusionStagedTerminal))] @@ -15665,6 +16329,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionCompactionCompleteEvent))] [JsonSerializable(typeof(SessionCompactionStartData))] [JsonSerializable(typeof(SessionCompactionStartEvent))] +[JsonSerializable(typeof(SessionCompletionReceiptData))] +[JsonSerializable(typeof(SessionCompletionReceiptEvent))] [JsonSerializable(typeof(SessionContextChangedData))] [JsonSerializable(typeof(SessionContextChangedEvent))] [JsonSerializable(typeof(SessionContextClearedData))] @@ -15710,6 +16376,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionMcpServersLoadedEvent))] [JsonSerializable(typeof(SessionModeChangedData))] [JsonSerializable(typeof(SessionModeChangedEvent))] +[JsonSerializable(typeof(SessionModeNoticeDeliveredData))] +[JsonSerializable(typeof(SessionModeNoticeDeliveredEvent))] [JsonSerializable(typeof(SessionModelChangeData))] [JsonSerializable(typeof(SessionModelChangeEvent))] [JsonSerializable(typeof(SessionPermissionsChangedData))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 7d076d14b0..5995abaaff 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -977,7 +977,7 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission } catch (Exception ex) { - _logger.LogError(ex, "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}", SessionId, requestId); + LogPermissionHandlerOrDeliveryFailed(ex, SessionId, requestId); try { await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, PermissionDecision.UserNotAvailable()); @@ -1975,6 +1975,9 @@ await InvokeRpcAsync( [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch tool metadata for {toolName}")] private partial void LogToolMetadataFetchFailed(Exception exception, string toolName); + [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); + internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 6ed05e3064..3af7ea7f12 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -322,6 +322,7 @@ private CopilotClientOptions(CopilotClientOptions? other) OnGitHubTelemetry = other.OnGitHubTelemetry; SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds; EnableRemoteSessions = other.EnableRemoteSessions; + ClientInfo = other.ClientInfo; Mode = other.Mode; } @@ -465,6 +466,16 @@ private CopilotClientOptions(CopilotClientOptions? other) /// public bool EnableRemoteSessions { get; set; } + /// + /// Declares the integrating application's identity, forwarded to the runtime on the + /// server.connect handshake. Declaring it lets the telemetry the + /// runtime emits on this connection be attributed to a consistent surface + /// (the application and its Copilot integration) instead of the runtime's own + /// build. All fields are optional; leave it to keep + /// the runtime's default attribution. + /// + public CopilotClientInfo? ClientInfo { get; set; } + /// /// Creates a shallow clone of this instance. /// @@ -531,6 +542,38 @@ public sealed class TelemetryConfig public bool? CaptureContent { get; set; } } +/// +/// Identifies the integrating application on the server.connect handshake. +/// +/// +/// Declaring it lets the telemetry the runtime emits on the connection be +/// attributed to a single, consistent surface instead of the runtime's own +/// build. All properties are optional; an unset property is omitted from the +/// handshake. +/// +public sealed class CopilotClientInfo +{ + /// + /// Name of the application using the SDK. + /// + public string? ApplicationName { get; set; } + + /// + /// Version of the application using the SDK. + /// + public string? ApplicationVersion { get; set; } + + /// + /// Optionally specifies a named integration within the application, such as an extension or plugin. + /// + public string? IntegrationName { get; set; } + + /// + /// Optionally specifies the version of that integration. + /// + public string? IntegrationVersion { get; set; } +} + /// /// Configuration for a custom session filesystem provider. /// @@ -2398,6 +2441,18 @@ public sealed class CapiSessionOptions /// [JsonPropertyName("enableWebSocketResponses")] public bool? EnableWebSocketResponses { get; set; } + + /// + /// Routing tier for model auto with V2 Auto. + /// + /// + /// Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto. + /// When omitted, the runtime uses its default on create and preserves the persisted or current + /// tier on resume. An explicit tier overrides the persisted tier on a cold resume; a conflicting + /// tier on a resident session resume is rejected by the runtime. + /// + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } } /// @@ -3116,6 +3171,21 @@ public sealed class ManagedSettings public ManagedSettingsPermissions? Permissions { get; set; } } +/// +/// Selects the model-facing shape of the built-in ask_user tool. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AskUserVariant +{ + /// Use the legacy user-input request flow. + [JsonStringEnumMemberName("legacy")] + Legacy, + + /// Use the elicitation request flow. + [JsonStringEnumMemberName("elicitation")] + Elicitation +} + /// /// Shared configuration properties for creating or resuming a Copilot session. /// Use when creating a new session, or @@ -3205,10 +3275,14 @@ protected SessionConfigBase(SessionConfigBase? other) ReasoningEffort = other.ReasoningEffort; ReasoningSummary = other.ReasoningSummary; ContextTier = other.ContextTier; + AskUserVariant = other.AskUserVariant; CreateSessionFsProvider = other.CreateSessionFsProvider; GitHubToken = other.GitHubToken; GitHubTokenProvider = other.GitHubTokenProvider; RemoteSession = other.RemoteSession; + FeatureFlags = other.FeatureFlags is not null + ? new Dictionary(other.FeatureFlags) + : null; ExpAssignments = other.ExpAssignments; EnableManagedSettings = other.EnableManagedSettings; ManagedSettings = other.ManagedSettings; @@ -3372,6 +3446,15 @@ protected SessionConfigBase(SessionConfigBase? other) /// System message configuration for the session. public SystemMessageConfig? SystemMessage { get; set; } + /// + /// Selects the model-facing shape of the built-in ask_user tool. + /// The default is . To use + /// , also provide + /// so the host can answer structured forms. + /// The runtime resolves this option when it creates or cold-resumes the session. + /// + public AskUserVariant? AskUserVariant { get; set; } + /// List of tool names to allow; only these tools will be available when specified. public IList? AvailableTools { get; set; } @@ -3470,7 +3553,11 @@ protected SessionConfigBase(SessionConfigBase? other) /// Handler for permission requests from the server. public Func>? OnPermissionRequest { get; set; } - /// Handler for user input requests from the agent. + /// + /// Handler for user input requests from the agent. When provided with the default + /// variant, enables the + /// question-and-answer form of the ask_user tool. + /// public Func>? OnUserInputRequest { get; set; } /// Slash commands registered for this session. @@ -3696,6 +3783,12 @@ protected SessionConfigBase(SessionConfigBase? other) [EditorBrowsable(EditorBrowsableState.Never)] public CopilotExpAssignmentResponse? ExpAssignments { get; set; } + /// + /// Feature-flag values resolved by the host for this session. + /// Re-supply them when resuming after a runtime restart. + /// + public IDictionary? FeatureFlags { get; set; } + /// /// Opt-in: when true, the runtime self-fetches enterprise managed /// settings (bypass-permissions policy) at session bootstrap using the diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 5f7944b2c4..95770dba8e 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -38,6 +38,8 @@ <_CopilotPlatform Condition="'$(_CopilotRid)' == 'osx-arm64'">darwin-arm64 <_CopilotBinary Condition="$(_CopilotRid.StartsWith('win-'))">copilot.exe <_CopilotBinary Condition="'$(_CopilotBinary)' == ''">copilot + <_CopilotRuntimeWrapper Condition="$(_CopilotRid.StartsWith('win-'))">copilot-runtime.exe + <_CopilotRuntimeWrapper Condition="'$(_CopilotRuntimeWrapper)' == ''">copilot-runtime <_CopilotRuntimeNodePath>$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\runtime.node + <_CopilotRuntimeWrapperPath Condition="'$(_CopilotRuntimeWrapperPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper) + <_CopilotRuntimeAssetManifest>$(_CopilotOutputDir)\.copilot-runtime-assets + <_CopilotExplicitCliMarker>$(_CopilotOutputDir)\.copilot-explicit-cli + + + + + + <_CopilotSafePreviousRuntimeAsset Include="@(_CopilotPreviousRuntimeAsset)" + Condition="!$([System.IO.Path]::IsPathRooted('%(Identity)')) And !$([System.String]::Copy('%(Identity)').Contains('..'))" /> + + + + + <_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*" + Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot-sdk\**\*;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\preloads\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sdk\**\*;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" /> + <_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*" + Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" /> + + + + + + + + + diff --git a/dotnet/test/E2E/BuiltinToolsE2ETests.cs b/dotnet/test/E2E/BuiltinToolsE2ETests.cs index 37067ecc60..143487dac4 100644 --- a/dotnet/test/E2E/BuiltinToolsE2ETests.cs +++ b/dotnet/test/E2E/BuiltinToolsE2ETests.cs @@ -112,21 +112,9 @@ public async Task Should_Create_A_New_File() Assert.Contains("Created by test", msg?.Data.Content ?? string.Empty); } - // TODO(cli-1.0.81-2): the grep and glob built-in tools shell out to the CLI's - // bundled ripgrep, which the runtime cannot locate when it is loaded in-process - // over FFI ("Failed to execute ripgrep: No such file or directory"). The tool - // then returns an error the recorded snapshots do not cover. Re-enable once the - // in-process runtime resolves its bundled binaries. - private static bool RipgrepUnavailable => E2ETestContext.UsesInProcessTransport; - [Fact] public async Task Should_Search_For_Patterns_In_Files() { - if (RipgrepUnavailable) - { - return; - } - await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "data.txt"), "apple\nbanana\napricot\ncherry\n"); var session = await CreateSessionAsync(); var msg = await session.SendAndWaitAsync(new MessageOptions @@ -141,11 +129,6 @@ public async Task Should_Search_For_Patterns_In_Files() [Fact] public async Task Should_Find_Files_By_Pattern() { - if (RipgrepUnavailable) - { - return; - } - Directory.CreateDirectory(Path.Join(Ctx.WorkDir, "src")); await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "src", "index.ts"), "export const index = 1;"); await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "README.md"), "# Readme"); diff --git a/dotnet/test/E2E/CanvasE2ETests.cs b/dotnet/test/E2E/CanvasE2ETests.cs index deb94a9b24..249440d993 100644 --- a/dotnet/test/E2E/CanvasE2ETests.cs +++ b/dotnet/test/E2E/CanvasE2ETests.cs @@ -123,7 +123,6 @@ private Task CreateCanvasSessionAsync(TestCanvasHandler handler) { OnPermissionRequest = PermissionHandler.ApproveAll, RequestCanvasRenderer = true, - RequestExtensions = true, ExtensionInfo = new ExtensionInfo { Source = "dotnet-sdk-tests", Name = "canvas-provider" }, Canvases = [ diff --git a/dotnet/test/E2E/ClientE2ETests.cs b/dotnet/test/E2E/ClientE2ETests.cs index b6bdfd90fd..282cc9ee67 100644 --- a/dotnet/test/E2E/ClientE2ETests.cs +++ b/dotnet/test/E2E/ClientE2ETests.cs @@ -41,9 +41,8 @@ public async Task Should_Start_And_Connect_To_Server(bool useStdio) [Fact] public async Task Should_Start_And_Connect_Over_InProcess_Ffi() { - // In-process FFI hosting resolves the CLI entrypoint (COPILOT_CLI_PATH or the - // bundled CLI binary) and its sibling native runtime library itself; if neither - // is available, StartAsync throws and the test fails hard. + // In-process FFI hosting loads the bundled runtime library directly; if it is + // unavailable, StartAsync throws and the test fails hard. using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForInProcess(), @@ -193,15 +192,18 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start(bool u var ex = await Assert.ThrowsAsync(() => client.StartAsync()); var errorMessage = ex.Message; - // On .NET Framework with stdio transport, the pipe error may not include stderr content. - if (errorMessage.Contains("pipe", StringComparison.OrdinalIgnoreCase)) +#if NETFRAMEWORK + // .NET Framework can surface the stdio pipe failure before redirected stderr is readable. + if (useStdio) { - // .NET Framework pipe behavior — just verify we got an IOException - Assert.Contains("pipe", errorMessage, StringComparison.OrdinalIgnoreCase); + Assert.True( + errorMessage.Contains("CLI", StringComparison.OrdinalIgnoreCase) + || errorMessage.Contains("pipe", StringComparison.OrdinalIgnoreCase), + $"Expected a CLI process or pipe error, got: {errorMessage}"); } else +#endif { - // Verify we get the stderr output in the error message Assert.Contains("stderr", errorMessage, StringComparison.OrdinalIgnoreCase); Assert.Contains("nonexistent", errorMessage, StringComparison.OrdinalIgnoreCase); } diff --git a/dotnet/test/E2E/ClientOptionsE2ETests.cs b/dotnet/test/E2E/ClientOptionsE2ETests.cs index 5391e4bdbc..1ff473bd01 100644 --- a/dotnet/test/E2E/ClientOptionsE2ETests.cs +++ b/dotnet/test/E2E/ClientOptionsE2ETests.cs @@ -296,7 +296,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request var session = await Ctx.CreateSessionAsync(client, new SessionConfig { ClientName = "advanced-create-client", - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ReasoningEffort = "medium", ReasoningSummary = ReasoningSummary.Detailed, ContextTier = ContextTier.LongContext, @@ -379,7 +379,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request Provider = "create-provider", Id = "create-model", Name = "Create Model", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "create-wire-model", MaxContextWindowTokens = 12_000, MaxPromptTokens = 10_000, @@ -392,7 +392,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath)); var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create"); Assert.Equal("advanced-create-client", createRequest.GetProperty("clientName").GetString()); - Assert.Equal("claude-sonnet-4.5", createRequest.GetProperty("model").GetString()); + Assert.Equal("claude-sonnet-5", createRequest.GetProperty("model").GetString()); Assert.Equal("medium", createRequest.GetProperty("reasoningEffort").GetString()); Assert.Equal("detailed", createRequest.GetProperty("reasoningSummary").GetString()); Assert.Equal("long_context", createRequest.GetProperty("contextTier").GetString()); @@ -442,7 +442,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques var session = await Ctx.CreateSessionAsync(client, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "azure", @@ -453,7 +453,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques BearerToken = "provider-bearer-token", Azure = new AzureOptions { ApiVersion = "2024-02-15-preview" }, Headers = new Dictionary { ["X-Provider-Wire"] = "yes" }, - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "azure-deployment", MaxPromptTokens = 8192, MaxOutputTokens = 1024, @@ -471,7 +471,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques Assert.Equal("provider-bearer-token", provider.GetProperty("bearerToken").GetString()); Assert.Equal("2024-02-15-preview", provider.GetProperty("azure").GetProperty("apiVersion").GetString()); Assert.Equal("yes", provider.GetProperty("headers").GetProperty("X-Provider-Wire").GetString()); - Assert.Equal("claude-sonnet-4.5", provider.GetProperty("modelId").GetString()); + Assert.Equal("claude-sonnet-5", provider.GetProperty("modelId").GetString()); Assert.Equal("azure-deployment", provider.GetProperty("wireModel").GetString()); Assert.Equal(8192, provider.GetProperty("maxPromptTokens").GetInt32()); Assert.Equal(1024, provider.GetProperty("maxOutputTokens").GetInt32()); diff --git a/dotnet/test/E2E/CopilotRequestE2EProvider.cs b/dotnet/test/E2E/CopilotRequestE2EProvider.cs index 89826b4f84..f4c250752a 100644 --- a/dotnet/test/E2E/CopilotRequestE2EProvider.cs +++ b/dotnet/test/E2E/CopilotRequestE2EProvider.cs @@ -160,9 +160,9 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) private static readonly string[] ChatCompletionStreamEvents = [ - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\n", - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"" + SyntheticText + "\"},\"finish_reason\":null}]}\n\n", - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"" + SyntheticText + "\"},\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}\n\n", "data: [DONE]\n\n", ]; @@ -172,7 +172,7 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) // runtime's Anthropic client fail with "stream ended without producing a Message". private static readonly string[] AnthropicStreamEvents = [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4.5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":1}}}\n\n", + "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":1}}}\n\n", "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n", "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"" + SyntheticText + "\"}}\n\n", "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n", @@ -184,13 +184,13 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) "{\"id\":\"resp_stub_1\",\"object\":\"response\",\"status\":\"completed\",\"output\":[{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"" + SyntheticText + "\"}]}],\"usage\":{\"input_tokens\":5,\"output_tokens\":7,\"total_tokens\":12}}"; private static readonly string BufferedChatCompletionJson = - "{\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"" + SyntheticText + "\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}"; + "{\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"" + SyntheticText + "\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}"; private static readonly string BufferedAnthropicMessageJson = - "{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4.5\",\"content\":[{\"type\":\"text\",\"text\":\"" + SyntheticText + "\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":7}}"; + "{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-5\",\"content\":[{\"type\":\"text\",\"text\":\"" + SyntheticText + "\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":7}}"; private const string ModelCatalogJson = - "{\"data\":[{\"id\":\"claude-sonnet-4.5\",\"name\":\"Claude Sonnet 4.5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-4.5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; + "{\"data\":[{\"id\":\"claude-sonnet-5\",\"name\":\"Claude Sonnet 5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; } /// A single request the callback intercepted. diff --git a/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs b/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs index fd00cc9b99..2fca912592 100644 --- a/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs @@ -76,15 +76,15 @@ public async Task Threads_The_Session_Id_Into_A_Byok_Session_Inference_Request() { OnPermissionRequest = PermissionHandler.ApproveAll, // BYOK providers require an explicit model id. - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "openai", WireApi = "responses", BaseUrl = "https://byok.invalid/v1", ApiKey = "byok-secret", - ModelId = "claude-sonnet-4.5", - WireModel = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", + WireModel = "claude-sonnet-5", }, }); var byokSessionId = session.SessionId; diff --git a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs index 80ccdb8c90..e6890d6992 100644 --- a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs @@ -349,7 +349,7 @@ private static (string Type, string Json)[] ResponseEvents(string text, string i ]; private const string ModelCatalogJson = - "{\"data\":[{\"id\":\"claude-sonnet-4.5\",\"name\":\"Claude Sonnet 4.5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"supported_endpoints\":[\"/responses\",\"ws:/responses\"],\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-4.5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; + "{\"data\":[{\"id\":\"claude-sonnet-5\",\"name\":\"Claude Sonnet 5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"supported_endpoints\":[\"/responses\",\"ws:/responses\"],\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; private static int GetFreePort() { diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index b3ca218190..bf1ed687c1 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -196,7 +196,30 @@ await session1.SendAsync(new MessageOptions if (disconnectOriginalClient) { + await using var lockObserver = Ctx.CreateClient(options: new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + }); + await lockObserver.StartAsync(); + await TestHelper.WaitForConditionAsync( + async () => + { + var result = await lockObserver.Rpc.Sessions.CheckInUseAsync([sessionId]); + return result.InUse.Contains(sessionId); + }, + timeout: PendingWorkTimeout, + timeoutMessage: $"Timed out waiting for session '{sessionId}' to acquire its lock."); + await suspendedClient.ForceStopAsync(); + + await TestHelper.WaitForConditionAsync( + async () => + { + var result = await lockObserver.Rpc.Sessions.CheckInUseAsync([sessionId]); + return !result.InUse.Contains(sessionId); + }, + timeout: PendingWorkTimeout, + timeoutMessage: $"Timed out waiting for session '{sessionId}' to release its lock."); } await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); diff --git a/dotnet/test/E2E/RewindE2ETests.cs b/dotnet/test/E2E/RewindE2ETests.cs index 74c0b8ab9f..e8379f988d 100644 --- a/dotnet/test/E2E/RewindE2ETests.cs +++ b/dotnet/test/E2E/RewindE2ETests.cs @@ -13,26 +13,37 @@ public class RewindE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "rewind", output) { private const string FileName = "rewind-sdk.txt"; + private const string OriginalFileContent = "Original rewind content"; + private const string PreparedFileContent = "Prepared rewind content"; private const string FileContent = "SDK rewind content"; [Fact] public async Task Should_Restore_Tracked_File_And_Conversation() { - // TODO(cli-1.0.81): Re-enable when Windows file-change tracking records built-in create tool writes. - if (OperatingSystem.IsWindows()) - return; - var filePath = Path.Join(Ctx.WorkDir, FileName); + await File.WriteAllTextAsync(filePath, OriginalFileContent); await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableFileChangeTracking = true, }); + var ready = await session.SendAndWaitAsync( + new MessageOptions + { + Prompt = $"Use the edit tool to replace the exact contents of {FileName} " + + $"from {OriginalFileContent} to {PreparedFileContent}. " + + "After the tool succeeds, reply with exactly SDK_REWIND_READY.", + }, + TimeSpan.FromSeconds(30)); + Assert.Equal("SDK_REWIND_READY", ready?.Data.Content); + Assert.Equal(PreparedFileContent, await File.ReadAllTextAsync(filePath)); + var response = await session.SendAndWaitAsync( new MessageOptions { - Prompt = $"Use the create tool to create {FileName} containing exactly {FileContent}. " + Prompt = $"Use the edit tool to replace the exact contents of {FileName} " + + $"from {PreparedFileContent} to {FileContent}. " + "After the tool succeeds, reply with exactly SDK_REWIND_DONE.", }, TimeSpan.FromSeconds(30)); @@ -47,9 +58,10 @@ await TestHelper.WaitForConditionAsync( { rewindPoints = await session.Rpc.History.ListRewindPointsAsync(); return rewindPoints.UnavailableReason is null - && rewindPoints.Points.Count == 1 - && rewindPoints.Points[0].CanRestoreFiles - && rewindPoints.Points[0].FileCount == 1; + && rewindPoints.Points.Count == 2 + && rewindPoints.Points[1].TurnChangedFiles + && rewindPoints.Points[1].CanRestoreFiles + && rewindPoints.Points[1].FileCount == 1; }, timeout: TimeSpan.FromSeconds(30), timeoutMessage: "Timed out waiting for a restorable file rewind point.", @@ -57,7 +69,9 @@ await TestHelper.WaitForConditionAsync( Assert.NotNull(rewindPoints); Assert.True(rewindPoints.FileChangeTrackingEnabled); - var rewindPoint = Assert.Single(rewindPoints.Points); + Assert.Equal(2, rewindPoints.Points.Count); + var rewindPoint = rewindPoints.Points[1]; + Assert.True(rewindPoint.TurnChangedFiles); Assert.True(rewindPoint.CanRestoreFiles); Assert.Equal(1, rewindPoint.FileCount); @@ -80,7 +94,7 @@ await TestHelper.WaitForConditionAsync( Path.GetFullPath(filePath), Path.GetFullPath(restoredFile), OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); - Assert.False(File.Exists(filePath)); + Assert.Equal(PreparedFileContent, await File.ReadAllTextAsync(filePath)); var events = await session.GetEventsAsync(); Assert.DoesNotContain(events, sessionEvent => sessionEvent.Id.ToString() == rewindPoint.EventId); diff --git a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs index 0d2942d4bf..ec923b31a1 100644 --- a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs +++ b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs @@ -33,7 +33,7 @@ public async Task Should_List_And_Toggle_Session_Skills() { var skillName = $"session-rpc-skill-{Guid.NewGuid():N}"; var skillsDir = CreateSkillDirectory(skillName, "Session skill controlled by RPC."); - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir], DisabledSkills = [skillName], @@ -56,7 +56,7 @@ public async Task Should_Ensure_Skills_Are_Loaded_And_List_Invoked_Skills() { var skillName = $"ensure-rpc-skill-{Guid.NewGuid():N}"; var skillsDir = CreateSkillDirectory(skillName, "Skill loaded explicitly by RPC."); - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir], }); @@ -79,7 +79,7 @@ public async Task Should_Reload_Session_Skills() Directory.CreateDirectory(skillsDir); var skillName = $"reload-rpc-skill-{Guid.NewGuid():N}"; - var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir] }); + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir] }); var before = await session.Rpc.Skills.ListAsync(); Assert.DoesNotContain(before.Skills, skill => string.Equals(skill.Name, skillName, StringComparison.Ordinal)); @@ -95,7 +95,7 @@ public async Task Should_Reload_Session_Skills() public async Task Should_List_Mcp_Servers_With_Configured_Server() { const string serverName = "rpc-list-mcp-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -111,7 +111,7 @@ public async Task Should_List_Mcp_Servers_With_Configured_Server() public async Task Should_Set_Mcp_Env_Value_Mode_And_Remove_GitHub_Server() { const string serverName = "github"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -137,7 +137,7 @@ public async Task Should_Set_Mcp_Env_Value_Mode_And_Remove_GitHub_Server() public async Task Should_Report_Mcp_Sampling_Failure_And_Cancel_Missing_Sampling() { const string serverName = "rpc-sampling-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -172,7 +172,7 @@ public async Task Should_Report_Mcp_Sampling_Failure_And_Cancel_Missing_Sampling [Fact] public async Task Should_List_Plugins() { - var session = await CreateSessionAsync(); + await using var session = await CreateSessionAsync(); var result = await session.Rpc.Plugins.ListAsync(); @@ -314,7 +314,7 @@ public async Task Should_Report_Error_When_Mcp_App_Resource_Is_Not_Available() [Fact] public async Task Should_Report_Error_When_Mcp_Host_Is_Not_Initialized() { - var session = await CreateSessionAsync(); + await using var session = await CreateSessionAsync(); await AssertFailureAsync( () => session.Rpc.Mcp.EnableAsync("missing-server"), @@ -333,7 +333,7 @@ await AssertFailureAsync( [Fact] public async Task Should_Report_Error_When_Mcp_Oauth_Server_Is_Not_Configured() { - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers("configured-stdio-server"), }); @@ -348,7 +348,7 @@ await AssertFailureAsync( public async Task Should_Report_Error_When_Mcp_Oauth_Server_Is_Not_Remote() { const string serverName = "configured-stdio-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); diff --git a/dotnet/test/E2E/RpcServerE2ETests.cs b/dotnet/test/E2E/RpcServerE2ETests.cs index 2df8593cc4..5e39c5ca5a 100644 --- a/dotnet/test/E2E/RpcServerE2ETests.cs +++ b/dotnet/test/E2E/RpcServerE2ETests.cs @@ -171,7 +171,7 @@ public async Task Should_Call_Rpc_Models_List_With_Typed_Result() var result = await client.Rpc.Models.ListAsync(); Assert.NotNull(result.Models); - Assert.Contains(result.Models, model => model.Id == "claude-sonnet-4.5"); + Assert.Contains(result.Models, model => model.Id == "claude-sonnet-5"); Assert.All(result.Models, model => Assert.False(string.IsNullOrWhiteSpace(model.Name))); } diff --git a/dotnet/test/E2E/RpcSessionStateE2ETests.cs b/dotnet/test/E2E/RpcSessionStateE2ETests.cs index 803c1c602b..196861cd9b 100644 --- a/dotnet/test/E2E/RpcSessionStateE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateE2ETests.cs @@ -22,14 +22,14 @@ private static async Task AssertImplementedFailureAsync(Func ac [Fact] public async Task Should_Call_Session_Rpc_Model_GetCurrent() { - await using var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); + await using var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-5" }); var result = await session.Rpc.Model.GetCurrentAsync(); Assert.NotNull(result.ModelId); Assert.NotEmpty(result.ModelId); // Strengthen: verify the configured model is actually in effect, not just any model - Assert.Equal("claude-sonnet-4.5", result.ModelId); + Assert.Equal("claude-sonnet-5", result.ModelId); } [Fact] @@ -48,12 +48,12 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo() await using var session = await isolatedCtx.CreateSessionAsync(isolatedClient, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); var before = await session.Rpc.Model.GetCurrentAsync(); - Assert.Equal("claude-sonnet-4.5", before.ModelId); + Assert.Equal("claude-sonnet-5", before.ModelId); var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-5.4", reasoningEffort: "high"); Assert.Equal("gpt-5.4", result.ModelId); @@ -281,14 +281,14 @@ public async Task Should_Call_Metadata_Snapshot_SetWorkingDirectory_And_RecordCo var branch = $"rpc-context-{Guid.NewGuid():N}"; await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", WorkingDirectory = firstDirectory, }); var initialSnapshot = await session.Rpc.Metadata.SnapshotAsync(); Assert.Equal(session.SessionId, initialSnapshot.SessionId); Assert.Equal(MetadataSnapshotCurrentMode.Interactive, initialSnapshot.CurrentMode); - Assert.Equal("claude-sonnet-4.5", initialSnapshot.SelectedModel); + Assert.Equal("claude-sonnet-5", initialSnapshot.SelectedModel); Assert.False(initialSnapshot.IsRemote); Assert.False(initialSnapshot.AlreadyInUse); Assert.NotEqual(default, initialSnapshot.StartTime); @@ -405,14 +405,14 @@ public async Task Should_Set_ReasoningEffort_And_Auto_Name() { await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", }); var reasoning = await session.Rpc.Model.SetReasoningEffortAsync("high"); Assert.Equal("high", reasoning.ReasoningEffort); var currentModel = await session.Rpc.Model.GetCurrentAsync(); - Assert.Equal("claude-sonnet-4.5", currentModel.ModelId); + Assert.Equal("claude-sonnet-5", currentModel.ModelId); Assert.Equal("high", currentModel.ReasoningEffort); var autoName = $"Auto Session {Guid.NewGuid():N}"; @@ -653,9 +653,9 @@ public async Task Should_Compact_Session_History_After_Messages() var contextInfo = await session.Rpc.Metadata.ContextInfoAsync( promptTokenLimit: 128_000, outputTokenLimit: 4_096, - selectedModel: "claude-sonnet-4.5"); + selectedModel: "claude-sonnet-5"); var context = Assert.IsType(contextInfo.ContextInfo); - Assert.Equal("claude-sonnet-4.5", context.ModelName); + Assert.Equal("claude-sonnet-5", context.ModelName); Assert.Equal(128_000, context.PromptTokenLimit); Assert.True(context.Limit >= context.PromptTokenLimit); Assert.True(context.TotalTokens > 0); @@ -666,7 +666,7 @@ public async Task Should_Compact_Session_History_After_Messages() context.SystemTokens + context.ConversationTokens + context.ToolDefinitionsTokens, context.TotalTokens); - var recomputed = await session.Rpc.Metadata.RecomputeContextTokensAsync("claude-sonnet-4.5"); + var recomputed = await session.Rpc.Metadata.RecomputeContextTokensAsync("claude-sonnet-5"); Assert.True(recomputed.SystemTokenCount > 0); Assert.True(recomputed.MessagesTokenCount > 0); Assert.Equal(recomputed.SystemTokenCount + recomputed.MessagesTokenCount, recomputed.TotalTokens); diff --git a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs index 72663c35a4..234a5d8694 100644 --- a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs @@ -32,7 +32,7 @@ public async Task Should_List_Models_For_Session() await using var client = CreateAuthenticatedClient(token); await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -41,7 +41,7 @@ public async Task Should_List_Models_For_Session() Assert.NotNull(result.List); Assert.NotEmpty(result.List); // The configured model must be present in the returned catalog. - Assert.Contains(result.List, model => model.GetRawText().Contains("claude-sonnet-4.5", StringComparison.Ordinal)); + Assert.Contains(result.List, model => model.GetRawText().Contains("claude-sonnet-5", StringComparison.Ordinal)); } [Fact] @@ -73,7 +73,7 @@ public async Task Should_Add_Byok_Provider_And_Model_At_Runtime() Provider = providerName, Id = modelId, Name = "SDK Runtime Model", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "wire-sdk-runtime-model", MaxContextWindowTokens = 4_096, MaxPromptTokens = 3_072, @@ -277,7 +277,7 @@ public async Task Should_Update_And_Clear_Live_Subagent_Settings() { ["general-purpose"] = new() { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EffortLevel = "high", ContextTier = SubagentSettingsEntryContextTier.Default, }, diff --git a/dotnet/test/E2E/SessionConfigE2ETests.cs b/dotnet/test/E2E/SessionConfigE2ETests.cs index 1bc4c52eb9..314ba2f0cf 100644 --- a/dotnet/test/E2E/SessionConfigE2ETests.cs +++ b/dotnet/test/E2E/SessionConfigE2ETests.cs @@ -31,7 +31,7 @@ public async Task Vision_Disabled_Then_Enabled_Via_SetModel() var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ModelCapabilities = new ModelCapabilitiesOverride { Supports = new ModelCapabilitiesOverrideSupports { Vision = false }, @@ -46,7 +46,7 @@ public async Task Vision_Disabled_Then_Enabled_Via_SetModel() // Switch vision on await session.SetModelAsync( - "claude-sonnet-4.5", + "claude-sonnet-5", reasoningEffort: null, modelCapabilities: new ModelCapabilitiesOverride { @@ -74,7 +74,7 @@ public async Task Vision_Enabled_Then_Disabled_Via_SetModel() var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ModelCapabilities = new ModelCapabilitiesOverride { Supports = new ModelCapabilitiesOverrideSupports { Vision = true }, @@ -89,7 +89,7 @@ public async Task Vision_Enabled_Then_Disabled_Via_SetModel() // Switch vision off await session.SetModelAsync( - "claude-sonnet-4.5", + "claude-sonnet-5", reasoningEffort: null, modelCapabilities: new ModelCapabilitiesOverride { @@ -216,7 +216,7 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Create() { var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = CreateProxyProvider("create-provider-header"), }); @@ -240,7 +240,7 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Resume() var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = CreateProxyProvider("resume-provider-header"), }); @@ -267,7 +267,7 @@ public async Task Should_Forward_Provider_Wire_Model() // tests for serialization coverage). var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "openai", @@ -300,14 +300,14 @@ public async Task Should_Use_Provider_Model_Id_As_Wire_Model() Type = "openai", BaseUrl = Ctx.ProxyUrl, ApiKey = "test-provider-key", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", }, }); await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); var exchange = Assert.Single(await Ctx.GetExchangesAsync()); - Assert.Equal("claude-sonnet-4.5", exchange.Request.Model); + Assert.Equal("claude-sonnet-5", exchange.Request.Model); await session.DisposeAsync(); } @@ -598,7 +598,7 @@ public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Crea var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableCitations = true, Provider = CreateAnthropicProvider(), }); @@ -645,7 +645,7 @@ public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Resu var session2 = await Ctx.ResumeSessionAsync(resumeClient, sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableCitations = true, Provider = CreateAnthropicProvider(), }); @@ -834,8 +834,8 @@ private static ProviderConfig CreateAnthropicProvider() Type = "anthropic", BaseUrl = "https://anthropic-citations.invalid/v1", ApiKey = "test-provider-key", - ModelId = "claude-sonnet-4.5", - WireModel = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", + WireModel = "claude-sonnet-5", }; } diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index 27ef7437f7..aababb670b 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -17,7 +17,7 @@ public class SessionE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : [Fact] public async Task ShouldCreateAndDisconnectSessions() { - var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); + var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-5" }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); diff --git a/dotnet/test/Harness/E2ETestBackend.cs b/dotnet/test/Harness/E2ETestBackend.cs index 04808ed7a9..71c8f85761 100644 --- a/dotnet/test/Harness/E2ETestBackend.cs +++ b/dotnet/test/Harness/E2ETestBackend.cs @@ -15,7 +15,7 @@ internal enum E2ETestBackend internal static class E2ETestBackendConfiguration { internal const string EnvironmentVariable = "COPILOT_SDK_E2E_BACKEND"; - private const string AnthropicDefaultModel = "claude-sonnet-4.5"; + private const string AnthropicDefaultModel = "claude-sonnet-5"; private const string OpenAIDefaultModel = "gpt-4.1"; private const string FakeCredential = "fake-byok-credential-for-e2e-tests"; diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index b61546c650..a59257b3da 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -464,6 +464,142 @@ public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unse Assert.False(agent.TryGetProperty("reasoningEffort", out _)); } + public static TheoryData CapiAutoTiers => new() + { + { AutoTier.Efficiency, "efficiency", null }, + { AutoTier.Balance, "balance", null }, + { AutoTier.Intelligence, "intelligence", null }, + { AutoTier.Efficiency, "efficiency", false }, + { AutoTier.Balance, "balance", false }, + { AutoTier.Intelligence, "intelligence", false }, + }; + + [Theory] + [MemberData(nameof(CapiAutoTiers))] + public async Task SessionRequests_Serialize_CapiAutoTier(AutoTier tier, string expectedTier, bool? enableWebSocketResponses) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var capi = new CapiSessionOptions { AutoTier = tier, EnableWebSocketResponses = enableWebSocketResponses }; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + await using var resumed = await client.ResumeSessionAsync("resume-with-auto-tier", new ResumeSessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + foreach (var method in new[] { "session.create", "session.resume" }) + { + var request = Assert.Single(server.Requests, request => request.Method == method); + var serializedCapi = request.Params.GetProperty("capi"); + Assert.Equal(expectedTier, serializedCapi.GetProperty("autoTier").GetString()); + if (enableWebSocketResponses.HasValue) + { + Assert.Equal(enableWebSocketResponses.Value, serializedCapi.GetProperty("enableWebSocketResponses").GetBoolean()); + } + else + { + Assert.False(serializedCapi.TryGetProperty("enableWebSocketResponses", out _)); + } + } + } + + [Theory] + [InlineData(false, null)] + [InlineData(true, null)] + [InlineData(true, false)] + public async Task SessionRequests_Omit_CapiAutoTier_WhenUnset(bool includeCapi, bool? enableWebSocketResponses) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var capi = includeCapi ? new CapiSessionOptions { EnableWebSocketResponses = enableWebSocketResponses } : null; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + await using var resumed = await client.ResumeSessionAsync("resume-without-auto-tier", new ResumeSessionConfig + { + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + foreach (var method in new[] { "session.create", "session.resume" }) + { + var request = Assert.Single(server.Requests, request => request.Method == method); + Assert.Equal(includeCapi, request.Params.TryGetProperty("capi", out var serializedCapi)); + if (includeCapi) + { + Assert.False(serializedCapi.TryGetProperty("autoTier", out _)); + if (enableWebSocketResponses.HasValue) + { + Assert.Equal(enableWebSocketResponses.Value, serializedCapi.GetProperty("enableWebSocketResponses").GetBoolean()); + } + else + { + Assert.Empty(serializedCapi.EnumerateObject()); + } + } + } + } + + [Fact] + public async Task CreateSessionAsync_Forwards_AskUserVariant() + { + 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 + { + AskUserVariant = AskUserVariant.Elicitation, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.Equal("elicitation", request.Params.GetProperty("askUserVariant").GetString()); + + server.ClearRequests(); + await using var defaultSession = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + var defaultRequest = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.False(defaultRequest.Params.TryGetProperty("askUserVariant", out _)); + } + + [Fact] + public async Task ResumeSessionAsync_Forwards_AskUserVariant_On_Cold_Resume() + { + 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.ResumeSessionAsync("ask-user-variant", new ResumeSessionConfig + { + AskUserVariant = AskUserVariant.Legacy, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.resume"); + Assert.Equal("legacy", request.Params.GetProperty("askUserVariant").GetString()); + + server.ClearRequests(); + await using var defaultSession = await client.ResumeSessionAsync("ask-user-variant-default", new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + var defaultRequest = Assert.Single(server.Requests, request => request.Method == "session.resume"); + Assert.False(defaultRequest.Params.TryGetProperty("askUserVariant", out _)); + } + [Fact] public async Task SessionRequests_Serialize_AdditionalDirectories() { diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 4bacdfe33d..2f213525f6 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -23,6 +23,13 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() BuiltinPluginDirectories = ["/plugins/core", "/plugins/github"], EnableRemoteSessions = true, SessionIdleTimeoutSeconds = 600, + ClientInfo = new CopilotClientInfo + { + ApplicationName = "example-app", + ApplicationVersion = "1.0.0", + IntegrationName = "example-integration", + IntegrationVersion = "2.0.0", + }, }; var clone = original.Clone(); @@ -38,6 +45,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() Assert.NotSame(original.BuiltinPluginDirectories, clone.BuiltinPluginDirectories); Assert.Equal(original.EnableRemoteSessions, clone.EnableRemoteSessions); Assert.Equal(original.SessionIdleTimeoutSeconds, clone.SessionIdleTimeoutSeconds); + Assert.Same(original.ClientInfo, clone.ClientInfo); } [Fact] @@ -73,6 +81,7 @@ public void SessionConfig_Clone_CopiesAllProperties() ReasoningEffort = "high", ReasoningSummary = ReasoningSummary.Detailed, ContextTier = ContextTier.LongContext, + AskUserVariant = AskUserVariant.Elicitation, ConfigDirectory = "/config", AvailableTools = ["tool1", "tool2"], ExcludedTools = ["tool3"], @@ -121,6 +130,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.ReasoningEffort, clone.ReasoningEffort); Assert.Equal(original.ReasoningSummary, clone.ReasoningSummary); Assert.Equal(original.ContextTier, clone.ContextTier); + Assert.Equal(original.AskUserVariant, clone.AskUserVariant); Assert.Equal(original.ConfigDirectory, clone.ConfigDirectory); Assert.Equal(original.AvailableTools, clone.AvailableTools); Assert.Equal(original.ExcludedTools, clone.ExcludedTools); diff --git a/dotnet/test/Unit/E2ETestBackendTests.cs b/dotnet/test/Unit/E2ETestBackendTests.cs index f7c39a5083..43b23c1a48 100644 --- a/dotnet/test/Unit/E2ETestBackendTests.cs +++ b/dotnet/test/Unit/E2ETestBackendTests.cs @@ -25,7 +25,7 @@ public void RejectsUnknownBackend() () => E2ETestBackendConfiguration.Parse("unknown")); [Theory] - [InlineData("anthropic-messages", "anthropic", null, "claude-sonnet-4.5")] + [InlineData("anthropic-messages", "anthropic", null, "claude-sonnet-5")] [InlineData("openai-responses", "openai", "responses", "gpt-4.1")] [InlineData("openai-completions", "openai", "completions", "gpt-4.1")] public void AppliesProvider( diff --git a/dotnet/test/Unit/GitHubTelemetryTests.cs b/dotnet/test/Unit/GitHubTelemetryTests.cs index a4a241e38d..6d76af5742 100644 --- a/dotnet/test/Unit/GitHubTelemetryTests.cs +++ b/dotnet/test/Unit/GitHubTelemetryTests.cs @@ -140,6 +140,89 @@ public async Task Connect_Does_Not_Opt_In_Without_Handler() "connect request should omit enableGitHubTelemetryForwarding (or send null) when no handler is registered"); } + [Fact] + public async Task Connect_Forwards_Declared_ClientInfo() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo + { + ApplicationName = "acme-developer-portal", + ApplicationVersion = "2.4.0", + IntegrationName = "copilot-assistant", + IntegrationVersion = "1.5.0", + }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.True(connectParams.TryGetProperty("clientInfo", out var clientInfo)); + Assert.Equal("acme-developer-portal", clientInfo.GetProperty("editorName").GetString()); + Assert.Equal("2.4.0", clientInfo.GetProperty("editorVersion").GetString()); + Assert.Equal("copilot-assistant", clientInfo.GetProperty("extensionName").GetString()); + Assert.Equal("1.5.0", clientInfo.GetProperty("extensionVersion").GetString()); + } + + [Fact] + public async Task Connect_Omits_ClientInfo_When_Unset() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.False( + connectParams.TryGetProperty("clientInfo", out _), + "connect request should omit clientInfo when none was declared"); + } + + [Fact] + public async Task Connect_Omits_Empty_ClientInfo_Fields() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo { ApplicationName = "example-app", ApplicationVersion = "" }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.True(connectParams.TryGetProperty("clientInfo", out var clientInfo)); + Assert.Equal("example-app", clientInfo.GetProperty("editorName").GetString()); + Assert.False(clientInfo.TryGetProperty("editorVersion", out _)); + Assert.False(clientInfo.TryGetProperty("extensionName", out _)); + Assert.False(clientInfo.TryGetProperty("extensionVersion", out _)); + } + + [Fact] + public async Task Connect_Omits_All_Empty_ClientInfo() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo + { + ApplicationName = "", + ApplicationVersion = "", + IntegrationName = "", + IntegrationVersion = "", + }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.False( + connectParams.TryGetProperty("clientInfo", out _), + "connect request should omit an all-empty clientInfo"); + } + [Fact] public async Task CreateSession_Does_Not_Opt_In_Without_Handler() { diff --git a/dotnet/test/Unit/MSBuildTargetsTests.cs b/dotnet/test/Unit/MSBuildTargetsTests.cs index a7d9cc0256..cab91e568f 100644 --- a/dotnet/test/Unit/MSBuildTargetsTests.cs +++ b/dotnet/test/Unit/MSBuildTargetsTests.cs @@ -48,6 +48,7 @@ public async Task PreinstalledCliBinaryPath_IsHonored_DownloadSkipped_AndCopiedT var outputPath = sandbox.ExpectedOutputBinary(); Assert.True(File.Exists(outputPath), $"Expected CLI to be copied to '{outputPath}'.\n{result.FailureMessage()}"); Assert.Equal(File.ReadAllText(preinstalled), File.ReadAllText(outputPath)); + Assert.True(File.Exists(Path.Combine(Path.GetDirectoryName(outputPath)!, ".copilot-explicit-cli"))); } [Fact] @@ -105,6 +106,35 @@ public async Task PreinstalledCliBinaryPath_WithSkipCliDownload_StillCopiesToOut Assert.True(File.Exists(sandbox.ExpectedOutputBinary()), result.FailureMessage()); } + [Fact] + public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput() + { + using var sandbox = MSBuildSandbox.Create(); + var preinstalled = sandbox.WritePreinstalledBinary("fake-cli-contents"); + sandbox.WriteRuntimeCacheAsset("prebuilds", GetNpmPlatform(), "runtime.node", "runtime"); + sandbox.WriteRuntimeCacheAsset("prebuilds", GetNpmPlatform(), + OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime", "wrapper"); + sandbox.WriteRuntimeCacheAsset("ripgrep", "bin", GetNpmPlatform(), "rg", "ripgrep"); + sandbox.WriteRuntimeCacheAsset("definitions", "future.json", "{}"); + sandbox.WriteRuntimeCacheAsset("app.js", "excluded"); + sandbox.WriteRuntimeCacheAsset("LICENSE.md", "excluded"); + sandbox.WriteRuntimeCacheAsset("README.md", "excluded"); + sandbox.WriteStaleOutputRuntimeAsset("obsolete", "tool", "stale"); + + var result = await sandbox.BuildAsync(new Dictionary + { + ["CopilotCliBinaryPath"] = preinstalled, + }); + + Assert.True(result.Succeeded, result.FailureMessage()); + Assert.Equal("ripgrep", File.ReadAllText(sandbox.ExpectedRuntimeAsset("ripgrep", "bin", GetNpmPlatform(), "rg"))); + Assert.Equal("{}", File.ReadAllText(sandbox.ExpectedRuntimeAsset("definitions", "future.json"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("app.js"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("LICENSE.md"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("README.md"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("obsolete", "tool"))); + } + [Fact] public async Task PreinstalledCliBinaryPath_NonExistentFile_FailsWithActionableError() { @@ -150,6 +180,17 @@ private static string FindTargetsFile([CallerFilePath] string? thisFile = null) "Could not locate GitHub.Copilot.SDK.targets relative to test assembly or source file."); } + private static string GetNpmPlatform() + { + var arch = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture + == System.Runtime.InteropServices.Architecture.Arm64 + ? "arm64" + : "x64"; + if (OperatingSystem.IsWindows()) return $"win32-{arch}"; + if (OperatingSystem.IsMacOS()) return $"darwin-{arch}"; + return $"linux-{arch}"; + } + /// /// A throwaway directory containing a minimal csproj that imports the SDK targets /// file. Disposing removes the directory tree. @@ -203,6 +244,42 @@ public string ExpectedOutputBinary() return Path.Combine(ProjectDir, "bin", "Debug", "net8.0", "runtimes", rid, "native", BinaryName); } + public void WriteRuntimeCacheAsset(params string[] pathAndContents) + { + var pathParts = pathAndContents.Take(pathAndContents.Length - 1).ToArray(); + var path = Path.Combine(ProjectDir, "obj", "Debug", "net8.0", "copilot-cli", "0.0.0-test", + GetNpmPlatform()); + foreach (var part in pathParts) + { + path = Path.Combine(path, part); + } + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, pathAndContents[^1]); + } + + public string ExpectedRuntimeAsset(params string[] pathParts) + { + var path = Path.Combine(ProjectDir, "bin", "Debug", "net8.0", "runtimes", GetPortableRid(), "native"); + foreach (var part in pathParts) + { + path = Path.Combine(path, part); + } + return path; + } + + public void WriteStaleOutputRuntimeAsset(params string[] pathAndContents) + { + var relativeParts = pathAndContents.Take(pathAndContents.Length - 1).ToArray(); + var path = ExpectedRuntimeAsset(relativeParts); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, pathAndContents[^1]); + var manifest = ExpectedRuntimeAsset(".copilot-runtime-assets"); + Directory.CreateDirectory(Path.GetDirectoryName(manifest)!); + File.WriteAllText( + manifest, + string.Join(Path.DirectorySeparatorChar.ToString(), relativeParts) + Environment.NewLine); + } + public async Task BuildAsync(IDictionary properties) { var args = new StringBuilder("build --nologo -clp:NoSummary"); diff --git a/dotnet/test/Unit/RuntimeWrapperTests.cs b/dotnet/test/Unit/RuntimeWrapperTests.cs new file mode 100644 index 0000000000..9a9dacb7d3 --- /dev/null +++ b/dotnet/test/Unit/RuntimeWrapperTests.cs @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.Rpc; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class RuntimeWrapperIsolationCollection +{ + public const string Name = "Runtime wrapper isolation"; +} + +[Collection(RuntimeWrapperIsolationCollection.Name)] +public sealed class RuntimeWrapperTests +{ +#if !NETFRAMEWORK + [Fact] + public async Task Managed_Launch_Fails_When_Bundled_Runtime_Pair_Is_Missing() + { + var originalBaseDirectory = AppContext.GetData("APP_CONTEXT_BASE_DIRECTORY"); + var emptyBaseDirectory = Path.Combine( + Path.GetTempPath(), + $"missing-copilot-runtime-{Guid.NewGuid():N}"); + Directory.CreateDirectory(emptyBaseDirectory); + + try + { + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", emptyBaseDirectory); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + Environment = new Dictionary(), + }); + + var exception = await Assert.ThrowsAsync(() => client.StartAsync()); + + Assert.Contains("runtime wrapper not found", exception.Message, StringComparison.OrdinalIgnoreCase); + } + finally + { + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", originalBaseDirectory); + Directory.Delete(emptyBaseDirectory); + } + } +#endif + + [Fact] + public async Task Explicit_Path_Does_Not_Require_Adjacent_Runtime_Node() + { + var explicitPath = Path.Combine( + Path.GetTempPath(), + $"missing-explicit-copilot-{Guid.NewGuid():N}"); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(path: explicitPath), + Environment = new Dictionary(), + }); + + var exception = await Assert.ThrowsAnyAsync(() => client.StartAsync()); + + Assert.DoesNotContain("runtime.node", exception.ToString(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task Copilot_Cli_Path_Does_Not_Require_Adjacent_Runtime_Node() + { + var explicitPath = Path.Combine( + Path.GetTempPath(), + $"missing-environment-copilot-{Guid.NewGuid():N}"); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + Environment = new Dictionary { ["COPILOT_CLI_PATH"] = explicitPath }, + }); + + var exception = await Assert.ThrowsAnyAsync(() => client.StartAsync()); + + Assert.DoesNotContain("runtime.node", exception.ToString(), StringComparison.OrdinalIgnoreCase); + } + +#if !NETFRAMEWORK + [Fact] + public async Task Marked_Bundled_Explicit_Cli_Does_Not_Require_Runtime_Pair() + { + var originalBaseDirectory = AppContext.GetData("APP_CONTEXT_BASE_DIRECTORY"); + var baseDirectory = Path.Combine( + Path.GetTempPath(), + $"explicit-bundled-copilot-{Guid.NewGuid():N}"); + var rid = GetPortableRid(); + var nativeDirectory = Path.Combine(baseDirectory, "runtimes", rid, "native"); + Directory.CreateDirectory(nativeDirectory); + var cliPath = Path.Combine(nativeDirectory, OperatingSystem.IsWindows() ? "copilot.exe" : "copilot"); + await File.WriteAllTextAsync(cliPath, "not an executable"); + await File.WriteAllTextAsync(Path.Combine(nativeDirectory, ".copilot-explicit-cli"), "explicit"); + + try + { + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", baseDirectory); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + Environment = new Dictionary(), + }); + + var exception = await Assert.ThrowsAnyAsync(() => client.StartAsync()); + + Assert.DoesNotContain("runtime wrapper", exception.ToString(), StringComparison.OrdinalIgnoreCase); + Assert.Contains(cliPath, exception.ToString(), StringComparison.OrdinalIgnoreCase); + } + finally + { + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", originalBaseDirectory); + Directory.Delete(baseDirectory, recursive: true); + } + } +#endif + + private static string GetPortableRid() + { + var os = OperatingSystem.IsWindows() ? "win" + : OperatingSystem.IsMacOS() ? "osx" + : "linux"; + var architecture = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture switch + { + System.Runtime.InteropServices.Architecture.X64 => "x64", + System.Runtime.InteropServices.Architecture.Arm64 => "arm64", + _ => throw new PlatformNotSupportedException(), + }; + return $"{os}-{architecture}"; + } +} diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 6edf168093..2414093797 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -630,6 +630,46 @@ public void ResumeSessionConfigClone_PreservesExpAssignments() Assert.Equal("exp-resume", clone.ExpAssignments!.Configs[0].Id); } + [Fact] + public void SessionRequests_CanSerializeFeatureFlags_WithSdkOptions() + { + var options = GetSerializerOptions(); + var flags = new Dictionary + { + ["ENABLED_TEST_FLAG"] = true, + ["DISABLED_TEST_FLAG"] = false, + }; + + foreach (var requestName in new[] { "CreateSessionRequest", "ResumeSessionRequest" }) + { + var requestType = GetNestedType(typeof(CopilotClient), requestName); + var request = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("FeatureFlags", flags)); + using var document = JsonDocument.Parse( + JsonSerializer.Serialize(request, requestType, options)); + var serializedFlags = document.RootElement.GetProperty("featureFlags"); + Assert.True(serializedFlags.GetProperty("ENABLED_TEST_FLAG").GetBoolean()); + Assert.False(serializedFlags.GetProperty("DISABLED_TEST_FLAG").GetBoolean()); + } + } + + [Fact] + public void SessionConfigClone_CopiesFeatureFlags() + { + var config = new SessionConfig + { + FeatureFlags = new Dictionary { ["TEST_FLAG"] = true }, + }; + + var clone = config.Clone(); + clone.FeatureFlags!["TEST_FLAG"] = false; + + Assert.True(config.FeatureFlags["TEST_FLAG"]); + Assert.False(clone.FeatureFlags["TEST_FLAG"]); + } + [Fact] public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions() { diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 326ac3f3c7..8aea505737 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -9,6 +9,45 @@ namespace GitHub.Copilot.Test.Unit; public class SessionEventSerializationTests { + public static TheoryData AutoTiers => new() + { + { AutoTier.Efficiency, "efficiency" }, + { AutoTier.Balance, "balance" }, + { AutoTier.Intelligence, "intelligence" }, + { null, null }, + }; + + [Theory] + [MemberData(nameof(AutoTiers))] + public void SessionEvent_Deserializes_AutoTier(AutoTier? expectedTier, string? wireTier) + { + foreach (var eventType in new[] { "session.start", "session.resume" }) + { + var autoTierProperty = wireTier is null ? "" : $""", "autoTier": "{wireTier}" """; + var json = $$""" + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "{{eventType}}", + "data": { + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1 + {{autoTierProperty}} + } + } + """; + + var sessionEvent = SessionEvent.FromJson(json); + var actualTier = eventType == "session.start" + ? Assert.IsType(sessionEvent).Data.AutoTier + : Assert.IsType(sessionEvent).Data.AutoTier; + Assert.Equal(expectedTier, actualTier); + } + } + public static TheoryData JsonElementBackedEvents => new() { { diff --git a/go/README.md b/go/README.md index ddd74b91aa..6c85f93ae1 100644 --- a/go/README.md +++ b/go/README.md @@ -104,7 +104,7 @@ Follow these steps to embed the CLI: 1. Run `go get -tool github.com/github/copilot-sdk/go/cmd/bundler`. This is a one-time setup step per project. 2. Run `go tool bundler` in your build environment just before building your application. -That's it! When your application calls `copilot.NewClient` without a `Connection` field (or with an empty `StdioConnection{}`) and no `COPILOT_CLI_PATH` environment variable, the SDK will automatically install the embedded CLI to a cache directory and use it for all operations. +That's it! When your application calls `copilot.NewClient` without a `Connection` field (or with an empty `StdioConnection{}`), the SDK automatically installs the embedded `copilot-runtime` executable and adjacent `runtime.node` to a cache directory for managed child-process connections. The bundler prepares the native runtime library required by the [in-process transport](#in-process-transport-experimental). It is included in the application only when building with the `copilot_inprocess` build tag. @@ -138,6 +138,9 @@ Resolution and requirements: always takes precedence. - Set `COPILOT_CLI_PATH` only when using an externally provisioned compatible runtime package; otherwise the bundled runtime is used. No `PATH` lookup is performed. - Embedded runtime versions are isolated in separate cache directories. Start fails loudly if the native runtime is unavailable. +- Managed child-process start fails if the embedded `copilot-runtime` and + `runtime.node` pair is unavailable; explicit paths and `COPILOT_CLI_PATH` + remain direct overrides. - Linux in-process bundles include both glibc and musl runtime packages and select the matching package automatically at startup. - Only one native runtime version may be loaded per process. @@ -195,7 +198,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `URIConnection{URL, ConnectionToken}` — connect to an already-running runtime (no process spawned) - `InProcessConnection{}` — **Experimental.** Host the runtime in-process via the native FFI library instead of spawning a child process. See [In-process transport](#in-process-transport-experimental) below. - When `Path` is empty for stdio/tcp, the SDK uses the bundled CLI (or `COPILOT_CLI_PATH` env var). + When `Path` is empty for stdio/tcp, the SDK uses `COPILOT_CLI_PATH` when set, then the bundled `copilot-runtime` and adjacent `runtime.node`. `StdioConnection` and `TCPConnection` accept an optional connection-level `Env`. Set environment variables via **either** the client-level `Env` option or the connection's `Env`, not both (setting both panics); prefer the connection-level `Env`. - `WorkingDirectory` (string): Working directory for the runtime process (default: current process working directory) @@ -224,7 +227,8 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `EnableSessionStore` (\*bool): Enables the cross-session store for search and retrieval across sessions. When unset in `ModeCopilotCli`, the runtime default applies (enabled). In `ModeEmpty`, defaults to disabled. - `GitHubTokenProvider` (GitHubTokenProvider): Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenResult` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenCancelled`. Cannot be combined with `GitHubToken`. - `OnPermissionRequest` (PermissionHandlerFunc): Optional handler called before each tool execution to approve or deny it. When nil, permission requests are emitted as events and left pending for manual resolution. `copilot.PermissionHandler.ApproveAll` approves requests when managed settings are disabled and returns an error when `EnableManagedSettings` is true. Custom handlers can inspect `RequiresManagedApproval()` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. -- `OnUserInputRequest` (UserInputHandler): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. +- `OnUserInputRequest` (UserInputHandler): 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` (AskUserVariant): Selects the model-facing shape of the `ask_user` tool. The zero value preserves legacy behavior; use `AskUserVariantElicitation` with `OnElicitationRequest`. - `Hooks` (\*SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. - `Commands` ([]CommandDefinition): Slash-commands registered for this session. See [Commands](#commands) section. - `OnElicitationRequest` (ElicitationHandler): Handler for elicitation requests from the server. See [Elicitation Requests](#elicitation-requests-serverclient) section. @@ -238,6 +242,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `Streaming` (*bool): Enable streaming delta events (nil = runtime default) - `Commands` ([]CommandDefinition): Slash-commands. See [Commands](#commands) section. - `OnElicitationRequest` (ElicitationHandler): Elicitation handler. See [Elicitation Requests](#elicitation-requests-serverclient) section. +- `AskUserVariant` (AskUserVariant): Selects the model-facing shape of the `ask_user` tool on cold resume. Re-supply `AskUserVariantElicitation` with `OnElicitationRequest`; the zero value preserves legacy behavior. - `GitHubTokenProvider` (GitHubTokenProvider): Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. ```go @@ -770,7 +775,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `SkipPe ## User Input Requests -Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler: +Enable the legacy question-and-answer `ask_user` tool by providing an `OnUserInputRequest` handler: ```go session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ diff --git a/go/client.go b/go/client.go index 4e44696a55..735e8ae414 100644 --- a/go/client.go +++ b/go/client.go @@ -341,6 +341,18 @@ func NewClient(options *ClientOptions) *Client { return client } +func resolveRuntimeExecutable(explicitPath, bundledRuntimePath string) (string, error) { + if explicitPath != "" { + return explicitPath, nil + } + if bundledRuntimePath == "" { + return "", errors.New( + "managed Copilot runtime unavailable: the embedded bundle does not contain copilot-runtime and adjacent runtime.node; regenerate the bundle, provide an explicit path, or set COPILOT_CLI_PATH", + ) + } + return bundledRuntimePath, nil +} + const defaultConnectionEnvVar = "COPILOT_SDK_DEFAULT_CONNECTION" // resolveDefaultConnection selects the transport when no explicit connection @@ -788,6 +800,13 @@ func hasManagedSettings(enableManagedSettings *bool, managedSettings *ManagedSet return (enableManagedSettings != nil && *enableManagedSettings) || managedSettings != nil } +func validateAskUserVariant(variant AskUserVariant) error { + if variant != "" && variant != AskUserVariantLegacy && variant != AskUserVariantElicitation { + return fmt.Errorf("invalid AskUserVariant %q: expected %q, %q, or unset", variant, AskUserVariantLegacy, AskUserVariantElicitation) + } + return nil +} + func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) { if config == nil { config = &SessionConfig{} @@ -795,6 +814,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.GitHubToken != "" && config.GitHubTokenProvider != nil { return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") } + if err := validateAskUserVariant(config.AskUserVariant); err != nil { + return nil, err + } if err := c.ensureConnected(ctx); err != nil { return nil, err @@ -842,6 +864,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.Capi = config.Capi req.Providers = config.Providers req.Models = config.Models + req.AskUserVariant = config.AskUserVariant req.EnableSessionTelemetry = config.EnableSessionTelemetry req.EnableCitations = config.EnableCitations req.EnableFileChangeTracking = config.EnableFileChangeTracking @@ -883,6 +906,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ExtensionSDKPath = config.ExtensionSDKPath req.ExtensionInfo = config.ExtensionInfo req.ExpAssignments = config.ExpAssignments + if config.FeatureFlags != nil { + req.FeatureFlags = &config.FeatureFlags + } req.EnableManagedSettings = config.EnableManagedSettings req.ManagedSettings = config.ManagedSettings @@ -1170,6 +1196,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.GitHubToken != "" && config.GitHubTokenProvider != nil { return nil, fmt.Errorf("GitHubToken and GitHubTokenProvider cannot be used together") } + if err := validateAskUserVariant(config.AskUserVariant); err != nil { + return nil, err + } if err := c.ensureConnected(ctx); err != nil { return nil, err @@ -1200,6 +1229,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.Capi = config.Capi req.Providers = config.Providers req.Models = config.Models + req.AskUserVariant = config.AskUserVariant req.EnableSessionTelemetry = config.EnableSessionTelemetry req.IsExperimentalMode = config.EnableExperimentalMode req.SkipCustomInstructions = config.SkipCustomInstructions @@ -1289,6 +1319,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.ExtensionSDKPath = config.ExtensionSDKPath req.ExtensionInfo = config.ExtensionInfo req.ExpAssignments = config.ExpAssignments + if config.FeatureFlags != nil { + req.FeatureFlags = &config.FeatureFlags + } req.EnableManagedSettings = config.EnableManagedSettings req.ManagedSettings = config.ManagedSettings if config.OnPermissionRequest != nil { @@ -1918,6 +1951,10 @@ func (c *Client) verifyProtocolVersion(ctx context.Context) error { if c.options.OnGitHubTelemetry != nil { connectReq.EnableGitHubTelemetryForwarding = Bool(true) } + // Declare the integrating host's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead of + // its own build. Nil when the app didn't supply it. + connectReq.ClientInfo = c.options.ClientInfo.toWire() rawConnectResult, err := c.client.Request(ctx, "connect", connectReq) if err != nil { var rpcErr *jsonrpc2.Error @@ -1954,8 +1991,9 @@ func (c *Client) verifyProtocolVersion(ctx context.Context) error { } type connectHandshakeRequest struct { - Token *string `json:"token,omitempty"` - EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` + Token *string `json:"token,omitempty"` + EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` + ClientInfo *rpc.ConnectClientInfo `json:"clientInfo,omitempty"` } // stderrBufferSize is the maximum number of bytes kept from the CLI process's @@ -1972,14 +2010,13 @@ func (c *Client) startCLIServer(ctx context.Context) error { return c.startInProcess(ctx) } - cliPath := c.cliPath - if cliPath == "" { - // If no CLI path is provided, attempt to use the embedded CLI if available - cliPath = embeddedcli.Path() + bundledRuntimePath := "" + if c.cliPath == "" { + bundledRuntimePath = embeddedcli.RuntimePath() } - if cliPath == "" { - // Default to "copilot" in PATH if no embedded CLI is available and no custom path is set - cliPath = "copilot" + cliPath, err := resolveRuntimeExecutable(c.cliPath, bundledRuntimePath) + if err != nil { + return err } // Start with user-provided CLIArgs, then add SDK-managed args @@ -2180,24 +2217,21 @@ func (c *Client) startInProcess(ctx context.Context) error { return errors.New("in-process transport unavailable: rebuild with -tags copilot_inprocess on a supported platform") } - runtimePath := c.cliPath - if runtimePath == "" { - // The in-process transport does not resolve a bare command name from PATH - // (unlike the child-process transport). - if p := getEnvValue(c.options.Env, "COPILOT_CLI_PATH"); p != "" { - runtimePath = p - } + cliEntrypoint := c.cliPath + if cliEntrypoint == "" { + cliEntrypoint = getEnvValue(c.options.Env, "COPILOT_CLI_PATH") } + runtimePath := cliEntrypoint if runtimePath == "" { - runtimePath = embeddedcli.Path() + runtimePath = embeddedcli.RuntimePath() } if runtimePath == "" { - return errors.New("in-process runtime unavailable: set COPILOT_CLI_PATH to a compatible runtime package or build with the bundled embedded runtime") + return errors.New("in-process runtime unavailable: build with the bundled embedded runtime or set COPILOT_CLI_PATH to a compatible runtime package") } config := c.inProcessHostConfig() - host, err := createInProcessHost(runtimePath, config) + host, err := createInProcessHost(runtimePath, cliEntrypoint, config) if err != nil { return err } diff --git a/go/client_test.go b/go/client_test.go index c6ab0808cb..e74c234637 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -24,6 +24,35 @@ import ( // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.go instead +func TestResolveRuntimeExecutable(t *testing.T) { + t.Run("managed launch requires bundled runtime pair", func(t *testing.T) { + _, err := resolveRuntimeExecutable("", "") + if err == nil || !strings.Contains(err.Error(), "copilot-runtime and adjacent runtime.node") { + t.Fatalf("expected missing managed runtime error, got %v", err) + } + }) + + t.Run("explicit path does not require bundled runtime pair", func(t *testing.T) { + path, err := resolveRuntimeExecutable("/explicit/copilot", "") + if err != nil { + t.Fatal(err) + } + if path != "/explicit/copilot" { + t.Fatalf("resolveRuntimeExecutable() = %q", path) + } + }) + + t.Run("managed launch selects bundled wrapper", func(t *testing.T) { + path, err := resolveRuntimeExecutable("", "/bundle/copilot-runtime") + if err != nil { + t.Fatal(err) + } + if path != "/bundle/copilot-runtime" { + t.Fatalf("resolveRuntimeExecutable() = %q", path) + } + }) +} + func TestClient_URLParsing(t *testing.T) { t.Run("should parse port-only URL format", func(t *testing.T) { client := NewClient(&ClientOptions{ @@ -247,6 +276,106 @@ func TestClient_BuiltinPluginDirectories(t *testing.T) { }) } +func TestClient_ClientInfo(t *testing.T) { + findConnect := func(requests []startupRPCRequest) map[string]any { + t.Helper() + for _, request := range requests { + if request.Method != "connect" { + continue + } + var params map[string]any + if err := json.Unmarshal(request.Params, ¶ms); err != nil { + t.Fatalf("decode connect params: %v", err) + } + return params + } + t.Fatal("connect was not called") + return nil + } + + t.Run("forwards a declared identity on the connect handshake", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{ + ApplicationName: "acme-developer-portal", + ApplicationVersion: "2.4.0", + IntegrationName: "copilot-assistant", + IntegrationVersion: "1.5.0", + }, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + params := findConnect(requests()) + want := map[string]any{ + "editorName": "acme-developer-portal", + "editorVersion": "2.4.0", + "extensionName": "copilot-assistant", + "extensionVersion": "1.5.0", + } + if !reflect.DeepEqual(params["clientInfo"], want) { + t.Fatalf("clientInfo = %v, want %v", params["clientInfo"], want) + } + }) + + t.Run("omits clientInfo when unset", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{Connection: URIConnection{URL: url}}) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + if _, ok := findConnect(requests())["clientInfo"]; ok { + t.Fatal("clientInfo should be omitted when unset") + } + }) + + t.Run("omits empty fields", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{ApplicationName: "example-app"}, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + want := map[string]any{"editorName": "example-app"} + if got := findConnect(requests())["clientInfo"]; !reflect.DeepEqual(got, want) { + t.Fatalf("clientInfo = %v, want %v", got, want) + } + }) + + t.Run("omits clientInfo when all fields empty", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{}, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + if _, ok := findConnect(requests())["clientInfo"]; ok { + t.Fatal("clientInfo should be omitted when all fields are empty") + } + }) +} + type startupRPCRequest struct { Method string Params json.RawMessage @@ -407,6 +536,66 @@ func newRuntimeShutdownRpcPair(t *testing.T) (*jsonrpc2.Client, *jsonrpc2.Client } func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { + tests := []struct { + name string + capi *CapiSessionOptions + want map[string]any + }{ + {"omitted", nil, nil}, + {"empty", &CapiSessionOptions{}, map[string]any{}}, + {"websocket only", &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, map[string]any{"enableWebSocketResponses": false}}, + {"efficiency", &CapiSessionOptions{AutoTier: AutoTierEfficiency}, map[string]any{"autoTier": "efficiency"}}, + {"balance", &CapiSessionOptions{AutoTier: AutoTierBalance}, map[string]any{"autoTier": "balance"}}, + {"intelligence", &CapiSessionOptions{AutoTier: AutoTierIntelligence}, map[string]any{"autoTier": "intelligence"}}, + {"efficiency with websocket", &CapiSessionOptions{AutoTier: AutoTierEfficiency, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "efficiency", "enableWebSocketResponses": false}}, + {"balance with websocket", &CapiSessionOptions{AutoTier: AutoTierBalance, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "balance", "enableWebSocketResponses": false}}, + {"intelligence with websocket", &CapiSessionOptions{AutoTier: AutoTierIntelligence, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "intelligence", "enableWebSocketResponses": false}}, + } + for _, tt := range tests { + t.Run(tt.name, 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, 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 + `","workspacePath":"/workspace"}`), nil + }) + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Model: "auto", + Capi: tt.capi, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + assertCapiOptions(t, <-createParams, tt.want) + + resumeParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil + }) + + _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{ + Model: "auto", + Capi: tt.capi, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + assertCapiOptions(t, <-resumeParams, tt.want) + }) + } +} + +func TestClient_ForwardsAskUserVariantToSessionRequests(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) t.Cleanup(server.Stop) client := &Client{ @@ -415,34 +604,74 @@ func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { sessions: make(map[string]*Session), } - createParams := make(chan json.RawMessage, 1) + createParams := 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 }) - - _, err := client.CreateSession(t.Context(), &SessionConfig{ - Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, + resumeParams := make(chan json.RawMessage, 2) + 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 != nil { + + if _, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: "ask-user-create", + AskUserVariant: AskUserVariantElicitation, + }); err != nil { t.Fatalf("CreateSession failed: %v", err) } - assertCapiEnableWebSocketResponses(t, <-createParams) + if _, err := client.ResumeSession(t.Context(), "ask-user-cold-resume", &ResumeSessionConfig{ + AskUserVariant: AskUserVariantLegacy, + }); err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + if _, err := client.CreateSession(t.Context(), &SessionConfig{SessionID: "ask-user-default-create"}); err != nil { + t.Fatalf("CreateSession with default failed: %v", err) + } + if _, err := client.ResumeSession(t.Context(), "ask-user-default-cold-resume", nil); err != nil { + t.Fatalf("ResumeSession with default failed: %v", err) + } - resumeParams := make(chan json.RawMessage, 1) - server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - resumeParams <- append(json.RawMessage(nil), params...) - return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil - }) + assertAskUserVariant(t, <-createParams, "elicitation") + assertAskUserVariant(t, <-resumeParams, "legacy") + assertAskUserVariant(t, <-createParams, "") + assertAskUserVariant(t, <-resumeParams, "") +} - _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{ - Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, - }) - if err != nil { - t.Fatalf("ResumeSessionWithOptions failed: %v", err) +func TestClient_RejectsInvalidAskUserVariant(t *testing.T) { + client := &Client{} + + if _, err := client.CreateSession(t.Context(), &SessionConfig{ + AskUserVariant: AskUserVariant("unknown"), + }); err == nil || !strings.Contains(err.Error(), "AskUserVariant") { + t.Fatalf("CreateSession error = %v, want invalid AskUserVariant error", err) + } + if _, err := client.ResumeSession(t.Context(), "cold-resume", &ResumeSessionConfig{ + AskUserVariant: AskUserVariant("unknown"), + }); err == nil || !strings.Contains(err.Error(), "AskUserVariant") { + t.Fatalf("ResumeSession error = %v, want invalid AskUserVariant error", err) + } +} + +func assertAskUserVariant(t *testing.T, params json.RawMessage, want string) { + 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["askUserVariant"] + if want == "" { + if present { + t.Fatalf("askUserVariant = %v, want omitted", got) + } + return + } + if got != want { + t.Fatalf("askUserVariant = %v, want %q", got, want) } - assertCapiEnableWebSocketResponses(t, <-resumeParams) } func TestClient_ForwardsAdditionalDirectoriesToSessionRequests(t *testing.T) { @@ -620,7 +849,7 @@ func TestClient_ForwardsNewSessionOptionsToSessionRequests(t *testing.T) { assertNewSessionOptions(t, <-resumeParams, false, false, "task", 15) } -func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) { +func assertCapiOptions(t *testing.T, params json.RawMessage, want map[string]any) { t.Helper() var decoded map[string]any @@ -628,12 +857,18 @@ func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) { t.Fatalf("failed to unmarshal request params: %v", err) } + if want == nil { + if _, present := decoded["capi"]; present { + t.Fatalf("expected capi to be omitted, got %v", decoded["capi"]) + } + return + } capi, ok := decoded["capi"].(map[string]any) if !ok { t.Fatalf("expected capi object in request params, got %T", decoded["capi"]) } - if capi["enableWebSocketResponses"] != false { - t.Fatalf("expected capi.enableWebSocketResponses=false, got %v", capi["enableWebSocketResponses"]) + if !reflect.DeepEqual(capi, want) { + t.Fatalf("expected capi %v, got %v", want, capi) } } @@ -3904,6 +4139,62 @@ func TestResumeSessionRequest_ExpAssignments(t *testing.T) { }) } +func TestSessionRequests_FeatureFlags(t *testing.T) { + featureFlags := map[string]bool{ + "ENABLED_TEST_FLAG": true, + "DISABLED_TEST_FLAG": false, + } + + for _, tc := range []struct { + name string + req any + }{ + {"create", createSessionRequest{FeatureFlags: &featureFlags}}, + {"resume", resumeSessionRequest{SessionID: "s1", FeatureFlags: &featureFlags}}, + } { + t.Run(tc.name, func(t *testing.T) { + data, err := json.Marshal(tc.req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + got := payload["featureFlags"].(map[string]any) + if got["ENABLED_TEST_FLAG"] != true || got["DISABLED_TEST_FLAG"] != false { + t.Errorf("Unexpected featureFlags: %v", got) + } + }) + } + + emptyFlags := map[string]bool{} + for _, tc := range []struct { + name string + req any + }{ + {"create empty", createSessionRequest{FeatureFlags: &emptyFlags}}, + {"resume empty", resumeSessionRequest{SessionID: "s1", FeatureFlags: &emptyFlags}}, + {"create unset", createSessionRequest{}}, + {"resume unset", resumeSessionRequest{SessionID: "s1"}}, + } { + t.Run(tc.name, func(t *testing.T) { + data, err := json.Marshal(tc.req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + _, present := payload["featureFlags"] + if strings.Contains(tc.name, "empty") != present { + t.Errorf("featureFlags presence = %v for %s", present, tc.name) + } + }) + } +} + func TestIsTerminal(t *testing.T) { t.Run("IsTerminal is serialized in tool definition", func(t *testing.T) { tool := Tool{ diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index e63d1fde66..89f99daf1b 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -19,6 +19,9 @@ import ( "encoding/json" "flag" "fmt" + "go/build" + "go/parser" + "go/token" "io" "net/http" "os" @@ -33,10 +36,11 @@ import ( const ( // Keep these URLs centralized so reviewers can verify all outbound calls in one place. - sdkModule = "github.com/github/copilot-sdk/go" - packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" - tarballURLFmt = "https://registry.npmjs.org/@github/copilot-%s/-/copilot-%s-%s.tgz" - licenseTarballFmt = "https://registry.npmjs.org/@github/copilot/-/copilot-%s.tgz" + sdkModule = "github.com/github/copilot-sdk/go" + packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" + tarballURLFmt = "https://registry.npmjs.org/@github/copilot-%s/-/copilot-%s-%s.tgz" + licenseTarballFmt = "https://registry.npmjs.org/@github/copilot/-/copilot-%s.tgz" + defaultPackageName = "main" ) // Platform info: npm package suffix, binary name @@ -89,23 +93,27 @@ func main() { return } + pkgName, err := detectPackageName(*output, goos, goarch) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to detect package name: %v; using package %s\n", err, pkgName) + } + fmt.Printf("Building bundle for %s (CLI version %s)\n", *platform, version) - binaryPath, sha256Hash, runtimeArtifactPath, runtimeHash, err := buildBundle(info, version, outputPath, goos) + bundle, err := buildBundle(info, version, outputPath, goos) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } - var muslBinaryPath, muslRuntimeArtifactPath string - var muslBinaryHash, muslRuntimeHash []byte + var muslBundle bundleArtifacts if goos == "linux" { muslInfo := platformInfo{ npmPlatform: strings.Replace(info.npmPlatform, "linux-", "linuxmusl-", 1), binaryName: info.binaryName, } muslOutputPath := filepath.Join(*output, defaultOutputFileName(version, "linuxmusl", goarch, info.binaryName)) - muslBinaryPath, muslBinaryHash, muslRuntimeArtifactPath, muslRuntimeHash, err = buildBundle( + muslBundle, err = buildBundle( muslInfo, version, muslOutputPath, @@ -116,21 +124,33 @@ func main() { os.Exit(1) } } + if err := downloadCLILicense(version, outputPath); err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to download CLI license: %v\n", err) + os.Exit(1) + } // Generate the Go file with embed directive if err := generateGoFile( goos, goarch, - binaryPath, + bundle.binaryPath, version, - sha256Hash, - runtimeArtifactPath, - runtimeHash, - muslBinaryPath, - muslBinaryHash, - muslRuntimeArtifactPath, - muslRuntimeHash, - "main", + bundle.binaryHash, + bundle.runtimeArtifactPath, + bundle.runtimeHash, + bundle.wrapperArtifactPath, + bundle.wrapperHash, + bundle.assetsArtifactPath, + bundle.assetsHash, + muslBundle.binaryPath, + muslBundle.binaryHash, + muslBundle.runtimeArtifactPath, + muslBundle.runtimeHash, + muslBundle.wrapperArtifactPath, + muslBundle.wrapperHash, + muslBundle.assetsArtifactPath, + muslBundle.assetsHash, + pkgName, ); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) @@ -186,6 +206,60 @@ func validPlatforms() []string { return result } +// detectPackageName reads package clauses from files that match the target +// platform and build constraints. It returns defaultPackageName with an error +// when detection fails. +func detectPackageName(dir, goos, goarch string) (string, error) { + if dir == "" { + dir = "." + } + + entries, err := os.ReadDir(dir) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to read package directory %q: %w", dir, err) + } + + buildContext := build.Default + buildContext.GOOS = goos + buildContext.GOARCH = goarch + + packageName := "" + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || + strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") || + strings.HasSuffix(name, "_test.go") || strings.HasPrefix(name, "zcopilot_") { + continue + } + matches, err := buildContext.MatchFile(dir, name) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to evaluate build constraints in %q: %w", filepath.Join(dir, name), err) + } + if !matches { + continue + } + + path := filepath.Join(dir, name) + file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.PackageClauseOnly) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to parse package clause in %q: %w", path, err) + } + + if packageName == "" { + packageName = file.Name.Name + continue + } + if packageName != file.Name.Name { + return defaultPackageName, fmt.Errorf("multiple packages %q and %q found in %q", packageName, file.Name.Name, dir) + } + } + + if packageName == "" { + return defaultPackageName, fmt.Errorf("no Go package found in %q", dir) + } + return packageName, nil +} + // detectCLIVersion detects the CLI version by: // 1. Running "go list -m" to get the copilot-sdk version from the user's go.mod // 2. Fetching the package-lock.json from the SDK repo at that version @@ -286,96 +360,131 @@ func isHex(s string) bool { return true } -// buildBundle downloads the CLI binary (and, when the CLI package ships it, the -// native in-process runtime library) and writes them to outputPath's directory. -// It returns the CLI bundle path and hash, plus the runtime-library artifact path -// and hash (both empty when the package does not ship the runtime library). -func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (string, []byte, string, []byte, error) { +type bundleArtifacts struct { + binaryPath string + binaryHash []byte + runtimeArtifactPath string + runtimeHash []byte + wrapperArtifactPath string + wrapperHash []byte + assetsArtifactPath string + assetsHash []byte +} + +// buildBundle downloads the CLI and native runtime artifacts from one platform package. +func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundleArtifacts, error) { outputDir := filepath.Dir(outputPath) if outputDir == "" { outputDir = "." } runtimeArtifactPath := filepath.Join(outputDir, runtimeLibArtifactName(cliVersion, info.npmPlatform, goos)) + wrapperArtifactPath := filepath.Join(outputDir, runtimeWrapperArtifactName(cliVersion, info.npmPlatform, info.binaryName)) + assetsArtifactPath := filepath.Join(outputDir, runtimeAssetsArtifactName(cliVersion, info.npmPlatform)) - // Check if output already exists - if _, err := os.Stat(outputPath); err == nil { + if filesExist(outputPath, runtimeArtifactPath, wrapperArtifactPath, assetsArtifactPath) { // Idempotent output avoids re-downloading in CI or local rebuilds. - fmt.Printf("Output %s already exists, skipping download\n", outputPath) - sha256Hash, err := sha256FileFromCompressed(outputPath) + fmt.Printf("Output runtime bundle for %s already exists, skipping download\n", info.npmPlatform) + binaryHash, err := sha256FileFromCompressed(outputPath) if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to hash existing output: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to hash existing output: %w", err) } - if err := downloadCLILicense(cliVersion, outputPath); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to download CLI license: %w", err) + runtimeHash, err := sha256FileFromCompressed(runtimeArtifactPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash existing runtime.node: %w", err) } - // Reuse an existing runtime-library artifact if present. - if _, err := os.Stat(runtimeArtifactPath); err == nil { - runtimeHash, err := sha256FileFromCompressed(runtimeArtifactPath) - if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to hash existing runtime library: %w", err) - } - return outputPath, sha256Hash, runtimeArtifactPath, runtimeHash, nil + wrapperHash, err := sha256FileFromCompressed(wrapperArtifactPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash existing runtime wrapper: %w", err) + } + assetsHash, err := sha256File(assetsArtifactPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash existing runtime assets: %w", err) } - return outputPath, sha256Hash, "", nil, nil + return bundleArtifacts{outputPath, binaryHash, runtimeArtifactPath, runtimeHash, wrapperArtifactPath, wrapperHash, assetsArtifactPath, assetsHash}, nil } + // Create temp directory for download tempDir, err := os.MkdirTemp("", "copilot-bundler-*") if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to create temp dir: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to create temp dir: %w", err) } defer os.RemoveAll(tempDir) - // Download the binary binaryPath, tarballPath, err := downloadCLIBinary(info.npmPlatform, info.binaryName, cliVersion, tempDir) if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to download CLI binary: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to download CLI binary: %w", err) } - // Create output directory if needed if outputDir != "." { if err := os.MkdirAll(outputDir, 0755); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to create output directory: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to create output directory: %w", err) } } - sha256Hash, err := sha256File(binaryPath) + binaryHash, err := sha256File(binaryPath) if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to hash output binary: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to hash output binary: %w", err) } if err := compressZstdFile(binaryPath, outputPath); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to write output binary: %w", err) - } - if err := downloadCLILicense(cliVersion, outputPath); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to download CLI license: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to write output binary: %w", err) } - // Extract the native in-process runtime library from the same tarball, if the - // package ships it (older CLI versions do not). Missing is not an error — the - // generated file simply omits the runtime embed for that platform. rawLibPath := filepath.Join(tempDir, "runtime.node") - found, err := extractOptionalFileFromTarball(tarballPath, tempDir, - "package/prebuilds/"+info.npmPlatform+"/runtime.node", "runtime.node") + if err := extractFileFromTarball( + tarballPath, + tempDir, + "package/prebuilds/"+info.npmPlatform+"/runtime.node", + "runtime.node", + ); err != nil { + return bundleArtifacts{}, fmt.Errorf("runtime package is missing prebuilds/%s/runtime.node: %w", info.npmPlatform, err) + } + runtimeHash, err := sha256File(rawLibPath) if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to extract runtime library: %w", err) + return bundleArtifacts{}, fmt.Errorf("failed to hash runtime.node: %w", err) } - var runtimeHash []byte - returnedRuntimeArtifact := "" - if found { - runtimeHash, err = sha256File(rawLibPath) - if err != nil { - return "", nil, "", nil, fmt.Errorf("failed to hash runtime library: %w", err) - } - if err := compressZstdFile(rawLibPath, runtimeArtifactPath); err != nil { - return "", nil, "", nil, fmt.Errorf("failed to write runtime library: %w", err) - } - returnedRuntimeArtifact = runtimeArtifactPath - fmt.Printf("Successfully created %s\n", runtimeArtifactPath) - } else { - fmt.Printf("Package %s does not ship a runtime library; in-process transport unavailable for this platform bundle\n", info.npmPlatform) + if err := compressZstdFile(rawLibPath, runtimeArtifactPath); err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to write runtime.node: %w", err) + } + + wrapperName := runtimeWrapperName(info.binaryName) + rawWrapperPath := filepath.Join(tempDir, wrapperName) + if err := extractFileFromTarball( + tarballPath, + tempDir, + "package/prebuilds/"+info.npmPlatform+"/"+wrapperName, + wrapperName, + ); err != nil { + return bundleArtifacts{}, fmt.Errorf("runtime package is missing prebuilds/%s/%s: %w", info.npmPlatform, wrapperName, err) + } + wrapperHash, err := sha256File(rawWrapperPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash runtime wrapper: %w", err) + } + if err := compressZstdFile(rawWrapperPath, wrapperArtifactPath); err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to write runtime wrapper: %w", err) + } + if err := createRuntimeAssetsArchive(tarballPath, assetsArtifactPath, info); err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to write runtime assets: %w", err) + } + assetsHash, err := sha256File(assetsArtifactPath) + if err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to hash runtime assets: %w", err) } fmt.Printf("Successfully created %s\n", outputPath) - return outputPath, sha256Hash, returnedRuntimeArtifact, runtimeHash, nil + fmt.Printf("Successfully created %s\n", runtimeArtifactPath) + fmt.Printf("Successfully created %s\n", wrapperArtifactPath) + fmt.Printf("Successfully created %s\n", assetsArtifactPath) + return bundleArtifacts{outputPath, binaryHash, runtimeArtifactPath, runtimeHash, wrapperArtifactPath, wrapperHash, assetsArtifactPath, assetsHash}, nil +} + +func filesExist(paths ...string) bool { + for _, path := range paths { + if _, err := os.Stat(path); err != nil { + return false + } + } + return true } // runtimeLibArtifactName builds the compressed runtime-library artifact filename. @@ -383,6 +492,117 @@ func runtimeLibArtifactName(version, npmPlatform, goos string) string { return fmt.Sprintf("zcopilotruntime_%s_%s.%s.zst", version, npmPlatform, runtimeLibExt(goos)) } +func runtimeWrapperArtifactName(version, npmPlatform, binaryName string) string { + return fmt.Sprintf("zcopilotruntimewrapper_%s_%s_%s.zst", version, npmPlatform, runtimeWrapperName(binaryName)) +} + +func runtimeAssetsArtifactName(version, npmPlatform string) string { + return fmt.Sprintf("zcopilotruntimeassets_%s_%s.tgz", version, npmPlatform) +} + +func runtimeWrapperName(binaryName string) string { + if filepath.Ext(binaryName) == ".exe" { + return "copilot-runtime.exe" + } + return "copilot-runtime" +} + +var hostlessExcludedTopLevel = map[string]bool{ + "app.js": true, "assets": true, "changelog.json": true, "copilot": true, "copilot.exe": true, + "copilot-sdk": true, "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, + "LICENSE.md": true, "npm-loader.js": true, "package.json": true, "preloads": true, "pvrecorder": true, + "queries": true, "README.md": true, "sdk": true, "sea-loader.js": true, "webview": true, +} + +func hostlessRuntimePath(name, npmPlatform, wrapperName string) (string, bool) { + relative, ok := strings.CutPrefix(name, "package/") + if !ok { + return "", false + } + parts := strings.Split(relative, "/") + topLevel := parts[0] + fileName := parts[len(parts)-1] + if hostlessExcludedTopLevel[topLevel] || + (strings.HasPrefix(topLevel, "tree-sitter") && strings.HasSuffix(topLevel, ".wasm")) || + (strings.HasPrefix(topLevel, "voice-") && strings.HasSuffix(topLevel, ".js")) || + fileName == "cli-native.node" || fileName == "runtime.node" || fileName == wrapperName || + strings.HasPrefix(fileName, "copilot-runtime-bin") { + return "", false + } + for _, part := range parts { + if part == "mediaremote-adapter" { + return "", false + } + } + if topLevel == "prebuilds" { + if len(parts) < 3 || parts[1] != npmPlatform { + return "", false + } + return strings.Join(parts[2:], "/"), true + } + return relative, true +} + +func createRuntimeAssetsArchive(tarballPath, outputPath string, info platformInfo) error { + sourceFile, err := os.Open(tarballPath) + if err != nil { + return err + } + defer sourceFile.Close() + gzipReader, err := gzip.NewReader(sourceFile) + if err != nil { + return err + } + defer gzipReader.Close() + outputFile, err := os.Create(outputPath) + if err != nil { + return err + } + defer outputFile.Close() + gzipWriter := gzip.NewWriter(outputFile) + tarWriter := tar.NewWriter(gzipWriter) + count := 0 + sourceTar := tar.NewReader(gzipReader) + for { + header, err := sourceTar.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + if header.Typeflag != tar.TypeReg { + continue + } + destination, include := hostlessRuntimePath( + header.Name, + info.npmPlatform, + runtimeWrapperName(info.binaryName), + ) + if !include { + continue + } + outputHeader := &tar.Header{ + Name: destination, Mode: header.Mode, Size: header.Size, Typeflag: tar.TypeReg, + Uid: 0, Gid: 0, + } + if err := tarWriter.WriteHeader(outputHeader); err != nil { + return err + } + if _, err := io.Copy(tarWriter, sourceTar); err != nil { + return err + } + count++ + } + if count == 0 { + return fmt.Errorf("runtime package contains no retained assets") + } + if err := tarWriter.Close(); err != nil { + return err + } + return gzipWriter.Close() +} + // runtimeLibExt returns the shared-library extension for the target OS. func runtimeLibExt(goos string) string { switch goos { @@ -406,10 +626,18 @@ func generateGoFile( sha256Hash []byte, runtimeArtifactPath string, runtimeHash []byte, + wrapperArtifactPath string, + wrapperHash []byte, + assetsArtifactPath string, + assetsHash []byte, muslBinaryPath string, muslBinaryHash []byte, muslRuntimeArtifactPath string, muslRuntimeHash []byte, + muslWrapperArtifactPath string, + muslWrapperHash []byte, + muslAssetsArtifactPath string, + muslAssetsHash []byte, pkgName string, ) error { binaryName := filepath.Base(binaryPath) @@ -428,12 +656,20 @@ func generateGoFile( licenseName, cliVersion, hashBase64, - "", - nil, - "", - nil, - "", - nil, + runtimeArtifactPath, + runtimeHash, + wrapperArtifactPath, + wrapperHash, + assetsArtifactPath, + assetsHash, + muslBinaryPath, + muslBinaryHash, + muslRuntimeArtifactPath, + muslRuntimeHash, + muslWrapperArtifactPath, + muslWrapperHash, + muslAssetsArtifactPath, + muslAssetsHash, ) if err := os.WriteFile(defaultPath, []byte(defaultContent), 0644); err != nil { return err @@ -449,10 +685,18 @@ func generateGoFile( hashBase64, runtimeArtifactPath, runtimeHash, + wrapperArtifactPath, + wrapperHash, + assetsArtifactPath, + assetsHash, muslBinaryPath, muslBinaryHash, muslRuntimeArtifactPath, muslRuntimeHash, + muslWrapperArtifactPath, + muslWrapperHash, + muslAssetsArtifactPath, + muslAssetsHash, ) if err := os.WriteFile(inProcessPath, []byte(inProcessContent), 0644); err != nil { return err @@ -472,24 +716,48 @@ func generatedGoFileContent( hashBase64, runtimeArtifactPath string, runtimeHash []byte, + wrapperArtifactPath string, + wrapperHash []byte, + assetsArtifactPath string, + assetsHash []byte, muslBinaryPath string, muslBinaryHash []byte, muslRuntimeArtifactPath string, muslRuntimeHash []byte, + muslWrapperArtifactPath string, + muslWrapperHash []byte, + muslAssetsArtifactPath string, + muslAssetsHash []byte, ) string { runtimeEmbed := "" runtimeConfig := "" runtimeReader := "" - if runtimeArtifactPath != "" { + if runtimeArtifactPath != "" && wrapperArtifactPath != "" && assetsArtifactPath != "" { runtimeArtifactName := filepath.Base(runtimeArtifactPath) runtimeHashBase64 := base64.StdEncoding.EncodeToString(runtimeHash) + wrapperArtifactName := filepath.Base(wrapperArtifactPath) + wrapperHashBase64 := base64.StdEncoding.EncodeToString(wrapperHash) + assetsArtifactName := filepath.Base(assetsArtifactPath) + assetsHashBase64 := base64.StdEncoding.EncodeToString(assetsHash) runtimeEmbed = fmt.Sprintf(` //go:embed %s var localEmbeddedCopilotRuntimeLib []byte -`, runtimeArtifactName) + +//go:embed %s +var localEmbeddedCopilotRuntimeExecutable []byte + +//go:embed %s +var localEmbeddedCopilotRuntimeAssets []byte +`, runtimeArtifactName, wrapperArtifactName, assetsArtifactName) runtimeConfig = fmt.Sprintf(` - RuntimeLib: runtimeLibReader(), - RuntimeLibHash: mustDecodeBase64(%q),`, runtimeHashBase64) + RuntimeLib: runtimeLibReader(), + RuntimeLibHash: mustDecodeBase64(%q), + RuntimeNode: runtimeLibReader(), + RuntimeNodeHash: mustDecodeBase64(%q), + RuntimeExecutable: runtimeExecutableReader(), + RuntimeExecutableHash: mustDecodeBase64(%q), + RuntimeAssets: bytes.NewReader(localEmbeddedCopilotRuntimeAssets), + RuntimeAssetsHash: mustDecodeBase64(%q),`, runtimeHashBase64, runtimeHashBase64, wrapperHashBase64, assetsHashBase64) runtimeReader = ` func runtimeLibReader() io.Reader { r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotRuntimeLib)) @@ -498,29 +766,53 @@ func runtimeLibReader() io.Reader { } return r } + +func runtimeExecutableReader() io.Reader { + r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotRuntimeExecutable)) + if err != nil { + panic("failed to create zstd reader: " + err.Error()) + } + return r +} ` } muslEmbed := "" muslConfig := "" muslReaders := "" - if muslBinaryPath != "" && muslRuntimeArtifactPath != "" { + if muslBinaryPath != "" && muslRuntimeArtifactPath != "" && muslWrapperArtifactPath != "" && muslAssetsArtifactPath != "" { muslBinaryName := filepath.Base(muslBinaryPath) muslBinaryHashBase64 := base64.StdEncoding.EncodeToString(muslBinaryHash) muslRuntimeName := filepath.Base(muslRuntimeArtifactPath) muslRuntimeHashBase64 := base64.StdEncoding.EncodeToString(muslRuntimeHash) + muslWrapperName := filepath.Base(muslWrapperArtifactPath) + muslWrapperHashBase64 := base64.StdEncoding.EncodeToString(muslWrapperHash) + muslAssetsName := filepath.Base(muslAssetsArtifactPath) + muslAssetsHashBase64 := base64.StdEncoding.EncodeToString(muslAssetsHash) muslEmbed = fmt.Sprintf(` //go:embed %s var localEmbeddedCopilotCLILinuxMusl []byte //go:embed %s var localEmbeddedCopilotRuntimeLibLinuxMusl []byte -`, muslBinaryName, muslRuntimeName) + +//go:embed %s +var localEmbeddedCopilotRuntimeExecutableLinuxMusl []byte + +//go:embed %s +var localEmbeddedCopilotRuntimeAssetsLinuxMusl []byte +`, muslBinaryName, muslRuntimeName, muslWrapperName, muslAssetsName) muslConfig = fmt.Sprintf(` - LinuxMuslCli: linuxMuslCLIReader(), - LinuxMuslCliHash: mustDecodeBase64(%q), - LinuxMuslRuntimeLib: linuxMuslRuntimeLibReader(), - LinuxMuslRuntimeLibHash: mustDecodeBase64(%q),`, muslBinaryHashBase64, muslRuntimeHashBase64) + LinuxMuslCli: linuxMuslCLIReader(), + LinuxMuslCliHash: mustDecodeBase64(%q), + LinuxMuslRuntimeLib: linuxMuslRuntimeLibReader(), + LinuxMuslRuntimeLibHash: mustDecodeBase64(%q), + LinuxMuslRuntimeNode: linuxMuslRuntimeLibReader(), + LinuxMuslRuntimeNodeHash: mustDecodeBase64(%q), + LinuxMuslRuntimeExecutable: linuxMuslRuntimeExecutableReader(), + LinuxMuslRuntimeExecutableHash: mustDecodeBase64(%q), + LinuxMuslRuntimeAssets: bytes.NewReader(localEmbeddedCopilotRuntimeAssetsLinuxMusl), + LinuxMuslRuntimeAssetsHash: mustDecodeBase64(%q),`, muslBinaryHashBase64, muslRuntimeHashBase64, muslRuntimeHashBase64, muslWrapperHashBase64, muslAssetsHashBase64) muslReaders = ` func linuxMuslCLIReader() io.Reader { r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotCLILinuxMusl)) @@ -537,6 +829,14 @@ func linuxMuslRuntimeLibReader() io.Reader { } return r } + +func linuxMuslRuntimeExecutableReader() io.Reader { + r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotRuntimeExecutableLinuxMusl)) + if err != nil { + panic("failed to create zstd reader: " + err.Error()) + } + return r +} ` } diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index badc791359..f41afe43f5 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -1,26 +1,182 @@ package main import ( + "archive/tar" + "bytes" + "compress/gzip" "go/parser" "go/token" + "io" "os" "path/filepath" "strings" "testing" ) -func TestGenerateGoFileGatesRuntimeEmbed(t *testing.T) { +func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *testing.T) { + dir := t.TempDir() + source := filepath.Join(dir, "package.tgz") + output := filepath.Join(dir, "assets.tgz") + writeTarGz(t, source, map[string]string{ + "package/prebuilds/linux-x64/runtime.node": "runtime", + "package/prebuilds/linux-x64/copilot-runtime": "wrapper", + "package/ripgrep/bin/linux-x64/rg": "ripgrep", + "package/definitions/future.json": "{}", + "package/app.js": "excluded", + "package/LICENSE.md": "excluded", + "package/README.md": "excluded", + }) + + if err := createRuntimeAssetsArchive(source, output, platformInfo{ + npmPlatform: "linux-x64", + binaryName: "copilot", + }); err != nil { + t.Fatal(err) + } + + files := readTarGz(t, output) + if files["ripgrep/bin/linux-x64/rg"] != "ripgrep" || files["definitions/future.json"] != "{}" { + t.Fatalf("retained assets = %#v", files) + } + for _, excluded := range []string{ + "runtime.node", "copilot-runtime", "app.js", "LICENSE.md", "README.md", + } { + if _, ok := files[excluded]; ok { + t.Fatalf("excluded asset %q was retained", excluded) + } + } +} + +func writeTarGz(t *testing.T, path string, files map[string]string) { + t.Helper() + var buffer bytes.Buffer + gzipWriter := gzip.NewWriter(&buffer) + tarWriter := tar.NewWriter(gzipWriter) + for name, content := range files { + header := &tar.Header{Name: name, Mode: 0755, Size: int64(len(content)), Typeflag: tar.TypeReg} + if err := tarWriter.WriteHeader(header); err != nil { + t.Fatal(err) + } + if _, err := tarWriter.Write([]byte(content)); err != nil { + t.Fatal(err) + } + } + if err := tarWriter.Close(); err != nil { + t.Fatal(err) + } + if err := gzipWriter.Close(); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, buffer.Bytes(), 0644); err != nil { + t.Fatal(err) + } +} + +func readTarGz(t *testing.T, path string) map[string]string { + t.Helper() + file, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer file.Close() + gzipReader, err := gzip.NewReader(file) + if err != nil { + t.Fatal(err) + } + files := map[string]string{} + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + return files + } + if err != nil { + t.Fatal(err) + } + content, err := io.ReadAll(tarReader) + if err != nil { + t.Fatal(err) + } + files[header.Name] = string(content) + } +} + +func TestDetectPackageName(t *testing.T) { + dir := t.TempDir() + files := map[string]string{ + "app_linux.go": "package application\n", + "app_test.go": "package application_test\n", + "app_windows.go": "package windowsapplication\n", + "tagged.go": "//go:build windows\n\npackage windowsapplication\n", + "zcopilot_linux_amd64.go": "package main\n", + "_ignored.go": "package ignored\n", + "zcopilot_inprocess_linux.go": "package main\n", + } + for name, content := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + + for _, test := range []struct { + goos string + want string + }{ + {goos: "linux", want: "application"}, + {goos: "windows", want: "windowsapplication"}, + } { + t.Run(test.goos, func(t *testing.T) { + got, err := detectPackageName(dir, test.goos, "amd64") + if err != nil { + t.Fatal(err) + } + if got != test.want { + t.Fatalf("detectPackageName() = %q, want %q", got, test.want) + } + }) + } +} + +func TestDetectPackageNameFallsBackForMultiplePackages(t *testing.T) { + dir := t.TempDir() + for name, content := range map[string]string{ + "one.go": "package one\n", + "two.go": "package two\n", + } { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + + got, err := detectPackageName(dir, "linux", "amd64") + if err == nil { + t.Fatal("detectPackageName() succeeded for a directory containing multiple packages") + } + if got != defaultPackageName { + t.Fatalf("detectPackageName() = %q, want fallback %q", got, defaultPackageName) + } +} + +func TestGenerateGoFileEmbedsRuntimeWrapperPair(t *testing.T) { dir := t.TempDir() binaryPath := filepath.Join(dir, "copilot.zst") runtimePath := filepath.Join(dir, "runtime.node.zst") + wrapperPath := filepath.Join(dir, "copilot-runtime.zst") + assetsPath := filepath.Join(dir, "runtime-assets.tgz") muslBinaryPath := filepath.Join(dir, "copilot-musl.zst") muslRuntimePath := filepath.Join(dir, "runtime-musl.node.zst") + muslWrapperPath := filepath.Join(dir, "copilot-runtime-musl.zst") + muslAssetsPath := filepath.Join(dir, "runtime-assets-musl.tgz") for _, path := range []string{ binaryPath, licensePathForOutput(binaryPath), runtimePath, + wrapperPath, + assetsPath, muslBinaryPath, muslRuntimePath, + muslWrapperPath, + muslAssetsPath, } { if err := os.WriteFile(path, []byte("test"), 0644); err != nil { t.Fatal(err) @@ -36,10 +192,18 @@ func TestGenerateGoFileGatesRuntimeEmbed(t *testing.T) { hash, runtimePath, hash, + wrapperPath, + hash, + assetsPath, + hash, muslBinaryPath, hash, muslRuntimePath, hash, + muslWrapperPath, + hash, + muslAssetsPath, + hash, "main", ); err != nil { t.Fatal(err) @@ -52,8 +216,20 @@ func TestGenerateGoFileGatesRuntimeEmbed(t *testing.T) { if !strings.Contains(string(defaultSource), "//go:build !copilot_inprocess") { t.Fatal("default embed file does not exclude copilot_inprocess builds") } - if strings.Contains(string(defaultSource), "localEmbeddedCopilotRuntimeLib") { - t.Fatal("default embed file includes the native runtime") + if !strings.Contains(string(defaultSource), "localEmbeddedCopilotRuntimeExecutable") { + t.Fatal("default embed file does not include the runtime wrapper") + } + if !strings.Contains(string(defaultSource), "RuntimeNode:") { + t.Fatal("default embed file does not configure runtime.node") + } + if !strings.Contains(string(defaultSource), "RuntimeAssets:") { + t.Fatal("default embed file does not configure retained runtime assets") + } + if !strings.Contains(string(defaultSource), "localEmbeddedCopilotCLILinuxMusl") { + t.Fatal("default embed file does not include the Linux musl CLI") + } + if !strings.Contains(string(defaultSource), "localEmbeddedCopilotRuntimeLibLinuxMusl") { + t.Fatal("default embed file does not include the Linux musl runtime") } if _, err := parser.ParseFile(token.NewFileSet(), "zcopilot_linux_amd64.go", defaultSource, parser.AllErrors); err != nil { t.Fatalf("default generated source is invalid: %v", err) diff --git a/go/inprocess_disabled.go b/go/inprocess_disabled.go index d0626a74d3..c100e3db6a 100644 --- a/go/inprocess_disabled.go +++ b/go/inprocess_disabled.go @@ -8,6 +8,6 @@ const inProcessAvailable = false var errInProcessUnavailable = errors.New("in-process transport unavailable") -func createInProcessHost(string, inProcessHostConfig) (inProcessHost, error) { +func createInProcessHost(string, string, inProcessHostConfig) (inProcessHost, error) { return nil, errInProcessUnavailable } diff --git a/go/inprocess_enabled.go b/go/inprocess_enabled.go index c20013d8ab..2c30d5f863 100644 --- a/go/inprocess_enabled.go +++ b/go/inprocess_enabled.go @@ -6,6 +6,6 @@ import "github.com/github/copilot-sdk/go/internal/ffihost" const inProcessAvailable = true -func createInProcessHost(runtimePath string, config inProcessHostConfig) (inProcessHost, error) { - return ffihost.Create(runtimePath, config.Environment, config.Args) +func createInProcessHost(runtimePath, cliEntrypoint string, config inProcessHostConfig) (inProcessHost, error) { + return ffihost.Create(runtimePath, cliEntrypoint, config.Environment, config.Args) } diff --git a/go/internal/e2e/client_options_e2e_test.go b/go/internal/e2e/client_options_e2e_test.go index 86332eb6f1..9c7b77c984 100644 --- a/go/internal/e2e/client_options_e2e_test.go +++ b/go/internal/e2e/client_options_e2e_test.go @@ -329,7 +329,7 @@ func TestClientOptionsE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ SessionID: sessionID, ClientName: "go-sdk-e2e-client", - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", ReasoningEffort: "low", ReasoningSummary: copilot.ReasoningSummaryNone, ContextTier: copilot.ContextTierLongContext, @@ -388,7 +388,7 @@ func TestClientOptionsE2E(t *testing.T) { expectedValues := map[string]any{ "sessionId": sessionID, "clientName": "go-sdk-e2e-client", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "reasoningEffort": "low", "reasoningSummary": "none", "contextTier": "long_context", diff --git a/go/internal/e2e/copilot_request_helpers_test.go b/go/internal/e2e/copilot_request_helpers_test.go index 81d14f4d94..cd1ac63cf7 100644 --- a/go/internal/e2e/copilot_request_helpers_test.go +++ b/go/internal/e2e/copilot_request_helpers_test.go @@ -47,8 +47,8 @@ func sseFrame(eventType string, data map[string]any) string { func modelCatalogJSON(supportedEndpoints []string) string { model := map[string]any{ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -56,7 +56,7 @@ func modelCatalogJSON(supportedEndpoints []string) string { "model_picker_enabled": true, "capabilities": map[string]any{ "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": map[string]any{ "max_context_window_tokens": 200000, @@ -141,7 +141,7 @@ func buildAnthropicMessageSSEBody(text string) string { "type": "message_start", "message": map[string]any{ "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", "content": []any{}, + "model": "claude-sonnet-5", "content": []any{}, "stop_reason": nil, "stop_sequence": nil, "usage": map[string]any{"input_tokens": 5, "output_tokens": 1}, }, @@ -188,7 +188,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { base := func() map[string]any { return map[string]any{ "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", - "created": 1, "model": "claude-sonnet-4.5", + "created": 1, "model": "claude-sonnet-5", } } c1 := base() @@ -215,7 +215,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": []any{map[string]any{"type": "text", "text": syntheticResponseText}}, "stop_reason": "end_turn", "stop_sequence": nil, @@ -225,7 +225,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { } raw, _ := json.Marshal(map[string]any{ - "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, "model": "claude-sonnet-4.5", + "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, "model": "claude-sonnet-5", "choices": []any{map[string]any{"index": 0, "message": map[string]any{"role": "assistant", "content": syntheticResponseText}, "finish_reason": "stop"}}, "usage": map[string]any{"prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12}, }) diff --git a/go/internal/e2e/copilot_request_session_id_e2e_test.go b/go/internal/e2e/copilot_request_session_id_e2e_test.go index f7673bd457..46a62db1ab 100644 --- a/go/internal/e2e/copilot_request_session_id_e2e_test.go +++ b/go/internal/e2e/copilot_request_session_id_e2e_test.go @@ -139,14 +139,14 @@ func TestCopilotRequestSessionID(t *testing.T) { before := len(transport.inferenceRecords()) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: &copilot.ProviderConfig{ Type: "openai", WireAPI: "responses", BaseURL: "https://byok.invalid/v1", APIKey: "byok-secret", - ModelID: "claude-sonnet-4.5", - WireModel: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", + WireModel: "claude-sonnet-5", }, }) if err != nil { diff --git a/go/internal/e2e/inprocess_ffi_e2e_test.go b/go/internal/e2e/inprocess_ffi_e2e_test.go index 6923384a28..7f7dcc3f20 100644 --- a/go/internal/e2e/inprocess_ffi_e2e_test.go +++ b/go/internal/e2e/inprocess_ffi_e2e_test.go @@ -8,8 +8,8 @@ import ( ) // TestInProcessFfiE2E is a smoke test for the in-process (FFI) transport. It -// starts a client that loads the native runtime cdylib next to the resolved CLI -// entrypoint, lets the native host spawn the worker, performs a purely local +// starts a client that loads the native runtime cdylib directly, lets the native +// host construct the server, performs a purely local // "ping" round-trip through the runtime, and stops cleanly. No auth or replay // proxy is involved, so it needs no snapshot. // diff --git a/go/internal/e2e/pending_work_resume_e2e_test.go b/go/internal/e2e/pending_work_resume_e2e_test.go index 00419aec56..58de8644ac 100644 --- a/go/internal/e2e/pending_work_resume_e2e_test.go +++ b/go/internal/e2e/pending_work_resume_e2e_test.go @@ -468,7 +468,32 @@ func TestPendingWorkResumeE2E(t *testing.T) { } if scenario.disconnectOriginalClient { + lockObserver := ctx.NewClient(func(opts *copilot.ClientOptions) { + stdio := opts.Connection.(copilot.StdioConnection) + opts.Connection = copilot.TCPConnection{Path: stdio.Path} + }) + t.Cleanup(func() { lockObserver.ForceStop() }) + if err := lockObserver.Start(t.Context()); err != nil { + t.Fatalf("Failed to start session lock observer: %v", err) + } + + waitForRPCCondition(t, pendingWorkTimeout, "session lock to be held before disconnect", func() (bool, error) { + result, err := lockObserver.RPC.Sessions.CheckInUse( + t.Context(), + &rpc.SessionsCheckInUseRequest{SessionIDs: []string{sessionID}}, + ) + return err == nil && containsString(result.InUse, sessionID), err + }) + suspendedClient.ForceStop() + + waitForRPCCondition(t, pendingWorkTimeout, "session lock to be released after disconnect", func() (bool, error) { + result, err := lockObserver.RPC.Sessions.CheckInUse( + t.Context(), + &rpc.SessionsCheckInUseRequest{SessionIDs: []string{sessionID}}, + ) + return err == nil && !containsString(result.InUse, sessionID), err + }) } resumedClient := ctx.NewClient(func(opts *copilot.ClientOptions) { diff --git a/go/internal/e2e/rewind_e2e_test.go b/go/internal/e2e/rewind_e2e_test.go index 5fc29a13e8..701e610775 100644 --- a/go/internal/e2e/rewind_e2e_test.go +++ b/go/internal/e2e/rewind_e2e_test.go @@ -14,8 +14,10 @@ import ( ) const ( - rewindFileName = "rewind-sdk.txt" - rewindFileContent = "SDK rewind content" + rewindFileName = "rewind-sdk.txt" + rewindFileOriginalContent = "Original rewind content" + rewindFilePreparedContent = "Prepared rewind content" + rewindFileContent = "SDK rewind content" ) func TestRewindE2E(t *testing.T) { @@ -24,14 +26,13 @@ func TestRewindE2E(t *testing.T) { t.Cleanup(func() { client.ForceStop() }) t.Run("should restore tracked file and conversation", func(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("blocked on CLI 1.0.81 file-change tracking regression on Windows") - } - ctx.ConfigureForTest(t) filePath := filepath.Join(ctx.WorkDir, rewindFileName) + if err := os.WriteFile(filePath, []byte(rewindFileOriginalContent), 0o600); err != nil { + t.Fatalf("Failed to create original file: %v", err) + } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableFileChangeTracking: copilot.Bool(true), OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) @@ -40,9 +41,30 @@ func TestRewindE2E(t *testing.T) { } defer session.Disconnect() + ready, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Use the edit tool to replace the exact contents of " + rewindFileName + " from " + + rewindFileOriginalContent + " to " + rewindFilePreparedContent + + ". After the tool succeeds, reply with exactly SDK_REWIND_READY.", + }) + if err != nil { + t.Fatalf("SendAndWait readiness turn failed: %v", err) + } + readyData, ok := ready.Data.(*copilot.AssistantMessageData) + if !ok || readyData.Content != "SDK_REWIND_READY" { + t.Fatalf("Expected SDK_REWIND_READY response, got %+v", ready) + } + content, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read prepared file: %v", err) + } + if string(content) != rewindFilePreparedContent { + t.Fatalf("Expected file content %q, got %q", rewindFilePreparedContent, content) + } + response, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ - Prompt: "Use the create tool to create " + rewindFileName + " containing exactly " + - rewindFileContent + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE.", + Prompt: "Use the edit tool to replace the exact contents of " + rewindFileName + " from " + + rewindFilePreparedContent + " to " + rewindFileContent + + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE.", }) if err != nil { t.Fatalf("SendAndWait failed: %v", err) @@ -51,9 +73,9 @@ func TestRewindE2E(t *testing.T) { if !ok || responseData.Content != "SDK_REWIND_DONE" { t.Fatalf("Expected SDK_REWIND_DONE response, got %+v", response) } - content, err := os.ReadFile(filePath) + content, err = os.ReadFile(filePath) if err != nil { - t.Fatalf("Failed to read created file: %v", err) + t.Fatalf("Failed to read updated file: %v", err) } if string(content) != rewindFileContent { t.Fatalf("Expected file content %q, got %q", rewindFileContent, content) @@ -63,10 +85,13 @@ func TestRewindE2E(t *testing.T) { if !rewindPoints.FileChangeTrackingEnabled { t.Fatal("Expected file change tracking to be enabled") } - if len(rewindPoints.Points) != 1 { - t.Fatalf("Expected one rewind point, got %+v", rewindPoints.Points) + if len(rewindPoints.Points) != 2 { + t.Fatalf("Expected two rewind points, got %+v", rewindPoints.Points) + } + rewindPoint := rewindPoints.Points[1] + if !rewindPoint.TurnChangedFiles { + t.Fatalf("Expected the edit turn to report changed files, got %+v", rewindPoint) } - rewindPoint := rewindPoints.Points[0] if !rewindPoint.CanRestoreFiles || rewindPoint.FileCount != 1 { t.Fatalf("Expected one restorable file, got %+v", rewindPoint) } @@ -99,8 +124,12 @@ func TestRewindE2E(t *testing.T) { t.Fatalf("Expected one restored file, got %+v", rewind.RestoredFiles) } assertSameRewindPath(t, filePath, rewind.RestoredFiles[0]) - if _, err := os.Stat(filePath); !os.IsNotExist(err) { - t.Fatalf("Expected rewound file to be removed, stat error: %v", err) + content, err = os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read restored file: %v", err) + } + if string(content) != rewindFilePreparedContent { + t.Fatalf("Expected restored file content %q, got %q", rewindFilePreparedContent, content) } events, err := session.GetEvents(t.Context()) @@ -124,9 +153,10 @@ func waitForRewindPoints(t *testing.T, session *copilot.Session) *rpc.HistoryLis t.Fatalf("ListRewindPoints failed: %v", err) } if result.UnavailableReason == nil && - len(result.Points) == 1 && - result.Points[0].CanRestoreFiles && - result.Points[0].FileCount == 1 { + len(result.Points) == 2 && + result.Points[1].TurnChangedFiles && + result.Points[1].CanRestoreFiles && + result.Points[1].FileCount == 1 { return result } if time.Now().After(deadline) { diff --git a/go/internal/e2e/rpc_e2e_test.go b/go/internal/e2e/rpc_e2e_test.go index fcf843814e..4380415701 100644 --- a/go/internal/e2e/rpc_e2e_test.go +++ b/go/internal/e2e/rpc_e2e_test.go @@ -128,7 +128,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -150,7 +150,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -194,7 +194,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) diff --git a/go/internal/e2e/rpc_server_e2e_test.go b/go/internal/e2e/rpc_server_e2e_test.go index 6ea9ad6851..f1aa5a19c7 100644 --- a/go/internal/e2e/rpc_server_e2e_test.go +++ b/go/internal/e2e/rpc_server_e2e_test.go @@ -64,12 +64,12 @@ func TestRPCServerE2E(t *testing.T) { if strings.TrimSpace(model.Name) == "" { t.Errorf("Model %q has empty Name", model.ID) } - if model.ID == "claude-sonnet-4.5" { + if model.ID == "claude-sonnet-5" { hasClaude = true } } if !hasClaude { - t.Errorf("Expected models list to contain 'claude-sonnet-4.5'") + t.Errorf("Expected models list to contain 'claude-sonnet-5'") } }) @@ -532,6 +532,7 @@ func TestRPCServerE2E(t *testing.T) { t.Run("should report implemented error when connecting unknown remote session", func(t *testing.T) { ctx := testharness.NewTestContext(t) + ctx.ConfigureWithoutSnapshot(t) client := ctx.NewClient() t.Cleanup(func() { client.ForceStop() }) if err := client.Start(t.Context()); err != nil { diff --git a/go/internal/e2e/rpc_session_state_e2e_test.go b/go/internal/e2e/rpc_session_state_e2e_test.go index 88673a9ba9..f4870dbac6 100644 --- a/go/internal/e2e/rpc_session_state_e2e_test.go +++ b/go/internal/e2e/rpc_session_state_e2e_test.go @@ -26,7 +26,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Run("should call session rpc model getCurrent", func(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -37,8 +37,8 @@ func TestRPCSessionStateE2E(t *testing.T) { if err != nil { t.Fatalf("Model.GetCurrent failed: %v", err) } - if result.ModelID == nil || *result.ModelID != "claude-sonnet-4.5" { - t.Fatalf("Expected current model claude-sonnet-4.5, got %+v", result) + if result.ModelID == nil || *result.ModelID != "claude-sonnet-5" { + t.Fatalf("Expected current model claude-sonnet-5, got %+v", result) } }) @@ -58,7 +58,7 @@ func TestRPCSessionStateE2E(t *testing.T) { switchCtx.ConfigureForTest(t) session, err := switchClient.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -485,7 +485,7 @@ func TestRPCSessionStateE2E(t *testing.T) { branch := "rpc-context-" + randomHex(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", WorkingDirectory: firstDirectory, OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) @@ -498,7 +498,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Fatalf("Metadata.Snapshot failed: %v", err) } if initial.SessionID != session.SessionID || initial.CurrentMode != rpc.MetadataSnapshotCurrentModeInteractive || - initial.SelectedModel == nil || *initial.SelectedModel != "claude-sonnet-4.5" || + initial.SelectedModel == nil || *initial.SelectedModel != "claude-sonnet-5" || initial.IsRemote || initial.AlreadyInUse || initial.StartTime.IsZero() || initial.ModifiedTime.IsZero() || initial.Workspace == nil || initial.WorkspacePath == nil || *initial.WorkspacePath == "" { t.Fatalf("Unexpected initial metadata snapshot: %+v", initial) @@ -630,7 +630,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Run("should set reasoning effort and auto name", func(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -648,9 +648,9 @@ func TestRPCSessionStateE2E(t *testing.T) { if err != nil { t.Fatalf("Model.GetCurrent failed: %v", err) } - if current.ModelID == nil || *current.ModelID != "claude-sonnet-4.5" || + if current.ModelID == nil || *current.ModelID != "claude-sonnet-5" || current.ReasoningEffort == nil || *current.ReasoningEffort != "high" { - t.Fatalf("Expected current model claude-sonnet-4.5/high, got %+v", current) + t.Fatalf("Expected current model claude-sonnet-5/high, got %+v", current) } autoName := "Auto Session " + randomHex(t) @@ -760,7 +760,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Fatal("Expected fresh session to be idle") } - model := "claude-sonnet-4.5" + model := "claude-sonnet-5" contextInfo, err := session.RPC.Metadata.ContextInfo(t.Context(), &rpc.MetadataContextInfoRequest{ PromptTokenLimit: 128000, OutputTokenLimit: 4096, diff --git a/go/internal/e2e/rpc_session_state_extras_e2e_test.go b/go/internal/e2e/rpc_session_state_extras_e2e_test.go index 99dc0b7572..c74ed45cc8 100644 --- a/go/internal/e2e/rpc_session_state_extras_e2e_test.go +++ b/go/internal/e2e/rpc_session_state_extras_e2e_test.go @@ -22,7 +22,7 @@ func TestRpcSessionStateExtras(t *testing.T) { authClient := newAuthenticatedClient(ctx, token) defer authClient.ForceStop() - session := createPortedSession(t, authClient, &copilot.SessionConfig{Model: "claude-sonnet-4.5"}) + session := createPortedSession(t, authClient, &copilot.SessionConfig{Model: "claude-sonnet-5"}) defer session.Disconnect() result, err := session.RPC.Model.List(t.Context()) @@ -38,13 +38,13 @@ func TestRpcSessionStateExtras(t *testing.T) { found := false for _, model := range result.List { data, err := json.Marshal(model) - if err == nil && strings.Contains(string(data), "claude-sonnet-4.5") { + if err == nil && strings.Contains(string(data), "claude-sonnet-5") { found = true break } } if !found { - t.Fatalf("Expected model list to include claude-sonnet-4.5, got %+v", result.List) + t.Fatalf("Expected model list to include claude-sonnet-5, got %+v", result.List) } }) diff --git a/go/internal/e2e/session_config_e2e_test.go b/go/internal/e2e/session_config_e2e_test.go index 2ce48e3b33..f1c267e230 100644 --- a/go/internal/e2e/session_config_e2e_test.go +++ b/go/internal/e2e/session_config_e2e_test.go @@ -119,8 +119,8 @@ func createAnthropicProvider() *copilot.ProviderConfig { Type: "anthropic", BaseURL: "https://anthropic-citations.invalid/v1", APIKey: "test-provider-key", - ModelID: "claude-sonnet-4.5", - WireModel: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", + WireModel: "claude-sonnet-5", } } @@ -184,6 +184,7 @@ func TestSessionConfigE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-sonnet-5", ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(false), @@ -208,7 +209,7 @@ func TestSessionConfigE2E(t *testing.T) { } // Switch vision on - if err := session.SetModel(t.Context(), "claude-sonnet-4.5", &copilot.SetModelOptions{ + if err := session.SetModel(t.Context(), "claude-sonnet-5", &copilot.SetModelOptions{ ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(true), @@ -238,6 +239,7 @@ func TestSessionConfigE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-sonnet-5", ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(true), @@ -262,7 +264,7 @@ func TestSessionConfigE2E(t *testing.T) { } // Switch vision off - if err := session.SetModel(t.Context(), "claude-sonnet-4.5", &copilot.SetModelOptions{ + if err := session.SetModel(t.Context(), "claude-sonnet-5", &copilot.SetModelOptions{ ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(false), @@ -420,7 +422,7 @@ func TestSessionConfigNewOptionsCopilotRequestE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableCitations: copilot.Bool(true), Provider: createAnthropicProvider(), }) @@ -481,7 +483,7 @@ func TestSessionConfigNewOptionsCopilotRequestE2E(t *testing.T) { session2, err := resumeClient.ResumeSessionWithOptions(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableCitations: copilot.Bool(true), Provider: createAnthropicProvider(), }) @@ -589,7 +591,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: createProxyProvider(ctx, providerHeaderName, "create-provider-header"), }) if err != nil { @@ -633,7 +635,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: createProxyProvider(ctx, providerHeaderName, "resume-provider-header"), }) if err != nil { @@ -677,7 +679,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { maxOutputTokens := 1024 session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: &copilot.ProviderConfig{ Type: "openai", BaseURL: ctx.ProxyURL, @@ -719,7 +721,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { Type: "openai", BaseURL: ctx.ProxyURL, APIKey: "test-provider-key", - ModelID: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", }, }) if err != nil { @@ -738,8 +740,8 @@ func TestSessionConfigExtrasE2E(t *testing.T) { if len(exchanges) != 1 { t.Fatalf("Expected exactly 1 exchange, got %d", len(exchanges)) } - if exchanges[0].Request.Model != "claude-sonnet-4.5" { - t.Errorf("Expected request model to be 'claude-sonnet-4.5', got %q", exchanges[0].Request.Model) + if exchanges[0].Request.Model != "claude-sonnet-5" { + t.Errorf("Expected request model to be 'claude-sonnet-5', got %q", exchanges[0].Request.Model) } }) diff --git a/go/internal/e2e/session_e2e_test.go b/go/internal/e2e/session_e2e_test.go index cf39b6784c..efc20250b1 100644 --- a/go/internal/e2e/session_e2e_test.go +++ b/go/internal/e2e/session_e2e_test.go @@ -23,7 +23,7 @@ func TestSessionE2E(t *testing.T) { t.Run("should create and disconnect sessions", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Model: "claude-sonnet-4.5"}) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Model: "claude-sonnet-5"}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -47,8 +47,8 @@ func TestSessionE2E(t *testing.T) { t.Errorf("Expected session.start sessionId to match") } - if !startOk || startData.SelectedModel == nil || *startData.SelectedModel != "claude-sonnet-4.5" { - t.Errorf("Expected selectedModel to be 'claude-sonnet-4.5', got %v", startData) + if !startOk || startData.SelectedModel == nil || *startData.SelectedModel != "claude-sonnet-5" { + t.Errorf("Expected selectedModel to be 'claude-sonnet-5', got %v", startData) } if err := session.Disconnect(); err != nil { diff --git a/go/internal/embeddedcli/embeddedcli.go b/go/internal/embeddedcli/embeddedcli.go index cd0be21895..2535cf5f20 100644 --- a/go/internal/embeddedcli/embeddedcli.go +++ b/go/internal/embeddedcli/embeddedcli.go @@ -1,7 +1,9 @@ package embeddedcli import ( + "archive/tar" "bytes" + "compress/gzip" "crypto/sha256" "fmt" "io" @@ -23,10 +25,10 @@ import ( // version-specific child directory so multiple versions can coexist. License, // when provided, is written next to the installed binary. // -// RuntimeLib and RuntimeLibHash are optional: when set, the native in-process -// runtime library (cdylib) is installed next to the CLI binary so the in-process -// (FFI) transport can load it. They are omitted for CLI packages that do not -// ship the native runtime. +// RuntimeExecutable and RuntimeNode form the adjacent out-of-process runtime +// pair. RuntimeAssets is a filtered npm package archive containing auxiliary +// binaries and resources. RuntimeLib is the same cdylib bytes installed under +// the natural platform name for the optional in-process transport. type Config struct { Cli io.Reader CliHash []byte @@ -36,12 +38,25 @@ type Config struct { RuntimeLib io.Reader RuntimeLibHash []byte + RuntimeExecutable io.Reader + RuntimeExecutableHash []byte + RuntimeNode io.Reader + RuntimeNodeHash []byte + RuntimeAssets io.Reader + RuntimeAssetsHash []byte + // LinuxMuslCli and LinuxMuslRuntimeLib are optional alternatives selected // automatically when the application runs on a musl-based Linux system. - LinuxMuslCli io.Reader - LinuxMuslCliHash []byte - LinuxMuslRuntimeLib io.Reader - LinuxMuslRuntimeLibHash []byte + LinuxMuslCli io.Reader + LinuxMuslCliHash []byte + LinuxMuslRuntimeLib io.Reader + LinuxMuslRuntimeLibHash []byte + LinuxMuslRuntimeExecutable io.Reader + LinuxMuslRuntimeExecutableHash []byte + LinuxMuslRuntimeNode io.Reader + LinuxMuslRuntimeNodeHash []byte + LinuxMuslRuntimeAssets io.Reader + LinuxMuslRuntimeAssetsHash []byte Dir string Version string @@ -60,6 +75,10 @@ func Setup(cfg Config) { if cfg.LinuxMuslRuntimeLib != nil && len(cfg.LinuxMuslRuntimeLibHash) != sha256.Size { panic(fmt.Sprintf("LinuxMuslRuntimeLibHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.LinuxMuslRuntimeLibHash))) } + validateRuntimePairConfig(cfg.RuntimeExecutable, cfg.RuntimeExecutableHash, cfg.RuntimeNode, cfg.RuntimeNodeHash, "") + validateRuntimePairConfig(cfg.LinuxMuslRuntimeExecutable, cfg.LinuxMuslRuntimeExecutableHash, cfg.LinuxMuslRuntimeNode, cfg.LinuxMuslRuntimeNodeHash, "LinuxMusl") + validateOptionalHash(cfg.RuntimeAssets, cfg.RuntimeAssetsHash, "RuntimeAssetsHash") + validateOptionalHash(cfg.LinuxMuslRuntimeAssets, cfg.LinuxMuslRuntimeAssetsHash, "LinuxMuslRuntimeAssetsHash") setupMu.Lock() defer setupMu.Unlock() if setupDone { @@ -93,13 +112,34 @@ func RuntimeLibPath() string { return runtimeLibPath } +// RuntimePath returns the installed copilot-runtime executable, or "" when the +// application bundle predates the out-of-process runtime pair. +func RuntimePath() string { + setupMu.Lock() + defer setupMu.Unlock() + if !setupDone { + return "" + } + pathInitialized = true + selectLinuxMuslBundle() + if config.RuntimeExecutable == nil { + return "" + } + if runtimePath == "" { + runtimePath = installRuntime() + } + return runtimePath +} + var ( - config Config - setupMu sync.Mutex - setupDone bool - pathInitialized bool - runtimeLibPath string - linuxMuslBundle bool + config Config + setupMu sync.Mutex + setupDone bool + pathInitialized bool + runtimeLibPath string + runtimePath string + runtimeAssetsInstalled bool + linuxMuslBundle bool ) func install() (path string) { @@ -118,6 +158,38 @@ func install() (path string) { fmt.Printf("installing embedded CLI at %s installation took %s\n", path, duration) }() } + installDir := configuredInstallDir() + path, err := installAt(installDir) + if err != nil { + logError("installing in configured directory", err) + return "" + } + return path +} + +func installRuntime() (path string) { + verbose := os.Getenv("COPILOT_CLI_INSTALL_VERBOSE") == "1" + logError := func(msg string, err error) { + if verbose { + fmt.Printf("embedded runtime installation error: %s: %v\n", msg, err) + } + } + if verbose { + start := time.Now() + defer func() { + fmt.Printf("installing embedded runtime at %s took %s\n", path, time.Since(start)) + }() + } + + path, err := installRuntimeAt(configuredInstallDir()) + if err != nil { + logError("installing in configured directory", err) + return "" + } + return path +} + +func configuredInstallDir() string { installDir := config.Dir if installDir == "" { if copilotHome := os.Getenv("COPILOT_HOME"); copilotHome != "" { @@ -131,12 +203,7 @@ func install() (path string) { installDir = filepath.Join(installDir, "copilot-sdk") } } - path, err := installAt(installDir) - if err != nil { - logError("installing in configured directory", err) - return "" - } - return path + return installDir } func selectLinuxMuslBundle() { @@ -152,6 +219,12 @@ func linuxMuslConfig(cfg Config) Config { cfg.CliHash = cfg.LinuxMuslCliHash cfg.RuntimeLib = cfg.LinuxMuslRuntimeLib cfg.RuntimeLibHash = cfg.LinuxMuslRuntimeLibHash + cfg.RuntimeExecutable = cfg.LinuxMuslRuntimeExecutable + cfg.RuntimeExecutableHash = cfg.LinuxMuslRuntimeExecutableHash + cfg.RuntimeNode = cfg.LinuxMuslRuntimeNode + cfg.RuntimeNodeHash = cfg.LinuxMuslRuntimeNodeHash + cfg.RuntimeAssets = cfg.LinuxMuslRuntimeAssets + cfg.RuntimeAssetsHash = cfg.LinuxMuslRuntimeAssetsHash return cfg } @@ -198,6 +271,9 @@ func installAt(installDir string) (string, error) { } runtimeLibPath = libPath } + if err := installRuntimeAssets(installDir); err != nil { + return "", err + } return finalPath, nil } @@ -229,12 +305,188 @@ func installAt(installDir string) (string, error) { if err != nil { return "", err } + if err := installRuntimeAssets(installDir); err != nil { + return "", err + } runtimeLibPath = libPath } return finalPath, nil } +func installRuntimeAt(installDir string) (string, error) { + version := sanitizeVersion(config.Version) + if version != "" { + installDir = filepath.Join(installDir, version) + } + if linuxMuslBundle { + installDir = filepath.Join(installDir, "linuxmusl") + } + if err := os.MkdirAll(installDir, 0755); err != nil { + return "", fmt.Errorf("creating install directory: %w", err) + } + + if release, _ := flock.Acquire(filepath.Join(installDir, ".copilot-cli.lock")); release != nil { + defer release() + } + path, err := installRuntimePair(installDir) + if err != nil { + return "", err + } + if err := installRuntimeAssets(installDir); err != nil { + return "", err + } + return path, nil +} + +func validateOptionalHash(reader io.Reader, hash []byte, name string) { + if reader != nil && len(hash) != sha256.Size { + panic(fmt.Sprintf("%s must be a SHA-256 hash (%d bytes), got %d bytes", name, sha256.Size, len(hash))) + } +} + +func installRuntimeAssets(installDir string) error { + if config.RuntimeAssets == nil || runtimeAssetsInstalled { + return nil + } + archiveBytes, err := io.ReadAll(config.RuntimeAssets) + if closer, ok := config.RuntimeAssets.(io.Closer); ok { + closer.Close() + } + if err != nil { + return fmt.Errorf("reading runtime assets: %w", err) + } + actual := sha256.Sum256(archiveBytes) + if !bytes.Equal(actual[:], config.RuntimeAssetsHash) { + return fmt.Errorf("runtime assets hash mismatch") + } + gzipReader, err := gzip.NewReader(bytes.NewReader(archiveBytes)) + if err != nil { + return fmt.Errorf("opening runtime assets: %w", err) + } + defer gzipReader.Close() + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("reading runtime assets: %w", err) + } + if header.Typeflag != tar.TypeReg { + continue + } + clean := filepath.Clean(filepath.FromSlash(header.Name)) + if !filepath.IsLocal(clean) { + return fmt.Errorf("unsafe runtime asset path %q", header.Name) + } + content, err := io.ReadAll(tarReader) + if err != nil { + return fmt.Errorf("reading runtime asset %q: %w", header.Name, err) + } + path := filepath.Join(installDir, clean) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return fmt.Errorf("creating runtime asset directory: %w", err) + } + hash := sha256.Sum256(content) + mode := os.FileMode(header.Mode & 0777) + if err := installVerifiedFile(path, bytes.NewReader(content), hash[:], mode, "runtime asset"); err != nil { + return err + } + } + runtimeAssetsInstalled = true + return nil +} + +func validateRuntimePairConfig(wrapper io.Reader, wrapperHash []byte, node io.Reader, nodeHash []byte, prefix string) { + if (wrapper == nil) != (node == nil) { + panic(prefix + "RuntimeExecutable and " + prefix + "RuntimeNode must be provided together") + } + if wrapper == nil { + return + } + if len(wrapperHash) != sha256.Size { + panic(fmt.Sprintf("%sRuntimeExecutableHash must be a SHA-256 hash (%d bytes), got %d bytes", prefix, sha256.Size, len(wrapperHash))) + } + if len(nodeHash) != sha256.Size { + panic(fmt.Sprintf("%sRuntimeNodeHash must be a SHA-256 hash (%d bytes), got %d bytes", prefix, sha256.Size, len(nodeHash))) + } +} + +func installRuntimePair(installDir string) (string, error) { + nodePath := filepath.Join(installDir, "runtime.node") + if err := installVerifiedFile(nodePath, config.RuntimeNode, config.RuntimeNodeHash, 0644, "runtime.node"); err != nil { + return "", err + } + wrapperPath := filepath.Join(installDir, runtimeExecutableName()) + if err := installVerifiedFile(wrapperPath, config.RuntimeExecutable, config.RuntimeExecutableHash, 0755, "runtime wrapper"); err != nil { + return "", err + } + return wrapperPath, nil +} + +func installVerifiedFile(path string, reader io.Reader, expectedHash []byte, mode os.FileMode, label string) error { + if _, err := os.Stat(path); err == nil { + existingHash, err := hashFile(path) + if err != nil { + return fmt.Errorf("hashing existing %s: %w", label, err) + } + if !bytes.Equal(existingHash, expectedHash) { + return fmt.Errorf("existing %s hash mismatch", label) + } + if runtime.GOOS != "windows" && mode.Perm()&0111 != 0 { + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("checking existing %s permissions: %w", label, err) + } + if info.Mode().Perm()&0111 == 0 { + if err := os.Chmod(path, info.Mode().Perm()|mode.Perm()&0111); err != nil { + return fmt.Errorf("restoring existing %s permissions: %w", label, err) + } + } + } + return nil + } + + tmp, err := os.CreateTemp(filepath.Dir(path), ".copilot-runtime-pair-*.tmp") + if err != nil { + return fmt.Errorf("creating temporary %s: %w", label, err) + } + tmpPath := tmp.Name() + h := sha256.New() + _, err = io.Copy(io.MultiWriter(tmp, h), reader) + if err1 := tmp.Chmod(mode); err1 != nil && err == nil { + err = err1 + } + if err1 := tmp.Close(); err1 != nil && err == nil { + err = err1 + } + if closer, ok := reader.(io.Closer); ok { + closer.Close() + } + if err != nil { + os.Remove(tmpPath) + return fmt.Errorf("writing %s: %w", label, err) + } + if !bytes.Equal(h.Sum(nil), expectedHash) { + os.Remove(tmpPath) + return fmt.Errorf("%s hash mismatch", label) + } + if err := os.Rename(tmpPath, path); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("installing %s: %w", label, err) + } + return nil +} + +func runtimeExecutableName() string { + if runtime.GOOS == "windows" { + return "copilot-runtime.exe" + } + return "copilot-runtime" +} + // installRuntimeLib writes the embedded runtime cdylib into installDir under its // natural platform file name, verifying its SHA-256. It is idempotent: an // existing file with a matching hash is reused; a mismatch is a hard error. diff --git a/go/internal/embeddedcli/embeddedcli_test.go b/go/internal/embeddedcli/embeddedcli_test.go index b0394e0f69..159b6e1505 100644 --- a/go/internal/embeddedcli/embeddedcli_test.go +++ b/go/internal/embeddedcli/embeddedcli_test.go @@ -1,7 +1,9 @@ package embeddedcli import ( + "archive/tar" "bytes" + "compress/gzip" "crypto/sha256" "os" "path/filepath" @@ -17,9 +19,143 @@ func resetGlobals() { setupDone = false pathInitialized = false runtimeLibPath = "" + runtimePath = "" + runtimeAssetsInstalled = false linuxMuslBundle = false } +func TestInstallRuntimeWritesRetainedAssets(t *testing.T) { + resetGlobals() + tempDir := t.TempDir() + cli := []byte("cli") + wrapper := []byte("wrapper") + node := []byte("runtime") + assets := runtimeAssetsArchive(t, map[string]assetFixture{ + "ripgrep/bin/test-platform/rg": {content: []byte("ripgrep"), mode: 0755}, + "definitions/future.json": {content: []byte("{}"), mode: 0644}, + }) + cliHash := sha256.Sum256(cli) + wrapperHash := sha256.Sum256(wrapper) + nodeHash := sha256.Sum256(node) + assetsHash := sha256.Sum256(assets) + Setup(Config{ + Cli: bytes.NewReader(cli), + CliHash: cliHash[:], + RuntimeExecutable: bytes.NewReader(wrapper), + RuntimeExecutableHash: wrapperHash[:], + RuntimeNode: bytes.NewReader(node), + RuntimeNodeHash: nodeHash[:], + RuntimeAssets: bytes.NewReader(assets), + RuntimeAssetsHash: assetsHash[:], + Version: "1.2.3", + Dir: tempDir, + }) + + gotWrapper, err := installRuntimeAt(tempDir) + if err != nil { + t.Fatal(err) + } + installDir := filepath.Dir(gotWrapper) + if got, err := os.ReadFile(filepath.Join(installDir, "ripgrep", "bin", "test-platform", "rg")); err != nil || string(got) != "ripgrep" { + t.Fatalf("ripgrep content=%q err=%v", got, err) + } + if got, err := os.ReadFile(filepath.Join(installDir, "definitions", "future.json")); err != nil || string(got) != "{}" { + t.Fatalf("definition content=%q err=%v", got, err) + } +} + +type assetFixture struct { + content []byte + mode int64 +} + +func runtimeAssetsArchive(t *testing.T, files map[string]assetFixture) []byte { + t.Helper() + var buffer bytes.Buffer + gzipWriter := gzip.NewWriter(&buffer) + tarWriter := tar.NewWriter(gzipWriter) + for name, fixture := range files { + header := &tar.Header{Name: name, Mode: fixture.mode, Size: int64(len(fixture.content)), Typeflag: tar.TypeReg} + if err := tarWriter.WriteHeader(header); err != nil { + t.Fatal(err) + } + if _, err := tarWriter.Write(fixture.content); err != nil { + t.Fatal(err) + } + } + if err := tarWriter.Close(); err != nil { + t.Fatal(err) + } + if err := gzipWriter.Close(); err != nil { + t.Fatal(err) + } + return buffer.Bytes() +} + +func TestInstallRuntimeWritesAdjacentPairWithoutCLI(t *testing.T) { + resetGlobals() + tempDir := t.TempDir() + cli := []byte("cli") + wrapper := []byte("wrapper") + node := []byte("runtime") + cliHash := sha256.Sum256(cli) + wrapperHash := sha256.Sum256(wrapper) + nodeHash := sha256.Sum256(node) + Setup(Config{ + Cli: bytes.NewReader(cli), + CliHash: cliHash[:], + RuntimeExecutable: bytes.NewReader(wrapper), + RuntimeExecutableHash: wrapperHash[:], + RuntimeNode: bytes.NewReader(node), + RuntimeNodeHash: nodeHash[:], + Version: "1.2.3", + Dir: tempDir, + }) + + gotWrapper, err := installRuntimeAt(tempDir) + if err != nil { + t.Fatal(err) + } + if gotWrapper != filepath.Join(tempDir, "1.2.3", runtimeExecutableName()) { + t.Fatalf("RuntimePath() = %q", gotWrapper) + } + if got, err := os.ReadFile(filepath.Join(filepath.Dir(gotWrapper), "runtime.node")); err != nil || !bytes.Equal(got, node) { + t.Fatalf("runtime.node content=%q err=%v", got, err) + } + if cliPath := filepath.Join(filepath.Dir(gotWrapper), binaryNameForOS()); fileExists(cliPath) { + t.Fatalf("managed runtime installation unexpectedly materialized CLI host at %q", cliPath) + } +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func TestInstallVerifiedFileRestoresExecutablePermission(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows does not use Unix execute bits") + } + path := filepath.Join(t.TempDir(), "copilot-runtime") + content := []byte("wrapper") + hash := sha256.Sum256(content) + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatal(err) + } + + if err := installVerifiedFile(path, bytes.NewReader(content), hash[:], 0755, "runtime wrapper"); err != nil { + t.Fatal(err) + } + + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm()&0111 == 0 { + t.Fatal("existing runtime wrapper was not made executable") + } +} + func mustPanic(t *testing.T, fn func()) { t.Helper() defer func() { diff --git a/go/internal/ffihost/ffihost.go b/go/internal/ffihost/ffihost.go index 30cd831281..9a824881de 100644 --- a/go/internal/ffihost/ffihost.go +++ b/go/internal/ffihost/ffihost.go @@ -159,8 +159,8 @@ type Host struct { // Create resolves the native library and prepares the host. environment and // args contain SDK-managed runtime options. -func Create(cliEntrypoint string, environment map[string]string, args []string) (*Host, error) { - libraryPath, err := ResolveLibraryPath(cliEntrypoint) +func Create(runtimeEntrypoint, cliEntrypoint string, environment map[string]string, args []string) (*Host, error) { + libraryPath, err := ResolveLibraryPath(runtimeEntrypoint) if err != nil { return nil, err } @@ -207,18 +207,13 @@ func (h *Host) Start() error { runtime.KeepAlive(argv) runtime.KeepAlive(env) if h.serverID == 0 { - return fmt.Errorf("copilot_runtime_host_start failed (library %q, entrypoint %q)", h.libraryPath, h.cliEntrypoint) + return fmt.Errorf("copilot_runtime_host_start failed (library %q)", h.libraryPath) } - // host_start spawned the worker child via libuv's uv_spawn, which installs a - // SIGCHLD handler without SA_ONSTACK on its first call. The Go runtime aborts - // ("non-Go code set up signal handler without SA_ONSTACK flag") when it later - // reaps one of its own os/exec children (e.g. a test-spawned MCP server) and - // the delivered SIGCHLD lands on a non-signal stack. Re-add SA_ONSTACK to that - // foreign handler now that it exists (implemented on darwin+linux; a no-op on - // other platforms, and before the first spawn there is nothing to fix — hence - // here rather than at library load). - rearmForeignSignalHandlers(h.lib.handle) + if h.cliEntrypoint != "" { + // A legacy embedded host may install a SIGCHLD handler without SA_ONSTACK. + rearmForeignSignalHandlers(h.lib.handle) + } callbackHandle := sharedOutboundCallback() callbackToken := uintptr(nextOutboundToken.Add(1)) @@ -229,7 +224,9 @@ func (h *Host) Start() error { outboundTargets.Delete(callbackToken) h.callbackToken = 0 h.lib.hostShutdown(h.serverID) - rearmForeignSignalHandlers(h.lib.handle) + if h.cliEntrypoint != "" { + rearmForeignSignalHandlers(h.lib.handle) + } h.serverID = 0 return fmt.Errorf("copilot_runtime_connection_open failed") } @@ -243,14 +240,12 @@ func (h *Host) Writer() io.WriteCloser { return hostWriter{h} } func (h *Host) Reader() io.ReadCloser { return h.recv } func (h *Host) buildArgv() []byte { - // A `.js` entrypoint (dev) is launched via node; the packaged single-file CLI - // embeds its own Node and is invoked directly. `--no-auto-update` pins the - // worker to the runtime package matching the loaded cdylib (avoids ABI skew). - var argv []string - if strings.HasSuffix(strings.ToLower(h.cliEntrypoint), ".js") { - argv = []string{"node", h.cliEntrypoint, "--embedded-host", "--no-auto-update"} - } else { - argv = []string{h.cliEntrypoint, "--embedded-host", "--no-auto-update"} + argv := make([]string, 0, len(h.args)+4) + if h.cliEntrypoint != "" { + if strings.HasSuffix(strings.ToLower(h.cliEntrypoint), ".js") { + argv = append(argv, "node") + } + argv = append(argv, h.cliEntrypoint, "--embedded-host", "--no-auto-update") } argv = append(argv, h.args...) b, _ := json.Marshal(argv) @@ -365,10 +360,10 @@ func (h *Host) Dispose() { } if serverID != 0 { h.lib.hostShutdown(serverID) - // libuv may restore a previously saved SIGCHLD action while tearing down - // its final child watcher, so repair the process-wide handler again after - // shutdown before Go reaps another os/exec child. - rearmForeignSignalHandlers(h.lib.handle) + if h.cliEntrypoint != "" { + // A legacy host may restore its saved SIGCHLD action during shutdown. + rearmForeignSignalHandlers(h.lib.handle) + } } h.recv.Close() } diff --git a/go/internal/ffihost/ffihost_test.go b/go/internal/ffihost/ffihost_test.go index bc588fa6a9..ccb48af419 100644 --- a/go/internal/ffihost/ffihost_test.go +++ b/go/internal/ffihost/ffihost_test.go @@ -25,10 +25,9 @@ func TestDisposeUnregistersOutboundTarget(t *testing.T) { } } -func TestBuildArgvAppendsManagedOptions(t *testing.T) { +func TestBuildArgvWithoutEntrypointContainsOnlyManagedOptions(t *testing.T) { host := &Host{ - cliEntrypoint: "copilot", - args: []string{"--log-level", "debug", "--remote"}, + args: []string{"--log-level", "debug", "--remote"}, } var argv []string @@ -36,7 +35,45 @@ func TestBuildArgvAppendsManagedOptions(t *testing.T) { t.Fatal(err) } - expected := []string{"copilot", "--embedded-host", "--no-auto-update", "--log-level", "debug", "--remote"} + expected := []string{"--log-level", "debug", "--remote"} + if len(argv) != len(expected) { + t.Fatalf("Expected %d arguments, got %d: %v", len(expected), len(argv), argv) + } + for i := range expected { + if argv[i] != expected[i] { + t.Fatalf("Expected argument %d to be %q, got %q", i, expected[i], argv[i]) + } + } +} + +func TestBuildArgvPreservesExplicitEntrypoint(t *testing.T) { + host := &Host{cliEntrypoint: "copilot", args: []string{"--remote"}} + + var argv []string + if err := json.Unmarshal(host.buildArgv(), &argv); err != nil { + t.Fatal(err) + } + + expected := []string{"copilot", "--embedded-host", "--no-auto-update", "--remote"} + if len(argv) != len(expected) { + t.Fatalf("Expected %d arguments, got %d: %v", len(expected), len(argv), argv) + } + for i := range expected { + if argv[i] != expected[i] { + t.Fatalf("Expected argument %d to be %q, got %q", i, expected[i], argv[i]) + } + } +} + +func TestBuildArgvUsesNodeForExplicitJavaScriptEntrypoint(t *testing.T) { + host := &Host{cliEntrypoint: "copilot.js"} + + var argv []string + if err := json.Unmarshal(host.buildArgv(), &argv); err != nil { + t.Fatal(err) + } + + expected := []string{"node", "copilot.js", "--embedded-host", "--no-auto-update"} if len(argv) != len(expected) { t.Fatalf("Expected %d arguments, got %d: %v", len(expected), len(argv), argv) } diff --git a/go/internal/ffihost/resolve.go b/go/internal/ffihost/resolve.go index c8d4052322..5205a01c19 100644 --- a/go/internal/ffihost/resolve.go +++ b/go/internal/ffihost/resolve.go @@ -63,7 +63,8 @@ func PrebuildsFolder() string { // entrypoint. It checks, in order: // // 1. The natural platform library name next to the CLI (bundled/flat layout). -// 2. prebuilds//runtime.node next to the CLI (dev/package layout). +// 2. runtime.node next to the CLI (out-of-process wrapper layout). +// 3. prebuilds//runtime.node next to the CLI (dev/package layout). // // It returns an error when neither exists. func ResolveLibraryPath(cliEntrypoint string) (string, error) { @@ -78,6 +79,11 @@ func ResolveLibraryPath(cliEntrypoint string) (string, error) { return flat, nil } + adjacent := filepath.Join(dir, "runtime.node") + if fileExists(adjacent) { + return adjacent, nil + } + if folder := PrebuildsFolder(); folder != "" { prebuilt := filepath.Join(dir, "prebuilds", folder, "runtime.node") if fileExists(prebuilt) { @@ -86,7 +92,7 @@ func ResolveLibraryPath(cliEntrypoint string) (string, error) { } return "", fmt.Errorf( - "in-process FFI runtime library not found next to %q (looked for %q and prebuilds/%s/runtime.node); "+ + "in-process FFI runtime library not found next to %q (looked for %q, runtime.node, and prebuilds/%s/runtime.node); "+ "use a runtime package that ships the native library", abs, NaturalLibraryName(), PrebuildsFolder()) } diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index edd5f925fe..5b4cdab32d 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -170,7 +170,7 @@ type AgentGetCurrentResult struct { Agent *AgentInfo `json:"agent,omitempty"` } -// Agent metadata, including identifiers, display details, source, tools, model, MCP +// Agent metadata, including identifiers, display details, source, tools, model, models, MCP // servers, skills, and file path. // Experimental: AgentInfo is part of an experimental API and may change or be removed. type AgentInfo struct { @@ -189,6 +189,11 @@ type AgentInfo struct { // Authored preferred model id for this agent. Runtime model selection may choose a // different model; omitted means no authored preference. Model *string `json:"model,omitempty"` + // Whether authored models are preferences or required constraints. + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` + // Authored preferred model ids for this agent, in priority order. Runtime model selection + // chooses the first available model; omitted means no authored preference. + Models []string `json:"models,omitzero"` // Name of the agent. Use `id` as the stable selection identifier. Name string `json:"name"` // Absolute local file path of the agent definition. Only set for file-based agents loaded @@ -1361,6 +1366,10 @@ type CanvasSessionContext struct { // Experimental: CapiSessionOptions is part of an experimental API and may change or be // removed. type CapiSessionOptions struct { + // Routing preference used when the session model is `auto`. The runtime persists the + // preference across cold resume. When omitted, the default routing behavior is used. + // Resuming an already-resident session cannot change its preference. + AutoTier *AutoTier `json:"autoTier,omitempty"` // Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when // the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses // transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting @@ -1600,7 +1609,8 @@ type CatalogSearchRequest struct { Kinds []CatalogCandidateKind `json:"kinds,omitzero"` // Maximum number of candidates to return. Defaults to 10 when omitted. Limit *int32 `json:"limit,omitempty"` - // Free-text search query. Never written to logs or telemetry. + // Free-text search query. Persisted as tool input for session continuity, but omitted from + // telemetry. Query string `json:"query"` } @@ -1733,6 +1743,10 @@ type CatalogNetworkFailureError struct { Message string `json:"message"` // Categorised failure, low cardinality so it can be aggregated without carrying a URL. Reason CatalogNetworkFailureReason `json:"reason"` + // Bounded cooldown in seconds before another catalog request should be attempted, when the + // authority supplied a numeric Retry-After value or the runtime applied its documented + // fallback. + RetryAfterSeconds *int32 `json:"retryAfterSeconds,omitempty"` // HTTP status code, when the failure was a rejected response. StatusCode *int32 `json:"statusCode,omitempty"` } @@ -2573,6 +2587,36 @@ type DiscoveredExtensionsEnableRequest struct { IDs []string `json:"ids"` } +// One server-discovered hook action from user, repository, plugin, or managed-policy +// configuration. +// Experimental: DiscoveredHook is part of an experimental API and may change or be removed. +type DiscoveredHook struct { + // Durable content hash used by hook enablement. Identical actions may intentionally share + // this key. Omitted when changing the user's disabled-hooks setting cannot change the + // action's current server-discovered state, including managed-policy hooks, session-start + // prompt actions, actions suppressed by disable-all settings, and projectless plugin + // actions that require project-directory expansion. + DisableKey *string `json:"disableKey,omitempty"` + // Whether this action is enabled under the server-side discovery settings. Concrete + // sessions may differ because they can add session-specific directories, plugins, or trust. + // False when its disable key is present in the user's disabled-hooks setting or disable-all + // settings suppress the action. + Enabled bool `json:"enabled"` + // Hook event that invokes this action. + HookType HookType `json:"hookType"` + // Deterministic identifier for this server-discovered action row. It remains stable while + // the project, origin, source, event, action content, and duplicate ordinal are unchanged. + // This is row identity, not the key persisted in disabledHooks. + ID string `json:"id"` + // Configuration tier that contributed this hook action. + Origin HookOrigin `json:"origin"` + // Input project path for which this server-side action was resolved. Set on every row + // returned for project-scoped discovery, including repeated user and policy actions. + ProjectPath *string `json:"projectPath,omitempty"` + // Human-readable source label, such as a hook file path, settings source, or plugin name. + Source *string `json:"source,omitempty"` +} + // MCP server discovered by `mcp.discover`, with config source, optional plugin source, // transport type, and enabled state. // Experimental: DiscoveredMCPServer is part of an experimental API and may change or be @@ -2599,6 +2643,8 @@ type EnqueueCommandParams struct { // Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO // with any in-flight items; if the session is idle, processing kicks off immediately. Command string `json:"command"` + // Optional user-facing text for the queue row. The command string is shown when omitted. + DisplayText *string `json:"displayText,omitempty"` } // Indicates whether the command was accepted into the local execution queue. @@ -3558,6 +3604,18 @@ func (FactoryRunFailureFactoryLimitReached) Type() FactoryRunFailureType { return FactoryRunFailureTypeFactoryLimitReached } +// The extension that owns the factory disconnected while the run was executing, so the host +// halted it. The run's journaled subagent results are preserved so a resume can reuse them. +type FactoryRunFailureFactoryProviderDisconnected struct { + // Factory run identifier. + RunID string `json:"runId"` +} + +func (FactoryRunFailureFactoryProviderDisconnected) factoryRunFailure() {} +func (FactoryRunFailureFactoryProviderDisconnected) Type() FactoryRunFailureType { + return FactoryRunFailureTypeFactoryProviderDisconnected +} + type FactoryRunFailureFactoryResumeDeclined struct { // Human-readable reason the resume did not proceed. Reason string `json:"reason"` @@ -3603,9 +3661,12 @@ type FactoryRunRequest struct { // Experimental: FactoryRunResult is part of an experimental API and may change or be // removed. type FactoryRunResult struct { + // One-based execution attempt represented by this envelope. Absent before the first attempt + // starts or when returned by an older runtime. + Attempt *int64 `json:"attempt,omitempty"` // Error message for an errored run. Error *string `json:"error,omitempty"` - // Machine-readable failure details for an errored run. + // Machine-readable failure details for a halted or errored run. Failure FactoryRunFailure `json:"failure,omitempty"` // Reason for a halted or cancelled run. Reason *string `json:"reason,omitempty"` @@ -4203,8 +4264,6 @@ type HistoryTruncateResult struct { // removed. // Internal: HookInvokeRequest is an internal SDK API and is not part of the public surface. type HookInvokeRequest struct { - // Internal: HookType is part of the SDK's internal API surface and is not intended for - // external use. HookType HookType `json:"hookType"` Input any `json:"input"` SessionID string `json:"sessionId"` @@ -4218,6 +4277,40 @@ type HookInvokeResponse struct { Output any `json:"output,omitempty"` } +// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +// Experimental: HooksDiscoverRequest is part of an experimental API and may change or be +// removed. +type HooksDiscoverRequest struct { + // When true, omit host-owned user and plugin hook rows and their diagnostics. + // Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks + // still contribute to each remaining row's effective enabled state. This filters sources + // rather than simulating a host with no settings. + ExcludeHostHooks *bool `json:"excludeHostHooks,omitempty"` + // Optional project directory paths whose trusted repository and project-expanded plugin + // hooks should be discovered. When omitted or empty, user, managed-policy, and globally + // enabled installed or explicit plugin hooks are returned without project expansion. + ProjectPaths []string `json:"projectPaths,omitzero"` +} + +// Server-discovered hook actions and partial-load diagnostics from user, repository, +// plugin, and managed-policy sources. Concrete sessions may include additional +// session-specific hook sources. +// Experimental: HooksDiscoverResult is part of an experimental API and may change or be +// removed. +type HooksDiscoverResult struct { + // Errors for hook sources or actions that could not be loaded, making the result partially + // incomplete. Other valid actions are still returned. Project-resolution and + // repository-settings errors are prefixed with their project path. + Errors []string `json:"errors"` + // All discovered hook actions. Byte-identical actions remain separate rows even when they + // share a disable key. + Hooks []DiscoveredHook `json:"hooks"` + // Non-fatal source-loading warnings. Discovery remains complete for the affected source, + // although the source had a recoverable issue. Repository-settings warnings are prefixed + // with their project path when attribution is available. + Warnings []string `json:"warnings"` +} + // Installed plugin record from global state, with marketplace, version, install time, // enabled state, cache path, and source. // Experimental: InstalledPlugin is part of an experimental API and may change or be removed. @@ -6874,6 +6967,10 @@ type ModelApplyStartupOverlayRequest struct { DeferredResume *bool `json:"deferredResume,omitempty"` // Model required by device-managed policy, when configured. DeviceManagedModel *string `json:"deviceManagedModel,omitempty"` + // 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. + PolicyHelperModel *string `json:"policyHelperModel,omitempty"` // Context tier selected by repository settings, when configured. RepoContextTier *string `json:"repoContextTier,omitempty"` // Model selected by repository settings, when configured. @@ -6915,6 +7012,12 @@ type ModelBillingPromo struct { // Human-readable promotion message. Does not include the expiry timestamp; consumers may // format endsAt and append it when present. Message *string `json:"message,omitempty"` + // Whether the service asked hosts to give this promotion a prominent surface, such as a + // dedicated banner, in addition to listing it with the model. `true` requests that surface + // and `false` asks for the model list only. Absent means the service expressed no + // preference — for example a response that predates the field — so hosts should apply their + // own default rather than read it as `false`. + ShowBanner *bool `json:"showBanner,omitempty"` } // Token-level pricing information for this model @@ -7208,8 +7311,8 @@ type ModelSwitchToRequest struct { RequireAvailable *bool `json:"requireAvailable,omitempty"` // When true, evaluate context-window compaction policy before applying the switch. RunCompactionPreflight *bool `json:"runCompactionPreflight,omitempty"` - // Origin to record on the effective `session.model_change` event. Defaults to `sdk` when - // omitted. + // 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. Source *ModelChangeSource `json:"source,omitempty"` // Output verbosity level to request for supported models Verbosity *Verbosity `json:"verbosity,omitempty"` @@ -9789,6 +9892,9 @@ type QueuePendingItems struct { ID string `json:"id"` // Whether this item is a queued user message or a queued slash command / model change Kind QueuePendingItemsKind `json:"kind"` + // Stable identity of the queued user message. Present for message rows and absent for slash + // commands and model changes. + MessageID *string `json:"messageId,omitempty"` } // Snapshot of the session's pending queued items and immediate-steering messages. @@ -9938,10 +10044,8 @@ type RegisterExtensionLaunchProviderResult struct { // Internal: RegisterExtensionToolsParams is an internal SDK API and is not part of the // public surface. type RegisterExtensionToolsParams struct { - // In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is - // excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, - // extension discovery/launch moves entirely into the runtime — the CLI passes pure config - // (search paths, disabled ids) via SessionOptions instead. + // In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK + // surface. // Internal: Loader is part of the SDK's internal API surface and is not intended for // external use. Loader any `json:"loader"` @@ -9957,8 +10061,7 @@ type RegisterExtensionToolsParams struct { // Internal: RegisterExtensionToolsResult is an internal SDK API and is not part of the // public surface. type RegisterExtensionToolsResult struct { - // In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an - // explicit `extensions.unregister` RPC in the SDK migration. + // In-process unsubscribe function used only by the CLI. // Internal: Unsubscribe is part of the SDK's internal API surface and is not intended for // external use. Unsubscribe any `json:"unsubscribe"` @@ -10303,11 +10406,14 @@ type SandboxConfigUserPolicyNetwork struct { AllowLocalNetwork *bool `json:"allowLocalNetwork,omitempty"` // Whether outbound network traffic is allowed at all. AllowOutbound *bool `json:"allowOutbound,omitempty"` - // HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - // cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - // Credentials go in the separate `username`/`password` fields. A credential-free http:// - // loopback proxy URL is routed through the localhost proxy automatically; an https:// or - // authenticated loopback URL is used as-is. + // HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + // requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + // accepted and routed through the IPv4 gateway), and does not support proxy credentials. + // macOS relies on applications honoring proxy environment variables. Windows also + // configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + // networking stack. Configure supported credentials in the separate `username` and + // `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + // while an https:// or authenticated loopback URL uses the URL form. Proxy *SandboxConfigUserPolicyNetworkProxy `json:"proxy,omitempty"` } @@ -10323,12 +10429,12 @@ type SandboxConfigUserPolicyNetworkProxy struct { // settings.json); the field is masked in the dialog and redacted by /settings show. Password *string `json:"password,omitempty"` // Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the - // scheme's standard port when omitted. Credentials must not be embedded here — a - // `user:pass@` authority is rejected; put them in the separate `username`/`password` - // fields. A credential-free http:// loopback URL is routed through the localhost proxy - // automatically; loopback covers localhost and any *.localhost subdomain, the whole - // 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or - // one with a username/password set, is used as-is. + // scheme's standard port when omitted; an explicit port must be between 1 and 65535. + // Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in + // the separate `username`/`password` fields. A credential-free http:// loopback proxy URL + // is routed through the localhost proxy automatically; loopback covers localhost and any + // *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback + // (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. URL string `json:"url"` // Optional username for proxy authentication. Combined with the URL (and `password`) into // `user:pass@host` when the sandboxed process routes through the proxy. @@ -10343,6 +10449,18 @@ type SandboxConfigUserPolicySeatbelt struct { KeychainAccess *bool `json:"keychainAccess,omitempty"` } +// Managed sandbox enforcement state for a session. +// Experimental: SandboxEnforcementStatus is part of an experimental API and may change or +// be removed. +type SandboxEnforcementStatus struct { + // Whether an enforcement failure has permanently blocked the session. + Blocked bool `json:"blocked"` + // The first sandbox enforcement failure that blocked the session. + Reason *string `json:"reason,omitempty"` + // Whether the effective managed policy requires an available sandbox backend. + Required bool `json:"required"` +} + // Register an absolute-time scheduled prompt. // Experimental: ScheduleAddAtRequest is part of an experimental API and may change or be // removed. @@ -11939,6 +12057,9 @@ type SessionOpenOptions struct { EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` // Whether shell-script safety heuristics are enabled. EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` + // Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by + // default. + EnableSkills *bool `json:"enableSkills,omitempty"` // Whether model responses stream as delta events. EnableStreaming *bool `json:"enableStreaming,omitempty"` // How MCP server environment values are interpreted. @@ -11962,6 +12083,16 @@ type SessionOpenOptions struct { ExpAssignments any `json:"expAssignments,omitempty"` // Feature-flag values resolved by the host. FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + // Whether the requesting SDK session has a skill provider. The provider remains ephemeral + // and is never persisted in session options or history. When enableSkills is false, it + // remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw + // sessions.open flows reject it because they cannot safely pre-register the callback + // handler. + // Experimental: HasSkillProvider is part of an experimental API and may change or be + // removed. + // Internal: HasSkillProvider is part of the SDK's internal API surface and is not intended + // for external use. + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` // Built-in subagent names to include in this session. When specified, only these built-ins // are available, subject to runtime availability and exclusions. Custom agents with the // same name remain available. @@ -12126,10 +12257,8 @@ func (SessionsOpenAttach) Kind() SessionOpenParamsKind { // Experimental: SessionsOpenCloud is part of an experimental API and may change or be // removed. type SessionsOpenCloud struct { - // In-process callback invoked when the cloud task is created (before connection). Marked - // internal because a function reference cannot cross the JSON-RPC boundary. Disappears in - // the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from - // 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. + // In-process callback invoked when the cloud task is created, before connection. Internal + // because function references cannot cross the JSON-RPC boundary. // Internal: OnTaskCreated is part of the SDK's internal API surface and is not intended for // external use. OnTaskCreated any `json:"onTaskCreated,omitempty"` @@ -12859,12 +12988,27 @@ type SessionsPruneOldRequest struct { OlderThanDays int64 `json:"olderThanDays"` } +// Pagination options for reading an inactive or active local session's persisted event +// journal. +// Experimental: SessionsReadPersistedEventsRequest is part of an experimental API and may +// change or be removed. +type SessionsReadPersistedEventsRequest struct { + // Opaque cursor returned by a previous persisted-event read. Omit on the first call. + Cursor *string `json:"cursor,omitempty"` + // Direction to page through persisted history. Forward starts at the beginning; backward + // starts with the newest events. Events in each page remain chronological. + Direction *EventsReadDirection `json:"direction,omitempty"` + // Maximum number of events to return in this batch (1–1000, default 200). + Max *int64 `json:"max,omitempty"` + // Session ID whose persisted event journal should be read. + SessionID string `json:"sessionId"` +} + // Optional registration options. // Experimental: SessionsRegisterExtensionToolsOnSessionOptions is part of an experimental // API and may change or be removed. type SessionsRegisterExtensionToolsOnSessionOptions struct { - // In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: - // replaced by runtime-side enable/disable RPCs in the SDK migration. + // In-process `() => boolean` gating callback used only by the CLI. // Internal: Enabled is part of the SDK's internal API surface and is not intended for // external use. Enabled any `json:"enabled,omitempty"` @@ -13048,8 +13192,9 @@ type SessionUpdateOptionsParams struct { EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` // Whether to enable cross-session store writes and reads. EnableSessionStore *bool `json:"enableSessionStore,omitempty"` - // Whether to enable skill directory scanning and loading. Falls back to - // enableConfigDiscovery when unset. + // Whether skill loading is enabled. Explicit false disables every source, including a bound + // SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, + // creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. EnableSkills *bool `json:"enableSkills,omitempty"` // Whether to stream model responses. EnableStreaming *bool `json:"enableStreaming,omitempty"` @@ -13467,6 +13612,66 @@ type SkillList struct { Skills []Skill `json:"skills"` } +// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched +// separately and lazily. +// Experimental: SkillProviderDescriptor is part of an experimental API and may change or be +// removed. +type SkillProviderDescriptor struct { + // Optional freeform argument hint used by slash-command catalogs. + ArgumentHint *string `json:"argumentHint,omitempty"` + // Description used in skill catalogs without fetching content. + Description string `json:"description"` + // Whether model invocation is disabled. Defaults to false. + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` + // Invocation and display name. + Name string `json:"name"` + // Whether users may invoke the skill directly. Defaults to true. + UserInvocable *bool `json:"userInvocable,omitempty"` +} + +// Identifies the target session. +// Experimental: SkillProviderListRequest is part of an experimental API and may change or +// be removed. +type SkillProviderListRequest struct { + // Target session identifier + SessionID string `json:"sessionId"` +} + +// Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to +// 1024 descriptors and 1 MiB of aggregate metadata. +// Experimental: SkillProviderListResult is part of an experimental API and may change or be +// removed. +// Internal: SkillProviderListResult is an internal SDK API and is not part of the public +// surface. +type SkillProviderListResult struct { + // Skill descriptors in provider order. Invocation names must be unique under + // case-insensitive comparison. + Skills []SkillProviderDescriptor `json:"skills"` +} + +// Identifies one SDK-provided skill by invocation name. +// Experimental: SkillProviderReadRequest is part of an experimental API and may change or +// be removed. +// Internal: SkillProviderReadRequest is an internal SDK API and is not part of the public +// surface. +type SkillProviderReadRequest struct { + // Invocation name of the skill to read. + Name string `json:"name"` + // Target session identifier + 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. +// 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 names to mark as disabled in global configuration, replacing any previous list. // Experimental: SkillsConfigSetDisabledSkillsRequest is part of an experimental API and may // change or be removed. @@ -13553,11 +13758,14 @@ type SkillsInvokedSkill struct { AllowedTools []string `json:"allowedTools,omitzero"` // Full content of the skill file Content string `json:"content"` + // Whether model invocation was disabled when this skill was invoked + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` // Turn number when the skill was invoked InvokedAtTurn int64 `json:"invokedAtTurn"` // Unique identifier for the skill Name string `json:"name"` - // Path to the SKILL.md file + // Path to the SKILL.md file, or an empty string for an SDK-provided skill without a + // filesystem identity Path string `json:"path"` } @@ -13691,6 +13899,8 @@ func (SlashCommandAgentPromptResult) Kind() SlashCommandInvocationResultKind { type SlashCommandCompletedResult struct { // Optional user-facing message describing the completed command Message *string `json:"message,omitempty"` + // Optional target session mode applied without submitting an agent prompt + Mode *SessionMode `json:"mode,omitempty"` // True when the invocation mutated user runtime settings; consumers caching settings should // refresh RuntimeSettingsChanged *bool `json:"runtimeSettingsChanged,omitempty"` @@ -13858,6 +14068,8 @@ type SubagentSettingsEntry struct { EffortLevel *string `json:"effortLevel,omitempty"` // Model override for matching subagents Model *string `json:"model,omitempty"` + // Whether the configured model strategy is preferred or required + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` } // Task completion notification with summary from the agent @@ -14721,14 +14933,12 @@ type UIElicitationStringOneOfFieldOneOf struct { // removed. type UIEphemeralQueryRequest struct { // In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. - // Marked internal: excluded from the public SDK surface. Replaced by an explicit - // cancellation token + cancel RPC in the SDK migration. + // Internal and excluded from the public SDK surface. // Internal: AbortSignal is part of the SDK's internal API surface and is not intended for // external use. AbortSignal any `json:"abortSignal,omitempty"` // In-process streaming callback `(text) => void` invoked with each token as the model emits - // it. Marked internal: excluded from the public SDK surface. In a process-separated SDK - // this is replaced by a streaming RPC that yields chunks and a final answer. + // it. Internal and excluded from the public SDK surface. // Internal: OnChunk is part of the SDK's internal API surface and is not intended for // external use. OnChunk any `json:"onChunk,omitempty"` @@ -15671,6 +15881,18 @@ const ( AgentInfoSourceUser AgentInfoSource = "user" ) +// Whether configured models are advisory preferences or required constraints +// Experimental: AgentModelPolicy is part of an experimental API and may change or be +// removed. +type AgentModelPolicy string + +const ( + // Treat the authored models as advisory preferences that callers may override. + AgentModelPolicyPreferred AgentModelPolicy = "preferred" + // Require subagent execution to use one of the authored models. + AgentModelPolicyRequired AgentModelPolicy = "required" +) + // Kind of attention required when status === "attention". Meaningful only when status === // "attention". // Experimental: AgentRegistryLiveTargetEntryAttentionKind is part of an experimental API @@ -15858,6 +16080,19 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) +// Routing preference used when the session model is `auto`. +// Experimental: AutoTier is part of an experimental API and may change or be removed. +type AutoTier string + +const ( + // Balance efficiency and intelligence. + AutoTierBalance AutoTier = "balance" + // Optimize for efficiency. + AutoTierEfficiency AutoTier = "efficiency" + // Optimize for intelligence. + AutoTierIntelligence AutoTier = "intelligence" +) + // Custom input-format kind. // Experimental: BuiltinToolFormatType is part of an experimental API and may change or be // removed. @@ -16099,14 +16334,20 @@ const ( CatalogNetworkFailureReasonConnectionRefused CatalogNetworkFailureReason = "connection-refused" // The authority's name could not be resolved. CatalogNetworkFailureReasonDns CatalogNetworkFailureReason = "dns" - // The authority returned a status the runtime treats as a failure. + // The authority returned another status the runtime treats as a failure. CatalogNetworkFailureReasonHTTPStatus CatalogNetworkFailureReason = "http-status" // No network is available, so nothing was attempted. CatalogNetworkFailureReasonOffline CatalogNetworkFailureReason = "offline" + // The configured proxy returned 407 and requires authentication. + CatalogNetworkFailureReasonProxyAuthenticationRequired CatalogNetworkFailureReason = "proxy-authentication-required" + // The authority rate-limited requests and supplied or implied a bounded cooldown. + CatalogNetworkFailureReasonRateLimited CatalogNetworkFailureReason = "rate-limited" // A redirect was refused by the runtime's redirect policy. CatalogNetworkFailureReasonRedirectRejected CatalogNetworkFailureReason = "redirect-rejected" // The response exceeded the permitted size. CatalogNetworkFailureReasonResponseTooLarge CatalogNetworkFailureReason = "response-too-large" + // The authority returned a transient 5xx response. + CatalogNetworkFailureReasonServiceUnavailable CatalogNetworkFailureReason = "service-unavailable" // The request exceeded its time budget. CatalogNetworkFailureReasonTimeout CatalogNetworkFailureReason = "timeout" // The TLS handshake or certificate validation failed. @@ -16569,6 +16810,7 @@ const ( FactoryRunFailureTypeFactoryAccountingIncomplete FactoryRunFailureType = "factory_accounting_incomplete" FactoryRunFailureTypeFactoryDurableFailure FactoryRunFailureType = "factory_durable_failure" FactoryRunFailureTypeFactoryLimitReached FactoryRunFailureType = "factory_limit_reached" + FactoryRunFailureTypeFactoryProviderDisconnected FactoryRunFailureType = "factory_provider_disconnected" FactoryRunFailureTypeFactoryResumeDeclined FactoryRunFailureType = "factory_resume_declined" ) @@ -16722,7 +16964,24 @@ const ( HMACAuthInfoHostHTTPSGitHubCom HMACAuthInfoHost = "https://github.com" ) -// Hook event name dispatched through the SDK callback transport. +// Configuration tier that contributed a discovered hook action. +// Experimental: HookOrigin is part of an experimental API and may change or be removed. +type HookOrigin string + +const ( + // Hook provided by an enabled installed or explicit plugin. Projectless rows omit + // projectPath and do not expand a project directory. + HookOriginPlugin HookOrigin = "plugin" + // Hook enforced by centrally managed policy. + HookOriginPolicy HookOrigin = "policy" + // Hook loaded from repository settings or the repository hook directory. + HookOriginRepository HookOrigin = "repository" + // Hook loaded from user settings or the user's hook directory. + HookOriginUser HookOrigin = "user" +) + +// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally +// support callback-only events. // Experimental: HookType is part of an experimental API and may change or be removed. type HookType string @@ -18627,7 +18886,7 @@ const ( SkillDiscoveryScopeProject SkillDiscoveryScope = "project" ) -// Source location type (e.g., project, personal-copilot, plugin, builtin) +// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) // Experimental: SkillSource is part of an experimental API and may change or be removed. type SkillSource string @@ -18646,6 +18905,8 @@ const ( SkillSourcePlugin SkillSource = "plugin" // Skill defined in the current project's skill directories. SkillSourceProject SkillSource = "project" + // Pathless skill supplied lazily by an SDK skill provider. + SkillSourceSDK SkillSource = "sdk" ) // Optional completion hint for the input (e.g. 'directory' for filesystem path completion) @@ -19286,6 +19547,32 @@ func (a *ServerExtensionsAPI) Enable(ctx context.Context, params *DiscoveredExte return &result, nil } +// Experimental: ServerHooksAPI contains experimental APIs that may change or be removed. +type ServerHooksAPI serverAPI + +// Discovers hook actions enabled under server-side discovery settings from user, +// repository, plugin, and managed-policy sources. +// +// RPC method: hooks.discover. +// +// Parameters: Optional project paths and host-exclusion behavior for server-scoped hook +// discovery. +// +// Returns: Server-discovered hook actions and partial-load diagnostics from user, +// repository, plugin, and managed-policy sources. Concrete sessions may include additional +// session-specific hook sources. +func (a *ServerHooksAPI) Discover(ctx context.Context, params *HooksDiscoverRequest) (*HooksDiscoverResult, error) { + raw, err := a.client.Request(ctx, "hooks.discover", params) + if err != nil { + return nil, err + } + var result HooksDiscoverResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ServerInstructionsAPI contains experimental APIs that may change or be // removed. type ServerInstructionsAPI serverAPI @@ -20270,6 +20557,31 @@ func (a *ServerSessionsAPI) PruneOld(ctx context.Context, params *SessionsPruneO return &result, nil } +// ReadPersistedEvents reads a page of durable events directly from a local session's +// persisted journal without creating, resuming, or activating the session. The initial +// backward read uses a bounded tail scan for fast first paint; cursor continuations +// preserve the session event-log paging semantics. Persisted events may omit payloads that +// are reconstructed only for an active session. +// +// RPC method: sessions.readPersistedEvents. +// +// Parameters: Pagination options for reading an inactive or active local session's +// persisted event journal. +// +// Returns: Batch of session events returned by a read, with cursor and continuation +// metadata. +func (a *ServerSessionsAPI) ReadPersistedEvents(ctx context.Context, params *SessionsReadPersistedEventsRequest) (*EventsReadResult, error) { + raw, err := a.client.Request(ctx, "sessions.readPersistedEvents", params) + if err != nil { + return nil, err + } + var result EventsReadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // ReleaseLock releases the in-use lock held by this process for a session. // // RPC method: sessions.releaseLock. @@ -20643,6 +20955,7 @@ type ServerRPC struct { Catalog *ServerCatalogAPI Commands *ServerCommandsAPI Extensions *ServerExtensionsAPI + Hooks *ServerHooksAPI Instructions *ServerInstructionsAPI LlmInference *ServerLlmInferenceAPI ManagedSettings *ServerManagedSettingsAPI @@ -20681,7 +20994,7 @@ func (a *ServerRPC) Ping(ctx context.Context, params *PingRequest) (*PingResult, // RegisterExtensionLaunchProvider registers the calling SDK client as the per-entrypoint // extension launch provider. Call before creating any sessions. When omitted, the runtime -// temporarily falls back to its built-in Node launcher for backward compatibility. +// uses its built-in extension launcher. // // RPC method: registerExtensionLaunchProvider. // Experimental: RegisterExtensionLaunchProvider is an experimental API and may change or be @@ -20707,6 +21020,7 @@ func NewServerRPC(client *jsonrpc2.Client) *ServerRPC { r.Catalog = (*ServerCatalogAPI)(&r.common) r.Commands = (*ServerCommandsAPI)(&r.common) r.Extensions = (*ServerExtensionsAPI)(&r.common) + r.Hooks = (*ServerHooksAPI)(&r.common) r.Instructions = (*ServerInstructionsAPI)(&r.common) r.LlmInference = (*ServerLlmInferenceAPI)(&r.common) r.ManagedSettings = (*ServerManagedSettingsAPI)(&r.common) @@ -21250,6 +21564,9 @@ func (a *CommandsAPI) Enqueue(ctx context.Context, params *EnqueueCommandParams) req := map[string]any{"sessionId": a.sessionID} if params != nil { req["command"] = params.Command + if params.DisplayText != nil { + req["displayText"] = *params.DisplayText + } } raw, err := a.client.Request(ctx, "session.commands.enqueue", req) if err != nil { @@ -25037,6 +25354,28 @@ func (a *RemoteAPI) NotifySteerableChanged(ctx context.Context, params *RemoteNo return &result, nil } +// Experimental: SandboxAPI contains experimental APIs that may change or be removed. +type SandboxAPI sessionAPI + +// GetEnforcementStatus returns whether managed policy requires sandbox enforcement and +// whether an enforcement failure has permanently blocked the session. +// +// RPC method: session.sandbox.getEnforcementStatus. +// +// Returns: Managed sandbox enforcement state for a session. +func (a *SandboxAPI) GetEnforcementStatus(ctx context.Context) (*SandboxEnforcementStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.sandbox.getEnforcementStatus", req) + if err != nil { + return nil, err + } + var result SandboxEnforcementStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ScheduleAPI contains experimental APIs that may change or be removed. type ScheduleAPI sessionAPI @@ -26586,6 +26925,7 @@ type SessionRPC struct { Provider *ProviderAPI Queue *QueueAPI Remote *RemoteAPI + Sandbox *SandboxAPI Schedule *ScheduleAPI Shell *ShellAPI Skills *SkillsAPI @@ -26907,6 +27247,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Provider = (*ProviderAPI)(&r.common) r.Queue = (*QueueAPI)(&r.common) r.Remote = (*RemoteAPI)(&r.common) + r.Sandbox = (*SandboxAPI)(&r.common) r.Schedule = (*ScheduleAPI)(&r.common) r.Shell = (*ShellAPI)(&r.common) r.Skills = (*SkillsAPI)(&r.common) @@ -27411,6 +27752,9 @@ func (a *InternalModelAPI) ApplyStartupOverlay(ctx context.Context, params *Mode if params.DeviceManagedModel != nil { req["deviceManagedModel"] = *params.DeviceManagedModel } + if params.PolicyHelperModel != nil { + req["policyHelperModel"] = *params.PolicyHelperModel + } if params.RepoContextTier != nil { req["repoContextTier"] = *params.RepoContextTier } diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 78788660f5..9242a889a8 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -1606,6 +1606,12 @@ func unmarshalFactoryRunFailure(data []byte) (FactoryRunFailure, error) { return nil, err } return &d, nil + case FactoryRunFailureTypeFactoryProviderDisconnected: + var d FactoryRunFailureFactoryProviderDisconnected + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case FactoryRunFailureTypeFactoryResumeDeclined: var d FactoryRunFailureFactoryResumeDeclined if err := json.Unmarshal(data, &d); err != nil { @@ -1661,6 +1667,17 @@ func (r FactoryRunFailureFactoryLimitReached) MarshalJSON() ([]byte, error) { }) } +func (r FactoryRunFailureFactoryProviderDisconnected) MarshalJSON() ([]byte, error) { + type alias FactoryRunFailureFactoryProviderDisconnected + return json.Marshal(struct { + Type FactoryRunFailureType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r FactoryRunFailureFactoryResumeDeclined) MarshalJSON() ([]byte, error) { type alias FactoryRunFailureFactoryResumeDeclined return json.Marshal(struct { @@ -1698,6 +1715,7 @@ func (r *FactoryRunTerminal) UnmarshalJSON(data []byte) error { func (r *FactoryRunResult) UnmarshalJSON(data []byte) error { type rawFactoryRunResult struct { + Attempt *int64 `json:"attempt,omitempty"` Error *string `json:"error,omitempty"` Failure json.RawMessage `json:"failure,omitempty"` Reason *string `json:"reason,omitempty"` @@ -1710,6 +1728,7 @@ func (r *FactoryRunResult) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &raw); err != nil { return err } + r.Attempt = raw.Attempt r.Error = raw.Error if raw.Failure != nil { value, err := unmarshalFactoryRunFailure(raw.Failure) @@ -5382,6 +5401,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` + EnableSkills *bool `json:"enableSkills,omitempty"` EnableStreaming *bool `json:"enableStreaming,omitempty"` EnvValueMode *SessionOpenOptionsEnvValueMode `json:"envValueMode,omitempty"` EventsLogDirectory *string `json:"eventsLogDirectory,omitempty"` @@ -5390,6 +5410,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { ExcludedTools []string `json:"excludedTools,omitzero"` ExpAssignments any `json:"expAssignments,omitempty"` FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` IncludedBuiltinSkills []string `json:"includedBuiltinSkills,omitzero"` InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` @@ -5462,6 +5483,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.EnableManagedSettings = raw.EnableManagedSettings r.EnableOnDemandInstructionDiscovery = raw.EnableOnDemandInstructionDiscovery r.EnableScriptSafety = raw.EnableScriptSafety + r.EnableSkills = raw.EnableSkills r.EnableStreaming = raw.EnableStreaming r.EnvValueMode = raw.EnvValueMode r.EventsLogDirectory = raw.EventsLogDirectory @@ -5470,6 +5492,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.ExcludedTools = raw.ExcludedTools r.ExpAssignments = raw.ExpAssignments r.FeatureFlags = raw.FeatureFlags + r.HasSkillProvider = raw.HasSkillProvider r.IncludedBuiltinAgents = raw.IncludedBuiltinAgents r.IncludedBuiltinSkills = raw.IncludedBuiltinSkills r.InstalledPlugins = raw.InstalledPlugins diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index 4c03a42c00..bbe9fbc74b 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -47,6 +47,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeAssistantFusionPhaseActivity: + var d AssistantFusionPhaseActivityData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeAssistantFusionPhaseCompleted: var d AssistantFusionPhaseCompletedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -443,6 +449,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionCompletionReceipt: + var d SessionCompletionReceiptData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionContextChanged: var d SessionContextChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -575,6 +587,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionModeNoticeDelivered: + var d SessionModeNoticeDeliveredData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionPermissionsChanged: var d SessionPermissionsChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -858,6 +876,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { Delivery *UserMessageDelivery `json:"delivery,omitempty"` InteractionID *string `json:"interactionId,omitempty"` IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` + MessageID *string `json:"messageId,omitempty"` NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitzero"` ParentAgentTaskID *string `json:"parentAgentTaskId,omitempty"` Source *string `json:"source,omitempty"` @@ -884,6 +903,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { r.Delivery = raw.Delivery r.InteractionID = raw.InteractionID r.IsAutopilotContinuation = raw.IsAutopilotContinuation + r.MessageID = raw.MessageID r.NativeDocumentPathFallbackPaths = raw.NativeDocumentPathFallbackPaths r.ParentAgentTaskID = raw.ParentAgentTaskID r.Source = raw.Source @@ -2095,6 +2115,7 @@ func (r PermissionPromptRequestWrite) MarshalJSON() ([]byte, error) { func (r *PermissionRequestedData) UnmarshalJSON(data []byte) error { type rawPermissionRequestedData struct { + AgentMode *SessionMode `json:"agentMode,omitempty"` PermissionRequest json.RawMessage `json:"permissionRequest"` PromptRequest json.RawMessage `json:"promptRequest,omitempty"` RequestID string `json:"requestId"` @@ -2105,6 +2126,7 @@ func (r *PermissionRequestedData) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &raw); err != nil { return err } + r.AgentMode = raw.AgentMode if raw.PermissionRequest != nil { value, err := unmarshalPermissionRequest(raw.PermissionRequest) if err != nil { diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 7f24ab6283..63709921d8 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -55,6 +55,9 @@ type SessionEventType string const ( SessionEventTypeAbort SessionEventType = "abort" SessionEventTypeAgentInterrupted SessionEventType = "agent.interrupted" + // Experimental: SessionEventTypeAssistantFusionPhaseActivity identifies an experimental + // event that may change or be removed. + SessionEventTypeAssistantFusionPhaseActivity SessionEventType = "assistant.fusion_phase_activity" // Experimental: SessionEventTypeAssistantFusionPhaseCompleted identifies an experimental // event that may change or be removed. SessionEventTypeAssistantFusionPhaseCompleted SessionEventType = "assistant.fusion_phase_completed" @@ -146,9 +149,12 @@ const ( SessionEventTypeSessionCanvasRemoved SessionEventType = "session.canvas.removed" // Experimental: SessionEventTypeSessionCanvasUnavailable identifies an experimental event // that may change or be removed. - SessionEventTypeSessionCanvasUnavailable SessionEventType = "session.canvas.unavailable" - SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" - SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" + SessionEventTypeSessionCanvasUnavailable SessionEventType = "session.canvas.unavailable" + SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" + SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" + // Experimental: SessionEventTypeSessionCompletionReceipt identifies an experimental event + // that may change or be removed. + SessionEventTypeSessionCompletionReceipt SessionEventType = "session.completion_receipt" SessionEventTypeSessionContextChanged SessionEventType = "session.context_changed" SessionEventTypeSessionContextCleared SessionEventType = "session.context_cleared" SessionEventTypeSessionCustomAgentsUpdated SessionEventType = "session.custom_agents_updated" @@ -183,6 +189,7 @@ const ( SessionEventTypeSessionMCPServerStatusChanged SessionEventType = "session.mcp_server_status_changed" SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" SessionEventTypeSessionModelChange SessionEventType = "session.model_change" + SessionEventTypeSessionModeNoticeDelivered SessionEventType = "session.mode_notice_delivered" // Experimental: SessionEventTypeSessionPermissionsChanged identifies an experimental event // that may change or be removed. SessionEventTypeSessionPermissionsChanged SessionEventType = "session.permissions_changed" @@ -473,6 +480,32 @@ func (*SessionAutopilotObjectiveChangedData) Type() SessionEventType { return SessionEventTypeSessionAutopilotObjectiveChanged } +// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +// Experimental: SessionCompletionReceiptData is part of an experimental API and may change or be removed. +type SessionCompletionReceiptData struct { + // One-based accepted completion receipt ordinal in the durable session history. + Attempt int64 `json:"attempt"` + // Inclusive durable event range summarized by this receipt. + EventRange CompletionReceiptEventRange `json:"eventRange"` + // Number of failed structured tool completions in the covered range. + FailedToolCount int64 `json:"failedToolCount"` + // Final structured tool completion in the covered range, when one exists. + FinalTool *CompletionReceiptFinalTool `json:"finalTool,omitempty"` + // Version of the completion receipt payload. + SchemaVersion int64 `json:"schemaVersion"` + // Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + SourceEventID string `json:"sourceEventId"` + // Runtime reason the completion decision was accepted. + StopReason CompletionReceiptStopReason `json:"stopReason"` + // Number of successful structured tool completions in the covered range. + SuccessfulToolCount int64 `json:"successfulToolCount"` +} + +func (*SessionCompletionReceiptData) sessionEventData() {} +func (*SessionCompletionReceiptData) Type() SessionEventType { + return SessionEventTypeSessionCompletionReceipt +} + // Canonical bytes for a content-addressed binary asset shared by reference across events type SessionBinaryAssetData struct { // Content-addressed id for this binary asset (e.g. "sha256:..."). @@ -768,7 +801,7 @@ func (*PendingMessagesModifiedData) Type() SessionEventType { return SessionEventTypePendingMessagesModified } -// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. // Experimental: SessionManagedSettingsResolvedData is part of an experimental API and may change or be removed. type SessionManagedSettingsResolvedData struct { // Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. @@ -783,13 +816,15 @@ type SessionManagedSettingsResolvedData struct { ManagedKeys []string `json:"managedKeys"` // Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. PermissionsAllowIntersected *bool `json:"permissionsAllowIntersected,omitempty"` + // Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + PolicyHelperManaged *bool `json:"policyHelperManaged,omitempty"` // Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. SandboxEnabledByUndeterminedPolicy *bool `json:"sandboxEnabledByUndeterminedPolicy,omitempty"` // Whether the server (account/org) managed-settings layer was present ServerManaged bool `json:"serverManaged"` // The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. Settings any `json:"settings,omitempty"` - // Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + // Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. Source ManagedSettingsResolvedSource `json:"source"` } @@ -880,6 +915,34 @@ type SessionErrorData struct { func (*SessionErrorData) sessionEventData() {} func (*SessionErrorData) Type() SessionEventType { return SessionEventTypeSessionError } +// Experimental content-safe activity signal for a running HydraFusion phase. +// Experimental: AssistantFusionPhaseActivityData is part of an experimental API and may change or be removed. +type AssistantFusionPhaseActivityData struct { + // Kind of real activity observed. + Activity FusionPhaseActivityKind `json:"activity"` + // Conversation scope in which the phase executes. + ConversationScope FusionConversationScope `json:"conversationScope"` + // Identifier of the HydraFusion turn containing the phase. + FusionID string `json:"fusionId"` + // HydraFusion orchestration pattern containing the phase. + Pattern FusionPattern `json:"pattern"` + // Stable identifier for the concrete phase. + PhaseID string `json:"phaseId"` + // Kind of phase currently executing. + PhaseKind FusionPhaseKind `json:"phaseKind"` + // Semantic role assigned to the phase. + Role string `json:"role"` + // Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + ToolCallID *string `json:"toolCallId,omitempty"` + // Cumulative private response bytes observed for this model call. The event never includes response text. + TotalResponseSizeBytes *int64 `json:"totalResponseSizeBytes,omitempty"` +} + +func (*AssistantFusionPhaseActivityData) sessionEventData() {} +func (*AssistantFusionPhaseActivityData) Type() SessionEventType { + return SessionEventTypeAssistantFusionPhaseActivity +} + // Experimental durable HydraFusion phase output and lossless replay checkpoint. // Experimental: AssistantFusionPhaseCompletedData is part of an experimental API and may change or be removed. type AssistantFusionPhaseCompletedData struct { @@ -1042,6 +1105,9 @@ type SessionFusionResolvedData struct { ModelUniverseVersion *string `json:"modelUniverseVersion,omitempty"` // Validated orchestration pattern selected for the turn. Pattern FusionPattern `json:"pattern"` + // Presentation-neutral phase plan for clients that render workflow progress. + // Experimental: PhasePlan is part of an experimental API and may change or be removed. + PhasePlan []FusionPhasePlanStep `json:"phasePlan,omitzero"` // Version of the validated execution-plan format. PlanVersion *string `json:"planVersion,omitempty"` // HydraFusion routing policy used to resolve the plan. @@ -1258,7 +1324,7 @@ type HookStartData struct { HookInvocationID string `json:"hookInvocationId"` // Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") HookType string `json:"hookType"` - // Input data passed to the hook + // Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. Input any `json:"input,omitempty"` // Tool call ID of the parent tool invocation when this event originates from a sub-agent ParentToolCallID *string `json:"parentToolCallId,omitempty"` @@ -1814,6 +1880,8 @@ type UserMessageData struct { InteractionID *string `json:"interactionId,omitempty"` // True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` + // Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + MessageID *string `json:"messageId,omitempty"` // Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitzero"` // Parent agent task ID for background telemetry correlated to this user turn @@ -1846,6 +1914,8 @@ func (*PermissionCompletedData) Type() SessionEventType { return SessionEventTyp // Permission request notification requiring client approval with request details type PermissionRequestedData struct { + // Agent mode captured from the owning turn when permission evaluation began. + AgentMode *SessionMode `json:"agentMode,omitempty"` // Details of the permission being requested PermissionRequest PermissionRequest `json:"permissionRequest"` // Derived user-facing permission prompt details for UI consumers @@ -1960,6 +2030,19 @@ type CommandQueuedData struct { func (*CommandQueuedData) sessionEventData() {} func (*CommandQueuedData) Type() SessionEventType { return SessionEventTypeCommandQueued } +// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +type SessionModeNoticeDeliveredData struct { + // Model-visible transition notice persisted for a mid-turn delivery + Content *string `json:"content,omitempty"` + // Mode established by the delivered transition notice + Mode SessionMode `json:"mode"` +} + +func (*SessionModeNoticeDeliveredData) sessionEventData() {} +func (*SessionModeNoticeDeliveredData) Type() SessionEventType { + return SessionEventTypeSessionModeNoticeDelivered +} + // Registered command dispatch request routed to the owning client type CommandExecuteData struct { // Raw argument string after the command name @@ -2128,6 +2211,8 @@ func (*SessionHandoffData) Type() SessionEventType { return SessionEventTypeSess type SessionStartData struct { // Whether the session was already in use by another client at start time AlreadyInUse *bool `json:"alreadyInUse,omitempty"` + // Auto routing preference selected at session creation time + AutoTier *AutoTier `json:"autoTier,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) @@ -2206,6 +2291,8 @@ func (*SessionSessionLimitsChangedData) Type() SessionEventType { type SessionResumeData struct { // Whether the session was already in use by another client at resume time AlreadyInUse *bool `json:"alreadyInUse,omitempty"` + // Auto routing preference active at resume time + AutoTier *AutoTier `json:"autoTier,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 @@ -2315,17 +2402,19 @@ type SkillInvokedData struct { Content string `json:"content"` // Description of the skill from its SKILL.md frontmatter Description *string `json:"description,omitempty"` + // Whether model invocation is disabled for this skill + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` // Model identifier active when the skill was invoked, when known Model *string `json:"model,omitempty"` // Name of the invoked skill Name string `json:"name"` - // File path to the SKILL.md definition + // File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity Path string `json:"path"` // Name of the plugin this skill originated from, when applicable PluginName *string `json:"pluginName,omitempty"` // Version of the plugin this skill originated from, when applicable PluginVersion *string `json:"pluginVersion,omitempty"` - // Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + // Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) Source *string `json:"source,omitempty"` // What triggered the skill invocation: `user-invoked` (explicit user action, such as via a slash command or UI affordance), `agent-invoked` (agent requested the skill), or `context-load` (loaded as part of another context, such as preloading skills configured on a custom agent or subagent) Trigger *SkillInvokedTrigger `json:"trigger,omitempty"` @@ -2439,6 +2528,8 @@ type SubagentCompletedData struct { FirstDispatchedModel *string `json:"firstDispatchedModel,omitempty"` // Model used by the sub-agent Model *string `json:"model,omitempty"` + // Why an explicit task-call model did not become the effective model + ModelOverrideReason *string `json:"modelOverrideReason,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent ToolCallID string `json:"toolCallId"` // Total tokens (input + output) consumed by the sub-agent @@ -2472,6 +2563,8 @@ type SubagentFailedData struct { FirstDispatchedModel *string `json:"firstDispatchedModel,omitempty"` // Model selected for the sub-agent, when known Model *string `json:"model,omitempty"` + // Why an explicit task-call model did not become the effective model + ModelOverrideReason *string `json:"modelOverrideReason,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent ToolCallID string `json:"toolCallId"` // Total tokens (input + output) consumed before the sub-agent failed @@ -2783,7 +2876,7 @@ func (*SessionWorkspaceFileChangedData) Type() SessionEventType { // Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping // Experimental: AssistantMessageReasoningBlocks is part of an experimental API and may change or be removed. type AssistantMessageReasoningBlocks struct { - // Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + // Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. Blocks []any `json:"blocks,omitzero"` // Model provider that produced these reasoning blocks. Provider string `json:"provider"` @@ -2808,6 +2901,8 @@ type AssistantMessageServerTools struct { type AssistantMessageToolRequest struct { // Arguments to pass to the tool, format depends on the tool Arguments any `json:"arguments,omitempty"` + // Hosted program that requested this client tool call + Caller *AssistantMessageToolRequestCaller `json:"caller,omitempty"` // Resolved intention summary describing what this specific call does IntentionSummary *string `json:"intentionSummary,omitempty"` // Name of the MCP server hosting this tool, when the tool is an MCP tool @@ -2824,6 +2919,14 @@ type AssistantMessageToolRequest struct { Type *AssistantMessageToolRequestType `json:"type,omitempty"` } +// Hosted program that requested this client tool call +type AssistantMessageToolRequestCaller struct { + // Provider-assigned identifier for the hosted caller. + CallerID string `json:"callerId"` + // Kind of hosted caller that requested the client tool call. + Type AssistantMessageToolRequestCallerType `json:"type"` +} + // Per-request cost and usage data from the CAPI copilot_usage response field type AssistantUsageCopilotUsage struct { // Itemized token usage breakdown @@ -3093,7 +3196,27 @@ type CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail struct { TokenType string `json:"tokenType"` } -// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +// Inclusive durable event range summarized by a completion receipt. +type CompletionReceiptEventRange struct { + // Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + EndEventID string `json:"endEventId"` + // Identifier of the user message that starts the covered exchange. + StartEventID string `json:"startEventId"` +} + +// Final structured tool completion in the covered event range. +type CompletionReceiptFinalTool struct { + // Process exit code from a structured shell result, when available. + ExitCode *int64 `json:"exitCode,omitempty"` + // Structured success or failure status from the tool completion event. + Status CompletionReceiptToolStatus `json:"status"` + // Unique identifier of the completed tool call. + ToolCallID string `json:"toolCallId"` + // Tool name from the matching tool execution start event, when available. + ToolName *string `json:"toolName,omitempty"` +} + +// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. type CustomAgentsUpdatedAgent struct { // Description of what the agent does Description string `json:"description"` @@ -3103,6 +3226,10 @@ type CustomAgentsUpdatedAgent struct { ID string `json:"id"` // Model override for this agent, if set Model *string `json:"model,omitempty"` + // Whether authored models are preferences or required constraints + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` + // Authored model ids in priority order, if configured + Models []string `json:"models,omitzero"` // Internal name of the agent Name string `json:"name"` // Source location: user, project, inherited, remote, or plugin @@ -3179,6 +3306,19 @@ type FusionFollowUpRecommendation struct { UserTurn FusionFollowUpAction `json:"userTurn"` } +// Presentation-neutral phase planned for a HydraFusion turn. +// Experimental: FusionPhasePlanStep is part of an experimental API and may change or be removed. +type FusionPhasePlanStep struct { + // Whether the phase executes only when an earlier phase requests it. + Conditional bool `json:"conditional"` + // Kind of phase that may execute. + Kind FusionPhaseKind `json:"kind"` + // Semantic role assigned to the phase. + Role string `json:"role"` + // Conversation scope in which the phase executes. + Scope FusionConversationScope `json:"scope"` +} + // Aggregate concrete-model usage for one HydraFusion phase. // Experimental: FusionPhaseUsage is part of an experimental API and may change or be removed. type FusionPhaseUsage struct { @@ -4325,7 +4465,7 @@ type SkillsLoadedSkill struct { Name string `json:"name"` // Absolute path to the skill file, if available Path *string `json:"path,omitempty"` - // Source location type (e.g., project, personal-copilot, plugin, builtin) + // Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) Source SkillSource `json:"source"` // Whether the skill can be invoked by the user as a slash command UserInvocable bool `json:"userInvocable"` @@ -4856,6 +4996,13 @@ const ( AgentInterruptedCancelPhasePreFirstToken AgentInterruptedCancelPhase = "pre_first_token" ) +// Hosted program caller type +type AssistantMessageToolRequestCallerType string + +const ( + AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestCallerType = "program" +) + // Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. type AssistantMessageToolRequestType string @@ -5031,6 +5178,34 @@ const ( CompactionTriggerThreshold CompactionTrigger = "threshold" ) +// Runtime reason the completion decision was accepted. +type CompletionReceiptStopReason string + +const ( + // The configured agentStop continuation limit was reached. + CompletionReceiptStopReasonAgentStopBlockLimit CompletionReceiptStopReason = "agent_stop_block_limit" + // The model reached a natural terminal response. + CompletionReceiptStopReasonNatural CompletionReceiptStopReason = "natural" + // A terminal tool ended the interaction. + CompletionReceiptStopReasonTerminalTool CompletionReceiptStopReason = "terminal_tool" +) + +// Structured terminal status from a tool completion event. +type CompletionReceiptToolStatus string + +const ( + // The permissions service denied the tool call. + CompletionReceiptToolStatusDenied CompletionReceiptToolStatus = "denied" + // The tool failed without a more specific structured status. + CompletionReceiptToolStatusFailure CompletionReceiptToolStatus = "failure" + // The user rejected the tool call. + CompletionReceiptToolStatusRejected CompletionReceiptToolStatus = "rejected" + // The tool completed successfully. + CompletionReceiptToolStatusSuccess CompletionReceiptToolStatus = "success" + // The tool exceeded its time budget. + CompletionReceiptToolStatusTimeout CompletionReceiptToolStatus = "timeout" +) + // The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) type ElicitationCompletedAction string @@ -5161,6 +5336,19 @@ const ( FusionPatternSingle FusionPattern = "single" ) +// Content-safe activity observed while a HydraFusion phase is running. +// Experimental: FusionPhaseActivityKind is part of an experimental API and may change or be removed. +type FusionPhaseActivityKind string + +const ( + // The provider produced additional private output bytes. + FusionPhaseActivityKindModelOutput FusionPhaseActivityKind = "model_output" + // A tool finished executing inside the phase. + FusionPhaseActivityKindToolCompleted FusionPhaseActivityKind = "tool_completed" + // A tool began executing inside the phase. + FusionPhaseActivityKindToolStarted FusionPhaseActivityKind = "tool_started" +) + // HydraFusion phase kind. // Experimental: FusionPhaseKind is part of an experimental API and may change or be removed. type FusionPhaseKind string @@ -5263,10 +5451,12 @@ const ( ManagedSettingsResolvedSourceClient ManagedSettingsResolvedSource = "client" // Only the device MDM/plist/registry/file channel contributed. ManagedSettingsResolvedSourceDevice ManagedSettingsResolvedSource = "device" - // More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + // More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. ManagedSettingsResolvedSourceMixed ManagedSettingsResolvedSource = "mixed" // No managed policy is in force (no channel contributed). ManagedSettingsResolvedSourceNone ManagedSettingsResolvedSource = "none" + // A policy helper registered by device or server policy contributed. Device registration takes priority when present. + ManagedSettingsResolvedSourcePolicyHelper ManagedSettingsResolvedSource = "policyHelper" // Only the server/account channel contributed. ManagedSettingsResolvedSourceServer ManagedSettingsResolvedSource = "server" ) diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go index ee9258b225..96bf53bb5e 100644 --- a/go/session_event_serialization_test.go +++ b/go/session_event_serialization_test.go @@ -14,6 +14,50 @@ var _ SessionEventData = (*rpc.UserMessageData)(nil) var _ rpc.EmbeddedTextResourceContents = EmbeddedTextResourceContents{} var _ EmbeddedTextResourceContents = rpc.EmbeddedTextResourceContents{} +func TestSessionEventAutoTier(t *testing.T) { + for _, eventType := range []string{"session.start", "session.resume"} { + for _, tier := range []AutoTier{"", AutoTierEfficiency, AutoTierBalance, AutoTierIntelligence} { + t.Run(eventType+"/"+string(tier), func(t *testing.T) { + data := map[string]any{ + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1, + } + if tier != "" { + data["autoTier"] = tier + } + wire, err := json.Marshal(map[string]any{ + "id": "00000000-0000-0000-0000-000000000001", + "timestamp": "2026-08-28T00:00:00Z", "parentId": nil, + "type": eventType, "data": data, + }) + if err != nil { + t.Fatal(err) + } + var event SessionEvent + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + var actual *AutoTier + switch eventType { + case "session.start": + actual = event.Data.(*SessionStartData).AutoTier + case "session.resume": + actual = event.Data.(*SessionResumeData).AutoTier + } + if tier == "" { + if actual != nil { + t.Fatalf("expected omitted autoTier, got %v", *actual) + } + } else if actual == nil || *actual != tier { + t.Fatalf("expected autoTier %q, got %v", tier, actual) + } + }) + } + } +} + func TestSessionEventAgentIDRoundTripsKnownEvent(t *testing.T) { var event SessionEvent if err := json.Unmarshal([]byte(`{ diff --git a/go/types.go b/go/types.go index 1d98e06158..772202a0bc 100644 --- a/go/types.go +++ b/go/types.go @@ -193,6 +193,13 @@ type ClientOptions struct { // directory are accessible from GitHub web and mobile. // Ignored when connecting to an existing runtime via [URIConnection]. EnableRemoteSessions bool + // ClientInfo declares the integrating application's identity, forwarded to the + // runtime on the `server.connect` handshake. Declaring it lets the + // telemetry the runtime emits on this connection be attributed to a + // consistent surface (the application and its Copilot integration) instead of + // the runtime's own build. All fields are optional; leave it nil to keep the + // runtime's default attribution. + ClientInfo *ClientInfo // Mode controls the default tool surface and feature flags presented to // sessions created by this client. The zero value ([ModeCopilotCli]) // matches legacy CLI defaults. Set to [ModeEmpty] to opt in to @@ -204,6 +211,54 @@ type ClientOptions struct { Mode ClientMode } +// ClientInfo identifies the integrating application on the `server.connect` handshake. +// +// Declaring it lets the telemetry the runtime emits on the connection be +// attributed to a single, consistent surface instead of the runtime's own +// build. All fields are optional; an empty field is omitted from the handshake. +type ClientInfo struct { + // ApplicationName is the name of the application using the SDK. + ApplicationName string + // ApplicationVersion is the version of the application using the SDK. + ApplicationVersion string + // IntegrationName optionally identifies a specific integration within the + // application, such as an extension or plugin. + IntegrationName string + // IntegrationVersion is the optional version of the named integration. + IntegrationVersion string +} + +// toWire maps the public [ClientInfo] onto the generated connect wire shape, +// omitting empty fields. It returns nil when no identity was supplied so the +// caller drops the clientInfo field and keeps the runtime's default attribution. +func (ci *ClientInfo) toWire() *rpc.ConnectClientInfo { + if ci == nil { + return nil + } + wire := &rpc.ConnectClientInfo{} + populated := false + if ci.ApplicationName != "" { + wire.EditorName = &ci.ApplicationName + populated = true + } + if ci.ApplicationVersion != "" { + wire.EditorVersion = &ci.ApplicationVersion + populated = true + } + if ci.IntegrationName != "" { + wire.ExtensionName = &ci.IntegrationName + populated = true + } + if ci.IntegrationVersion != "" { + wire.ExtensionVersion = &ci.IntegrationVersion + populated = true + } + if !populated { + return nil + } + return wire +} + // CloudSessionRepository is GitHub repository metadata associated with a cloud session. type CloudSessionRepository struct { Owner string `json:"owner"` @@ -1251,6 +1306,16 @@ type GitHubMCPToolConfig struct { DisableFormDeferral *bool `json:"disableFormDeferral,omitempty"` } +// AskUserVariant selects the model-facing shape of the ask_user tool. +type AskUserVariant string + +const ( + // AskUserVariantLegacy uses the legacy user-input request implementation. + AskUserVariantLegacy AskUserVariant = "legacy" + // AskUserVariantElicitation uses the elicitation-based implementation. + AskUserVariantElicitation AskUserVariant = "elicitation" +) + // SessionConfig configures a new session type SessionConfig struct { // SessionID is an optional custom session ID @@ -1340,8 +1405,13 @@ type SessionConfig struct { // GitHubTokenProvider acquires session-scoped GitHub tokens on demand. It // cannot be combined with GitHubToken. GitHubTokenProvider GitHubTokenProvider - // OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool) + // OnUserInputRequest handles legacy question-and-answer requests from the agent + // and enables the legacy ask_user tool. OnUserInputRequest UserInputHandler + // AskUserVariant selects the model-facing shape of the ask_user tool. + // The zero value preserves legacy behavior. AskUserVariantElicitation also + // requires OnElicitationRequest so the host can answer structured forms. + AskUserVariant AskUserVariant // Hooks configures hook handlers for session lifecycle events Hooks *SessionHooks // WorkingDirectory is the working directory for the session. @@ -1559,6 +1629,9 @@ type SessionConfig struct { // intended for trusted out-of-process integrators, and is not intended for // general external use. ExpAssignments *CopilotExpAssignmentResponse + // FeatureFlags contains feature-flag values resolved by the host for this session. + // Re-supply them when resuming after a runtime restart. + FeatureFlags map[string]bool // EnableManagedSettings, when set to true, opts the runtime into // self-fetching enterprise managed settings (bypass-permissions policy) at // session bootstrap using the session's GitHubToken. Requires GitHubToken to @@ -1914,8 +1987,13 @@ type ResumeSessionConfig struct { // OnMCPAuthRequest is an optional handler for MCP OAuth requests from MCP servers. // See SessionConfig.OnMCPAuthRequest. OnMCPAuthRequest MCPAuthHandler - // OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool) + // OnUserInputRequest handles legacy question-and-answer requests from the agent + // and enables the legacy ask_user tool. OnUserInputRequest UserInputHandler + // AskUserVariant selects the model-facing shape of the ask_user tool. + // The zero value preserves legacy behavior. AskUserVariantElicitation also + // requires OnElicitationRequest so the host can answer structured forms. + AskUserVariant AskUserVariant // Hooks configures hook handlers for session lifecycle events Hooks *SessionHooks // WorkingDirectory is the working directory for the session. @@ -2085,6 +2163,9 @@ type ResumeSessionConfig struct { // intended for trusted out-of-process integrators, and is not intended for // general external use. ExpAssignments *CopilotExpAssignmentResponse + // FeatureFlags contains host-resolved feature-flag values to apply on resume. + // See SessionConfig.FeatureFlags. + FeatureFlags map[string]bool // EnableManagedSettings injects the same opt-in flag on resume. See // SessionConfig.EnableManagedSettings. Re-supply on resume so the runtime // re-applies the managed-settings self-fetch after a CLI process restart. @@ -2205,6 +2286,18 @@ func (p ProviderConfig) MarshalJSON() ([]byte, error) { return json.Marshal(aux) } +// AutoTier selects the routing tier for model "auto" with V2 Auto. +type AutoTier = rpc.AutoTier + +const ( + // AutoTierEfficiency selects the efficiency routing tier. + AutoTierEfficiency = rpc.AutoTierEfficiency + // AutoTierBalance selects the balance routing tier. + AutoTierBalance = rpc.AutoTierBalance + // AutoTierIntelligence selects the intelligence routing tier. + AutoTierIntelligence = rpc.AutoTierIntelligence +) + // CapiSessionOptions configures provider-scoped Copilot API (CAPI) session behavior. // // WebSocket transport is the default for the CAPI Responses API whenever the @@ -2219,6 +2312,14 @@ type CapiSessionOptions struct { // WebSocket transport. Enabled by default when the model advertises // ws:/responses support; set to Bool(false) to force HTTP Responses transport. EnableWebSocketResponses *bool `json:"enableWebSocketResponses,omitempty"` + + // AutoTier selects the routing tier for model "auto" with V2 Auto. + // Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto. + // When unset, the runtime uses its default on create and preserves the + // persisted or current tier on resume. An explicit tier overrides the + // persisted tier on a cold resume; a conflicting tier on a resident + // session resume is rejected by the runtime. + AutoTier AutoTier `json:"autoTier,omitempty"` } // AzureProviderOptions contains Azure-specific provider configuration @@ -2509,6 +2610,7 @@ type createSessionRequest struct { ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` RequestPermission *bool `json:"requestPermission,omitempty"` RequestUserInput *bool `json:"requestUserInput,omitempty"` + AskUserVariant AskUserVariant `json:"askUserVariant,omitempty"` RequestExitPlanMode *bool `json:"requestExitPlanMode,omitempty"` RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` Hooks *bool `json:"hooks,omitempty"` @@ -2557,6 +2659,7 @@ type createSessionRequest struct { ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"` ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"` + FeatureFlags *map[string]bool `json:"featureFlags,omitempty"` EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` @@ -2606,6 +2709,7 @@ type resumeSessionRequest struct { ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` RequestPermission *bool `json:"requestPermission,omitempty"` RequestUserInput *bool `json:"requestUserInput,omitempty"` + AskUserVariant AskUserVariant `json:"askUserVariant,omitempty"` RequestExitPlanMode *bool `json:"requestExitPlanMode,omitempty"` RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` Hooks *bool `json:"hooks,omitempty"` @@ -2656,6 +2760,7 @@ type resumeSessionRequest struct { ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"` ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"` + FeatureFlags *map[string]bool `json:"featureFlags,omitempty"` EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` diff --git a/go/zsession_events.go b/go/zsession_events.go index 38ea78087b..b5f6fa34f0 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -12,6 +12,8 @@ type ( AgentInterruptedActivity = rpc.AgentInterruptedActivity AgentInterruptedCancelPhase = rpc.AgentInterruptedCancelPhase AgentInterruptedData = rpc.AgentInterruptedData + AgentModelPolicy = rpc.AgentModelPolicy + AssistantFusionPhaseActivityData = rpc.AssistantFusionPhaseActivityData AssistantFusionPhaseCompletedData = rpc.AssistantFusionPhaseCompletedData AssistantFusionPhaseFailedData = rpc.AssistantFusionPhaseFailedData AssistantFusionPhaseStartedData = rpc.AssistantFusionPhaseStartedData @@ -23,6 +25,8 @@ type ( AssistantMessageServerTools = rpc.AssistantMessageServerTools AssistantMessageStartData = rpc.AssistantMessageStartData AssistantMessageToolRequest = rpc.AssistantMessageToolRequest + AssistantMessageToolRequestCaller = rpc.AssistantMessageToolRequestCaller + AssistantMessageToolRequestCallerType = rpc.AssistantMessageToolRequestCallerType AssistantMessageToolRequestType = rpc.AssistantMessageToolRequestType AssistantReasoningData = rpc.AssistantReasoningData AssistantReasoningDeltaData = rpc.AssistantReasoningDeltaData @@ -95,6 +99,10 @@ type ( CompactionCompleteCompactionTokensUsed = rpc.CompactionCompleteCompactionTokensUsed CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail = rpc.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail CompactionTrigger = rpc.CompactionTrigger + CompletionReceiptEventRange = rpc.CompletionReceiptEventRange + CompletionReceiptFinalTool = rpc.CompletionReceiptFinalTool + CompletionReceiptStopReason = rpc.CompletionReceiptStopReason + CompletionReceiptToolStatus = rpc.CompletionReceiptToolStatus ContextTier = rpc.ContextTier CustomAgentsUpdatedAgent = rpc.CustomAgentsUpdatedAgent ElicitationCompletedAction = rpc.ElicitationCompletedAction @@ -124,7 +132,9 @@ type ( FusionFollowUpAction = rpc.FusionFollowUpAction FusionFollowUpRecommendation = rpc.FusionFollowUpRecommendation FusionPattern = rpc.FusionPattern + FusionPhaseActivityKind = rpc.FusionPhaseActivityKind FusionPhaseKind = rpc.FusionPhaseKind + FusionPhasePlanStep = rpc.FusionPhasePlanStep FusionPhaseStatus = rpc.FusionPhaseStatus FusionPhaseUsage = rpc.FusionPhaseUsage FusionScores = rpc.FusionScores @@ -263,6 +273,7 @@ type ( SessionCanvasUnavailableData = rpc.SessionCanvasUnavailableData SessionCompactionCompleteData = rpc.SessionCompactionCompleteData SessionCompactionStartData = rpc.SessionCompactionStartData + SessionCompletionReceiptData = rpc.SessionCompletionReceiptData SessionContextChangedData = rpc.SessionContextChangedData SessionContextClearedData = rpc.SessionContextClearedData SessionCustomAgentsUpdatedData = rpc.SessionCustomAgentsUpdatedData @@ -292,6 +303,7 @@ type ( SessionMode = rpc.SessionMode SessionModeChangedData = rpc.SessionModeChangedData SessionModelChangeData = rpc.SessionModelChangeData + SessionModeNoticeDeliveredData = rpc.SessionModeNoticeDeliveredData SessionPermissionsChangedData = rpc.SessionPermissionsChangedData SessionPlanChangedData = rpc.SessionPlanChangedData SessionRemoteSteerableChangedData = rpc.SessionRemoteSteerableChangedData @@ -424,6 +436,9 @@ const ( AgentInterruptedActivityToolCall = rpc.AgentInterruptedActivityToolCall AgentInterruptedCancelPhaseMidStream = rpc.AgentInterruptedCancelPhaseMidStream AgentInterruptedCancelPhasePreFirstToken = rpc.AgentInterruptedCancelPhasePreFirstToken + AgentModelPolicyPreferred = rpc.AgentModelPolicyPreferred + AgentModelPolicyRequired = rpc.AgentModelPolicyRequired + AssistantMessageToolRequestCallerTypeProgram = rpc.AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestTypeCustom = rpc.AssistantMessageToolRequestTypeCustom AssistantMessageToolRequestTypeFunction = rpc.AssistantMessageToolRequestTypeFunction AssistantUsageAPIEndpointChatCompletions = rpc.AssistantUsageAPIEndpointChatCompletions @@ -487,6 +502,14 @@ const ( CompactionTriggerMemoryPressure = rpc.CompactionTriggerMemoryPressure CompactionTriggerModelSwitch = rpc.CompactionTriggerModelSwitch CompactionTriggerThreshold = rpc.CompactionTriggerThreshold + CompletionReceiptStopReasonAgentStopBlockLimit = rpc.CompletionReceiptStopReasonAgentStopBlockLimit + CompletionReceiptStopReasonNatural = rpc.CompletionReceiptStopReasonNatural + CompletionReceiptStopReasonTerminalTool = rpc.CompletionReceiptStopReasonTerminalTool + CompletionReceiptToolStatusDenied = rpc.CompletionReceiptToolStatusDenied + CompletionReceiptToolStatusFailure = rpc.CompletionReceiptToolStatusFailure + CompletionReceiptToolStatusRejected = rpc.CompletionReceiptToolStatusRejected + CompletionReceiptToolStatusSuccess = rpc.CompletionReceiptToolStatusSuccess + CompletionReceiptToolStatusTimeout = rpc.CompletionReceiptToolStatusTimeout ContextTierDefault = rpc.ContextTierDefault ContextTierLongContext = rpc.ContextTierLongContext ElicitationCompletedActionAccept = rpc.ElicitationCompletedActionAccept @@ -520,6 +543,9 @@ const ( FusionPatternCascade = rpc.FusionPatternCascade FusionPatternCritique = rpc.FusionPatternCritique FusionPatternSingle = rpc.FusionPatternSingle + FusionPhaseActivityKindModelOutput = rpc.FusionPhaseActivityKindModelOutput + FusionPhaseActivityKindToolCompleted = rpc.FusionPhaseActivityKindToolCompleted + FusionPhaseActivityKindToolStarted = rpc.FusionPhaseActivityKindToolStarted FusionPhaseKindCritic = rpc.FusionPhaseKindCritic FusionPhaseKindDraft = rpc.FusionPhaseKindDraft FusionPhaseKindFollowUp = rpc.FusionPhaseKindFollowUp @@ -548,6 +574,7 @@ const ( ManagedSettingsResolvedSourceDevice = rpc.ManagedSettingsResolvedSourceDevice ManagedSettingsResolvedSourceMixed = rpc.ManagedSettingsResolvedSourceMixed ManagedSettingsResolvedSourceNone = rpc.ManagedSettingsResolvedSourceNone + ManagedSettingsResolvedSourcePolicyHelper = rpc.ManagedSettingsResolvedSourcePolicyHelper ManagedSettingsResolvedSourceServer = rpc.ManagedSettingsResolvedSourceServer MCPHeadersRefreshCompletedOutcomeHeaders = rpc.MCPHeadersRefreshCompletedOutcomeHeaders MCPHeadersRefreshCompletedOutcomeNone = rpc.MCPHeadersRefreshCompletedOutcomeNone @@ -666,6 +693,7 @@ const ( ScheduleOriginUser = rpc.ScheduleOriginUser SessionEventTypeAbort = rpc.SessionEventTypeAbort SessionEventTypeAgentInterrupted = rpc.SessionEventTypeAgentInterrupted + SessionEventTypeAssistantFusionPhaseActivity = rpc.SessionEventTypeAssistantFusionPhaseActivity SessionEventTypeAssistantFusionPhaseCompleted = rpc.SessionEventTypeAssistantFusionPhaseCompleted SessionEventTypeAssistantFusionPhaseFailed = rpc.SessionEventTypeAssistantFusionPhaseFailed SessionEventTypeAssistantFusionPhaseStarted = rpc.SessionEventTypeAssistantFusionPhaseStarted @@ -732,6 +760,7 @@ const ( SessionEventTypeSessionCanvasUnavailable = rpc.SessionEventTypeSessionCanvasUnavailable SessionEventTypeSessionCompactionComplete = rpc.SessionEventTypeSessionCompactionComplete SessionEventTypeSessionCompactionStart = rpc.SessionEventTypeSessionCompactionStart + SessionEventTypeSessionCompletionReceipt = rpc.SessionEventTypeSessionCompletionReceipt SessionEventTypeSessionContextChanged = rpc.SessionEventTypeSessionContextChanged SessionEventTypeSessionContextCleared = rpc.SessionEventTypeSessionContextCleared SessionEventTypeSessionCustomAgentsUpdated = rpc.SessionEventTypeSessionCustomAgentsUpdated @@ -754,6 +783,7 @@ const ( SessionEventTypeSessionMCPServerStatusChanged = rpc.SessionEventTypeSessionMCPServerStatusChanged SessionEventTypeSessionModeChanged = rpc.SessionEventTypeSessionModeChanged SessionEventTypeSessionModelChange = rpc.SessionEventTypeSessionModelChange + SessionEventTypeSessionModeNoticeDelivered = rpc.SessionEventTypeSessionModeNoticeDelivered SessionEventTypeSessionPermissionsChanged = rpc.SessionEventTypeSessionPermissionsChanged SessionEventTypeSessionPlanChanged = rpc.SessionEventTypeSessionPlanChanged SessionEventTypeSessionRemoteSteerableChanged = rpc.SessionEventTypeSessionRemoteSteerableChanged @@ -813,6 +843,7 @@ const ( SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot SkillSourcePlugin = rpc.SkillSourcePlugin SkillSourceProject = rpc.SkillSourceProject + SkillSourceSDK = rpc.SkillSourceSDK SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper SystemMessageRoleSystem = rpc.SystemMessageRoleSystem SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted diff --git a/java/README.md b/java/README.md index 10eb72ce50..f01911c159 100644 --- a/java/README.md +++ b/java/README.md @@ -20,7 +20,11 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A To use the SDK, you'll need: - Java 17 or later. **JDK 25 recommended**. The distributed jar is a multi-release jar (MR-JAR) and is compiled on JDK 25 with `maven.compiler.release` set to 17. This means, when run on JDK 25 and later, the SDK automatically uses virtual threads for its default internal executor. -- GitHub Copilot CLI 1.0.55-5 or later installed and in `PATH` (or provide custom `cliPath`) + +Managed stdio and TCP connections materialize the platform classifier's +`copilot-runtime[.exe]` and adjacent `runtime.node` by default. An explicit +`cliPath` or `COPILOT_CLI_PATH` environment variable overrides the bundled +runtime. ## Installation @@ -32,14 +36,14 @@ Replace `${copilot.sdk.version}` with the latest release from Maven Central. com.github copilot-sdk-java - 1.0.13-preview.1 + 1.0.13-preview.4 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.1' +implementation 'com.github:copilot-sdk-java:1.0.13-preview.4' ``` #### Snapshot Builds @@ -58,7 +62,7 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.14-preview.1-SNAPSHOT + 1.0.14-preview.4-SNAPSHOT ``` @@ -67,7 +71,7 @@ Snapshot builds of the next development version are published to Maven Central S Replace `${copilot.sdk.version}` with the latest release from Maven Central. ```groovy -implementation 'com.github:copilot-sdk-java:1.0.14-preview.1-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-preview.4-SNAPSHOT' ``` ## In-process mode (experimental) @@ -176,14 +180,21 @@ directly. `CopilotClientOptions.setCwd(...)` sets the runtime process working directory, which otherwise inherits the current process working directory. `SessionConfig.setWorkingDirectory(...)` sets the session working directory, which otherwise defaults to the runtime process working directory. +`SessionConfig.setAskUserVariant(AskUserVariant.ELICITATION)` selects the +structured form-based `ask_user` tool when an elicitation handler is also set. +The default is `AskUserVariant.LEGACY`. Re-supply the option and handler through +`ResumeSessionConfig` on a cold resume. + For rotating per-session GitHub credentials, use `SessionConfig.setGitHubTokenProvider(...)` (or the equivalent `ResumeSessionConfig` setter) instead of `setGitHubToken(...)`: ```java -var config = new SessionConfig().setGitHubTokenProvider(args -> - acquireForHost(args.host()).thenApply(token -> - GitHubTokenProviderResult.token(token, 8 * 60 * 60))); +var config = new SessionConfig() + .setGitHubTokenProvider(args -> + acquireForHost(args.host()).thenApply(token -> + GitHubTokenProviderResult.token(token, 8 * 60 * 60))) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); ``` The remaining lifetime is required and must be positive when the callback @@ -330,6 +341,34 @@ Chain fluent modifiers to set tool options: For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md). +## Auto routing tiers + +Use `CapiSessionOptions.setAutoTier(...)` to select `AutoTier.EFFICIENCY`, +`AutoTier.BALANCE`, or `AutoTier.INTELLIGENCE`. This option is meaningful only +with model `auto` (Auto mode V2). +It requires a runtime version that supports `capi.autoTier`. + +```java +import com.github.copilot.rpc.AutoTier; +import com.github.copilot.rpc.CapiSessionOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; + +var config = new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("auto") + .setCapi(new CapiSessionOptions().setAutoTier(AutoTier.BALANCE)); +``` + +The same options work with `ResumeSessionConfig.setCapi(...)` and can be combined +with `setEnableWebSocketResponses(false)`. The SDK omits an unset (`null`) tier: +the runtime chooses its default on create and preserves the persisted/current +tier on resume. An explicit tier overrides the persisted tier on cold resume; +the runtime rejects a conflicting tier when the session is already resident +in memory. The SDK does not choose a default or manage tier persistence. +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) +for the lifecycle rules. + ## Session Store `enableSessionStore` on `SessionConfig` enables the cross-session store for search and retrieval across sessions. When unset in the default `CopilotClientMode.COPILOT_CLI` mode, the runtime default applies (enabled). In `CopilotClientMode.EMPTY` mode, defaults to disabled. @@ -564,7 +603,7 @@ mvn clean verify -Dcopilot.native.libc=glibc mvn clean package -pl copilot-native -DskipTests -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=true ``` -On Linux, the classifier JAR contains `runtime.node`, `platform.properties`, and `copilot` under `native/linux-x64` or `native/linux-arm64`. On Windows, it contains those resources under `native/win32-x64` or `native/win32-arm64`, with the CLI named `copilot.exe`. On Apple Silicon macOS, it contains them under `native/darwin-arm64`. The placeholder JAR remains OS-neutral and contains no native binaries. Unsupported hosts retain the placeholder-only behavior. +Each classifier JAR includes `runtime.node`, `platform.properties`, and `copilot-runtime` (or `copilot-runtime.exe`) under its `native/` directory. It does not contain the legacy `copilot` SEA. The placeholder JAR remains OS-neutral and contains no native binaries. Unsupported hosts retain the placeholder-only behavior. ## License diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index f3694457d8..c03be9909e 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.1-SNAPSHOT + 1.0.14-preview.4-SNAPSHOT ../pom.xml @@ -126,7 +126,7 @@ @@ -203,12 +203,15 @@ - + + + + - + - + @@ -243,7 +246,6 @@ inprocess linux-x64 - copilot @@ -306,7 +308,6 @@ linux-x64 - copilot @@ -420,7 +421,6 @@ win32-x64 - copilot.exe @@ -530,7 +530,6 @@ darwin-arm64 - copilot diff --git a/java/copilot-native/scripts/create-native-classifier-test-fixture.mjs b/java/copilot-native/scripts/create-native-classifier-test-fixture.mjs index 095d8c1e9e..08eccb644a 100644 --- a/java/copilot-native/scripts/create-native-classifier-test-fixture.mjs +++ b/java/copilot-native/scripts/create-native-classifier-test-fixture.mjs @@ -13,14 +13,14 @@ export function createNativeClassifierTestFixture({ outputPath, repoRoot, }) { - const cliFilename = classifier.startsWith("win32") - ? "copilot.exe" - : "copilot"; + const runtimeFilename = classifier.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; const nativeVersion = readPinnedNativeVersion(repoRoot, classifier); const prefix = `native/${classifier}`; writeStoredZip(outputPath, [ [`${prefix}/runtime.node`, "test runtime"], - [`${prefix}/${cliFilename}`, "test cli"], + [`${prefix}/${runtimeFilename}`, "test runtime wrapper"], [ `${prefix}/platform.properties`, `classifier=${classifier}\nversion=${nativeVersion}\n`, diff --git a/java/copilot-native/scripts/fetch-native.mjs b/java/copilot-native/scripts/fetch-native.mjs index 7b68f04065..4ee91b6aa6 100644 --- a/java/copilot-native/scripts/fetch-native.mjs +++ b/java/copilot-native/scripts/fetch-native.mjs @@ -3,18 +3,16 @@ *--------------------------------------------------------------------------------------------*/ /** - * Downloads the `runtime.node` native binary for a single platform classifier - * and stages it for packaging into a classifier JAR. + * Downloads the native runtime artifacts for one platform classifier. * * Steps: * 1. Read the pinned version and the SHA-512 `integrity` value for * `@github/copilot-` from `nodejs/package-lock.json`. * 2. `npm pack` that exact version into the staging directory. * 3. Verify the downloaded tarball against the `integrity` value. - * 4. Extract `package/prebuilds//runtime.node` to - * `//native//runtime.node`. - * 5. Extract `package/copilot` (or `package/copilot.exe` on Windows) to - * `//native//copilot`. + * 4. Stage the hostless runtime tree, flattening the selected prebuild directory + * beside the package's retained top-level runtime assets. + * 5. Write an inventory consumed by the SDK's generic classpath extractor. * 6. Write `//native//platform.properties`. * * Usage: node fetch-native.mjs @@ -25,6 +23,28 @@ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +const excludedTopLevel = new Set([ + 'app.js', + 'assets', + 'changelog.json', + 'copilot', + 'copilot.exe', + 'copilot-sdk', + 'foundry-local-sdk', + 'index.js', + 'LICENSE.md', + 'napi-oop-runtime', + 'npm-loader.js', + 'package.json', + 'preloads', + 'pvrecorder', + 'queries', + 'README.md', + 'sdk', + 'sea-loader.js', + 'webview', +]); + const [repoRoot, stagingDir, classifier] = process.argv.slice(2); if (!repoRoot || !stagingDir || !classifier) { @@ -52,34 +72,35 @@ const outDir = path.join(stagingDir, classifier); const resourceDir = path.join(outDir, 'native', classifier); const runtimePath = path.join(resourceDir, 'runtime.node'); const isWindows = classifier.startsWith('win32'); -const cliTarballMember = isWindows ? 'package/copilot.exe' : 'package/copilot'; -const cliFilename = isWindows ? 'copilot.exe' : 'copilot'; -const cliPath = path.join(resourceDir, cliFilename); +const wrapperFilename = isWindows ? 'copilot-runtime.exe' : 'copilot-runtime'; +const wrapperPath = path.join(resourceDir, wrapperFilename); +const inventoryPath = path.join(resourceDir, 'runtime-assets.list'); const platformPropertiesPath = path.join(resourceDir, 'platform.properties'); const expectedPlatformProperties = `classifier=${classifier}\nversion=${version}\n`; +const stagingSchema = 'hostless-runtime-v2'; const stampPath = path.join(outDir, '.version'); // Idempotence: skip the download only when every required staged artifact // matches the package identity recorded in the stamp. if ( fs.existsSync(runtimePath) && - fs.existsSync(cliPath) && + fs.existsSync(wrapperPath) && + fs.existsSync(inventoryPath) && fs.existsSync(platformPropertiesPath) && fs.existsSync(stampPath) ) { const stampLines = fs.readFileSync(stampPath, 'utf8').trim().split('\n'); - const stampVersion = stampLines[0] || ''; - const stampIntegrity = stampLines[1] || ''; - const stampRuntimeDigest = stampLines[2] || ''; - const stampCliDigest = stampLines[3] || ''; - const currentRuntimeDigest = digestFile(runtimePath); - const currentCliDigest = digestFile(cliPath); + const stampSchema = stampLines[0] || ''; + const stampVersion = stampLines[1] || ''; + const stampIntegrity = stampLines[2] || ''; + const stampTreeDigest = stampLines[3] || ''; + const currentTreeDigest = digestTree(resourceDir); const currentPlatformProperties = fs.readFileSync(platformPropertiesPath, 'utf8'); if ( + stampSchema === stagingSchema && stampVersion === version && stampIntegrity === integrity && - stampRuntimeDigest === currentRuntimeDigest && - stampCliDigest === currentCliDigest && + stampTreeDigest === currentTreeDigest && currentPlatformProperties === expectedPlatformProperties ) { console.log(`${packageName}@${version} already staged at ${runtimePath}`); @@ -107,28 +128,99 @@ if (actual !== integrity) { } console.log(`Integrity verified (${integrity.slice(0, 20)}...).`); -const memberPath = `package/prebuilds/${classifier}/runtime.node`; -execFileSync('tar', ['-xzf', tarballPath, '-C', outDir, memberPath], { stdio: 'inherit' }); -fs.renameSync(path.join(outDir, memberPath), runtimePath); - -// Extract the copilot CLI executable (necessary-and-sufficient runtime artifact invariant: -// host_start needs both runtime.node and the copilot CLI from the same package version). -execFileSync('tar', ['-xzf', tarballPath, '-C', outDir, cliTarballMember], { stdio: 'inherit' }); -fs.renameSync(path.join(outDir, cliTarballMember), cliPath); -if (!isWindows) { - fs.chmodSync(cliPath, 0o755); +const inventory = []; +const members = execFileSync('tar', ['-tzf', tarballPath], { encoding: 'utf8' }) + .split(/\r?\n/) + .filter(Boolean); +for (const member of members) { + const destinationRelative = hostlessRuntimePath(member, classifier); + if (destinationRelative === null) { + continue; + } + const listing = execFileSync('tar', ['-tvzf', tarballPath, member], { encoding: 'utf8' }).trim(); + if (listing.startsWith('d')) { + continue; + } + if (!listing.startsWith('-')) { + throw new Error(`Unsupported runtime package entry: ${member}`); + } + const content = execFileSync('tar', ['-xOzf', tarballPath, member], { + encoding: null, + maxBuffer: 512 * 1024 * 1024, + }); + const destination = path.resolve(resourceDir, destinationRelative); + const resourceRoot = `${path.resolve(resourceDir)}${path.sep}`; + if (!destination.startsWith(resourceRoot)) { + throw new Error(`Runtime package entry escapes staging directory: ${member}`); + } + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.writeFileSync(destination, content); + const mode = listing.slice(0, 10).includes('x') ? 0o755 : 0o644; + fs.chmodSync(destination, mode); + inventory.push(`${mode.toString(8)}\t${destinationRelative.split(path.sep).join('/')}`); } +inventory.sort(); +fs.writeFileSync(inventoryPath, `${inventory.join('\n')}\n`); -fs.rmSync(path.join(outDir, 'package'), { recursive: true, force: true }); fs.rmSync(tarballPath, { force: true }); +if (!fs.existsSync(runtimePath) || !fs.existsSync(wrapperPath)) { + throw new Error(`Package ${packageName}@${version} is missing the runtime wrapper pair`); +} fs.writeFileSync(platformPropertiesPath, expectedPlatformProperties); -const runtimeDigest = digestFile(runtimePath); -const cliDigest = digestFile(cliPath); -fs.writeFileSync(stampPath, `${version}\n${integrity}\n${runtimeDigest}\n${cliDigest}\n`); +const treeDigest = digestTree(resourceDir); +fs.writeFileSync(stampPath, `${stagingSchema}\n${version}\n${integrity}\n${treeDigest}\n`); console.log(`Staged ${runtimePath}`); -function digestFile(filePath) { - return `sha512-${createHash('sha512').update(fs.readFileSync(filePath)).digest('base64')}`; +function hostlessRuntimePath(packageRelative, platform) { + if (packageRelative.includes('\\')) { + return null; + } + const parts = packageRelative.split('/'); + if (parts[0] !== 'package' || parts.some((part) => !part || part === '..')) { + return null; + } + parts.shift(); + const topLevel = parts[0]; + const fileName = parts.at(-1); + if ( + excludedTopLevel.has(topLevel) || + (topLevel.startsWith('tree-sitter') && topLevel.endsWith('.wasm')) || + (topLevel.startsWith('voice-') && topLevel.endsWith('.js')) || + fileName === 'cli-native.node' || + parts.includes('mediaremote-adapter') || + fileName.startsWith('copilot-runtime-bin') + ) { + return null; + } + if (topLevel === 'prebuilds') { + if (parts[1] !== platform || parts.length < 3) { + return null; + } + return path.join(...parts.slice(2)); + } + return path.join(...parts); +} + +function walkFiles(directory) { + const files = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...walkFiles(entryPath)); + } else if (entry.isFile()) { + files.push(entryPath); + } + } + return files; +} + +function digestTree(directory) { + const hash = createHash('sha512'); + for (const file of walkFiles(directory).sort()) { + const relative = path.relative(directory, file).split(path.sep).join('/'); + hash.update(relative).update('\0').update(fs.readFileSync(file)).update('\0'); + } + return `sha512-${hash.digest('base64')}`; } diff --git a/java/copilot-native/scripts/fetch-native.test.mjs b/java/copilot-native/scripts/fetch-native.test.mjs index 3ca1e2fde6..582ffa397f 100644 --- a/java/copilot-native/scripts/fetch-native.test.mjs +++ b/java/copilot-native/scripts/fetch-native.test.mjs @@ -7,29 +7,41 @@ import { createHash } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { spawnSync } from 'node:child_process'; +import { execFileSync, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import test from 'node:test'; const version = '1.0.79'; const integrity = 'sha512-test-integrity'; const runtimeContent = 'runtime content'; -const cliContent = 'cli content'; +const wrapperContent = 'wrapper content'; +const stagingSchema = 'hostless-runtime-v2'; const scriptPath = fileURLToPath(new URL('./fetch-native.mjs', import.meta.url)); for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64', 'darwin-arm64']) { - test(`${classifier}: missing CLI does not use incremental fast path`, (t) => { + test(`${classifier}: complete hostless artifacts use incremental fast path without a CLI`, (t) => { const fixture = createFixture(t, classifier); - fs.rmSync(fixture.cliPath); + + const result = runScript(fixture); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /already staged/); + assert.equal(fs.existsSync(fixture.npmMarkerPath), false); + }); + + test(`${classifier}: missing runtime wrapper does not use incremental fast path`, (t) => { + const fixture = createFixture(t, classifier); + fs.rmSync(fixture.wrapperPath); const result = runScript(fixture); assertRestagingAttempted(fixture, result); }); - test(`${classifier}: stale CLI does not use incremental fast path`, (t) => { + test(`${classifier}: legacy staging schema does not use incremental fast path`, (t) => { const fixture = createFixture(t, classifier); - fs.writeFileSync(fixture.cliPath, 'stale CLI content'); + const stampPath = path.join(fixture.stagingDir, classifier, '.version'); + fs.writeFileSync(stampPath, fs.readFileSync(stampPath, 'utf8').replace(stagingSchema, 'hostless-runtime-v1')); const result = runScript(fixture); @@ -45,6 +57,15 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' assertRestagingAttempted(fixture, result); }); + test(`${classifier}: missing retained runtime asset does not use incremental fast path`, (t) => { + const fixture = createFixture(t, classifier); + fs.rmSync(fixture.ripgrepPath); + + const result = runScript(fixture); + + assertRestagingAttempted(fixture, result); + }); + test(`${classifier}: complete matching artifacts use incremental fast path`, (t) => { const fixture = createFixture(t, classifier); @@ -56,6 +77,55 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' }); } +test('stages retained package assets and excludes CLI-only content', (t) => { + const classifier = 'linux-x64'; + const fixture = createFixture(t, classifier); + const packageRoot = path.join(fixture.repoRoot, 'package-root', 'package'); + fs.mkdirSync(path.join(packageRoot, 'prebuilds', classifier), { recursive: true }); + fs.mkdirSync(path.join(packageRoot, 'ripgrep', 'bin', classifier), { recursive: true }); + fs.mkdirSync(path.join(packageRoot, 'definitions'), { recursive: true }); + fs.writeFileSync(path.join(packageRoot, 'copilot'), 'excluded'); + fs.writeFileSync(path.join(packageRoot, 'prebuilds', classifier, 'runtime.node'), runtimeContent); + fs.writeFileSync(path.join(packageRoot, 'prebuilds', classifier, 'copilot-runtime'), wrapperContent); + fs.writeFileSync(path.join(packageRoot, 'ripgrep', 'bin', classifier, 'rg'), 'ripgrep content'); + fs.chmodSync(path.join(packageRoot, 'ripgrep', 'bin', classifier, 'rg'), 0o755); + fs.writeFileSync(path.join(packageRoot, 'definitions', 'future.json'), '{}'); + fs.writeFileSync(path.join(packageRoot, 'app.js'), 'excluded'); + fs.writeFileSync(path.join(packageRoot, 'LICENSE.md'), 'excluded'); + fs.writeFileSync(path.join(packageRoot, 'README.md'), 'excluded'); + const tarball = path.join(fixture.repoRoot, 'fixture.tgz'); + execFileSync('tar', ['-czf', tarball, '-C', path.dirname(packageRoot), 'package']); + const packageIntegrity = digest(fs.readFileSync(tarball)); + fs.writeFileSync( + path.join(fixture.repoRoot, 'nodejs', 'package-lock.json'), + JSON.stringify({ + packages: { + [`node_modules/@github/copilot-${classifier}`]: { version, integrity: packageIntegrity }, + }, + }), + ); + const fakeNpmPath = path.join(fixture.fakeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); + const fakeNpm = + process.platform === 'win32' + ? '@copy "%FETCH_NATIVE_TARBALL%" "%4\\fixture.tgz" >nul\r\n@echo fixture.tgz\r\n' + : '#!/bin/sh\ncp "$FETCH_NATIVE_TARBALL" "$4/fixture.tgz"\nprintf "fixture.tgz\\n"\n'; + fs.writeFileSync(fakeNpmPath, fakeNpm); + fs.chmodSync(fakeNpmPath, 0o755); + fs.rmSync(path.join(fixture.stagingDir, classifier), { recursive: true, force: true }); + + const result = runScript(fixture, { FETCH_NATIVE_TARBALL: tarball }); + + assert.equal(result.status, 0, result.stderr); + const resourceDir = path.join(fixture.stagingDir, classifier, 'native', classifier); + assert.equal(fs.readFileSync(path.join(resourceDir, 'ripgrep', 'bin', classifier, 'rg'), 'utf8'), 'ripgrep content'); + assert.equal(fs.readFileSync(path.join(resourceDir, 'definitions', 'future.json'), 'utf8'), '{}'); + assert.equal(fs.existsSync(path.join(resourceDir, 'app.js')), false); + assert.equal(fs.existsSync(path.join(resourceDir, 'copilot')), false); + assert.equal(fs.existsSync(path.join(resourceDir, 'LICENSE.md')), false); + assert.equal(fs.existsSync(path.join(resourceDir, 'README.md')), false); + assert.match(fs.readFileSync(path.join(resourceDir, 'runtime-assets.list'), 'utf8'), /ripgrep\/bin\/linux-x64\/rg/); +}); + function createFixture(t, classifier) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fetch-native-test-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); @@ -79,14 +149,25 @@ function createFixture(t, classifier) { ); const runtimePath = path.join(resourceDir, 'runtime.node'); - const cliPath = path.join(resourceDir, classifier.startsWith('win32') ? 'copilot.exe' : 'copilot'); + const wrapperPath = path.join( + resourceDir, + classifier.startsWith('win32') ? 'copilot-runtime.exe' : 'copilot-runtime', + ); const platformPropertiesPath = path.join(resourceDir, 'platform.properties'); + const ripgrepPath = path.join(resourceDir, 'ripgrep', 'bin', classifier, 'rg'); + const inventoryPath = path.join(resourceDir, 'runtime-assets.list'); + fs.mkdirSync(path.dirname(ripgrepPath), { recursive: true }); fs.writeFileSync(runtimePath, runtimeContent); - fs.writeFileSync(cliPath, cliContent); + fs.writeFileSync(wrapperPath, wrapperContent); + fs.writeFileSync(ripgrepPath, 'ripgrep content'); + fs.writeFileSync( + inventoryPath, + `644\truntime.node\n755\tcopilot-runtime\n755\tripgrep/bin/${classifier}/rg\n`, + ); fs.writeFileSync(platformPropertiesPath, `classifier=${classifier}\nversion=${version}\n`); fs.writeFileSync( path.join(stagingDir, classifier, '.version'), - `${version}\n${integrity}\n${digest(runtimeContent)}\n${digest(cliContent)}\n`, + `${stagingSchema}\n${version}\n${integrity}\n${digestTree(resourceDir)}\n`, ); const fakeNpmPath = path.join(fakeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); @@ -104,18 +185,20 @@ function createFixture(t, classifier) { fakeBinDir, npmMarkerPath, runtimePath, - cliPath, + wrapperPath, + ripgrepPath, platformPropertiesPath, }; } -function runScript(fixture) { +function runScript(fixture, extraEnv = {}) { return spawnSync(process.execPath, [scriptPath, fixture.repoRoot, fixture.stagingDir, fixture.classifier], { encoding: 'utf8', env: { ...process.env, PATH: `${fixture.fakeBinDir}${path.delimiter}${process.env.PATH}`, FETCH_NATIVE_NPM_MARKER: fixture.npmMarkerPath, + ...extraEnv, }, }); } @@ -125,6 +208,29 @@ function assertRestagingAttempted(fixture, result) { assert.equal(fs.readFileSync(fixture.npmMarkerPath, 'utf8').trim(), 'invoked'); } +function digestTree(directory) { + const hash = createHash('sha512'); + for (const file of walkFiles(directory).sort()) { + const relative = path.relative(directory, file).split(path.sep).join('/'); + hash.update(relative).update('\0').update(fs.readFileSync(file)).update('\0'); + } + + return `sha512-${hash.digest('base64')}`; +} + function digest(content) { return `sha512-${createHash('sha512').update(content).digest('base64')}`; } + +function walkFiles(directory) { + const files = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...walkFiles(entryPath)); + } else { + files.push(entryPath); + } + } + return files; +} diff --git a/java/copilot-native/scripts/validate-native-artifact.mjs b/java/copilot-native/scripts/validate-native-artifact.mjs index dda86a917a..9af4ffd772 100644 --- a/java/copilot-native/scripts/validate-native-artifact.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.mjs @@ -27,13 +27,13 @@ export function validateNativeClassifierJar({ } const archive = readJar(jarPath); - const cliFilename = classifier.startsWith("win32") - ? "copilot.exe" - : "copilot"; + const runtimeFilename = classifier.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; const resourcePrefix = `native/${classifier}/`; const requiredEntries = [ `${resourcePrefix}runtime.node`, - `${resourcePrefix}${cliFilename}`, + `${resourcePrefix}${runtimeFilename}`, `${resourcePrefix}platform.properties`, ]; diff --git a/java/copilot-native/scripts/validate-native-artifact.test.mjs b/java/copilot-native/scripts/validate-native-artifact.test.mjs index eb99d432db..25851bb575 100644 --- a/java/copilot-native/scripts/validate-native-artifact.test.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.test.mjs @@ -181,7 +181,7 @@ test("rejects missing native resources", (t) => { expectedFilename: artifactName, repoRoot: fixture.repoRoot, }), - /copilot\.exe/, + /copilot-runtime\.exe/, ); }); @@ -189,7 +189,7 @@ test("rejects incorrect pinned package metadata", (t) => { const fixture = createFixture(t); writeStoredZip(fixture.jarPath, [ ["native/win32-x64/runtime.node", "runtime"], - ["native/win32-x64/copilot.exe", "cli"], + ["native/win32-x64/copilot-runtime.exe", "runtime wrapper"], [ "native/win32-x64/platform.properties", "classifier=win32-x64\nversion=0.0.1\n", @@ -217,7 +217,7 @@ test("rejects Linux resources in a Windows classifier", (t) => { }); writeStoredZip(fixture.jarPath, [ ["native/win32-x64/runtime.node", "runtime"], - ["native/win32-x64/copilot.exe", "cli"], + ["native/win32-x64/copilot-runtime.exe", "runtime wrapper"], [ "native/win32-x64/platform.properties", "classifier=win32-x64\nversion=9.8.7\n", @@ -245,7 +245,7 @@ test("rejects Windows resources in a Linux classifier", (t) => { const linuxJarPath = path.join(fixture.root, linuxArtifactName); writeStoredZip(linuxJarPath, [ ["native/linux-x64/runtime.node", "runtime"], - ["native/linux-x64/copilot", "cli"], + ["native/linux-x64/copilot-runtime", "runtime wrapper"], [ "native/linux-x64/platform.properties", "classifier=linux-x64\nversion=9.8.6\n", @@ -459,7 +459,7 @@ test("local publication validation rejects cross-classifier contamination", (t) path.join(publicationDirectory, `${artifactId}-${version}-linux-x64.jar`), [ ["native/linux-x64/runtime.node", "runtime"], - ["native/linux-x64/copilot", "cli"], + ["native/linux-x64/copilot-runtime", "runtime wrapper"], [ "native/linux-x64/platform.properties", "classifier=linux-x64\nversion=9.8.6\n", diff --git a/java/pom.xml b/java/pom.xml index c5a17d66d8..a9e6836242 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.1-SNAPSHOT + 1.0.14-preview.4-SNAPSHOT pom GitHub Copilot SDK :: Java :: Parent @@ -63,7 +63,7 @@ DO NOT EDIT MANUALLY. Updated by the update-copilot-dependency workflow. --> - ^1.0.82-0 + ^1.0.83-3 true @@ -139,7 +139,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.10.3.0 + 4.10.4.0 com.diffplug.spotless diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index 985ae5d5db..44a6d9fe9d 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,9 +6,9 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.83-3", "json-schema": "^0.4.0", - "tsx": "^4.23.12" + "tsx": "^4.23.13" } }, "node_modules/@esbuild/aix-ppc64": { @@ -428,9 +428,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.82-0.tgz", - "integrity": "sha512-fSZVNAzFFYaS6btYD0+cKF7SrrtOklhpkPs/cIMZY7Fgxoa6rfZrlTWXlQNgNIdwLp51XxwTfxnZO+D9+MQ5yg==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-3.tgz", + "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -439,20 +439,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82-0", - "@github/copilot-darwin-x64": "1.0.82-0", - "@github/copilot-linux-arm64": "1.0.82-0", - "@github/copilot-linux-x64": "1.0.82-0", - "@github/copilot-linuxmusl-arm64": "1.0.82-0", - "@github/copilot-linuxmusl-x64": "1.0.82-0", - "@github/copilot-win32-arm64": "1.0.82-0", - "@github/copilot-win32-x64": "1.0.82-0" + "@github/copilot-darwin-arm64": "1.0.83-3", + "@github/copilot-darwin-x64": "1.0.83-3", + "@github/copilot-linux-arm64": "1.0.83-3", + "@github/copilot-linux-x64": "1.0.83-3", + "@github/copilot-linuxmusl-arm64": "1.0.83-3", + "@github/copilot-linuxmusl-x64": "1.0.83-3", + "@github/copilot-win32-arm64": "1.0.83-3", + "@github/copilot-win32-x64": "1.0.83-3" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.82-0.tgz", - "integrity": "sha512-TzBYfyvxcw3z9Mu7U8TsFo/Nq7m5XS6ahT71aPL+gx/YId0kmenI27b9daXsK6LA1D0gsFGwNBDKINddqntt1g==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-3.tgz", + "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.82-0.tgz", - "integrity": "sha512-Lm/U5Q8kN8yEeBTWKTfIxXAgXaT6zqdBzAAO7lA4DWHZ92AEeZZiVTs6jEWsQ2aWB2uMs6zbn0vi6t+/jIqCGw==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-3.tgz", + "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.82-0.tgz", - "integrity": "sha512-YERVMC1Q4p6l6KQHL5rVOI52rWbvgp9IwyzUBaVSGrfFuqu5BEvZ9bgHPsxTYi3Npkt5KVOXEyPMU5rAY09qQQ==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-3.tgz", + "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", "cpu": [ "arm64" ], @@ -498,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.82-0.tgz", - "integrity": "sha512-z2hxMVjqt4+xDRFTZv3/0K3X+aqcJhd6zPO2JxCpOVTh5CNZFaWk+XIa2iXAPWxFqdKJsQ4muXMl3zInaAOkRw==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-3.tgz", + "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.82-0.tgz", - "integrity": "sha512-EcUCv2PKhBzCCvpTaS511VYTDWyhudyIRPvBpc9gFNO3hjlgiNDusf4k9vP6+E3/lHenwGYzMsyRHcYIOy4vcQ==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-3.tgz", + "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.82-0.tgz", - "integrity": "sha512-1fKVjUiZ1tdb0/d/re90EpFGXhlIPfjENp2Wo/2Kj592dWO3+IwM2qV/AOdMQ4pacW5iYHII7nibx1/EYq3LGQ==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-3.tgz", + "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.82-0.tgz", - "integrity": "sha512-H341wuxQHhwe/yLmORHzwC3DGzFZgGzh+TpwfyK2zjeYVbwDZ3Bax8M+9CzIED9jIBdEfAmfGzxKUHFugpXTtQ==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-3.tgz", + "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.82-0.tgz", - "integrity": "sha512-f1ba3gG8NaoYWFHtHaHcLN4It7mclkWdCOXvwFPqPEwqCEIx/+Zh6VHiOeIcNWRS0elRP6QYDCKaTDy1TW27uQ==", + "version": "1.0.83-3", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-3.tgz", + "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", "cpu": [ "x64" ], @@ -648,9 +648,9 @@ "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/tsx": { - "version": "4.23.12", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", - "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "license": "MIT", "dependencies": { "esbuild": "~0.28.0" diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index f5d6bf66da..a9f84732a2 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,8 +7,8 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.83-3", "json-schema": "^0.4.0", - "tsx": "^4.23.12" + "tsx": "^4.23.13" } } diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index cf091d3c12..0a1505f9c2 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.13-preview.1 +//DEPS com.github:copilot-sdk-java:1.0.13-preview.4 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index 4f9ece6354..dd22744f48 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.1-SNAPSHOT + 1.0.14-preview.4-SNAPSHOT ../pom.xml @@ -79,7 +79,7 @@ com.fasterxml.jackson.core jackson-databind - 2.22.1 + 2.22.2 com.fasterxml.jackson.core @@ -89,14 +89,14 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.22.1 + 2.22.2 com.github.spotbugs spotbugs-annotations - 4.10.3 + 4.10.4 provided @@ -632,6 +632,8 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the **/AskUserTest.java **/CompactionTest.java + + **/CopilotClientTest.java **/CopilotSessionTest.java **/ErrorHandlingTest.java **/EventFidelityTest.java diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java b/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java new file mode 100644 index 0000000000..06c80ba22e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.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; + +/** + * Whether configured models are advisory preferences or required constraints + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AgentModelPolicy { + /** The {@code preferred} variant. */ + PREFERRED("preferred"), + /** The {@code required} variant. */ + REQUIRED("required"); + + private final String value; + AgentModelPolicy(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AgentModelPolicy fromValue(String value) { + for (AgentModelPolicy v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AgentModelPolicy value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java new file mode 100644 index 0000000000..64d8d58283 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.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 javax.annotation.processing.Generated; + +/** + * Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantFusionPhaseActivityEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.fusion_phase_activity"; } + + @JsonProperty("data") + private AssistantFusionPhaseActivityEventData data; + + public AssistantFusionPhaseActivityEventData getData() { return data; } + public void setData(AssistantFusionPhaseActivityEventData data) { this.data = data; } + + /** Data payload for {@link AssistantFusionPhaseActivityEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantFusionPhaseActivityEventData( + /** Identifier of the HydraFusion turn containing the phase. */ + @JsonProperty("fusionId") String fusionId, + /** Stable identifier for the concrete phase. */ + @JsonProperty("phaseId") String phaseId, + /** Kind of phase currently executing. */ + @JsonProperty("phaseKind") FusionPhaseKind phaseKind, + /** HydraFusion orchestration pattern containing the phase. */ + @JsonProperty("pattern") FusionPattern pattern, + /** Semantic role assigned to the phase. */ + @JsonProperty("role") String role, + /** Conversation scope in which the phase executes. */ + @JsonProperty("conversationScope") FusionConversationScope conversationScope, + /** Kind of real activity observed. */ + @JsonProperty("activity") FusionPhaseActivityKind activity, + /** Cumulative private response bytes observed for this model call. The event never includes response text. */ + @JsonProperty("totalResponseSizeBytes") Long totalResponseSizeBytes, + /** Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. */ + @JsonProperty("toolCallId") String toolCallId + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java index d2ad87f7c4..ee9c5fd485 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java @@ -24,7 +24,7 @@ public record AssistantMessageReasoningBlocks( /** Model provider that produced these reasoning blocks. */ @JsonProperty("provider") String provider, - /** Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. */ + /** Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. */ @JsonProperty("blocks") List blocks ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequest.java index 2013734012..bcc7c8206d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequest.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequest.java @@ -36,6 +36,8 @@ public record AssistantMessageToolRequest( /** Original tool name on the MCP server, when the tool is an MCP tool */ @JsonProperty("mcpToolName") String mcpToolName, /** Resolved intention summary describing what this specific call does */ - @JsonProperty("intentionSummary") String intentionSummary + @JsonProperty("intentionSummary") String intentionSummary, + /** Hosted program that requested this client tool call */ + @JsonProperty("caller") AssistantMessageToolRequestCaller caller ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCaller.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCaller.java new file mode 100644 index 0000000000..cb3d05a649 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCaller.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; + +/** + * Hosted program that requested this client tool call + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AssistantMessageToolRequestCaller( + /** Kind of hosted caller that requested the client tool call. */ + @JsonProperty("type") AssistantMessageToolRequestCallerType type, + /** Provider-assigned identifier for the hosted caller. */ + @JsonProperty("callerId") String callerId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCallerType.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCallerType.java new file mode 100644 index 0000000000..05be5068d9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageToolRequestCallerType.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 javax.annotation.processing.Generated; + +/** + * Hosted program caller type + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AssistantMessageToolRequestCallerType { + /** The {@code program} variant. */ + PROGRAM("program"); + + private final String value; + AssistantMessageToolRequestCallerType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AssistantMessageToolRequestCallerType fromValue(String value) { + for (AssistantMessageToolRequestCallerType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AssistantMessageToolRequestCallerType value: " + value); + } +} 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 new file mode 100644 index 0000000000..254543160a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTier.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; + +/** + * Routing preference used when the session model is `auto`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutoTier { + /** The {@code efficiency} variant. */ + EFFICIENCY("efficiency"), + /** The {@code balance} variant. */ + BALANCE("balance"), + /** The {@code intelligence} variant. */ + INTELLIGENCE("intelligence"); + + private final String value; + 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) { + for (AutoTier v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutoTier value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.java new file mode 100644 index 0000000000..7b05d1c9cd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.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; + +/** + * Inclusive durable event range summarized by a completion receipt. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompletionReceiptEventRange( + /** Identifier of the user message that starts the covered exchange. */ + @JsonProperty("startEventId") String startEventId, + /** Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. */ + @JsonProperty("endEventId") String endEventId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java new file mode 100644 index 0000000000..74d78d7ee1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.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; + +/** + * Final structured tool completion in the covered event range. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompletionReceiptFinalTool( + /** Unique identifier of the completed tool call. */ + @JsonProperty("toolCallId") String toolCallId, + /** Tool name from the matching tool execution start event, when available. */ + @JsonProperty("toolName") String toolName, + /** Structured success or failure status from the tool completion event. */ + @JsonProperty("status") CompletionReceiptToolStatus status, + /** Process exit code from a structured shell result, when available. */ + @JsonProperty("exitCode") Long exitCode +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.java new file mode 100644 index 0000000000..49874b0275 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.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; + +/** + * Runtime reason the completion decision was accepted. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum CompletionReceiptStopReason { + /** The {@code natural} variant. */ + NATURAL("natural"), + /** The {@code terminal_tool} variant. */ + TERMINAL_TOOL("terminal_tool"), + /** The {@code agent_stop_block_limit} variant. */ + AGENT_STOP_BLOCK_LIMIT("agent_stop_block_limit"); + + private final String value; + CompletionReceiptStopReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CompletionReceiptStopReason fromValue(String value) { + for (CompletionReceiptStopReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown CompletionReceiptStopReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.java new file mode 100644 index 0000000000..e82a1e6986 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.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; + +/** + * Structured terminal status from a tool completion event. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum CompletionReceiptToolStatus { + /** The {@code success} variant. */ + SUCCESS("success"), + /** The {@code failure} variant. */ + FAILURE("failure"), + /** The {@code timeout} variant. */ + TIMEOUT("timeout"), + /** The {@code rejected} variant. */ + REJECTED("rejected"), + /** The {@code denied} variant. */ + DENIED("denied"); + + private final String value; + CompletionReceiptToolStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CompletionReceiptToolStatus fromValue(String value) { + for (CompletionReceiptToolStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown CompletionReceiptToolStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java b/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java index c2f195e486..762f0b1ac8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. + * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. * * @since 1.0.0 */ @@ -37,6 +37,10 @@ public record CustomAgentsUpdatedAgent( /** Whether the agent can be selected by the user */ @JsonProperty("userInvocable") Boolean userInvocable, /** Model override for this agent, if set */ - @JsonProperty("model") String model + @JsonProperty("model") String model, + /** Authored model ids in priority order, if configured */ + @JsonProperty("models") List models, + /** Whether authored models are preferences or required constraints */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java new file mode 100644 index 0000000000..a95a08cf91 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.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; + +/** + * Content-safe activity observed while a HydraFusion phase is running. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum FusionPhaseActivityKind { + /** The {@code model_output} variant. */ + MODEL_OUTPUT("model_output"), + /** The {@code tool_started} variant. */ + TOOL_STARTED("tool_started"), + /** The {@code tool_completed} variant. */ + TOOL_COMPLETED("tool_completed"); + + private final String value; + FusionPhaseActivityKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static FusionPhaseActivityKind fromValue(String value) { + for (FusionPhaseActivityKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown FusionPhaseActivityKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java new file mode 100644 index 0000000000..cfb07a789a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.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; + +/** + * Presentation-neutral phase planned for a HydraFusion turn. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record FusionPhasePlanStep( + /** Kind of phase that may execute. */ + @JsonProperty("kind") FusionPhaseKind kind, + /** Semantic role assigned to the phase. */ + @JsonProperty("role") String role, + /** Conversation scope in which the phase executes. */ + @JsonProperty("scope") FusionConversationScope scope, + /** Whether the phase executes only when an earlier phase requests it. */ + @JsonProperty("conditional") Boolean conditional +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HookStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/HookStartEvent.java index 3b8b41fafb..030e08caf8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/HookStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HookStartEvent.java @@ -38,7 +38,7 @@ public record HookStartEventData( @JsonProperty("hookInvocationId") String hookInvocationId, /** Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") */ @JsonProperty("hookType") String hookType, - /** Input data passed to the hook */ + /** Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. */ @JsonProperty("input") Object input, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java index 32386f898a..90ce9de668 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java @@ -22,6 +22,8 @@ public enum ManagedSettingsResolvedSource { DEVICE("device"), /** The {@code client} variant. */ CLIENT("client"), + /** The {@code policyHelper} variant. */ + POLICYHELPER("policyHelper"), /** The {@code mixed} variant. */ MIXED("mixed"), /** The {@code none} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java index b7aae9ec80..a854d5ffa4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java @@ -40,6 +40,8 @@ public record PermissionRequestedEventData( @JsonProperty("permissionRequest") Object permissionRequest, /** Derived user-facing permission prompt details for UI consumers */ @JsonProperty("promptRequest") Object promptRequest, + /** Agent mode captured from the owning turn when permission evaluation began. */ + @JsonProperty("agentMode") SessionMode agentMode, /** Neutral risk metadata supplied by the tool host. Consumers may display this value but must not use it to bypass the permission decision. */ @JsonProperty("riskAssessment") Object riskAssessment, /** When true, this permission was already resolved by a permissionRequest hook and requires no client action */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java new file mode 100644 index 0000000000..15670c992a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * 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.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCompletionReceiptEvent extends SessionEvent { + + @Override + public String getType() { return "session.completion_receipt"; } + + @JsonProperty("data") + private SessionCompletionReceiptEventData data; + + public SessionCompletionReceiptEventData getData() { return data; } + public void setData(SessionCompletionReceiptEventData data) { this.data = data; } + + /** Data payload for {@link SessionCompletionReceiptEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionCompletionReceiptEventData( + /** Version of the completion receipt payload. */ + @JsonProperty("schemaVersion") Long schemaVersion, + /** One-based accepted completion receipt ordinal in the durable session history. */ + @JsonProperty("attempt") Long attempt, + /** Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. */ + @JsonProperty("sourceEventId") String sourceEventId, + /** Inclusive durable event range summarized by this receipt. */ + @JsonProperty("eventRange") CompletionReceiptEventRange eventRange, + /** Runtime reason the completion decision was accepted. */ + @JsonProperty("stopReason") CompletionReceiptStopReason stopReason, + /** Final structured tool completion in the covered range, when one exists. */ + @JsonProperty("finalTool") CompletionReceiptFinalTool finalTool, + /** Number of successful structured tool completions in the covered range. */ + @JsonProperty("successfulToolCount") Long successfulToolCount, + /** Number of failed structured tool completions in the covered range. */ + @JsonProperty("failedToolCount") Long failedToolCount + ) { + } +} 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 e22561b914..193aa56b8f 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 @@ -39,6 +39,7 @@ @JsonSubTypes.Type(value = SessionWarningEvent.class, name = "session.warning"), @JsonSubTypes.Type(value = SessionModelChangeEvent.class, name = "session.model_change"), @JsonSubTypes.Type(value = SessionModeChangedEvent.class, name = "session.mode_changed"), + @JsonSubTypes.Type(value = SessionModeNoticeDeliveredEvent.class, name = "session.mode_notice_delivered"), @JsonSubTypes.Type(value = SessionSessionLimitsChangedEvent.class, name = "session.session_limits_changed"), @JsonSubTypes.Type(value = SessionPermissionsChangedEvent.class, name = "session.permissions_changed"), @JsonSubTypes.Type(value = SessionPlanChangedEvent.class, name = "session.plan_changed"), @@ -55,6 +56,7 @@ @JsonSubTypes.Type(value = SessionCompactionStartEvent.class, name = "session.compaction_start"), @JsonSubTypes.Type(value = SessionCompactionCompleteEvent.class, name = "session.compaction_complete"), @JsonSubTypes.Type(value = SessionTaskCompleteEvent.class, name = "session.task_complete"), + @JsonSubTypes.Type(value = SessionCompletionReceiptEvent.class, name = "session.completion_receipt"), @JsonSubTypes.Type(value = SessionFusionRouteStartedEvent.class, name = "session.fusion_route_started"), @JsonSubTypes.Type(value = SessionFusionRouteFailedEvent.class, name = "session.fusion_route_failed"), @JsonSubTypes.Type(value = SessionFusionResolvedEvent.class, name = "session.fusion_resolved"), @@ -66,6 +68,7 @@ @JsonSubTypes.Type(value = AgentInterruptedEvent.class, name = "agent.interrupted"), @JsonSubTypes.Type(value = AssistantIntentEvent.class, name = "assistant.intent"), @JsonSubTypes.Type(value = AssistantFusionPhaseStartedEvent.class, name = "assistant.fusion_phase_started"), + @JsonSubTypes.Type(value = AssistantFusionPhaseActivityEvent.class, name = "assistant.fusion_phase_activity"), @JsonSubTypes.Type(value = AssistantFusionPhaseCompletedEvent.class, name = "assistant.fusion_phase_completed"), @JsonSubTypes.Type(value = AssistantFusionPhaseFailedEvent.class, name = "assistant.fusion_phase_failed"), @JsonSubTypes.Type(value = AssistantServerToolProgressEvent.class, name = "assistant.server_tool_progress"), @@ -172,6 +175,7 @@ public abstract sealed class SessionEvent permits SessionWarningEvent, SessionModelChangeEvent, SessionModeChangedEvent, + SessionModeNoticeDeliveredEvent, SessionSessionLimitsChangedEvent, SessionPermissionsChangedEvent, SessionPlanChangedEvent, @@ -188,6 +192,7 @@ public abstract sealed class SessionEvent permits SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionTaskCompleteEvent, + SessionCompletionReceiptEvent, SessionFusionRouteStartedEvent, SessionFusionRouteFailedEvent, SessionFusionResolvedEvent, @@ -199,6 +204,7 @@ public abstract sealed class SessionEvent permits AgentInterruptedEvent, AssistantIntentEvent, AssistantFusionPhaseStartedEvent, + AssistantFusionPhaseActivityEvent, AssistantFusionPhaseCompletedEvent, AssistantFusionPhaseFailedEvent, AssistantServerToolProgressEvent, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java index 7553c6eb43..d63bee33b3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import javax.annotation.processing.Generated; /** @@ -62,6 +63,8 @@ public record SessionFusionResolvedEventData( @JsonProperty("scores") FusionScores scores, /** Validated orchestration pattern selected for the turn. */ @JsonProperty("pattern") FusionPattern pattern, + /** Presentation-neutral phase plan for clients that render workflow progress. */ + @JsonProperty("phasePlan") List phasePlan, /** Concrete model selected for the primary solver phase. */ @JsonProperty("primaryModel") String primaryModel, /** Concrete model selected for the review or judge phase, when required. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java index ac3763248a..e5428d26db 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -35,7 +35,7 @@ public final class SessionManagedSettingsResolvedEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionManagedSettingsResolvedEventData( - /** Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. */ + /** Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. */ @JsonProperty("source") ManagedSettingsResolvedSource source, /** Whether the server (account/org) managed-settings layer was present */ @JsonProperty("serverManaged") Boolean serverManaged, @@ -43,6 +43,8 @@ public record SessionManagedSettingsResolvedEventData( @JsonProperty("deviceManaged") Boolean deviceManaged, /** Whether a session-local permissions layer injected by the SDK host was present */ @JsonProperty("clientManaged") Boolean clientManaged, + /** Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. */ + @JsonProperty("policyHelperManaged") Boolean policyHelperManaged, /** Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. */ @JsonProperty("failClosed") Boolean failClosed, /** Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java new file mode 100644 index 0000000000..9e24109f9a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * 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.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionModeNoticeDeliveredEvent extends SessionEvent { + + @Override + public String getType() { return "session.mode_notice_delivered"; } + + @JsonProperty("data") + private SessionModeNoticeDeliveredEventData data; + + public SessionModeNoticeDeliveredEventData getData() { return data; } + public void setData(SessionModeNoticeDeliveredEventData data) { this.data = data; } + + /** Data payload for {@link SessionModeNoticeDeliveredEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionModeNoticeDeliveredEventData( + /** Mode established by the delivered transition notice */ + @JsonProperty("mode") SessionMode mode, + /** Model-visible transition notice persisted for a mid-turn delivery */ + @JsonProperty("content") String content + ) { + } +} 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 a3f39d7696..de54f8a3ce 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 @@ -51,6 +51,8 @@ public record SessionResumeEventData( @JsonProperty("verbosity") Verbosity verbosity, /** Context tier currently selected at resume time; null when no tier is active */ @JsonProperty("contextTier") ContextTier contextTier, + /** Auto routing preference active at resume time */ + @JsonProperty("autoTier") AutoTier autoTier, /** 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 */ 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 bf8b4e91cf..b977ae036f 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 @@ -55,6 +55,8 @@ public record SessionStartEventData( @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, + /** Auto routing preference selected at session creation time */ + @JsonProperty("autoTier") AutoTier autoTier, /** Session limits configured at session creation time, if any */ @JsonProperty("sessionLimits") SessionLimitsConfig sessionLimits, /** Working directory and git context at session start */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java index 6ad04f9699..e8675a6bbc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java @@ -39,13 +39,15 @@ public record SkillInvokedEventData( @JsonProperty("name") String name, /** Model identifier active when the skill was invoked, when known */ @JsonProperty("model") String model, - /** File path to the SKILL.md definition */ + /** File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity */ @JsonProperty("path") String path, /** Full content of the skill file, injected into the conversation for the model */ @JsonProperty("content") String content, /** Tool names that should be auto-approved when this skill is active */ @JsonProperty("allowedTools") List allowedTools, - /** Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) */ + /** Whether model invocation is disabled for this skill */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, + /** Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) */ @JsonProperty("source") String source, /** Name of the plugin this skill originated from, when applicable */ @JsonProperty("pluginName") String pluginName, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java index b681faaae8..622dc5d88a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) * * @since 1.0.0 */ @@ -29,7 +29,9 @@ public enum SkillSource { /** The {@code custom} variant. */ CUSTOM("custom"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code sdk} variant. */ + SDK("sdk"); private final String value; SkillSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java index 932d9affe5..2335a9d9ea 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java @@ -27,7 +27,7 @@ public record SkillsLoadedSkill( @JsonProperty("commandName") String commandName, /** Description of what the skill does */ @JsonProperty("description") String description, - /** Source location type (e.g., project, personal-copilot, plugin, builtin) */ + /** Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) */ @JsonProperty("source") SkillSource source, /** Whether the skill can be invoked by the user as a slash command */ @JsonProperty("userInvocable") Boolean userInvocable, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java index 62f6803652..26459bb955 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java @@ -50,6 +50,8 @@ public record SubagentCompletedEventData( @JsonProperty("explicitModelOverride") String explicitModelOverride, /** Whether the explicit task-call model matched the user's configured preference */ @JsonProperty("explicitModelMatchesPreference") Boolean explicitModelMatchesPreference, + /** Why an explicit task-call model did not become the effective model */ + @JsonProperty("modelOverrideReason") String modelOverrideReason, /** Whether the first model actually dispatched matched the user's configured preference */ @JsonProperty("configuredModelMatchesActual") Boolean configuredModelMatchesActual, /** Total number of tool calls made by the sub-agent */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java index 1d1413c64d..54464f8ddf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java @@ -52,6 +52,8 @@ public record SubagentFailedEventData( @JsonProperty("explicitModelOverride") String explicitModelOverride, /** Whether the explicit task-call model matched the user's configured preference */ @JsonProperty("explicitModelMatchesPreference") Boolean explicitModelMatchesPreference, + /** Why an explicit task-call model did not become the effective model */ + @JsonProperty("modelOverrideReason") String modelOverrideReason, /** Whether the first model actually dispatched matched the user's configured preference */ @JsonProperty("configuredModelMatchesActual") Boolean configuredModelMatchesActual, /** Total number of tool calls made before the sub-agent failed */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java index 7d839087da..43a886ea01 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java @@ -37,6 +37,8 @@ public final class UserMessageEvent extends SessionEvent { public record UserMessageEventData( /** The user's message text as displayed in the timeline */ @JsonProperty("content") String content, + /** Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots */ + @JsonProperty("messageId") String messageId, /** Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching */ @JsonProperty("transformedContent") String transformedContent, /** Files, selections, or GitHub references attached to the message */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java index f239c82e61..3d9f9c2d7e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. + * Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. * * @since 1.0.0 */ @@ -41,6 +41,10 @@ public record AgentInfo( @JsonProperty("tools") List tools, /** Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. */ @JsonProperty("model") String model, + /** Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. */ + @JsonProperty("models") List models, + /** Whether authored models are preferences or required constraints. */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy, /** MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. */ @JsonProperty("mcpServers") Map mcpServers, /** Skill names preloaded into this agent's context. Omitted means none. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java new file mode 100644 index 0000000000..99158516f0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.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; + +/** + * Whether configured models are advisory preferences or required constraints + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AgentModelPolicy { + /** The {@code preferred} variant. */ + PREFERRED("preferred"), + /** The {@code required} variant. */ + REQUIRED("required"); + + private final String value; + AgentModelPolicy(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AgentModelPolicy fromValue(String value) { + for (AgentModelPolicy v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AgentModelPolicy value: " + value); + } +} 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 new file mode 100644 index 0000000000..a4433e1ea9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutoTier.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; + +/** + * Routing preference used when the session model is `auto`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutoTier { + /** The {@code efficiency} variant. */ + EFFICIENCY("efficiency"), + /** The {@code balance} variant. */ + BALANCE("balance"), + /** The {@code intelligence} variant. */ + INTELLIGENCE("intelligence"); + + private final String value; + 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) { + for (AutoTier v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutoTier value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java index 27fd29128d..e77117b2cb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java @@ -21,6 +21,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record CapiSessionOptions( + /** Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. */ + @JsonProperty("autoTier") AutoTier autoTier, /** Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. */ @JsonProperty("enableWebSocketResponses") Boolean enableWebSocketResponses ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java index d4afb32c3d..01d92e908a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java @@ -36,6 +36,10 @@ public final class CatalogNetworkFailureError extends CatalogSearchResult { @JsonProperty("statusCode") private Long statusCode; + /** Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. */ + @JsonProperty("retryAfterSeconds") + private Long retryAfterSeconds; + /** Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ @JsonProperty("message") private String message; @@ -46,6 +50,9 @@ public final class CatalogNetworkFailureError extends CatalogSearchResult { public Long getStatusCode() { return statusCode; } public void setStatusCode(Long statusCode) { this.statusCode = statusCode; } + public Long getRetryAfterSeconds() { return retryAfterSeconds; } + public void setRetryAfterSeconds(Long retryAfterSeconds) { this.retryAfterSeconds = retryAfterSeconds; } + public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java index c28d1cc4a7..ac821b062d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java @@ -26,6 +26,12 @@ public enum CatalogNetworkFailureReason { TLS("tls"), /** The {@code connection-refused} variant. */ CONNECTION_REFUSED("connection-refused"), + /** The {@code proxy-authentication-required} variant. */ + PROXY_AUTHENTICATION_REQUIRED("proxy-authentication-required"), + /** The {@code rate-limited} variant. */ + RATE_LIMITED("rate-limited"), + /** The {@code service-unavailable} variant. */ + SERVICE_UNAVAILABLE("service-unavailable"), /** The {@code http-status} variant. */ HTTP_STATUS("http-status"), /** The {@code response-too-large} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java index ee5e703621..68b90a19c9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java @@ -27,7 +27,7 @@ public record CatalogSearchParams( /** Protocol version and capabilities the caller requires. */ @JsonProperty("contract") CatalogClientContract contract, - /** Free-text search query. Never written to logs or telemetry. */ + /** Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. */ @JsonProperty("query") String query, /** Maximum number of candidates to return. Defaults to 10 when omitted. */ @JsonProperty("limit") Long limit, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java new file mode 100644 index 0000000000..fce707243d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * One server-discovered hook action from user, repository, plugin, or managed-policy configuration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record DiscoveredHook( + /** Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. */ + @JsonProperty("id") String id, + /** Hook event that invokes this action. */ + @JsonProperty("hookType") HookType hookType, + /** Configuration tier that contributed this hook action. */ + @JsonProperty("origin") HookOrigin origin, + /** Human-readable source label, such as a hook file path, settings source, or plugin name. */ + @JsonProperty("source") String source, + /** Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. */ + @JsonProperty("projectPath") String projectPath, + /** Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. */ + @JsonProperty("enabled") Boolean enabled, + /** Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. */ + @JsonProperty("disableKey") String disableKey +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java index bb28f40887..71ee3c49e6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java @@ -23,13 +23,15 @@ public record FactoryRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java new file mode 100644 index 0000000000..bf3bfd14cd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.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; + +/** + * Configuration tier that contributed a discovered hook action. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum HookOrigin { + /** The {@code user} variant. */ + USER("user"), + /** The {@code repository} variant. */ + REPOSITORY("repository"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code policy} variant. */ + POLICY("policy"); + + private final String value; + HookOrigin(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static HookOrigin fromValue(String value) { + for (HookOrigin v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown HookOrigin value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java index 8d7cd913c3..1a185958de 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Hook event name dispatched through the SDK callback transport. + * Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. * * @since 1.0.0 */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java new file mode 100644 index 0000000000..a0e7610394 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.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 java.util.List; +import javax.annotation.processing.Generated; + +/** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @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 HooksDiscoverParams( + /** Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. */ + @JsonProperty("projectPaths") List projectPaths, + /** When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. */ + @JsonProperty("excludeHostHooks") Boolean excludeHostHooks +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java new file mode 100644 index 0000000000..f30d4837ae --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.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.List; +import javax.annotation.processing.Generated; + +/** + * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + * + * @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 HooksDiscoverResult( + /** All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. */ + @JsonProperty("hooks") List hooks, + /** Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. */ + @JsonProperty("warnings") List warnings, + /** Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. */ + @JsonProperty("errors") List errors +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java index 087ca1c15a..d44087dcff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java @@ -28,6 +28,8 @@ public record ModelBillingPromo( /** UTC ISO 8601 timestamp marking when the promotion ends. Optional: an open-ended promotion omits this field. When present, the API only surfaces a promo whose expiry parses and is in the future, so consumers should treat a past value as expired. */ @JsonProperty("endsAt") String endsAt, /** Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. */ - @JsonProperty("message") String message + @JsonProperty("message") String message, + /** Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. */ + @JsonProperty("showBanner") Boolean showBanner ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java index f3b2f99188..f815f565f5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java @@ -23,6 +23,8 @@ public record QueuePendingItems( /** Stable opaque id for the canonical queued item. Batch rows share one id. */ @JsonProperty("id") String id, + /** Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. */ + @JsonProperty("messageId") String messageId, /** Whether this item is a queued user message or a queued slash command / model change */ @JsonProperty("kind") QueuePendingItemsKind kind, /** Human-readable text to display for this queue entry in the UI */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java index 1e56acb53f..9c5e3e475f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java @@ -25,7 +25,7 @@ public record SandboxConfigUserPolicyNetwork( @JsonProperty("allowOutbound") Boolean allowOutbound, /** Whether traffic to local/loopback addresses is allowed. */ @JsonProperty("allowLocalNetwork") Boolean allowLocalNetwork, - /** HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. */ + /** HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. */ @JsonProperty("proxy") SandboxConfigUserPolicyNetworkProxy proxy ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java index 74ff86919e..2946269ec1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SandboxConfigUserPolicyNetworkProxy( - /** Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ + /** Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ @JsonProperty("url") String url, /** Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. */ @JsonProperty("username") String username, 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 new file mode 100644 index 0000000000..b6f69bf7dc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.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 hooks} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerHooksApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerHooksApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture discover(HooksDiscoverParams params) { + return caller.invoke("hooks.discover", params, HooksDiscoverResult.class); + } + +} 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 111cee2560..caa7f92d07 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 @@ -25,6 +25,8 @@ public final class ServerRpc { private final RpcCaller caller; + /** API methods for the {@code hooks} namespace. */ + public final ServerHooksApi hooks; /** API methods for the {@code models} namespace. */ public final ServerModelsApi models; /** API methods for the {@code tools} namespace. */ @@ -71,6 +73,7 @@ public final class ServerRpc { */ public ServerRpc(RpcCaller caller) { this.caller = caller; + this.hooks = new ServerHooksApi(caller); this.models = new ServerModelsApi(caller); this.tools = new ServerToolsApi(caller); this.account = new ServerAccountApi(caller); 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 52481a7d73..870f7ac3cf 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 @@ -94,6 +94,17 @@ public CompletableFuture getMetadata(SessionsGetMetad return caller.invoke("sessions.getMetadata", params, SessionsGetMetadataResult.class); } + /** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture readPersistedEvents(SessionsReadPersistedEventsParams params) { + return caller.invoke("sessions.readPersistedEvents", params, SessionsReadPersistedEventsResult.class); + } + /** * Limit for non-empty local session IDs. * diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsEnqueueParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsEnqueueParams.java index d7725bc9cf..073038d2fc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsEnqueueParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsEnqueueParams.java @@ -27,6 +27,8 @@ public record SessionCommandsEnqueueParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. */ - @JsonProperty("command") String command + @JsonProperty("command") String command, + /** Optional user-facing text for the queue row. The command string is shown when omitted. */ + @JsonProperty("displayText") String displayText ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java index 0cb66280c0..c9f9de2dcc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java @@ -26,13 +26,15 @@ public record SessionFactoryCancelResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java index 6742faf03e..2d6a5f52a9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java @@ -26,13 +26,15 @@ public record SessionFactoryGetRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java index 71b12f94f1..1a9dee5926 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java @@ -26,13 +26,15 @@ public record SessionFactoryRunFromToolResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java index 46083f2284..d8ce481895 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java @@ -26,13 +26,15 @@ public record SessionFactoryRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, 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 a1d25ae7ec..dfb593fd21 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 @@ -30,6 +30,8 @@ public record SessionModelApplyStartupOverlayParams( @JsonProperty("deviceManagedModel") String deviceManagedModel, /** Model required by server-managed policy, when configured. */ @JsonProperty("serverManagedModel") String serverManagedModel, + /** 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. */ + @JsonProperty("policyHelperModel") String policyHelperModel, /** Model selected by repository settings, when configured. */ @JsonProperty("repoModel") String repoModel, /** Reasoning effort selected by repository settings, when configured. */ 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 a7c60d28cd..d7e36cf3c9 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 @@ -38,7 +38,7 @@ public record SessionModelSwitchToParams( @JsonProperty("modelCapabilities") 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") ContextTier contextTier, - /** Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. */ + /** 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") 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") Boolean deferIfModelChangeQueued, 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 34706a68e1..5f4753b1ac 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 @@ -111,6 +111,10 @@ public record SessionOpenOptions( @JsonProperty("allowAllMcpServerInstructions") Boolean allowAllMcpServerInstructions, /** Additional directories to search for skills. */ @JsonProperty("skillDirectories") List skillDirectories, + /** Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. */ + @JsonProperty("enableSkills") Boolean enableSkills, + /** Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. */ + @JsonProperty("hasSkillProvider") Boolean hasSkillProvider, /** Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. */ @JsonProperty("includedBuiltinSkills") List includedBuiltinSkills, /** Skill IDs disabled for this session. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index 2e8a069a4b..27a5c2db06 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -140,7 +140,7 @@ public record SessionOptionsUpdateParams( @JsonProperty("enableHostGitOperations") Boolean enableHostGitOperations, /** Whether to enable cross-session store writes and reads. */ @JsonProperty("enableSessionStore") Boolean enableSessionStore, - /** Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. */ + /** Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. */ @JsonProperty("enableSkills") Boolean enableSkills, /** Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. */ @JsonProperty("contextTier") OptionsUpdateContextTier contextTier, 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 05cfd396d4..b9bd6b8226 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 @@ -29,6 +29,8 @@ public final class SessionRpc { private final RpcCaller caller; private final String sessionId; + /** API methods for the {@code sandbox} namespace. */ + public final SessionSandboxApi sandbox; /** API methods for the {@code gitHubAuth} namespace. */ public final SessionGitHubAuthApi gitHubAuth; /** API methods for the {@code debug} namespace. */ @@ -115,6 +117,7 @@ public final class SessionRpc { public SessionRpc(RpcCaller caller, String sessionId) { this.caller = caller; this.sessionId = sessionId; + this.sandbox = new SessionSandboxApi(caller, sessionId); this.gitHubAuth = new SessionGitHubAuthApi(caller, sessionId); this.debug = new SessionDebugApi(caller, sessionId); this.canvas = new SessionCanvasApi(caller, sessionId); 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 new file mode 100644 index 0000000000..55efb9da78 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sandbox} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionSandboxApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionSandboxApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getEnforcementStatus() { + return caller.invoke("session.sandbox.getEnforcementStatus", java.util.Map.of("sessionId", this.sessionId), SessionSandboxGetEnforcementStatusResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusParams.java new file mode 100644 index 0000000000..ec92981e04 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusParams.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 SessionSandboxGetEnforcementStatusParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusResult.java new file mode 100644 index 0000000000..c1a0e77a1a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxGetEnforcementStatusResult.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; + +/** + * Managed sandbox enforcement state for a 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 SessionSandboxGetEnforcementStatusResult( + /** Whether the effective managed policy requires an available sandbox backend. */ + @JsonProperty("required") Boolean required, + /** Whether an enforcement failure has permanently blocked the session. */ + @JsonProperty("blocked") Boolean blocked, + /** The first sandbox enforcement failure that blocked the session. */ + @JsonProperty("reason") String reason +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiEphemeralQueryParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiEphemeralQueryParams.java index b384238b28..0f02f7258d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiEphemeralQueryParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiEphemeralQueryParams.java @@ -28,9 +28,9 @@ public record SessionUiEphemeralQueryParams( @JsonProperty("sessionId") String sessionId, /** Question to answer from the current conversation context. */ @JsonProperty("question") String question, - /** In-process streaming callback `(text) => void` invoked with each token as the model emits it. Marked internal: excluded from the public SDK surface. In a process-separated SDK this is replaced by a streaming RPC that yields chunks and a final answer. */ + /** In-process streaming callback `(text) => void` invoked with each token as the model emits it. Internal and excluded from the public SDK surface. */ @JsonProperty("onChunk") Object onChunk, - /** In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Marked internal: excluded from the public SDK surface. Replaced by an explicit cancellation token + cancel RPC in the SDK migration. */ + /** In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Internal and excluded from the public SDK surface. */ @JsonProperty("abortSignal") Object abortSignal ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsOpenCloud.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsOpenCloud.java index 8e4a74bd88..b0be6d42dd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsOpenCloud.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsOpenCloud.java @@ -40,7 +40,7 @@ public final class SessionsOpenCloud extends SessionsOpenParams { @JsonProperty("options") private SessionOpenOptions options; - /** In-process callback invoked when the cloud task is created (before connection). Marked internal because a function reference cannot cross the JSON-RPC boundary. Disappears in the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. */ + /** In-process callback invoked when the cloud task is created, before connection. Internal because function references cannot cross the JSON-RPC boundary. */ @JsonProperty("onTaskCreated") private Object onTaskCreated; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java new file mode 100644 index 0000000000..4da93409be --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.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; + +/** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * @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 SessionsReadPersistedEventsParams( + /** Session ID whose persisted event journal should be read. */ + @JsonProperty("sessionId") String sessionId, + /** Opaque cursor returned by a previous persisted-event read. Omit on the first call. */ + @JsonProperty("cursor") String cursor, + /** Maximum number of events to return in this batch (1–1000, default 200). */ + @JsonProperty("max") Long max, + /** Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. */ + @JsonProperty("direction") EventsReadDirection direction +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java new file mode 100644 index 0000000000..f022df5ae2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.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 com.github.copilot.generated.SessionEvent; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Batch of session events returned by a read, with cursor and continuation metadata. + * + * @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 SessionsReadPersistedEventsResult( + /** Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. */ + @JsonProperty("events") List events, + /** Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). */ + @JsonProperty("cursor") String cursor, + /** True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. */ + @JsonProperty("hasMore") Boolean hasMore, + /** Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. */ + @JsonProperty("cursorStatus") EventsCursorStatus cursorStatus +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionOptions.java index 440c01b2a8..e19737e32e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionOptions.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionsRegisterExtensionToolsOnSessionOptions( - /** In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: replaced by runtime-side enable/disable RPCs in the SDK migration. */ + /** In-process `() => boolean` gating callback used only by the CLI. */ @JsonProperty("enabled") Object enabled ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionParams.java index d7eb48f2b3..7fcb3322e9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionParams.java @@ -26,7 +26,7 @@ public record SessionsRegisterExtensionToolsOnSessionParams( /** Session to register extension tools on. */ @JsonProperty("sessionId") String sessionId, - /** In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, extension discovery/launch moves entirely into the runtime — the CLI passes pure config (search paths, disabled ids) via SessionOptions instead. */ + /** In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK surface. */ @JsonProperty("loader") Object loader, /** Optional registration options. */ @JsonProperty("options") SessionsRegisterExtensionToolsOnSessionOptions options diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionResult.java index 63cc5fb0fb..5cb5af063b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsRegisterExtensionToolsOnSessionResult.java @@ -24,7 +24,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionsRegisterExtensionToolsOnSessionResult( - /** In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration. */ + /** In-process unsubscribe function used only by the CLI. */ @JsonProperty("unsubscribe") Object unsubscribe ) { } 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 new file mode 100644 index 0000000000..a678b95fa3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.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; + +/** + * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillProviderDescriptor( + /** Invocation and display name. */ + @JsonProperty("name") String name, + /** Description used in skill catalogs without fetching content. */ + @JsonProperty("description") String description, + /** Whether users may invoke the skill directly. Defaults to true. */ + @JsonProperty("userInvocable") Boolean userInvocable, + /** Whether model invocation is disabled. Defaults to false. */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, + /** Optional freeform argument hint used by slash-command catalogs. */ + @JsonProperty("argumentHint") String argumentHint +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java new file mode 100644 index 0000000000..e11fcf0fbd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.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 SkillProviderListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java new file mode 100644 index 0000000000..228ec9240b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.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; + +/** + * Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. + * + * @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 SkillProviderListResult( + /** Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. */ + @JsonProperty("skills") List skills +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java new file mode 100644 index 0000000000..895078384b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.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; + +/** + * Identifies one SDK-provided skill by invocation name. + * + * @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 SkillProviderReadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Invocation name of the skill to read. */ + @JsonProperty("name") String name +) { +} 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 new file mode 100644 index 0000000000..0f17696565 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.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; + +/** + * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. + * + * @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 SkillProviderReadResult( + /** Complete SKILL.md text. 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/SkillSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java index 8d723548be..bef1995e6c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) * * @since 1.0.0 */ @@ -29,7 +29,9 @@ public enum SkillSource { /** The {@code custom} variant. */ CUSTOM("custom"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code sdk} variant. */ + SDK("sdk"); private final String value; SkillSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java index a020c89ecf..5e1f6fd90e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java @@ -24,12 +24,14 @@ public record SkillsInvokedSkill( /** Unique identifier for the skill */ @JsonProperty("name") String name, - /** Path to the SKILL.md file */ + /** Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity */ @JsonProperty("path") String path, /** Full content of the skill file */ @JsonProperty("content") String content, /** Tools that should be auto-approved when this skill is active, captured at invocation time */ @JsonProperty("allowedTools") List allowedTools, + /** Whether model invocation was disabled when this skill was invoked */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, /** Turn number when the skill was invoked */ @JsonProperty("invokedAtTurn") Long invokedAtTurn ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java index b2a1970a36..c457181435 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java @@ -32,6 +32,10 @@ public final class SlashCommandCompletedResult extends SlashCommandInvocationRes @JsonProperty("message") private String message; + /** Optional target session mode applied without submitting an agent prompt */ + @JsonProperty("mode") + private SessionMode mode; + /** True when the invocation mutated user runtime settings; consumers caching settings should refresh */ @JsonProperty("runtimeSettingsChanged") private Boolean runtimeSettingsChanged; @@ -39,6 +43,9 @@ public final class SlashCommandCompletedResult extends SlashCommandInvocationRes public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } + public SessionMode getMode() { return mode; } + public void setMode(SessionMode mode) { this.mode = mode; } + public Boolean getRuntimeSettingsChanged() { return runtimeSettingsChanged; } public void setRuntimeSettingsChanged(Boolean runtimeSettingsChanged) { this.runtimeSettingsChanged = runtimeSettingsChanged; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java index 29426d931b..4a366864c3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java @@ -23,6 +23,8 @@ public record SubagentSettingsEntry( /** Model override for matching subagents */ @JsonProperty("model") String model, + /** Whether the configured model strategy is preferred or required */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy, /** Reasoning effort override for matching subagents */ @JsonProperty("effortLevel") String effortLevel, /** Context tier override for matching subagents */ 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 acc683a720..ff0d121945 100644 --- a/java/sdk/src/main/java/com/github/copilot/CliServerManager.java +++ b/java/sdk/src/main/java/com/github/copilot/CliServerManager.java @@ -11,6 +11,7 @@ import java.net.Socket; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -19,6 +20,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.github.copilot.ffi.NativeRuntimeLoader; import com.github.copilot.rpc.CopilotClientOptions; /** @@ -64,7 +66,7 @@ void setConnectionToken(String connectionToken) { ProcessInfo startCliServer() throws IOException, InterruptedException { clearStderrBuffer(); - String cliPath = options.getCliPath() != null ? options.getCliPath() : "copilot"; + RuntimeLaunch launch = resolveCliLaunch(); var args = new ArrayList(); if (options.getCliArgs() != null) { @@ -106,7 +108,7 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { args.add("--remote"); } - List command = resolveCliCommand(cliPath, args); + List command = resolveCliCommand(launch.executable(), args); var pb = new ProcessBuilder(command); pb.redirectErrorStream(false); @@ -122,51 +124,7 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { pb.directory(new File(options.getCwd())); } - if (options.getEnvironment() != null) { - pb.environment().clear(); - pb.environment().putAll(options.getEnvironment()); - } - pb.environment().remove("NODE_DEBUG"); - - // Set auth token in environment if provided - if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { - pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); - } - - // Set Copilot home directory if configured - if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) { - pb.environment().put("COPILOT_HOME", options.getCopilotHome()); - } - - // Set connection token for TCP mode - if (connectionToken != null && !connectionToken.isEmpty()) { - pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken); - } - - // Set telemetry environment variables if configured - if (options.getTelemetry() != null) { - var telemetry = options.getTelemetry(); - pb.environment().put("COPILOT_OTEL_ENABLED", "true"); - if (telemetry.getOtlpEndpoint() != null) { - pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint()); - } - if (telemetry.getOtlpProtocol() != null) { - pb.environment().put("OTEL_EXPORTER_OTLP_PROTOCOL", telemetry.getOtlpProtocol()); - } - if (telemetry.getFilePath() != null) { - pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath()); - } - if (telemetry.getExporterType() != null) { - pb.environment().put("COPILOT_OTEL_EXPORTER_TYPE", telemetry.getExporterType()); - } - if (telemetry.getSourceName() != null) { - pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName()); - } - if (telemetry.getCaptureContent().isPresent()) { - pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", - telemetry.getCaptureContent().get() ? "true" : "false"); - } - } + configureProcessEnvironment(pb); Process process = pb.start(); @@ -310,6 +268,75 @@ private List resolveCliCommand(String cliPath, List args) { return result; } + void configureProcessEnvironment(ProcessBuilder pb) { + if (options.getEnvironment() != null) { + pb.environment().clear(); + pb.environment().putAll(options.getEnvironment()); + } + pb.environment().remove("NODE_DEBUG"); + + // Set auth token in environment if provided + if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { + pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); + } + + // Set Copilot home directory if configured + if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) { + pb.environment().put("COPILOT_HOME", options.getCopilotHome()); + } + + // Set connection token for TCP mode + if (connectionToken != null && !connectionToken.isEmpty()) { + pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken); + } + + // Set telemetry environment variables if configured + if (options.getTelemetry() != null) { + var telemetry = options.getTelemetry(); + pb.environment().put("COPILOT_OTEL_ENABLED", "true"); + if (telemetry.getOtlpEndpoint() != null) { + pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint()); + } + if (telemetry.getOtlpProtocol() != null) { + pb.environment().put("OTEL_EXPORTER_OTLP_PROTOCOL", telemetry.getOtlpProtocol()); + } + if (telemetry.getFilePath() != null) { + pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath()); + } + if (telemetry.getExporterType() != null) { + pb.environment().put("COPILOT_OTEL_EXPORTER_TYPE", telemetry.getExporterType()); + } + if (telemetry.getSourceName() != null) { + pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName()); + } + if (telemetry.getCaptureContent().isPresent()) { + pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + telemetry.getCaptureContent().get() ? "true" : "false"); + } + } + } + + RuntimeLaunch resolveCliLaunch() throws IOException { + return resolveCliLaunch(System.getenv(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV)); + } + + RuntimeLaunch resolveCliLaunch(String inheritedCliPath) throws IOException { + if (options.getCliPath() != null) { + return new RuntimeLaunch(options.getCliPath()); + } + + var environment = options.getEnvironment(); + String envCliPath = environment != null + ? environment.get(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV) + : inheritedCliPath; + if (envCliPath != null && !envCliPath.isBlank()) { + return new RuntimeLaunch(envCliPath); + } + + Path wrapper = NativeRuntimeLoader.resolveRuntimeWrapper(); + return new RuntimeLaunch(wrapper.toString()); + } + static URI parseCliUrl(String url) { // If it's just a port number, treat as localhost try { @@ -337,4 +364,7 @@ static URI parseCliUrl(String url) { */ record ProcessInfo(Process process, Integer port) { } + + record RuntimeLaunch(String executable) { + } } 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 ea2b0b67dd..c986e829a4 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -8,6 +8,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -479,7 +480,8 @@ void setInProcessTransportFactory(InProcessTransportFactory factory) { private static InProcessTransport openInProcessTransport(CopilotClientOptions options) throws IOException { FfiRuntimeHost host = new FfiRuntimeHost(); try { - host.start(resolveInProcessEntrypoint(), options); + Path explicitEntrypoint = NativeRuntimeLoader.resolveConfiguredEntrypoint(); + host.start(explicitEntrypoint == null ? null : explicitEntrypoint.toString(), options); } catch (RuntimeException | Error e) { host.close(); throw e; @@ -487,15 +489,6 @@ private static InProcessTransport openInProcessTransport(CopilotClientOptions op return new InProcessTransport(host.getReceiveStream(), host.getSendStream(), host); } - /** - * Resolves the runtime entrypoint handed to the in-process host. The copilot - * CLI executable is resolved from the same bundled location as - * {@code runtime.node} — no environment variables or PATH search. - */ - private static String resolveInProcessEntrypoint() throws IOException { - return NativeRuntimeLoader.resolveEntrypoint().toString(); - } - private static void closeRuntimeHost(AutoCloseable host) { try { host.close(); @@ -649,6 +642,13 @@ private void verifyProtocolVersion(Connection connection) throws Exception { if (this.options.getOnGitHubTelemetry() != null) { connectParams.put("enableGitHubTelemetryForwarding", true); } + // Declare the integrating application's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead of + // its own build. Omitted when the app didn't supply it (or supplied no fields). + var clientInfo = this.options.getClientInfo(); + if (clientInfo != null && !clientInfo.isEmpty()) { + connectParams.put("clientInfo", clientInfo); + } var connectResponse = connection.rpc.invoke("connect", connectParams, ConnectResult.class).get(30, TimeUnit.SECONDS); serverVersion = connectResponse.protocolVersion() != null 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 4254c04ec4..d54889cc4b 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -119,6 +119,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setReasoningEffort(config.getReasoningEffort()); request.setReasoningSummary(config.getReasoningSummary()); request.setContextTier(config.getContextTier()); + request.setAskUserVariant(config.getAskUserVariant()); request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); @@ -200,6 +201,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setGitHubToken(config.getGitHubToken()); request.setRemoteSession(config.getRemoteSession()); request.setCloud(config.getCloud()); + request.setFeatureFlags(config.getFeatureFlags()); request.setExpAssignments(config.getExpAssignments()); config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); request.setManagedSettings(config.getManagedSettings()); @@ -255,6 +257,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setReasoningEffort(config.getReasoningEffort()); request.setReasoningSummary(config.getReasoningSummary()); request.setContextTier(config.getContextTier()); + request.setAskUserVariant(config.getAskUserVariant()); request.setTools(config.getTools()); request.setSystemMessage(config.getSystemMessage()); request.setAvailableTools(config.getAvailableTools()); @@ -338,6 +341,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo } request.setGitHubToken(config.getGitHubToken()); request.setRemoteSession(config.getRemoteSession()); + request.setFeatureFlags(config.getFeatureFlags()); request.setExpAssignments(config.getExpAssignments()); config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); request.setManagedSettings(config.getManagedSettings()); diff --git a/java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java b/java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java index 5e7d2d461a..cb5bba1af1 100644 --- a/java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java +++ b/java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java @@ -88,13 +88,13 @@ private static Path resolveLibraryPath() throws IOException { * Starts the in-process runtime and opens a connection. * * @param entrypointPath - * runtime entrypoint path passed in {@code argv_json} + * optional explicit legacy CLI entrypoint passed in + * {@code argv_json} * @param options * client options used to construct {@code argv_json} and * {@code env_json} */ public void start(String entrypointPath, CopilotClientOptions options) { - Objects.requireNonNull(entrypointPath, "entrypointPath must not be null"); Objects.requireNonNull(options, "options must not be null"); if (disposed.get()) { throw new IllegalStateException("FfiRuntimeHost is already closed."); @@ -108,8 +108,7 @@ public void start(String entrypointPath, CopilotClientOptions options) { int hostHandle = runHostStartOnBlockingThread(argvJson, envJson); if (hostHandle == 0) { String lib = libraryPath != null ? libraryPath : ""; - throw new IllegalStateException( - "copilot_runtime_host_start failed (library '" + lib + "', entrypoint '" + entrypointPath + "')."); + throw new IllegalStateException("copilot_runtime_host_start failed (library '" + lib + "')."); } // Hold operationLock while publishing handles to serialize with close(). @@ -270,12 +269,14 @@ private int runHostStartOnBlockingThread(byte[] argvJson, byte[] envJson) { private static byte[] buildArgvJson(String entrypointPath, CopilotClientOptions options) { List argv = new ArrayList<>(); - if (entrypointPath.toLowerCase().endsWith(".js")) { - argv.add("node"); + if (entrypointPath != null) { + if (entrypointPath.toLowerCase().endsWith(".js")) { + argv.add("node"); + } + argv.add(entrypointPath); + argv.add("--embedded-host"); + argv.add("--no-auto-update"); } - argv.add(entrypointPath); - argv.add("--embedded-host"); - argv.add("--no-auto-update"); String logLevel = options.getLogLevel(); if (logLevel != null && !logLevel.isBlank()) { diff --git a/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java b/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java index 6733f4afb2..bd4b185a07 100644 --- a/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java +++ b/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java @@ -5,9 +5,12 @@ package com.github.copilot.ffi; import java.io.FileNotFoundException; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.channels.FileChannel; import java.nio.file.AccessDeniedException; import java.nio.file.AtomicMoveNotSupportedException; @@ -42,7 +45,10 @@ public final class NativeRuntimeLoader { static final String RUNTIME_FILENAME = "runtime.node"; static final String CLI_FILENAME = "copilot"; static final String CLI_FILENAME_WINDOWS = "copilot.exe"; + static final String RUNTIME_WRAPPER_FILENAME = "copilot-runtime"; + static final String RUNTIME_WRAPPER_FILENAME_WINDOWS = "copilot-runtime.exe"; static final String PLATFORM_PROPERTIES_FILENAME = "platform.properties"; + static final String RUNTIME_ASSETS_FILENAME = "runtime-assets.list"; /** Environment variable that overrides where the runtime is loaded from. */ public static final String COPILOT_CLI_PATH_ENV = "COPILOT_CLI_PATH"; static final String VERSION_RESOURCE = "copilot-runtime.properties"; @@ -125,10 +131,10 @@ public static Path resolve() throws IOException { } /** - * Resolves the copilot CLI executable from the same location as the bundled - * {@code runtime.node}. The CLI is used as {@code argv[0]} in - * {@code copilot_runtime_host_start} — the Rust runtime spawns it as a child - * process. + * Resolves the legacy copilot CLI entrypoint from the same location as the + * bundled {@code runtime.node}. Callers may pass this entrypoint through + * {@code copilot_runtime_host_start} when legacy extension hosting is + * requested. * *

* This method calls {@link #resolve()} to locate {@code runtime.node}, then @@ -144,6 +150,73 @@ public static Path resolveEntrypoint() throws IOException { return resolveEntrypoint(configuredCli, resolve()); } + /** + * Resolves an explicitly configured legacy CLI entrypoint, if it has a + * compatible adjacent runtime library. + * + * @return the absolute CLI path, or {@code null} when no compatible override is + * configured + * @throws IOException + * if the configured files cannot be inspected + */ + public static Path resolveConfiguredEntrypoint() throws IOException { + String configuredCli = System.getenv(COPILOT_CLI_PATH_ENV); + if (configuredCli == null || configuredCli.isBlank()) { + return null; + } + Path configuredPath = Path.of(configuredCli).toAbsolutePath().normalize(); + return resolveFromCliPath(configuredCli) != null && Files.isRegularFile(configuredPath) + && Files.size(configuredPath) > 0 ? configuredPath : null; + } + + /** + * Resolves the out-of-process runtime wrapper from the platform classifier JAR + * and extracts it beside {@code runtime.node}. + * + * @return absolute path to the runtime wrapper executable + * @throws IOException + * if the classifier artifacts cannot be extracted + */ + public static Path resolveRuntimeWrapper() throws IOException { + ClassLoader loader = NativeRuntimeLoader.class.getClassLoader(); + String classifier = PlatformDetector.detectClassifier(); + String version = readVersion(loader); + return resolveRuntimeWrapper(defaultCacheBase(), loader, classifier, version); + } + + static Path resolveRuntimeWrapper(Path cacheBase, ClassLoader loader, String classifier, String version) + throws IOException { + Path runtimePath = extractRuntimeToCache(cacheBase, loader, classifier, version, DEFAULT_PUBLISHER, false); + Path cacheDir = runtimePath.getParent(); + String wrapperName = classifier.startsWith("win32-") + ? RUNTIME_WRAPPER_FILENAME_WINDOWS + : RUNTIME_WRAPPER_FILENAME; + Path cachedWrapper = cacheDir.resolve(wrapperName); + if (isValidCachedCli(cachedWrapper)) { + return cachedWrapper; + } + + String resourcePath = "native/" + classifier + "/" + wrapperName; + URL resource = loader.getResource(resourcePath); + if (resource == null) { + throw new FileNotFoundException("Runtime wrapper not found on classpath: " + resourcePath + + " — add the matching classifier JAR to the classpath"); + } + + Path temp = Files.createTempFile(cacheDir, "runtime-wrapper-tmp-", ""); + try { + copyResourceToTemp(resource, resourcePath, temp); + makeExecutable(temp); + DEFAULT_PUBLISHER.publish(temp, cachedWrapper); + } finally { + tryDelete(temp); + } + if (!isValidCachedCli(cachedWrapper)) { + throw new IOException("Published runtime wrapper is not a non-empty executable file: " + cachedWrapper); + } + return cachedWrapper; + } + static Path resolveEntrypoint(String configuredCli, Path runtimePath) throws IOException { if (configuredCli != null && !configuredCli.isBlank()) { Path configuredPath = Path.of(configuredCli).toAbsolutePath().normalize(); @@ -318,6 +391,11 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier */ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier, String version, AtomicPublisher publisher) throws IOException { + return extractRuntimeToCache(cacheBase, loader, classifier, version, publisher, true); + } + + private static Path extractRuntimeToCache(Path cacheBase, ClassLoader loader, String classifier, String version, + AtomicPublisher publisher, boolean extractCli) throws IOException { String resourcePath = "native/" + classifier + "/" + RUNTIME_FILENAME; String nativeVersion = readNativePackageVersion(loader, classifier); Path cacheDir = cacheBase.resolve(version).resolve(nativeVersion).resolve(classifier); @@ -325,7 +403,10 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier // Step 1 — fast path: return an existing valid cache entry. if (isValidCachedFile(cached)) { - extractCliToCache(cacheDir, loader, classifier, publisher); + extractRuntimeAssetsToCache(cacheDir, loader, classifier, publisher); + if (extractCli) { + extractCliToCache(cacheDir, loader, classifier, publisher); + } return cached; } @@ -348,12 +429,68 @@ static Path extractToCache(Path cacheBase, ClassLoader loader, String classifier tryDelete(temp); } - // Step 5 — also extract the copilot CLI executable alongside runtime.node. - extractCliToCache(cacheDir, loader, classifier, publisher); + extractRuntimeAssetsToCache(cacheDir, loader, classifier, publisher); + if (extractCli) { + extractCliToCache(cacheDir, loader, classifier, publisher); + } return cached; } + private static void extractRuntimeAssetsToCache(Path cacheDir, ClassLoader loader, String classifier, + AtomicPublisher publisher) throws IOException { + String inventoryResourcePath = "native/" + classifier + "/" + RUNTIME_ASSETS_FILENAME; + URL inventoryResource = loader.getResource(inventoryResourcePath); + if (inventoryResource == null) { + return; + } + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(inventoryResource.openStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) { + continue; + } + String[] fields = line.split("\\t", 2); + if (fields.length != 2) { + throw new IOException("Invalid runtime asset inventory entry: " + line); + } + boolean executable = (Integer.parseInt(fields[0], 8) & 0111) != 0; + Path relative = Path.of(fields[1]).normalize(); + if (relative.isAbsolute() || relative.startsWith("..")) { + throw new IOException("Unsafe runtime asset inventory path: " + fields[1]); + } + Path cached = cacheDir.resolve(relative).normalize(); + if (!cached.startsWith(cacheDir)) { + throw new IOException("Runtime asset escapes cache directory: " + fields[1]); + } + if (isValidCachedFile(cached) && (!executable || isWindows() || Files.isExecutable(cached))) { + continue; + } + + String resourcePath = "native/" + classifier + "/" + fields[1]; + URL resource = loader.getResource(resourcePath); + if (resource == null) { + throw new FileNotFoundException("Runtime asset not found on classpath: " + resourcePath); + } + Files.createDirectories(cached.getParent()); + Path temp = Files.createTempFile(cached.getParent(), "runtime-asset-tmp-", ""); + try { + copyResourceToTemp(resource, resourcePath, temp); + if (executable) { + makeExecutable(temp); + } + publisher.publish(temp, cached); + } finally { + tryDelete(temp); + } + } + } catch (NumberFormatException ex) { + throw new IOException("Invalid runtime asset mode in " + inventoryResourcePath, ex); + } + } + /** * Extracts the copilot CLI executable from the classpath to the same cache * directory as {@code runtime.node}. Idempotent — skips extraction if already diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java b/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java new file mode 100644 index 0000000000..17c6a1333f --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/AskUserVariant.java @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Selects how the built-in {@code ask_user} tool collects user input. + */ +public enum AskUserVariant { + + /** Uses the legacy question-and-answer experience. */ + LEGACY("legacy"), + + /** Uses structured elicitation to collect user input. */ + ELICITATION("elicitation"); + + private final String value; + + AskUserVariant(String value) { + this.value = value; + } + + /** + * Returns the wire-format value. + * + * @return the value used in JSON serialization + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Creates an {@code AskUserVariant} from its wire-format value. + * + * @param value + * the wire-format value + * @return the matching variant, or {@code null} when {@code value} is + * {@code null} + * @throws IllegalArgumentException + * if the value is not {@code legacy} or {@code elicitation} + */ + @JsonCreator + public static AskUserVariant fromValue(String value) { + if (value == null) { + return null; + } + for (AskUserVariant variant : values()) { + if (variant.value.equals(value)) { + return variant; + } + } + throw new IllegalArgumentException("Unknown AskUserVariant value: " + value); + } +} 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 new file mode 100644 index 0000000000..f9117abfb2 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Routing tier for the {@code auto} model with Auto mode V2. + * + * @see CapiSessionOptions#setAutoTier(AutoTier) + */ +public enum AutoTier { + + /** Prioritize efficiency. */ + EFFICIENCY("efficiency"), + + /** Balance efficiency and intelligence. */ + BALANCE("balance"), + + /** Prioritize intelligence. */ + INTELLIGENCE("intelligence"); + + private final String value; + + AutoTier(String value) { + this.value = value; + } + + /** + * Returns the JSON value for this routing tier. + * + * @return the string value used in JSON serialization + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Deserializes a JSON string into its routing tier. + * + * @param value + * 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 + */ + @JsonCreator + public static AutoTier fromValue(String value) { + if (value == null) { + return null; + } + for (AutoTier tier : values()) { + if (tier.value.equals(value)) { + return tier; + } + } + throw new IllegalArgumentException("Unknown AutoTier value: " + value); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java index d94d59f67b..e401762302 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java @@ -29,9 +29,40 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CapiSessionOptions { + @JsonProperty("autoTier") + private AutoTier autoTier; + @JsonProperty("enableWebSocketResponses") private Boolean enableWebSocketResponses; + /** + * Gets the routing tier for the {@code auto} model (Auto mode V2). + * + * @return the explicit tier, or {@code null} to leave tier selection to the + * runtime + */ + public AutoTier getAutoTier() { + return autoTier; + } + + /** + * Sets the routing tier, meaningful only with model {@code auto} (Auto mode + * V2). Requires a runtime version that supports {@code capi.autoTier}. + *

+ * When omitted, the runtime chooses its default on create and preserves the + * persisted or current tier on resume. An explicit tier overrides the persisted + * tier on cold resume; the runtime rejects a conflicting tier when resuming a + * session already resident in memory. + * + * @param autoTier + * the routing tier, or {@code null} to omit it from the request + * @return this config for method chaining + */ + public CapiSessionOptions setAutoTier(AutoTier autoTier) { + this.autoTier = autoTier; + return this; + } + /** * Gets whether CAPI Responses API WebSocket transport is enabled. * diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java new file mode 100644 index 0000000000..ee9d97e1e9 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Identity of the integrating application, declared on the + * {@code server.connect} handshake. + *

+ * Declaring it lets the telemetry the runtime emits on the connection be + * attributed to a single, consistent surface (the application and its Copilot + * integration) instead of the runtime's own build. All fields are optional; an + * empty field is omitted from the handshake. + * + *

Example Usage

+ * + *
{@code
+ * var options = new CopilotClientOptions().setClientInfo(new ClientInfo().setApplicationName("acme-developer-portal")
+ * 		.setApplicationVersion("2.4.0").setIntegrationName("copilot-assistant").setIntegrationVersion("1.5.0"));
+ * }
+ * + * @see CopilotClientOptions#setClientInfo(ClientInfo) + * @since 1.6.0 + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class ClientInfo { + + private String applicationName; + + private String applicationVersion; + + private String integrationName; + + private String integrationVersion; + + /** + * Gets the name of the application using the SDK. + * + * @return the application name, or {@code null} + */ + @JsonProperty("editorName") + public String getApplicationName() { + return applicationName; + } + + /** + * Sets the name of the application using the SDK. + * + * @param applicationName + * the application name + * @return this client info for method chaining + */ + @JsonProperty("editorName") + public ClientInfo setApplicationName(String applicationName) { + this.applicationName = applicationName; + return this; + } + + /** + * Gets the version of the application using the SDK. + * + * @return the application version, or {@code null} + */ + @JsonProperty("editorVersion") + public String getApplicationVersion() { + return applicationVersion; + } + + /** + * Sets the version of the application using the SDK. + * + * @param applicationVersion + * the application version + * @return this client info for method chaining + */ + @JsonProperty("editorVersion") + public ClientInfo setApplicationVersion(String applicationVersion) { + this.applicationVersion = applicationVersion; + return this; + } + + /** + * Gets the optional name of a specific integration within the application, such + * as an extension or plugin. + * + * @return the integration name, or {@code null} + */ + @JsonProperty("extensionName") + public String getIntegrationName() { + return integrationName; + } + + /** + * Sets the optional name of a specific integration within the application, such + * as an extension or plugin. + * + * @param integrationName + * the integration name + * @return this client info for method chaining + */ + @JsonProperty("extensionName") + public ClientInfo setIntegrationName(String integrationName) { + this.integrationName = integrationName; + return this; + } + + /** + * Gets the optional version of the named integration. + * + * @return the integration version, or {@code null} + */ + @JsonProperty("extensionVersion") + public String getIntegrationVersion() { + return integrationVersion; + } + + /** + * Sets the optional version of the named integration. + * + * @param integrationVersion + * the integration version + * @return this client info for method chaining + */ + @JsonProperty("extensionVersion") + public ClientInfo setIntegrationVersion(String integrationVersion) { + this.integrationVersion = integrationVersion; + return this; + } + + /** + * Returns whether no field carries a non-empty value, in which case the SDK + * omits {@code clientInfo} from the handshake so the runtime keeps its default + * attribution. + * + * @return {@code true} when every field is {@code null} or empty + */ + @JsonIgnore + public boolean isEmpty() { + return isBlank(applicationName) && isBlank(applicationVersion) && isBlank(integrationName) + && isBlank(integrationVersion); + } + + private static boolean isBlank(String value) { + return value == null || value.isEmpty(); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java index d3515509b5..c67947bf48 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java @@ -55,6 +55,7 @@ public class CopilotClientOptions { private String cliPath; private String cliUrl; private RuntimeConnection connection; + private ClientInfo clientInfo; private String copilotHome; private String cwd; private Map environment; @@ -192,19 +193,20 @@ public CopilotClientOptions setCliArgs(String[] cliArgs) { } /** - * Gets the path to the Copilot CLI executable. + * Gets the path to an explicitly configured Copilot executable. * - * @return the CLI path, or {@code null} to use "copilot" from PATH + * @return the executable path, or {@code null} to use the bundled runtime + * wrapper */ public String getCliPath() { return cliPath; } /** - * Sets the path to the Copilot CLI executable. + * Sets the path to the Copilot CLI or runtime wrapper executable. * * @param cliPath - * the path to the CLI executable + * the path to the executable * @return this options instance for method chaining */ public CopilotClientOptions setCliPath(String cliPath) { @@ -677,6 +679,36 @@ public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) { return this; } + /** + * Gets the integrating application's declared identity. + * + * @return the client info, or {@code null} + * @since 1.6.0 + */ + public ClientInfo getClientInfo() { + return clientInfo; + } + + /** + * Declares the integrating application's identity, forwarded to the runtime on + * the {@code server.connect} handshake. + *

+ * Declaring it lets the telemetry the runtime emits on this connection be + * attributed to a consistent surface (the application and its Copilot + * integration) instead of the runtime's own build. All fields on + * {@link ClientInfo} are optional; leave this unset to keep the runtime's + * default attribution. + * + * @param clientInfo + * the application identity to declare + * @return this options instance for method chaining + * @since 1.6.0 + */ + public CopilotClientOptions setClientInfo(ClientInfo clientInfo) { + this.clientInfo = Objects.requireNonNull(clientInfo, "clientInfo must not be null"); + return this; + } + /** * Gets the server-wide idle timeout for sessions in seconds. * @@ -829,6 +861,7 @@ public CopilotClientOptions clone() { copy.cliPath = this.cliPath; copy.cliUrl = this.cliUrl; copy.connection = this.connection; + copy.clientInfo = this.clientInfo; copy.copilotHome = this.copilotHome; copy.cwd = this.cwd; copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; 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 403893987d..f6b5001e7a 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 @@ -45,6 +45,9 @@ public final class CreateSessionRequest { @JsonProperty("contextTier") private String contextTier; + @JsonProperty("askUserVariant") + private AskUserVariant askUserVariant; + @JsonProperty("tools") private List tools; @@ -236,6 +239,9 @@ public final class CreateSessionRequest { @JsonProperty("expAssignments") private CopilotExpAssignmentResponse expAssignments; + @JsonProperty("featureFlags") + private Map featureFlags; + @JsonProperty("enableManagedSettings") @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableManagedSettings; @@ -309,6 +315,16 @@ public void setContextTier(String contextTier) { this.contextTier = contextTier; } + /** Gets the ask-user variant. @return the ask-user variant */ + public AskUserVariant getAskUserVariant() { + return askUserVariant; + } + + /** Sets the ask-user variant. @param askUserVariant the ask-user variant */ + public void setAskUserVariant(AskUserVariant askUserVariant) { + this.askUserVariant = askUserVariant; + } + /** Gets the tools. @return the tool definitions */ public List getTools() { return tools == null ? null : Collections.unmodifiableList(tools); @@ -1113,6 +1129,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) { this.expAssignments = expAssignments; } + /** Gets host-resolved feature flags. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** Sets host-resolved feature flags. @param featureFlags the feature flags */ + public void setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + } + /** * Gets the self-fetch managed settings flag. @return the flag, or {@code null} * if not set 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 a55c3454e7..ac4f71e07b 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 @@ -63,6 +63,7 @@ public class ResumeSessionConfig { private String reasoningEffort; private String reasoningSummary; private String contextTier; + private AskUserVariant askUserVariant; private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; private McpAuthHandler onMcpAuthRequest; @@ -111,6 +112,7 @@ public class ResumeSessionConfig { private String remoteSession; private CopilotExpAssignmentResponse expAssignments; private Boolean enableManagedSettings; + private Map featureFlags; private ManagedSettings managedSettings; /** @@ -793,6 +795,31 @@ public ResumeSessionConfig setContextTier(String contextTier) { return this; } + /** + * Gets the experience used by the built-in {@code ask_user} tool. + * + * @return the ask-user variant, or {@code null} to use the legacy experience + */ + public AskUserVariant getAskUserVariant() { + return askUserVariant; + } + + /** + * Sets the model-facing shape of the built-in {@code ask_user} tool when the + * session is resumed by a new client. + *

+ * When unset, the option is omitted and the legacy shape is used. Set an + * elicitation handler when selecting {@link AskUserVariant#ELICITATION}. + * + * @param askUserVariant + * the ask-user variant + * @return this config instance for method chaining + */ + public ResumeSessionConfig setAskUserVariant(AskUserVariant askUserVariant) { + this.askUserVariant = askUserVariant; + return this; + } + /** * Gets the permission request handler. * @@ -1969,6 +1996,23 @@ public ResumeSessionConfig setExpAssignments(CopilotExpAssignmentResponse expAss return this; } + /** Gets host-resolved feature-flag values. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** + * Sets feature-flag values resolved by the host to apply on resume. + * + * @param featureFlags + * the feature flags + * @return this config for method chaining + */ + public ResumeSessionConfig setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + return this; + } + /** * Gets whether the runtime self-fetches enterprise managed settings at session * bootstrap on resume. @@ -2052,6 +2096,7 @@ public ResumeSessionConfig clone() { copy.reasoningEffort = this.reasoningEffort; copy.reasoningSummary = this.reasoningSummary; copy.contextTier = this.contextTier; + copy.askUserVariant = this.askUserVariant; copy.modelCapabilities = this.modelCapabilities; copy.onPermissionRequest = this.onPermissionRequest; copy.onUserInputRequest = this.onUserInputRequest; @@ -2102,6 +2147,7 @@ public ResumeSessionConfig clone() { copy.gitHubToken = this.gitHubToken; copy.gitHubTokenProvider = this.gitHubTokenProvider; copy.remoteSession = this.remoteSession; + copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null; copy.expAssignments = this.expAssignments; copy.enableManagedSettings = this.enableManagedSettings; copy.managedSettings = this.managedSettings; 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 9b8e897fda..776d58137b 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 @@ -47,6 +47,9 @@ public final class ResumeSessionRequest { @JsonProperty("contextTier") private String contextTier; + @JsonProperty("askUserVariant") + private AskUserVariant askUserVariant; + @JsonProperty("tools") private List tools; @@ -238,6 +241,9 @@ public final class ResumeSessionRequest { @JsonProperty("expAssignments") private CopilotExpAssignmentResponse expAssignments; + @JsonProperty("featureFlags") + private Map featureFlags; + @JsonProperty("enableManagedSettings") @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableManagedSettings; @@ -311,6 +317,16 @@ public void setContextTier(String contextTier) { this.contextTier = contextTier; } + /** Gets the ask-user variant. @return the ask-user variant */ + public AskUserVariant getAskUserVariant() { + return askUserVariant; + } + + /** Sets the ask-user variant. @param askUserVariant the ask-user variant */ + public void setAskUserVariant(AskUserVariant askUserVariant) { + this.askUserVariant = askUserVariant; + } + /** Gets the tools. @return the tool definitions */ public List getTools() { return tools == null ? null : Collections.unmodifiableList(tools); @@ -1128,6 +1144,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) { this.expAssignments = expAssignments; } + /** Gets host-resolved feature flags. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** Sets host-resolved feature flags. @param featureFlags the feature flags */ + public void setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + } + /** * Gets the self-fetch managed settings flag. @return the flag, or {@code null} * if not set 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 9f6ddb5efa..cbcacd9771 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 @@ -46,6 +46,7 @@ public class SessionConfig { private String reasoningEffort; private String reasoningSummary; private String contextTier; + private AskUserVariant askUserVariant; private List tools; private SystemMessageConfig systemMessage; private List availableTools; @@ -112,6 +113,7 @@ public class SessionConfig { private CloudSessionOptions cloud; private CopilotExpAssignmentResponse expAssignments; private Boolean enableManagedSettings; + private Map featureFlags; private ManagedSettings managedSettings; /** @@ -254,6 +256,30 @@ public SessionConfig setContextTier(String contextTier) { return this; } + /** + * Gets the experience used by the built-in {@code ask_user} tool. + * + * @return the ask-user variant, or {@code null} to use the legacy experience + */ + public AskUserVariant getAskUserVariant() { + return askUserVariant; + } + + /** + * Sets the model-facing shape of the built-in {@code ask_user} tool. + *

+ * When unset, the option is omitted and the legacy shape is used. Set an + * elicitation handler when selecting {@link AskUserVariant#ELICITATION}. + * + * @param askUserVariant + * the ask-user variant + * @return this config instance for method chaining + */ + public SessionConfig setAskUserVariant(AskUserVariant askUserVariant) { + this.askUserVariant = askUserVariant; + return this; + } + /** * Gets the custom tools for this session. * @@ -894,7 +920,9 @@ public UserInputHandler getOnUserInputRequest() { /** * Sets a handler for user input requests from the agent. *

- * When provided, enables the ask_user tool for the agent to request user input. + * When provided, enables the legacy question-and-answer form of the + * {@code ask_user} tool. Use an elicitation handler with + * {@link AskUserVariant#ELICITATION}. * * @param onUserInputRequest * the user input handler @@ -2097,6 +2125,23 @@ public SessionConfig setExpAssignments(CopilotExpAssignmentResponse expAssignmen return this; } + /** Gets host-resolved feature-flag values. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** + * Sets feature-flag values resolved by the host for this session. + * + * @param featureFlags + * the feature flags + * @return this config instance for method chaining + */ + public SessionConfig setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + return this; + } + /** * Gets whether the runtime self-fetches enterprise managed settings at session * bootstrap. @@ -2173,6 +2218,7 @@ public SessionConfig clone() { copy.reasoningEffort = this.reasoningEffort; copy.reasoningSummary = this.reasoningSummary; copy.contextTier = this.contextTier; + copy.askUserVariant = this.askUserVariant; copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; copy.systemMessage = this.systemMessage; copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; @@ -2243,6 +2289,7 @@ public SessionConfig clone() { copy.gitHubTokenProvider = this.gitHubTokenProvider; copy.remoteSession = this.remoteSession; copy.cloud = this.cloud; + copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null; copy.expAssignments = this.expAssignments; copy.enableManagedSettings = this.enableManagedSettings; copy.managedSettings = this.managedSettings; 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 17e8f131f7..dccb4e9add 100644 --- a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java @@ -9,12 +9,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; 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.github.copilot.rpc.AutoTier; import com.github.copilot.rpc.CapiSessionOptions; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.SessionConfig; @@ -29,6 +33,7 @@ void defaultsAreNull() { var capi = new CapiSessionOptions(); assertNull(capi.getEnableWebSocketResponses()); + assertNull(capi.getAutoTier()); } @Test @@ -37,6 +42,8 @@ void fluentSetterReturnsSameInstance() { assertSame(capi, capi.setEnableWebSocketResponses(true)); assertEquals(Boolean.TRUE, capi.getEnableWebSocketResponses()); + assertSame(capi, capi.setAutoTier(AutoTier.BALANCE)); + assertEquals(AutoTier.BALANCE, capi.getAutoTier()); } @Test @@ -46,6 +53,7 @@ void serializesEnableWebSocketResponses() { JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertTrue(json.get("enableWebSocketResponses").asBoolean()); + assertTrue(json.path("autoTier").isMissingNode()); } @Test @@ -55,6 +63,44 @@ void omitsUnsetEnableWebSocketResponses() { JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertTrue(json.path("enableWebSocketResponses").isMissingNode()); + assertTrue(json.path("autoTier").isMissingNode()); + assertEquals(0, json.size()); + } + + @ParameterizedTest + @CsvSource({"EFFICIENCY,efficiency", "BALANCE,balance", "INTELLIGENCE,intelligence"}) + void autoTierCanonicalValuesRoundTripAndForward(AutoTier tier, String value) throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var capi = new CapiSessionOptions().setAutoTier(tier); + JsonNode json = mapper.valueToTree(capi); + assertEquals(value, json.get("autoTier").asText()); + assertEquals(1, json.size()); + assertEquals(tier, mapper.treeToValue(json, CapiSessionOptions.class).getAutoTier()); + + capi.setEnableWebSocketResponses(false); + var create = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setModel("auto").setCapi(capi), + "session-1"); + var resume = SessionRequestBuilder.buildResumeRequest("session-1", new ResumeSessionConfig().setCapi(capi)); + for (Object request : new Object[]{create, resume}) { + JsonNode requestJson = mapper.valueToTree(request); + assertEquals(value, requestJson.get("capi").get("autoTier").asText()); + assertFalse(requestJson.get("capi").get("enableWebSocketResponses").asBoolean()); + assertEquals(2, requestJson.get("capi").size()); + } + } + + @Test + void autoTierRejectsNoncanonicalValues() { + for (String value : new String[]{"balanced", "Balance", "unknown"}) { + assertThrows(IllegalArgumentException.class, () -> AutoTier.fromValue(value)); + } + assertNull(AutoTier.fromValue(null)); + } + + @Test + void clearingAutoTierOmitsIt() { + var capi = new CapiSessionOptions().setAutoTier(AutoTier.BALANCE).setAutoTier(null); + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertEquals(0, json.size()); } @@ -67,6 +113,7 @@ void createRequestIncludesCapiWhenSet() { assertNotNull(request.getCapi()); assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean()); + assertTrue(json.get("capi").path("autoTier").isMissingNode()); } @Test @@ -89,6 +136,7 @@ void resumeRequestIncludesCapiWhenSet() { assertNotNull(request.getCapi()); assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean()); + assertTrue(json.get("capi").path("autoTier").isMissingNode()); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java index 68555a35b4..a227be04b9 100644 --- a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java @@ -9,9 +9,13 @@ import java.io.IOException; import java.net.ServerSocket; import java.net.URI; +import java.nio.file.Path; +import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import com.github.copilot.ffi.NativeRuntimeLoader; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.TelemetryConfig; @@ -22,6 +26,47 @@ */ class CliServerManagerTest { + @TempDir + Path tempDir; + + @Test + void explicitCliPathDoesNotRequireRuntimeBundle() throws Exception { + Path explicit = tempDir.resolve("copilot"); + var manager = new CliServerManager(new CopilotClientOptions().setCliPath(explicit.toString())); + + assertEquals(explicit.toString(), manager.resolveCliLaunch().executable()); + } + + @Test + void inheritedCliPathEnvironmentOverrideDoesNotRequireRuntimeBundle() throws Exception { + Path inherited = tempDir.resolve("copilot-runtime"); + var manager = new CliServerManager(new CopilotClientOptions()); + + assertEquals(inherited.toString(), manager.resolveCliLaunch(inherited.toString()).executable()); + } + + @Test + void configuredEnvironmentCliPathOverridesInheritedEnvironment() throws Exception { + Path inherited = tempDir.resolve("inherited-copilot-runtime"); + Path configured = tempDir.resolve("configured-copilot-runtime"); + var options = new CopilotClientOptions() + .setEnvironment(Map.of(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV, configured.toString())); + var manager = new CliServerManager(options); + + assertEquals(configured.toString(), manager.resolveCliLaunch(inherited.toString()).executable()); + } + + @Test + void explicitCliPathOverridesEnvironment() throws Exception { + Path explicit = tempDir.resolve("explicit-copilot-runtime"); + Path configured = tempDir.resolve("configured-copilot-runtime"); + var options = new CopilotClientOptions().setCliPath(explicit.toString()) + .setEnvironment(Map.of(NativeRuntimeLoader.COPILOT_CLI_PATH_ENV, configured.toString())); + var manager = new CliServerManager(options); + + assertEquals(explicit.toString(), manager.resolveCliLaunch("inherited-copilot-runtime").executable()); + } + // ===== parseCliUrl tests ===== @Test 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 4c5a3fbef0..2433e5f67a 100644 --- a/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java @@ -18,6 +18,7 @@ import com.github.copilot.generated.SessionEvent; import com.github.copilot.generated.rpc.SessionLimitsConfig; import com.github.copilot.rpc.AutoModeSwitchResponse; +import com.github.copilot.rpc.AskUserVariant; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.DefaultAgentConfig; import com.github.copilot.rpc.ExitPlanModeResult; @@ -119,6 +120,7 @@ void sessionConfigCloneBasic() { original.setModel("gpt-4o"); original.setReasoningSummary("detailed"); original.setContextTier("long_context"); + original.setAskUserVariant(AskUserVariant.ELICITATION); original.setPluginDirectories(List.of("/plugins/a", "/plugins/b")); original.setDisabledMcpServers(List.of("local-files", "remote-github")); original.setLargeOutput( @@ -133,6 +135,7 @@ void sessionConfigCloneBasic() { assertEquals(original.getModel(), cloned.getModel()); assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary()); assertEquals(original.getContextTier(), cloned.getContextTier()); + assertEquals(original.getAskUserVariant(), cloned.getAskUserVariant()); assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories()); assertEquals(original.getDisabledMcpServers(), cloned.getDisabledMcpServers()); assertEquals(original.getLargeOutput(), cloned.getLargeOutput()); @@ -198,6 +201,7 @@ void resumeSessionConfigCloneBasic() { original.setModel("o1"); original.setReasoningSummary("none"); original.setContextTier("long_context"); + original.setAskUserVariant(AskUserVariant.LEGACY); original.setPluginDirectories(List.of("/plugins/r")); original.setDisabledMcpServers(List.of("local-files-r")); original.setLargeOutput( @@ -210,6 +214,7 @@ void resumeSessionConfigCloneBasic() { assertEquals(original.getModel(), cloned.getModel()); assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary()); assertEquals(original.getContextTier(), cloned.getContextTier()); + assertEquals(original.getAskUserVariant(), cloned.getAskUserVariant()); assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories()); assertEquals(original.getDisabledMcpServers(), cloned.getDisabledMcpServers()); assertEquals(original.getLargeOutput(), cloned.getLargeOutput()); diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java b/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java index 3025c64c39..b4159d839c 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java @@ -77,11 +77,11 @@ void threadsSessionIdForCapiAndByok() throws Exception { // BYOK session. int before = handler.inferenceRequests().size(); ProviderConfig provider = new ProviderConfig().setType("openai").setWireApi("responses") - .setBaseUrl("https://byok.invalid/v1").setApiKey("byok-secret").setModelId("claude-sonnet-4.5") - .setWireModel("claude-sonnet-4.5"); + .setBaseUrl("https://byok.invalid/v1").setApiKey("byok-secret").setModelId("claude-sonnet-5") + .setWireModel("claude-sonnet-5"); CopilotSession byokSession = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setModel("claude-sonnet-4.5").setProvider(provider)) + .setModel("claude-sonnet-5").setProvider(provider)) .get(); String byokSessionId = byokSession.getSessionId(); diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java b/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java index aa173ef30e..fa2a6354be 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java @@ -134,7 +134,7 @@ static String anthropicMessageSseBody(String text) { startMessage.put("id", "msg_stub_1"); startMessage.put("type", "message"); startMessage.put("role", "assistant"); - startMessage.put("model", "claude-sonnet-4.5"); + startMessage.put("model", "claude-sonnet-5"); startMessage.put("content", List.of()); startMessage.put("stop_reason", null); startMessage.put("stop_sequence", null); @@ -251,7 +251,7 @@ static HttpResponse buildInferenceResponse(String url, String bodyT body.put("id", "msg_stub_1"); body.put("type", "message"); body.put("role", "assistant"); - body.put("model", "claude-sonnet-4.5"); + body.put("model", "claude-sonnet-5"); body.put("content", List.of(Map.of("type", "text", "text", text))); body.put("stop_reason", "end_turn"); body.put("stop_sequence", null); @@ -301,14 +301,14 @@ static String modelCatalog(List supportedEndpoints) { Map capabilities = new LinkedHashMap<>(); capabilities.put("type", "chat"); - capabilities.put("family", "claude-sonnet-4.5"); + capabilities.put("family", "claude-sonnet-5"); capabilities.put("tokenizer", "o200k_base"); capabilities.put("limits", limits); capabilities.put("supports", supports); Map model = new LinkedHashMap<>(); - model.put("id", "claude-sonnet-4.5"); - model.put("name", "Claude Sonnet 4.5"); + model.put("id", "claude-sonnet-5"); + model.put("name", "Claude Sonnet 5"); model.put("object", "model"); model.put("vendor", "Anthropic"); model.put("version", "1"); @@ -416,7 +416,7 @@ private static Map chatChunkBase() { base.put("id", "chatcmpl-stub-1"); base.put("object", "chat.completion.chunk"); base.put("created", 1); - base.put("model", "claude-sonnet-4.5"); + base.put("model", "claude-sonnet-5"); return base; } @@ -459,7 +459,7 @@ private static Map chatCompletion(String text) { root.put("id", "chatcmpl-stub-1"); root.put("object", "chat.completion"); root.put("created", 1); - root.put("model", "claude-sonnet-4.5"); + root.put("model", "claude-sonnet-5"); root.put("choices", List.of(choice)); root.put("usage", chatUsage()); return root; diff --git a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java index 7b0deb9977..5a6e378b2a 100644 --- a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java +++ b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.github.copilot.generated.rpc.GitHubTelemetryNotification; +import com.github.copilot.rpc.ClientInfo; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.ResumeSessionConfig; @@ -204,6 +205,96 @@ void clientOmitsForwardingWhenNoHandler() throws Exception { } } + @Test + void connectForwardsDeclaredClientInfo() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setApplicationName("acme-developer-portal") + .setApplicationVersion("2.4.0").setIntegrationName("copilot-assistant") + .setIntegrationVersion("1.5.0")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals(4, clientInfo.size(), "clientInfo should carry only the four declared fields"); + assertEquals("acme-developer-portal", clientInfo.path("editorName").asText()); + assertEquals("2.4.0", clientInfo.path("editorVersion").asText()); + assertEquals("copilot-assistant", clientInfo.path("extensionName").asText()); + assertEquals("1.5.0", clientInfo.path("extensionVersion").asText()); + } + } + + @Test + void connectOmitsClientInfoWhenUnset() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + assertFalse(connectParams.has("clientInfo"), + "connect request should omit clientInfo when none was declared"); + } + } + + @Test + void connectOmitsEmptyClientInfoFields() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setApplicationName("example-app")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals("example-app", clientInfo.path("editorName").asText()); + assertFalse(clientInfo.has("editorVersion"), "unset editorVersion should be omitted"); + assertFalse(clientInfo.has("extensionName"), "unset extensionName should be omitted"); + assertFalse(clientInfo.has("extensionVersion"), "unset extensionVersion should be omitted"); + } + } + + @Test + void connectDropsEmptyClientInfoFields() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setApplicationName("example-app").setApplicationVersion("")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals(1, clientInfo.size(), "clientInfo should carry only the non-empty field"); + assertEquals("example-app", clientInfo.path("editorName").asText()); + assertFalse(clientInfo.has("editorVersion"), "empty editorVersion should be dropped"); + } + } + + @Test + void connectOmitsAllEmptyClientInfo() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setApplicationName("").setApplicationVersion("") + .setIntegrationName("").setIntegrationVersion("")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + assertFalse(connectParams.has("clientInfo"), "connect request should omit an all-empty clientInfo"); + } + } + + @Test + void optionsRetainAndCloneClientInfo() { + var info = new ClientInfo().setApplicationName("example-app"); + var options = new CopilotClientOptions().setClientInfo(info); + assertSame(info, options.getClientInfo()); + + var copy = options.clone(); + assertSame(info, copy.getClientInfo()); + } + @Test void optionsRetainAndCloneTelemetryHandler() { Function> handler = n -> CompletableFuture diff --git a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java index 2393f334b2..6c9753025a 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java @@ -135,7 +135,7 @@ void testShouldCallRpcModelsListWithTypedResult() throws Exception { var result = client.getRpc().models.list().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertNotNull(result.models()); - assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-4.5".equals(model.id()))); + assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-5".equals(model.id()))); result.models().forEach(model -> { assertFalse(model.id().isBlank()); assertFalse(model.name().isBlank()); diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java index 18045f3e86..f7fc58d429 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java @@ -142,7 +142,7 @@ void testShouldUpdateAndClearLiveSubagentSettings() throws Exception { session.getRpc().tools.updateSubagentSettings(new SessionToolsUpdateSubagentSettingsParams(null, new SessionToolsUpdateSubagentSettingsParams.SessionToolsUpdateSubagentSettingsParamsSubagents( Map.of("general-purpose", - new SubagentSettingsEntry("gpt-5-mini", "low", + new SubagentSettingsEntry("gpt-5-mini", null, "low", SubagentSettingsEntryContextTier.LONG_CONTEXT)), List.of("legacy-agent"), null, null))) .get(30, TimeUnit.SECONDS); diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java new file mode 100644 index 0000000000..5213cdbb8d --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * 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.assertNull; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.github.copilot.generated.AutoTier; +import com.github.copilot.generated.SessionEvent; +import com.github.copilot.generated.SessionResumeEvent; +import com.github.copilot.generated.SessionStartEvent; + +/** + * Verifies auto routing preferences on generated session lifecycle events. + */ +class SessionAutoTierEventTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + @ParameterizedTest + @CsvSource({"session.start,EFFICIENCY,efficiency", "session.start,BALANCE,balance", + "session.start,INTELLIGENCE,intelligence", "session.resume,EFFICIENCY,efficiency", + "session.resume,BALANCE,balance", "session.resume,INTELLIGENCE,intelligence"}) + void canonicalAutoTierRoundTrips(String type, AutoTier tier, String value) throws Exception { + String json = """ + {"type":"%s","data":{"selectedModel":"auto","autoTier":"%s"}} + """.formatted(type, value); + + var event = MAPPER.readValue(json, SessionEvent.class); + assertEquals(tier, autoTier(event, type)); + String serialized = MAPPER.writeValueAsString(event); + assertEquals(value, MAPPER.readTree(serialized).path("data").path("autoTier").asText()); + assertEquals(tier, autoTier(MAPPER.readValue(serialized, SessionEvent.class), type)); + } + + @ParameterizedTest + @ValueSource(strings = {"session.start", "session.resume"}) + void missingOrNullAutoTierRemainsOptional(String type) throws Exception { + for (String data : new String[]{"{}", "{\"autoTier\":null}"}) { + String json = """ + {"type":"%s","data":%s} + """.formatted(type, data); + + var event = MAPPER.readValue(json, SessionEvent.class); + assertNull(autoTier(event, type)); + var serialized = MAPPER.readTree(MAPPER.writeValueAsString(event)); + assertFalse(serialized.path("data").has("autoTier")); + } + } + + private static AutoTier autoTier(SessionEvent event, String type) { + if ("session.start".equals(type)) { + return assertInstanceOf(SessionStartEvent.class, event).getData().autoTier(); + } + return assertInstanceOf(SessionResumeEvent.class, event).getData().autoTier(); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java b/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java index 925fd6d873..e786bda994 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java @@ -125,7 +125,7 @@ void testShouldForwardProviderWireModel() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = client - .createSession(new SessionConfig().setModel("claude-sonnet-4.5") + .createSession(new SessionConfig().setModel("claude-sonnet-5") .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) .setApiKey("test-provider-key").setWireModel("test-wire-model") .setMaxOutputTokens(1024)) @@ -149,7 +149,7 @@ void testShouldUseProviderModelIdAsWireModel() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(new SessionConfig() .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) - .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5")) + .setApiKey("test-provider-key").setModelId("claude-sonnet-5")) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); @@ -158,7 +158,7 @@ void testShouldUseProviderModelIdAsWireModel() throws Exception { assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); @SuppressWarnings("unchecked") Map request = (Map) exchanges.get(0).get("request"); - assertEquals("claude-sonnet-4.5", request.get("model")); + assertEquals("claude-sonnet-5", request.get("model")); } } @@ -272,7 +272,7 @@ void testShouldEnableCitationsForAnthropicFileAttachmentsOnCreate() throws Excep var handler = new CopilotRequestTestSupport.RecordingRequestHandler(SYNTHETIC_TEXT); try (CopilotClient client = newLlmClient(ctx, handler)) { - CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-4.5") + CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5") .setEnableCitations(true).setProvider(createAnthropicProvider()) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); @@ -296,7 +296,7 @@ void testShouldEnableCitationsForAnthropicFileAttachmentsOnResume() throws Excep CopilotSession session1 = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); CopilotSession session2 = client.resumeSession(session1.getSessionId(), - new ResumeSessionConfig().setModel("claude-sonnet-4.5").setEnableCitations(true) + new ResumeSessionConfig().setModel("claude-sonnet-5").setEnableCitations(true) .setProvider(createAnthropicProvider()) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .get(); @@ -388,7 +388,7 @@ private static BlobAttachment createPdfAttachment() { private static ProviderConfig createAnthropicProvider() { return new ProviderConfig().setType("anthropic").setBaseUrl("https://anthropic-citations.invalid/v1") - .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5").setWireModel("claude-sonnet-4.5"); + .setApiKey("test-provider-key").setModelId("claude-sonnet-5").setWireModel("claude-sonnet-5"); } private static String singleInferenceRequestBody(CopilotRequestTestSupport.RecordingRequestHandler handler) { 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 529c42f2bb..b75e710720 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -213,7 +213,7 @@ void testHandlerReceivesCorrectEventData() { SessionStartEvent startEvent = createSessionStartEvent(); startEvent.setData(new SessionStartEvent.SessionStartEventData("my-session-123", null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null)); + null, null, null, null, null, null, null, null, null, null, null)); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -890,7 +890,7 @@ private SessionStartEvent createSessionStartEvent() { private SessionStartEvent createSessionStartEvent(String sessionId) { var event = new SessionStartEvent(); var data = new SessionStartEvent.SessionStartEventData(sessionId, null, null, null, null, null, null, null, - null, null, null, null, null, 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 9d76d18ee2..edc40175d0 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import com.github.copilot.generated.rpc.SessionLimitsConfig; +import com.github.copilot.rpc.AskUserVariant; import com.github.copilot.rpc.AutoModeSwitchResponse; import com.github.copilot.rpc.CloudSessionOptions; import com.github.copilot.rpc.CloudSessionRepository; @@ -78,6 +79,42 @@ void testGitHubTokenProviderResultRedactsToken() { assertFalse(result.toString().contains("do-not-print")); } + @Test + void askUserVariantIsForwardedAndSerializedForCreateAndColdResume() throws Exception { + var createRequest = SessionRequestBuilder.buildCreateRequest( + new SessionConfig().setAskUserVariant(AskUserVariant.ELICITATION), "create-session"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("resume-session", + new ResumeSessionConfig().setAskUserVariant(AskUserVariant.LEGACY)); + var mapper = JsonRpcClient.getObjectMapper(); + + assertEquals(AskUserVariant.ELICITATION, createRequest.getAskUserVariant()); + assertEquals("elicitation", + mapper.readTree(mapper.writeValueAsBytes(createRequest)).path("askUserVariant").asText()); + assertEquals(AskUserVariant.LEGACY, resumeRequest.getAskUserVariant()); + assertEquals("legacy", + mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).path("askUserVariant").asText()); + } + + @Test + void askUserVariantDefaultsToOmittedLegacyBehavior() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "create-session"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("resume-session", new ResumeSessionConfig()); + + assertNull(createRequest.getAskUserVariant()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(createRequest)).has("askUserVariant")); + assertNull(resumeRequest.getAskUserVariant()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(resumeRequest)).has("askUserVariant")); + } + + @Test + void askUserVariantAcceptsOnlySupportedWireValues() { + assertEquals(AskUserVariant.LEGACY, AskUserVariant.fromValue("legacy")); + assertEquals(AskUserVariant.ELICITATION, AskUserVariant.fromValue("elicitation")); + assertThrows(IllegalArgumentException.class, () -> AskUserVariant.fromValue("ELICITATION")); + assertThrows(IllegalArgumentException.class, () -> AskUserVariant.fromValue("unsupported")); + } + @Test void testBuildCreateRequestHooksNonNullButEmpty() { // Hooks object exists but hasHooks() returns false @@ -1055,6 +1092,26 @@ void testBuildRequestsOmitExpAssignmentsWhenUnset() throws Exception { assertFalse(resumeJson.contains("\"expAssignments\""), "expAssignments should be omitted when null"); } + @Test + void testBuildRequestsPropagateAndSerializeFeatureFlags() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var flags = Map.of("ENABLED_TEST_FLAG", true, "DISABLED_TEST_FLAG", false); + + var createConfig = new SessionConfig().setFeatureFlags(flags); + CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(createConfig, "session-1"); + assertEquals(flags, createRequest.getFeatureFlags()); + var createJson = mapper.readTree(mapper.writeValueAsString(createRequest)); + assertTrue(createJson.path("featureFlags").path("ENABLED_TEST_FLAG").asBoolean()); + assertFalse(createJson.path("featureFlags").path("DISABLED_TEST_FLAG").asBoolean()); + + var resumeConfig = new ResumeSessionConfig().setFeatureFlags(flags); + ResumeSessionRequest resumeRequest = SessionRequestBuilder.buildResumeRequest("session-1", resumeConfig); + assertEquals(flags, resumeRequest.getFeatureFlags()); + var resumeJson = mapper.readTree(mapper.writeValueAsString(resumeRequest)); + assertTrue(resumeJson.path("featureFlags").path("ENABLED_TEST_FLAG").asBoolean()); + assertFalse(resumeJson.path("featureFlags").path("DISABLED_TEST_FLAG").asBoolean()); + } + @Test void testClonePreservesAndForwardsExpAssignments() throws Exception { var mapper = JsonRpcClient.getObjectMapper(); diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java index 23408faa2a..0294b511ec 100644 --- a/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java +++ b/java/sdk/src/test/java/com/github/copilot/e2e/InProcessTransportIT.java @@ -50,10 +50,10 @@ * *

* Run with {@code mvn verify -Pinprocess} from the {@code java} reactor root, - * which builds the {@code copilot-sdk-java-runtime} artifact and sets - * {@code COPILOT_CLI_PATH} to the pinned CLI whose sibling {@code runtime.node} - * this test loads, and forces {@code forkCount=1} because the FFI host and env - * guard mutate process-global state. + * which builds the {@code copilot-sdk-java-runtime} artifact and sets the + * classifier JAR containing {@code runtime.node}, and forces + * {@code forkCount=1} because the FFI host and env guard mutate process-global + * state. * *

* {@link RequireInProcess} disables this test unless the {@code -Pinprocess} @@ -86,11 +86,6 @@ void shouldStartPingAndStopOverInProcessFfi() throws Exception { // replay proxy, mirroring how a session-level in-process test would // redirect COPILOT_API_URL. `ping` never reaches the network, but this // demonstrates the guard's intended usage for future in-process tests. - // COPILOT_CLI_PATH is intentionally NOT set here: NativeRuntimeLoader and - // CopilotClient.resolveInProcessEntrypoint() read it via - // System.getenv(), which is a JVM-startup-time snapshot that native - // setenv() calls made after the JVM starts cannot update — it must be - // set before the JVM starts (see the -Pinprocess Maven profile). try (InProcessEnvGuard envGuard = new InProcessEnvGuard(Map.of("COPILOT_API_URL", ctx.getProxyUrl()))) { CopilotClientOptions options = new CopilotClientOptions().setConnection(RuntimeConnection.forInProcess()); try (CopilotClient client = new CopilotClient(options)) { diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java new file mode 100644 index 0000000000..19c14a0644 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/e2e/OutOfProcessTransportIT.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.e2e; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.PingResponse; +import com.github.copilot.rpc.RuntimeConnection; + +/** + * Failsafe smoke test for the managed out-of-process runtime wrapper. + */ +@AllowCopilotExperimental +@RequireInProcess +class OutOfProcessTransportIT { + + @Test + void shouldStartPingAndStopOverStdio() throws Exception { + CopilotClientOptions options = new CopilotClientOptions().setConnection(RuntimeConnection.forStdio()); + try (CopilotClient client = new CopilotClient(options)) { + client.start().get(); + + PingResponse pong = client.ping("wrapper message").get(); + assertEquals("pong: wrapper message", pong.message()); + assertNotNull(pong.timestamp()); + + client.stop().get(); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java index 9321618575..fd3f92100d 100644 --- a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java +++ b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java @@ -5,11 +5,9 @@ package com.github.copilot.e2e; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeFalse; import java.nio.file.Files; import java.nio.file.Path; @@ -37,6 +35,8 @@ class RewindIT { private static final String FILE_NAME = "rewind-sdk.txt"; + private static final String ORIGINAL_FILE_CONTENT = "Original rewind content"; + private static final String PREPARED_FILE_CONTENT = "Prepared rewind content"; private static final String FILE_CONTENT = "SDK rewind content"; private static E2ETestContext ctx; @@ -55,23 +55,27 @@ static void teardown() throws Exception { @Test void shouldRestoreTrackedFileAndConversation() throws Exception { - assumeFalse(System.getProperty("os.name").startsWith("Windows"), - "blocked on CLI 1.0.81 file-change tracking regression on Windows"); - ctx.configureForTest("rewind", "should_restore_tracked_file_and_conversation"); Path filePath = ctx.getWorkDir().resolve(FILE_NAME); + Files.writeString(filePath, ORIGINAL_FILE_CONTENT); try (CopilotClient client = ctx.createClient(); - CopilotSession session = client - .createSession( - new SessionConfig().setModel("claude-sonnet-4.5").setEnableFileChangeTracking(true) - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5") + .setEnableFileChangeTracking(true).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .get(30, TimeUnit.SECONDS)) { + AssistantMessageEvent ready = session + .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of " + + FILE_NAME + " from " + ORIGINAL_FILE_CONTENT + " to " + PREPARED_FILE_CONTENT + + ". After the tool succeeds, reply with exactly SDK_REWIND_READY."), 30_000) + .get(60, TimeUnit.SECONDS); + assertNotNull(ready); + assertEquals("SDK_REWIND_READY", ready.getData().content()); + assertEquals(PREPARED_FILE_CONTENT, Files.readString(filePath)); + AssistantMessageEvent response = session - .sendAndWait(new MessageOptions().setPrompt( - "Use the create tool to create " + FILE_NAME + " containing exactly " + FILE_CONTENT - + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE."), - 30_000) + .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of " + + FILE_NAME + " from " + PREPARED_FILE_CONTENT + " to " + FILE_CONTENT + + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE."), 30_000) .get(60, TimeUnit.SECONDS); assertNotNull(response); @@ -80,8 +84,9 @@ void shouldRestoreTrackedFileAndConversation() throws Exception { SessionHistoryListRewindPointsResult rewindPoints = waitForRewindPoints(session); assertTrue(Boolean.TRUE.equals(rewindPoints.fileChangeTrackingEnabled())); - assertEquals(1, rewindPoints.points().size()); - var rewindPoint = rewindPoints.points().get(0); + assertEquals(2, rewindPoints.points().size()); + var rewindPoint = rewindPoints.points().get(1); + assertTrue(Boolean.TRUE.equals(rewindPoint.turnChangedFiles())); assertTrue(Boolean.TRUE.equals(rewindPoint.canRestoreFiles())); assertEquals(1L, rewindPoint.fileCount()); @@ -98,7 +103,7 @@ void shouldRestoreTrackedFileAndConversation() throws Exception { assertTrue(rewind.eventsRemoved() != null && rewind.eventsRemoved() > 0); assertEquals(1, rewind.restoredFiles().size()); assertSamePath(filePath, rewind.restoredFiles().get(0)); - assertFalse(Files.exists(filePath)); + assertEquals(PREPARED_FILE_CONTENT, Files.readString(filePath)); var events = session.getMessages().get(10, TimeUnit.SECONDS); assertTrue(events.stream().noneMatch(event -> event.getId().toString().equals(rewindPoint.eventId()))); @@ -110,16 +115,19 @@ private static SessionHistoryListRewindPointsResult waitForRewindPoints(CopilotS SessionHistoryListRewindPointsResult result; do { result = session.getRpc().history.listRewindPoints().get(10, TimeUnit.SECONDS); - if (result.unavailableReason() == null && !result.points().isEmpty() - && Boolean.TRUE.equals(result.points().get(0).canRestoreFiles())) { + if (result.unavailableReason() == null && result.points().size() == 2 + && Boolean.TRUE.equals(result.points().get(1).turnChangedFiles()) + && Boolean.TRUE.equals(result.points().get(1).canRestoreFiles())) { return result; } TimeUnit.MILLISECONDS.sleep(100); } while (System.nanoTime() < deadline); assertNull(result.unavailableReason(), "Timed out waiting for rewind points to become available"); - assertFalse(result.points().isEmpty(), "Timed out waiting for a rewind point"); - assertTrue(Boolean.TRUE.equals(result.points().get(0).canRestoreFiles()), + assertTrue(result.points().size() >= 2, "Timed out waiting for both rewind points"); + assertTrue(Boolean.TRUE.equals(result.points().get(1).turnChangedFiles()), + "Timed out waiting for the edit turn to capture file changes"); + assertTrue(Boolean.TRUE.equals(result.points().get(1).canRestoreFiles()), "Timed out waiting for rewind file restoration to become available"); return result; } diff --git a/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java b/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java index cc98d24f6b..545b316c8c 100644 --- a/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java @@ -99,6 +99,47 @@ public boolean connectionClose(int connectionId) { assertEquals("1", env.get("COPILOT_DISABLE_KEYTAR")); } + @Test + void startWithoutEntrypointPassesOnlyRuntimeOptions() throws Exception { + AtomicReference argvJson = new AtomicReference<>(); + NativeBinding binding = new NativeBinding() { + @Override + public int hostStart(byte[] argv, int argvLen, byte[] env, int envLen) { + argvJson.set(argv); + return 11; + } + + @Override + public boolean hostShutdown(int serverId) { + return true; + } + + @Override + public int connectionOpen(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource, + int extSourceLen, byte[] extName, int extNameLen, byte[] connToken, int connTokenLen) { + return 21; + } + + @Override + public boolean connectionWrite(int connectionId, byte[] data, int dataLen) { + return true; + } + + @Override + public boolean connectionClose(int connectionId) { + return true; + } + }; + + try (FfiRuntimeHost host = new FfiRuntimeHost(binding, "/tmp/runtime.node")) { + host.start(null, new CopilotClientOptions().setLogLevel("debug")); + } + + List argv = MAPPER.readValue(argvJson.get(), new TypeReference>() { + }); + assertEquals(List.of("--log-level", "debug"), argv); + } + @Test void callbackExceptionIsContainedAndDoesNotEscapeAcrossFfiBoundary() { AtomicBoolean callbackReturned = new AtomicBoolean(false); diff --git a/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java b/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java index fc8bd51011..84ef7d8de4 100644 --- a/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java @@ -34,17 +34,13 @@ class NativeRuntimeLoaderTest { - private static final String TEST_CLASSIFIER = PlatformDetector.detectClassifier(); - private static final String OTHER_CLASSIFIER = TEST_CLASSIFIER.equals("darwin-arm64") - ? "linux-x64" - : "darwin-arm64"; - private static final String TEST_CLI_FILENAME = TEST_CLASSIFIER.startsWith("win32") - ? NativeRuntimeLoader.CLI_FILENAME_WINDOWS - : NativeRuntimeLoader.CLI_FILENAME; + private static final String TEST_CLASSIFIER = "linux-x64"; + private static final String OTHER_CLASSIFIER = "darwin-arm64"; private static final String TEST_VERSION = "1.2.3-test"; private static final String TEST_NATIVE_VERSION = "0.0.1-test"; private static final byte[] FAKE_BINARY_CONTENT = "fake runtime.node binary content".getBytes(); private static final byte[] FAKE_CLI_CONTENT = "fake copilot CLI content".getBytes(); + private static final byte[] FAKE_WRAPPER_CONTENT = "fake runtime wrapper content".getBytes(); private static final byte[] OTHER_BINARY_CONTENT = "other runtime.node binary content".getBytes(); private static final byte[] OTHER_CLI_CONTENT = "other copilot CLI content".getBytes(); @@ -160,19 +156,24 @@ void resolveEntrypointUsesConfiguredCliWhenRuntimeIsInPrebuilds(@TempDir Path te @Test void resolveFromCliPathReturnsAbsolutePathForRelativeCliPath() throws Exception { Path workingDirectory = Path.of("").toAbsolutePath(); - Path fakeCliDir = Files.createTempDirectory(Path.of("target").toAbsolutePath(), "relative-cli-test-"); - Path fakeCliPath = fakeCliDir.resolve("copilot"); - Files.createFile(fakeCliPath); - Path runtimeNode = fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME); - Files.write(runtimeNode, FAKE_BINARY_CONTENT); - - Path relativeCliPath = workingDirectory.relativize(fakeCliPath); - - assertEquals(runtimeNode, NativeRuntimeLoader.resolveFromCliPath(relativeCliPath.toString())); + Path fakeCliDir = Files.createTempDirectory(workingDirectory.resolve("target"), "relative-cli-"); + try { + Path fakeCliPath = Files.createFile(fakeCliDir.resolve("copilot")); + Path runtimeNode = Files.write(fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME), + FAKE_BINARY_CONTENT); + Path relativeCliPath = workingDirectory.relativize(fakeCliPath); + + assertEquals(runtimeNode, NativeRuntimeLoader.resolveFromCliPath(relativeCliPath.toString())); + } finally { + Files.deleteIfExists(fakeCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME)); + Files.deleteIfExists(fakeCliDir.resolve("copilot")); + Files.deleteIfExists(fakeCliDir); + } } @Test void cliPathOverrideTakesPriorityOverClasspathExtraction(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); // Create a valid runtime.node alongside the fake CLI path Path fakeCliDir = tempDir.resolve("cli-dir"); Files.createDirectories(fakeCliDir); @@ -197,6 +198,7 @@ void cliPathOverrideTakesPriorityOverClasspathExtraction(@TempDir Path tempDir) @Test void extractToCacheCopiesResourceToVersionedCacheDirectory(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -211,6 +213,7 @@ void extractToCacheCopiesResourceToVersionedCacheDirectory(@TempDir Path tempDir @Test void extractToCacheReturnsCachedFileOnSecondCall(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -230,6 +233,7 @@ void extractToCacheReturnsCachedFileOnSecondCall(@TempDir Path tempDir) throws E @Test void changedNativeVersionDoesNotReuseCachedArtifactsForSameSdkVersion(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader firstLoader = classLoaderWithNativeArtifacts(tempDir.resolve("native-v1"), TEST_CLASSIFIER, "1.0.0", FAKE_BINARY_CONTENT, FAKE_CLI_CONTENT); @@ -241,13 +245,16 @@ void changedNativeVersionDoesNotReuseCachedArtifactsForSameSdkVersion(@TempDir P assertNotEquals(firstRuntime, secondRuntime, "Different native versions must use different cache entries"); assertBytesEqual(FAKE_BINARY_CONTENT, Files.readAllBytes(firstRuntime)); - assertBytesEqual(FAKE_CLI_CONTENT, Files.readAllBytes(firstRuntime.getParent().resolve(TEST_CLI_FILENAME))); + assertBytesEqual(FAKE_CLI_CONTENT, + Files.readAllBytes(firstRuntime.getParent().resolve(NativeRuntimeLoader.CLI_FILENAME))); assertBytesEqual(OTHER_BINARY_CONTENT, Files.readAllBytes(secondRuntime)); - assertBytesEqual(OTHER_CLI_CONTENT, Files.readAllBytes(secondRuntime.getParent().resolve(TEST_CLI_FILENAME))); + assertBytesEqual(OTHER_CLI_CONTENT, + Files.readAllBytes(secondRuntime.getParent().resolve(NativeRuntimeLoader.CLI_FILENAME))); } @Test void extractToCacheThrowsWhenClasspathResourceMissing(@TempDir Path tempDir) { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader emptyLoader = new URLClassLoader(new URL[0], null); @@ -257,6 +264,7 @@ void extractToCacheThrowsWhenClasspathResourceMissing(@TempDir Path tempDir) { @Test void extractToCacheThrowsWhenNativeMetadataMissing(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path resourceDir = tempDir.resolve("native").resolve(TEST_CLASSIFIER); Files.createDirectories(resourceDir); Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME), FAKE_BINARY_CONTENT); @@ -270,6 +278,7 @@ void extractToCacheThrowsWhenNativeMetadataMissing(@TempDir Path tempDir) throws @Test void extractedBinaryContentsMatchClasspathResource(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -281,6 +290,7 @@ void extractedBinaryContentsMatchClasspathResource(@TempDir Path tempDir) throws @Test void extractToCacheFiltersClasspathByClassifier(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT); writeRuntimeResource(tempDir, OTHER_CLASSIFIER, OTHER_BINARY_CONTENT); @@ -294,6 +304,7 @@ void extractToCacheFiltersClasspathByClassifier(@TempDir Path tempDir) throws Ex @Test void extractToCacheRepairsInvalidCacheEntry(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); Path cached = cacheBase.resolve(TEST_VERSION).resolve(TEST_NATIVE_VERSION).resolve(TEST_CLASSIFIER) .resolve(NativeRuntimeLoader.RUNTIME_FILENAME); @@ -309,13 +320,13 @@ void extractToCacheRepairsInvalidCacheEntry(@TempDir Path tempDir) throws Except @Test void nonExecutableCachedCliIsNotAcceptedAsValid(@TempDir Path tempDir) throws Exception { - assumeTrue(!TEST_CLASSIFIER.startsWith("win32")); + assumeLinuxX64(); assumeTrue(Files.getFileStore(tempDir).supportsFileAttributeView("posix")); Path cacheBase = tempDir.resolve("cache"); Path cacheDir = cacheBase.resolve(TEST_VERSION).resolve(TEST_NATIVE_VERSION).resolve(TEST_CLASSIFIER); Files.createDirectories(cacheDir); Files.write(cacheDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME), FAKE_BINARY_CONTENT); - Path cachedCli = Files.write(cacheDir.resolve(TEST_CLI_FILENAME), FAKE_CLI_CONTENT); + Path cachedCli = Files.write(cacheDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT); Files.setPosixFilePermissions(cachedCli, PosixFilePermissions.fromString("rw-------")); ClassLoader loader = classLoaderWithRuntimeAndCliResources(tempDir, TEST_CLASSIFIER); @@ -330,6 +341,7 @@ void nonExecutableCachedCliIsNotAcceptedAsValid(@TempDir Path tempDir) throws Ex @Test void bundledCliSiblingIsUsedWhenClasspathResourceAbsent(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path bundledCliDir = tempDir.resolve("bundled-cli"); Files.createDirectories(bundledCliDir); Path runtimeNode = bundledCliDir.resolve(NativeRuntimeLoader.RUNTIME_FILENAME); @@ -347,6 +359,7 @@ void bundledCliSiblingIsUsedWhenClasspathResourceAbsent(@TempDir Path tempDir) t @Test void classpathResourceWinsOverBundledCliSibling(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); // Source 3: bundled CLI dir with runtime.node (should NOT win) Path bundledCliDir = tempDir.resolve("bundled-cli"); Files.createDirectories(bundledCliDir); @@ -368,6 +381,7 @@ void classpathResourceWinsOverBundledCliSibling(@TempDir Path tempDir) throws Ex @Test void bundledCliSiblingIsIgnoredWhenRuntimeNodeMissing(@TempDir Path tempDir) { + assumeLinuxX64(); Path bundledCliDir = tempDir.resolve("bundled-cli-no-runtime"); // bundledCliDir doesn't even exist — no runtime.node present @@ -399,12 +413,12 @@ void defaultPublisherMovesSourceToTarget(@TempDir Path tempDir) throws Exception @Test void cliIsExecutableBeforeAtomicPublication(@TempDir Path tempDir) throws Exception { - assumeTrue(!TEST_CLASSIFIER.startsWith("win32")); + assumeLinuxX64(); assumeTrue(Files.getFileStore(tempDir).supportsFileAttributeView("posix")); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeAndCliResources(tempDir, TEST_CLASSIFIER); NativeRuntimeLoader.AtomicPublisher publisher = (temp, cached) -> { - if (cached.getFileName().toString().equals(TEST_CLI_FILENAME)) { + if (cached.getFileName().toString().equals(NativeRuntimeLoader.CLI_FILENAME)) { assertTrue(Files.isExecutable(temp), "CLI temp file must be executable before atomic publication"); } Files.move(temp, cached, StandardCopyOption.REPLACE_EXISTING); @@ -415,6 +429,7 @@ void cliIsExecutableBeforeAtomicPublication(@TempDir Path tempDir) throws Except @Test void extractionCleansUpTempFileWhenPublicationFails(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -435,6 +450,7 @@ void extractionCleansUpTempFileWhenPublicationFails(@TempDir Path tempDir) throw @Test void extractionCleansUpTempFileWhenPublisherThrowsIllegalStateException(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -462,6 +478,7 @@ void extractionCleansUpTempFileWhenPublisherThrowsIllegalStateException(@TempDir @Test void concurrentExtractionByMultipleThreadsBothSucceed(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); int threadCount = 8; @@ -499,6 +516,7 @@ void concurrentExtractionByMultipleThreadsBothSucceed(@TempDir Path tempDir) thr @Test void resolveWithNullCliEnvExtractsFromClasspath(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader loader = classLoaderWithRuntimeResource(tempDir, TEST_CLASSIFIER); @@ -509,8 +527,54 @@ void resolveWithNullCliEnvExtractsFromClasspath(@TempDir Path tempDir) throws Ex assertTrue(Files.size(result) > 0); } + @Test + void resolveRuntimeWrapperExtractsAdjacentPairFromAbsentCache(@TempDir Path tempDir) throws Exception { + Path cacheBase = tempDir.resolve("cache"); + assertFalse(Files.exists(cacheBase)); + ClassLoader loader = classLoaderWithRuntimeWrapperArtifacts(tempDir, TEST_CLASSIFIER, TEST_NATIVE_VERSION); + + Path wrapper = NativeRuntimeLoader.resolveRuntimeWrapper(cacheBase, loader, TEST_CLASSIFIER, TEST_VERSION); + + assertEquals(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME, wrapper.getFileName().toString()); + assertTrue(Files.isRegularFile(wrapper)); + assertTrue(Files.isRegularFile(wrapper.resolveSibling(NativeRuntimeLoader.RUNTIME_FILENAME))); + assertFalse(Files.exists(wrapper.resolveSibling(NativeRuntimeLoader.CLI_FILENAME))); + } + + @Test + void resolveRuntimeWrapperExtractsRetainedRuntimeAssets(@TempDir Path tempDir) throws Exception { + Path resourceDir = tempDir.resolve("native").resolve(TEST_CLASSIFIER); + writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT); + Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT); + Path ripgrep = resourceDir.resolve("ripgrep/bin/linux-x64/rg"); + Files.createDirectories(ripgrep.getParent()); + Files.writeString(ripgrep, "ripgrep"); + Files.writeString(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_ASSETS_FILENAME), + "644\truntime.node\n" + "755\tcopilot-runtime\n" + "755\tripgrep/bin/linux-x64/rg\n"); + ClassLoader loader = new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); + + Path wrapper = NativeRuntimeLoader.resolveRuntimeWrapper(tempDir.resolve("cache"), loader, TEST_CLASSIFIER, + TEST_VERSION); + + Path installedRipgrep = wrapper.getParent().resolve("ripgrep/bin/linux-x64/rg"); + assertEquals("ripgrep", Files.readString(installedRipgrep)); + assertTrue(Files.isExecutable(installedRipgrep)); + } + + @Test + void resolveRuntimeWrapperRejectsClassifierWithoutWrapper(@TempDir Path tempDir) throws Exception { + writeRuntimeResource(tempDir, TEST_CLASSIFIER, FAKE_BINARY_CONTENT); + ClassLoader loader = new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); + + IOException error = assertThrows(IOException.class, () -> NativeRuntimeLoader + .resolveRuntimeWrapper(tempDir.resolve("cache"), loader, TEST_CLASSIFIER, TEST_VERSION)); + + assertTrue(error.getMessage().contains(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME)); + } + @Test void resolveThrowsWhenNoSourceIsAvailable(@TempDir Path tempDir) { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader emptyLoader = new URLClassLoader(new URL[0], null); @@ -521,6 +585,7 @@ void resolveThrowsWhenNoSourceIsAvailable(@TempDir Path tempDir) { @Test void resolveFallsBackToRuntimeAlongsideBundledCli(@TempDir Path tempDir) throws Exception { + assumeLinuxX64(); Path cacheBase = tempDir.resolve("cache"); ClassLoader emptyLoader = new URLClassLoader(new URL[0], null); Path bundledCli = tempDir.resolve("copilot"); @@ -538,6 +603,17 @@ void resolveFallsBackToRuntimeAlongsideBundledCli(@TempDir Path tempDir) throws // Helpers // ------------------------------------------------------------------------- + private static void assumeLinuxX64() { + String actualClassifier; + try { + actualClassifier = PlatformDetector.detectClassifier(); + } catch (IllegalStateException ex) { + actualClassifier = "unsupported"; + } + assumeTrue(TEST_CLASSIFIER.equals(actualClassifier), + "Requires linux-x64; detected " + actualClassifier + "; see #2323"); + } + private static ClassLoader classLoaderWithVersionResource(Path tempDir, String version) throws IOException { Path propsFile = tempDir.resolve(NativeRuntimeLoader.VERSION_RESOURCE); Files.writeString(propsFile, "version=" + version + "\n"); @@ -553,7 +629,7 @@ private static ClassLoader classLoaderWithRuntimeAndCliResources(Path tempDir, S throws IOException { writeRuntimeResource(tempDir, classifier, FAKE_BINARY_CONTENT); Path resourceDir = tempDir.resolve("native").resolve(classifier); - Files.write(resourceDir.resolve(TEST_CLI_FILENAME), FAKE_CLI_CONTENT); + Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT); return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); } @@ -561,7 +637,19 @@ private static ClassLoader classLoaderWithNativeArtifacts(Path tempDir, String c byte[] runtimeContent, byte[] cliContent) throws IOException { writeRuntimeResource(tempDir, classifier, runtimeContent); Path resourceDir = tempDir.resolve("native").resolve(classifier); - Files.write(resourceDir.resolve(TEST_CLI_FILENAME), cliContent); + Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), cliContent); + Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT); + Files.writeString(resourceDir.resolve("platform.properties"), + "classifier=" + classifier + "\nversion=" + nativeVersion + "\n"); + return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); + } + + private static ClassLoader classLoaderWithRuntimeWrapperArtifacts(Path tempDir, String classifier, + String nativeVersion) throws IOException { + writeRuntimeResource(tempDir, classifier, FAKE_BINARY_CONTENT); + Path resourceDir = tempDir.resolve("native").resolve(classifier); + Files.write(resourceDir.resolve(NativeRuntimeLoader.RUNTIME_WRAPPER_FILENAME), FAKE_WRAPPER_CONTENT); + Files.write(resourceDir.resolve(NativeRuntimeLoader.CLI_FILENAME), FAKE_CLI_CONTENT); Files.writeString(resourceDir.resolve("platform.properties"), "classifier=" + classifier + "\nversion=" + nativeVersion + "\n"); return new URLClassLoader(new URL[]{tempDir.toUri().toURL()}, null); 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 602089d012..f86b3dfbcb 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 @@ -326,10 +326,10 @@ void sessionModelGetCurrentParams_record() { @Test void sessionModelSwitchToParams_record() { - var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-4.5", "high", null, null, null, null, - null, null, null, null, null, null, null, null); + var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-5", "high", null, null, null, null, null, + null, null, null, null, null, null, null); assertEquals("sess-32", params.sessionId()); - assertEquals("claude-sonnet-4.5", params.modelId()); + assertEquals("claude-sonnet-5", params.modelId()); assertEquals("high", params.reasoningEffort()); assertNull(params.reasoningSummary()); assertNull(params.verbosity()); @@ -470,7 +470,7 @@ void pingResult_fields() { @Test void sessionAgentListResult_with_items() { var item = new AgentInfo("name1", "Name One", "Desc 1", "/path/to/agent1", null, null, null, null, null, null, - null, null); + null, null, null, null); var result = new SessionAgentListResult(List.of(item)); assertEquals(1, result.agents().size()); assertEquals("name1", result.agents().get(0).name()); @@ -482,7 +482,7 @@ void sessionAgentListResult_with_items() { @Test void sessionAgentGetCurrentResult_nested() { var agent = new AgentInfo("agent-1", "Agent One", "Does things", null, null, null, null, null, null, null, null, - null); + null, null, null); var result = new SessionAgentGetCurrentResult(agent); assertEquals("agent-1", result.agent().name()); assertEquals("Agent One", result.agent().displayName()); @@ -498,7 +498,8 @@ void sessionAgentGetCurrentResult_null_agent() { @Test void sessionAgentReloadResult_with_items() { - var item = new AgentInfo("a", "A", "Desc", "/path/to/a", null, null, null, null, null, null, null, null); + var item = new AgentInfo("a", "A", "Desc", "/path/to/a", null, null, null, null, null, null, null, null, null, + null); var result = new SessionAgentReloadResult(List.of(item)); assertEquals(1, result.agents().size()); assertEquals("a", result.agents().get(0).name()); @@ -507,7 +508,7 @@ void sessionAgentReloadResult_with_items() { @Test void sessionAgentSelectResult_nested() { var agent = new AgentInfo("selected", "Selected", "The selected agent", "/path/to/selected", null, null, null, - null, null, null, null, null); + null, null, null, null, null, null, null); var result = new SessionAgentSelectResult(agent); assertEquals("selected", result.agent().name()); } @@ -656,8 +657,8 @@ void sessionMcpListResult_status_enum_all_values() { @Test void sessionModelGetCurrentResult_record() { - var result = new SessionModelGetCurrentResult("claude-sonnet-4.5", null, null); - assertEquals("claude-sonnet-4.5", result.modelId()); + var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, null); + assertEquals("claude-sonnet-5", result.modelId()); } @Test @@ -816,7 +817,7 @@ void modelsListResult_nested() { var limits = new ModelCapabilitiesLimits(100000L, 8192L, 128000L, null); var capabilities = new ModelCapabilities(supports, limits); var policy = new ModelPolicy(ModelPolicyState.ENABLED, null); - var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount"); + 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, policy, billing, null, null, null, null, null, null, null, null); @@ -834,6 +835,7 @@ void modelsListResult_nested() { assertEquals(Double.valueOf(25.0), result.models().get(0).billing().promo().discountPercent()); assertEquals("2026-08-01T00:00:00Z", result.models().get(0).billing().promo().endsAt()); assertEquals("Summer discount", result.models().get(0).billing().promo().message()); + assertTrue(result.models().get(0).billing().promo().showBanner()); } @Test diff --git a/justfile b/justfile index c84166862f..69666bc00a 100644 --- a/justfile +++ b/justfile @@ -9,7 +9,7 @@ format: format-go format-python format-nodejs format-dotnet format-rust lint: lint-go lint-python lint-nodejs lint-dotnet lint-rust # Run tests for all languages -test: test-go test-python test-nodejs test-dotnet test-rust test-corrections +test: test-go test-python test-nodejs test-dotnet test-rust test-harness test-corrections # Format Go code format-go: @@ -66,6 +66,11 @@ test-nodejs: @echo "=== Testing Node.js code ===" @cd nodejs && npm test +# Run test harness tests +test-harness: + @echo "=== Testing test harness ===" + @cd test/harness && npm test + # Test .NET code test-dotnet: @echo "=== Testing .NET code ===" @@ -168,4 +173,3 @@ validate-docs-go: validate-docs-cs: @echo "=== Validating C# documentation ===" @cd scripts/docs-validation && npm run validate:cs - diff --git a/nodejs/README.md b/nodejs/README.md index 93f9c3fa6b..57a8bf484b 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -95,6 +95,7 @@ new CopilotClient(options?: CopilotClientOptions) - `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). There is no top-level `cliUrl` shortcut; use this factory for URL-based connections. - `RuntimeConnection.forInProcess()` — host the runtime in-process over its native C ABI (FFI). **Experimental.** Because the runtime shares this process, `env`, `telemetry`, and `workingDirectory` are rejected with this transport; set them on the host process instead. - The child-process transports (`forStdio`/`forTcp`) also accept a per-connection `env`. Set it there or via the top-level `env` option — not both (setting both throws). + - Managed child-process connections materialize the bundled `copilot-runtime` and adjacent `runtime.node`, then launch the wrapper by default. An explicit connection `path` or `COPILOT_CLI_PATH` overrides the bundled runtime. - `mode?: "empty" | "copilot-cli"` - Defaulting strategy. Use `"empty"` for multi-user server mode; defaults to `"copilot-cli"`. - `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd). - `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`. @@ -131,6 +132,7 @@ Create a new conversation session. - `sessionId?: string` - Custom session ID. - `model?: string` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** +- `capi?: CapiSessionOptions` - Copilot API options. With `model: "auto"`, set `autoTier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics. - `reasoningEffort?: "low" | "medium" | "high" | "xhigh" | "max"` - Reasoning effort level for models that support it. Use `listModels()` to check which models support this option. - `tools?: Tool[]` - Custom tools exposed to the CLI. Tools without `handler` are declaration-only and must be resolved via pending tool-call RPCs. - `systemMessage?: SystemMessageConfig` - System message customization (see below) @@ -140,7 +142,8 @@ Create a new conversation session. - `gitHubTokenProvider?: GitHubTokenProvider` - Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `gitHubToken`. - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. - `onPermissionRequest?: PermissionHandler` - 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. `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?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section. +- `onUserInputRequest?: UserInputHandler` - 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?: "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. @@ -959,7 +962,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `skipPe ## User Input Requests -Enable the agent to ask questions to the user using the `ask_user` tool by providing an `onUserInputRequest` handler: +Enable the legacy question-and-answer `ask_user` tool by providing an `onUserInputRequest` handler: ```typescript const session = await client.createSession({ @@ -991,6 +994,7 @@ Register an `onElicitationRequest` handler to let your client act as an elicitat const session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll, + askUserVariant: "elicitation", onElicitationRequest: async (context) => { // context.sessionId - Session that triggered the request // context.message - Description of what information is needed @@ -1012,6 +1016,9 @@ const session = await client.createSession({ console.log(session.capabilities.ui?.elicitation); // true ``` +Set `askUserVariant: "elicitation"` to expose the structured form as the model's +`ask_user` tool. Omit it to retain the legacy SDK behavior. + When `onElicitationRequest` is provided, the SDK sends `requestElicitation: true` during session create/resume, which enables `session.capabilities.ui.elicitation` on the session. In multi-client scenarios: diff --git a/nodejs/docs/factories.md b/nodejs/docs/factories.md index 903a3de8b1..23b9d0fed3 100644 --- a/nodejs/docs/factories.md +++ b/nodejs/docs/factories.md @@ -139,6 +139,8 @@ Run by registered name or handle: const run = await session.factory.run("review-changed", { args: { files: ["src/a.ts"] }, limits: { maxAiCredits: 3 }, + notifyOnComplete: true, + logPhaseNames: true, }); if (run.status === "completed") { @@ -153,7 +155,12 @@ The name overload is: ```ts session.factory.run( name: string, - options?: { args?: JsonValue; limits?: FactoryLimits }, + options?: { + args?: JsonValue; + limits?: FactoryLimits; + notifyOnComplete?: boolean; + logPhaseNames?: boolean; + }, ): Promise; ``` @@ -162,6 +169,8 @@ Resume by run ID without resending the name or arguments: ```ts const run = await session.factory.resume(runId, { limits: { maxAiCredits: 6 }, + notifyOnComplete: true, + logPhaseNames: true, }); ``` @@ -170,10 +179,16 @@ The signature is: ```ts session.factory.resume( runId: string, - options?: { limits?: FactoryLimits }, + options?: { + limits?: FactoryLimits; + notifyOnComplete?: boolean; + logPhaseNames?: boolean; + }, ): Promise; ``` +Set `notifyOnComplete` to `true` for factories that are likely to be invoked by an agent, so the originating session is notified when the factory completes. Set it to `false` for factories intended to be invoked programmatically, where the caller awaits the result directly. Set `logPhaseNames` to emit factory phase names to the session transcript. Both options apply to new and resumed runs. + Both resolve with the run envelope (`FactoryRunResult`) for **every** outcome — `completed`, `error`, `halted`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. The model's `run_factory` tool requests permission before the durable row exists; declining it creates no run row. 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 `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, or `factory_storage_corrupt`. An agent that no longer has a prior run's ID in context can recover it with `factories_manage` and `operation: "runs"`, which lists the session's factory runs with their IDs and statuses. This matters for resume: a run that reached a limit keeps its journal, so resuming it replays completed work for free, while restarting it from scratch pays for that work twice. @@ -219,8 +234,13 @@ The calling session can inspect its own factory runs: ```ts const runs = await session.factory.listRuns(); +const runsPage = await session.factory.listRuns({ + afterSeq, + beforeSeq, + limit, +}); const detail = await session.factory.getRunDetail(runId); -const page = await session.factory.getRunProgress(runId, { +const progressPage = await session.factory.getRunProgress(runId, { phaseId, afterSeq, beforeSeq, @@ -228,7 +248,8 @@ const page = await session.factory.getRunProgress(runId, { }); ``` -- `listRuns()` returns the newest default page of this session's durable factory runs. +- `listRuns()` returns only the runs array from the newest default page of this session's durable factory runs. This overload preserves the original convenience API. +- `listRuns({ afterSeq, beforeSeq, limit })` returns the full page. Its `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` fields let callers continue paging without raw RPC calls. - `getRunDetail(runId)` returns phases, prompt-safe agent summaries, and the latest progress page. - `getRunProgress(runId, options?)` pages progress forward, backward, by phase, or from the latest tail. diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 3b8e6cb861..bfa5c8a0c6 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -658,8 +658,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82-0", - "integrity": "sha512-fSZVNAzFFYaS6btYD0+cKF7SrrtOklhpkPs/cIMZY7Fgxoa6rfZrlTWXlQNgNIdwLp51XxwTfxnZO+D9+MQ5yg==", + "version": "1.0.83-3", + "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -668,19 +668,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82-0", - "@github/copilot-darwin-x64": "1.0.82-0", - "@github/copilot-linux-arm64": "1.0.82-0", - "@github/copilot-linux-x64": "1.0.82-0", - "@github/copilot-linuxmusl-arm64": "1.0.82-0", - "@github/copilot-linuxmusl-x64": "1.0.82-0", - "@github/copilot-win32-arm64": "1.0.82-0", - "@github/copilot-win32-x64": "1.0.82-0" + "@github/copilot-darwin-arm64": "1.0.83-3", + "@github/copilot-darwin-x64": "1.0.83-3", + "@github/copilot-linux-arm64": "1.0.83-3", + "@github/copilot-linux-x64": "1.0.83-3", + "@github/copilot-linuxmusl-arm64": "1.0.83-3", + "@github/copilot-linuxmusl-x64": "1.0.83-3", + "@github/copilot-win32-arm64": "1.0.83-3", + "@github/copilot-win32-x64": "1.0.83-3" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-TzBYfyvxcw3z9Mu7U8TsFo/Nq7m5XS6ahT71aPL+gx/YId0kmenI27b9daXsK6LA1D0gsFGwNBDKINddqntt1g==", + "version": "1.0.83-3", + "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", "cpu": [ "arm64" ], @@ -694,8 +694,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82-0", - "integrity": "sha512-Lm/U5Q8kN8yEeBTWKTfIxXAgXaT6zqdBzAAO7lA4DWHZ92AEeZZiVTs6jEWsQ2aWB2uMs6zbn0vi6t+/jIqCGw==", + "version": "1.0.83-3", + "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", "cpu": [ "x64" ], @@ -709,8 +709,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-YERVMC1Q4p6l6KQHL5rVOI52rWbvgp9IwyzUBaVSGrfFuqu5BEvZ9bgHPsxTYi3Npkt5KVOXEyPMU5rAY09qQQ==", + "version": "1.0.83-3", + "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", "cpu": [ "arm64" ], @@ -724,8 +724,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82-0", - "integrity": "sha512-z2hxMVjqt4+xDRFTZv3/0K3X+aqcJhd6zPO2JxCpOVTh5CNZFaWk+XIa2iXAPWxFqdKJsQ4muXMl3zInaAOkRw==", + "version": "1.0.83-3", + "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", "cpu": [ "x64" ], @@ -739,8 +739,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-EcUCv2PKhBzCCvpTaS511VYTDWyhudyIRPvBpc9gFNO3hjlgiNDusf4k9vP6+E3/lHenwGYzMsyRHcYIOy4vcQ==", + "version": "1.0.83-3", + "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", "cpu": [ "arm64" ], @@ -754,8 +754,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82-0", - "integrity": "sha512-1fKVjUiZ1tdb0/d/re90EpFGXhlIPfjENp2Wo/2Kj592dWO3+IwM2qV/AOdMQ4pacW5iYHII7nibx1/EYq3LGQ==", + "version": "1.0.83-3", + "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", "cpu": [ "x64" ], @@ -769,8 +769,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-H341wuxQHhwe/yLmORHzwC3DGzFZgGzh+TpwfyK2zjeYVbwDZ3Bax8M+9CzIED9jIBdEfAmfGzxKUHFugpXTtQ==", + "version": "1.0.83-3", + "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", "cpu": [ "arm64" ], @@ -784,8 +784,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82-0", - "integrity": "sha512-f1ba3gG8NaoYWFHtHaHcLN4It7mclkWdCOXvwFPqPEwqCEIx/+Zh6VHiOeIcNWRS0elRP6QYDCKaTDy1TW27uQ==", + "version": "1.0.83-3", + "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 89863520e0..adbc639cb0 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index ad675a88c7..09df5b1ff1 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.83-3", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 9b853aa597..83b0a5f483 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -16,7 +16,7 @@ import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import { createRequire } from "node:module"; import { Socket } from "node:net"; -import { dirname, isAbsolute, join } from "node:path"; +import { dirname, isAbsolute, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { createMessageConnection, @@ -34,6 +34,7 @@ import { registerClientSessionApiHandlers, } from "./generated/rpc.js"; import type { + ConnectClientInfo, GitHubTelemetryNotification, GitHubTokenAcquireRequest, GitHubTokenAcquireResult, @@ -43,6 +44,7 @@ import type { import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession } from "./session.js"; import type { FfiRuntimeHost } from "./ffiRuntimeHost.js"; +import { materializeRuntimeBundle } from "./runtimeArtifacts.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { createCopilotRequestAdapter } from "./copilotRequestHandler.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; @@ -51,6 +53,7 @@ import { ToolSet } from "./toolSet.js"; import type { AutoModeSwitchRequest, AutoModeSwitchResponse, + CopilotClientInfo, CopilotClientMode, CopilotClientOptions, CustomAgentConfig, @@ -258,6 +261,22 @@ function toWireCustomAgents(agents: CustomAgentConfig[] | undefined): unknown[] }); } +/** + * Map the public {@link CopilotClientInfo} onto the generated connect wire + * shape, dropping empty fields. Returns `undefined` when no field carries a + * non-empty value so the caller omits `clientInfo` from the handshake and keeps + * the runtime's default attribution. + */ +function clientInfoToWire(info: CopilotClientInfo | undefined): ConnectClientInfo | undefined { + if (info == null) return undefined; + const wire: ConnectClientInfo = {}; + if (info.applicationName) wire.editorName = info.applicationName; + if (info.applicationVersion) wire.editorVersion = info.applicationVersion; + if (info.integrationName) wire.extensionName = info.integrationName; + if (info.integrationVersion) wire.extensionVersion = info.integrationVersion; + return Object.keys(wire).length > 0 ? wire : undefined; +} + /** * Convert a {@link LargeToolOutputConfig} from the public API shape * (`outputDirectory`) to the wire shape (`outputDir`). @@ -365,16 +384,19 @@ function getCliPlatformPackageNames(): string[] { return variants.map((variant) => `@github/copilot-${variant}-${arch}`); } +interface BundledCliPackage { + root: string; + platform: string; +} + /** - * Gets the path to the bundled CLI from the platform-specific @github/copilot-* - * package. Uses index.js directly rather than the native binary so the CLI runs - * under the current Node.js runtime. + * Resolves the current platform package and its npm prebuilds folder. * * In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions * bundled with esbuild format:"cjs"), import.meta is empty so we fall back to * walking node_modules to find the package. */ -function getBundledCliPath(): string { +function getBundledCliPackage(): BundledCliPackage { const packageNames = getCliPlatformPackageNames(); if (typeof import.meta.resolve === "function") { @@ -383,7 +405,10 @@ function getBundledCliPath(): string { try { const packageEntryUrl = import.meta.resolve(packageName); const packageEntryPath = fileURLToPath(packageEntryUrl); - return join(dirname(packageEntryPath), "index.js"); + return { + root: dirname(packageEntryPath), + platform: packageName.slice("@github/copilot-".length), + }; } catch { // Try the next candidate platform package. } @@ -400,9 +425,13 @@ function getBundledCliPath(): string { const searchPaths = req.resolve.paths("@github/copilot") ?? []; for (const base of searchPaths) { for (const packageName of packageNames) { - const candidate = join(base, ...packageName.split("/"), "index.js"); + const root = join(base, ...packageName.split("/")); + const candidate = join(root, "index.js"); if (existsSync(candidate)) { - return candidate; + return { + root, + platform: packageName.slice("@github/copilot-".length), + }; } } } @@ -413,6 +442,14 @@ function getBundledCliPath(): string { ); } +function getBundledRuntimePath(): string { + const bundled = getBundledCliPackage(); + return materializeRuntimeBundle({ + packageRoot: bundled.root, + platform: bundled.platform, + }); +} + /** * Main client for interacting with the Copilot CLI. * @@ -503,6 +540,7 @@ export class CopilotClient { sessionIdleTimeoutSeconds: number; enableRemoteSessions: boolean; mode: CopilotClientMode; + clientInfo?: CopilotClientInfo; }; private isExternalServer: boolean = false; private forceStopping: boolean = false; @@ -520,6 +558,7 @@ export class CopilotClient { private _rpc: ReturnType | null = null; private _internalRpc: ReturnType | null = null; private processExitPromise: Promise | null = null; // Rejects when CLI process exits + private processTransportError: Error | null = null; private negotiatedProtocolVersion: number | null = null; /** Connection-level session filesystem config, set via constructor option. */ private sessionFsConfig: SessionFsConfig | null = null; @@ -733,10 +772,14 @@ export class CopilotClient { conn.kind === "stdio" || conn.kind === "tcp" ? conn.env : undefined; const effectiveEnv = connEnv ?? options.env ?? process.env; this.resolvedEnv = effectiveEnv; - this.resolvedCliPath = - conn.kind === "stdio" || conn.kind === "tcp" - ? (conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath()) - : undefined; + if (conn.kind === "stdio" || conn.kind === "tcp") { + const explicitCliPath = conn.path ?? effectiveEnv.COPILOT_CLI_PATH; + if (explicitCliPath) { + this.resolvedCliPath = explicitCliPath; + } else { + this.resolvedCliPath = getBundledRuntimePath(); + } + } // Collect extra CLI args from the connection variant (if any). const connArgs: readonly string[] = @@ -754,6 +797,7 @@ export class CopilotClient { sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0, enableRemoteSessions: options.enableRemoteSessions ?? false, mode: options.mode ?? "copilot-cli", + clientInfo: options.clientInfo, }; // Empty mode: validate at construction time that the app supplied a @@ -951,6 +995,8 @@ export class CopilotClient { return; } + this.forceStopping = false; + this.processTransportError = null; this.state = "connecting"; try { @@ -997,8 +1043,10 @@ export class CopilotClient { this.state = "connected"; } catch (error) { + const startupError = this.processTransportError ?? error; + await this.forceStop(); this.state = "error"; - throw error; + throw startupError; } } @@ -1678,6 +1726,7 @@ export class CopilotClient { requestPermission: !!config.onPermissionRequest, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, + askUserVariant: config.askUserVariant, ...(config.enableMcpApps ? { requestMcpApps: true } : {}), ...(config.githubMcpToolConfig != null ? { githubMcpToolConfig: config.githubMcpToolConfig } @@ -1720,6 +1769,7 @@ export class CopilotClient { gitHubTokenProviderRegistrationId, remoteSession: config.remoteSession, cloud: config.cloud, + featureFlags: config.featureFlags, expAssignments: config.expAssignments, enableManagedSettings: config.enableManagedSettings, managedSettings: config.managedSettings, @@ -1946,6 +1996,7 @@ export class CopilotClient { config.onPermissionRequest !== defaultJoinSessionPermissionHandler, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, + askUserVariant: config.askUserVariant, ...(config.enableMcpApps ? { requestMcpApps: true } : {}), ...(config.githubMcpToolConfig != null ? { githubMcpToolConfig: config.githubMcpToolConfig } @@ -1990,6 +2041,7 @@ export class CopilotClient { gitHubTokenProviderRegistrationId, remoteSession: config.remoteSession, openCanvases: config.openCanvases, + featureFlags: config.featureFlags, expAssignments: config.expAssignments, enableManagedSettings: config.enableManagedSettings, managedSettings: config.managedSettings, @@ -2186,6 +2238,7 @@ export class CopilotClient { const connectParams: { token?: string; enableGitHubTelemetryForwarding?: boolean; + clientInfo?: ConnectClientInfo; } = { token: this.effectiveConnectionToken }; // Opt in to GitHub telemetry forwarding at the connection level when a // handler is registered (mirrors the runtime, which reads this flag on the @@ -2194,6 +2247,14 @@ export class CopilotClient { if (this.onGitHubTelemetry != null) { connectParams.enableGitHubTelemetryForwarding = true; } + // Declare the integrating application's identity so the runtime attributes + // the telemetry it emits on this connection to a consistent surface + // instead of its own build. Empty fields are dropped, and an + // all-empty identity is omitted entirely. + const clientInfo = clientInfoToWire(this.options.clientInfo); + if (clientInfo != null) { + connectParams.clientInfo = clientInfo; + } const result = await raceAgainstExit(this.internalRpc.connect(connectParams)); serverVersion = result.protocolVersion; } catch (err) { @@ -2715,27 +2776,27 @@ export class CopilotClient { // Set up a promise that rejects when the process exits (used to race against RPC calls) this.processExitPromise = new Promise((_, rejectProcessExit) => { this.cliProcess!.on("exit", (code) => { - // Give a small delay for stderr to be fully captured - setTimeout(() => { - const stderrOutput = this.stderrBuffer.trim(); - if (stderrOutput) { - rejectProcessExit( - new Error( - `CLI server exited with code ${code}\nstderr: ${stderrOutput}` - ) - ); - } else { - rejectProcessExit( - new Error(`CLI server exited unexpectedly with code ${code}`) - ); - } - }, 50); + if (this.messageWriter) { + this.messageWriter.suppressWriteErrors = true; + } + const stderrOutput = this.stderrBuffer.trim(); + if (stderrOutput) { + rejectProcessExit( + new Error( + `CLI server exited with code ${code}\nstderr: ${stderrOutput}` + ) + ); + } else { + rejectProcessExit( + new Error(`CLI server exited unexpectedly with code ${code}`) + ); + } }); }); // Prevent unhandled rejection when process exits normally (we only use this in Promise.race) this.processExitPromise.catch(() => {}); - this.cliProcess.on("exit", (code) => { + this.cliProcess.on("close", (code) => { if (!resolved) { resolved = true; const stderrOutput = this.stderrBuffer.trim(); @@ -2780,7 +2841,15 @@ export class CopilotClient { /** Starts the in-process FFI runtime with SDK-managed typed options. */ private async startInProcessFfi(): Promise { - const entrypoint = this.resolveCliPathForFfi(); + const explicitEntrypoint = this.resolvedEnv.COPILOT_CLI_PATH; + const runtimeLibrary = explicitEntrypoint + ? join( + dirname(resolve(explicitEntrypoint)), + "prebuilds", + CopilotClient.getNapiPrebuildsFolder(explicitEntrypoint), + "runtime.node" + ) + : join(dirname(getBundledRuntimePath()), "runtime.node"); // Load the FFI host lazily so the native `koffi` addon (and its // platform-specific `koffi.node`) is only loaded on the in-process path; // out-of-process (stdio/tcp) consumers never touch the native dependency. @@ -2815,12 +2884,7 @@ export class CopilotClient { args.push("--remote"); } - const host = FfiRuntimeHost.create( - entrypoint, - CopilotClient.getNapiPrebuildsFolder(entrypoint), - environment, - args - ); + const host = FfiRuntimeHost.create(runtimeLibrary, explicitEntrypoint, environment, args); this.ffiHost = host; await host.start(); } @@ -2843,20 +2907,6 @@ export class CopilotClient { this.connection.listen(); } - /** - * Resolves the CLI entrypoint used for in-process FFI hosting: `COPILOT_CLI_PATH` - * when set, otherwise the bundled platform-package entrypoint. - */ - private resolveCliPathForFfi(): string { - return this.resolvedEnv.COPILOT_CLI_PATH ?? getBundledCliPath(); - } - - /** - * Returns the napi prebuilds folder name for the current host — the - * `-` convention (e.g. `win32-x64`, `darwin-arm64`, - * `linux-x64`, `linuxmusl-x64`) under which the runtime ships - * `prebuilds//runtime.node`. - */ private static getNapiPrebuildsFolder(entrypoint: string): string { const arch = process.arch; if (arch !== "x64" && arch !== "arm64") { @@ -2900,6 +2950,10 @@ export class CopilotClient { } this.state = "error"; const reason = err instanceof Error ? (err.stack ?? err.message) : String(err); + const stderrOutput = this.stderrBuffer.trim(); + this.processTransportError = new Error( + `CLI server connection failed: ${reason}${stderrOutput ? `\nstderr: ${stderrOutput}` : ""}` + ); this.logDebug(`stdin pipe error: ${reason}`); try { this.connection?.dispose(); diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index d756308734..ac0ccdb7ce 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -85,6 +85,8 @@ export { type FactoryRunResult, type FactoryRunStatus, type FactoryRunSummary, + type FactoryListRunsOptions, + type FactoryRunsPage, type FactoryRunDetail, type FactoryProgressPage, type FactoryProgressLine, diff --git a/nodejs/src/factory.ts b/nodejs/src/factory.ts index 8a6c787471..6212f462b4 100644 --- a/nodejs/src/factory.ts +++ b/nodejs/src/factory.ts @@ -4,6 +4,8 @@ import type { FactoryGetRunProgressRequest, + FactoryListRunsRequest, + FactoryListRunsResult, FactoryProgressPage, FactoryRunDetail, FactoryRunResult, @@ -26,6 +28,22 @@ export type { FactoryRunSummary, } from "./generated/rpc.js"; +/** + * Options for paging durable factory runs. + * + * @experimental Part of the experimental Agent Factories surface and may + * change or be removed in future SDK or CLI releases. + */ +export type FactoryListRunsOptions = FactoryListRunsRequest; + +/** + * A page of durable factory runs and its paging metadata. + * + * @experimental Part of the experimental Agent Factories surface and may + * change or be removed in future SDK or CLI releases. + */ +export type FactoryRunsPage = FactoryListRunsResult; + /** * Run statuses a factory run can no longer move away from. * @@ -242,6 +260,10 @@ export interface RunOptions { args?: TArgs; /** Optional per-invocation resource ceiling overrides. */ limits?: FactoryLimits; + /** Whether to notify the originating session when the factory completes. */ + notifyOnComplete?: boolean; + /** Whether to emit factory phase names to the session transcript. */ + logPhaseNames?: boolean; /** * Prior run whose persisted identity, arguments, journal, and accounting should be resumed. * @@ -259,6 +281,10 @@ export interface RunOptions { export interface ResumeOptions { /** Optional per-invocation resource ceiling overrides. */ limits?: FactoryLimits; + /** Whether to notify the originating session when the factory completes. */ + notifyOnComplete?: boolean; + /** Whether to emit factory phase names to the session transcript. */ + logPhaseNames?: boolean; } /** @@ -328,8 +354,20 @@ export interface SessionFactoryApi { waitForRun(runId: string, options?: { signal?: AbortSignal }): Promise; /** * List the newest default page of this session's durable factory runs. + * + * This backwards-compatible overload returns only the runs array. Pass + * paging options to receive the full page, including its cursors and + * truncation metadata. */ listRuns(): Promise; + /** + * Page this session's durable factory runs. + * + * `afterSeq` and `beforeSeq` are exclusive cursors. The result includes + * `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` so callers + * can continue paging without using the raw RPC client. + */ + listRuns(options: FactoryListRunsOptions): Promise; /** Read durable phases, direct agents, and the latest progress tail for a run. */ getRunDetail(runId: string): Promise; /** Page durable progress forward, backward, or from the latest tail. */ diff --git a/nodejs/src/ffiRuntimeHost.ts b/nodejs/src/ffiRuntimeHost.ts index a92aa1589a..4795e325ce 100644 --- a/nodejs/src/ffiRuntimeHost.ts +++ b/nodejs/src/ffiRuntimeHost.ts @@ -7,10 +7,8 @@ * and speaking JSON-RPC over its C ABI (FFI) instead of spawning a CLI child process * and communicating over stdio/TCP. * - * The native `host_start` export spawns the CLI worker itself - * (`node --embedded-host` for a `.js` entrypoint, or ` - * --embedded-host` for a packaged binary), so the SDK never launches the worker - * directly. LSP `Content-Length:`-framed JSON-RPC bytes are pumped across the ABI: + * The native `host_start` export constructs the Rust server synchronously in this + * process. LSP `Content-Length:`-framed JSON-RPC bytes are pumped across the ABI: * writes go to `connection_write`; inbound frames arrive on a native callback that * feeds {@link FfiRuntimeHost.receiveStream}. The existing `vscode-jsonrpc` * `StreamMessageReader`/`StreamMessageWriter` handle framing unchanged — this is a @@ -19,7 +17,7 @@ import { existsSync } from "node:fs"; import koffi from "koffi"; -import { dirname, join, resolve } from "node:path"; +import { resolve } from "node:path"; import { PassThrough, Writable } from "node:stream"; const SYMBOL_PREFIX = "copilot_runtime_"; @@ -97,14 +95,12 @@ function loadLibrary(libraryPath: string): FfiLibrary { return loadedLibrary; } -function buildArgvJson(cliEntrypoint: string, args: readonly string[]): Buffer { - // A `.js` entrypoint is launched via node; the packaged single-file CLI binary - // embeds its own Node and is invoked directly. `--no-auto-update` pins the worker - // to the bundled pkg matching the loaded cdylib, instead of drifting to a newer - // version installed under the user's `~/.copilot/pkg` (which would cause ABI skew). - const argv = cliEntrypoint.toLowerCase().endsWith(".js") - ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"] - : [cliEntrypoint, "--embedded-host", "--no-auto-update"]; +function buildArgvJson(cliEntrypoint: string | undefined, args: readonly string[]): Buffer { + const argv = cliEntrypoint + ? cliEntrypoint.toLowerCase().endsWith(".js") + ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"] + : [cliEntrypoint, "--embedded-host", "--no-auto-update"] + : []; argv.push(...args); return Buffer.from(JSON.stringify(argv), "utf8"); } @@ -140,7 +136,7 @@ export class FfiRuntimeHost { private constructor( private readonly libraryPath: string, - private readonly cliEntrypoint: string, + private readonly cliEntrypoint: string | undefined, private readonly environment: Record | undefined, private readonly args: readonly string[] ) { @@ -161,41 +157,38 @@ export class FfiRuntimeHost { } /** - * Resolves the cdylib next to the given CLI entrypoint and prepares the FFI host. - * The cdylib is resolved as `prebuilds//runtime.node` relative to - * the entrypoint directory (the napi-rs `-` layout, e.g. - * `linux-x64`). Throws if it cannot be found. + * Loads the runtime cdylib at the given path and prepares the FFI host. */ static create( - cliEntrypoint: string, - prebuildsFolder: string, + libraryPath: string, + cliEntrypoint: string | undefined, environment: Record | undefined, args: readonly string[] ): FfiRuntimeHost { - const fullEntrypoint = resolve(cliEntrypoint); - const distDir = dirname(fullEntrypoint); - const libraryPath = join(distDir, "prebuilds", prebuildsFolder, "runtime.node"); - if (!existsSync(libraryPath)) { - throw new Error(`FFI runtime library not found. Looked for '${libraryPath}'.`); + const fullLibraryPath = resolve(libraryPath); + if (!existsSync(fullLibraryPath)) { + throw new Error(`FFI runtime library not found at '${fullLibraryPath}'.`); } - return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args); + return new FfiRuntimeHost( + fullLibraryPath, + cliEntrypoint ? resolve(cliEntrypoint) : undefined, + environment, + args + ); } - /** - * Starts the in-process runtime: spawns the CLI worker via the native host, - * waits for readiness, and opens the FFI JSON-RPC connection. - */ + /** Starts the in-process Rust runtime and opens the FFI JSON-RPC connection. */ async start(): Promise { const argvJson = buildArgvJson(this.cliEntrypoint, this.args); const envJson = buildEnvJson(this.environment); - // The native host spawns the CLI worker itself and has no cwd parameter, so the - // worker inherits this process's cwd. A custom working directory is intentionally + // The native host has no cwd parameter, so it uses this process's cwd. A custom + // working directory is intentionally // unsupported for the in-process transport (rejected by the client constructor) // rather than mutating the shared process-global cwd here. - // host_start blocks until the worker connects back and signals readiness - // (up to ~30s); run it as an async FFI call so the Node event loop isn't blocked. + // host_start constructs the native engine synchronously; run it as an async FFI + // call so the Node event loop isn't blocked. this.serverId = await new Promise((resolvePromise, rejectPromise) => { this.lib.hostStart.async( argvJson, @@ -212,9 +205,7 @@ export class FfiRuntimeHost { ); }); if (!this.serverId) { - throw new Error( - `copilot_runtime_host_start failed (library '${this.libraryPath}', entrypoint '${this.cliEntrypoint}').` - ); + throw new Error(`copilot_runtime_host_start failed (library '${this.libraryPath}').`); } this.outboundCallback = koffi.register( diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index db0ea63dcc..716f76cddc 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -5,7 +5,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; -import type { AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; +import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; /** A value that can be represented losslessly on the SDK JSON wire. */ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; @@ -552,7 +552,13 @@ export type CatalogNetworkFailureReason = | "tls" /** The connection was refused or reset. */ | "connection-refused" - /** The authority returned a status the runtime treats as a failure. */ + /** The configured proxy returned 407 and requires authentication. */ + | "proxy-authentication-required" + /** The authority rate-limited requests and supplied or implied a bounded cooldown. */ + | "rate-limited" + /** The authority returned a transient 5xx response. */ + | "service-unavailable" + /** The authority returned another status the runtime treats as a failure. */ | "http-status" /** The response exceeded the permitted size. */ | "response-too-large" @@ -863,6 +869,64 @@ export type DiscoveredExtensionMode = | "load_only" /** Extensions are loaded and the agent can create, reload, and manage them. */ | "load_and_augment"; +/** + * Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HookType". + */ +/** @experimental */ +export type HookType = + /** Runs before a tool is invoked. */ + | "preToolUse" + /** Runs before an MCP tool is invoked. */ + | "preMcpToolCall" + /** Runs after a tool completes successfully. */ + | "postToolUse" + /** Runs after a tool fails. */ + | "postToolUseFailure" + /** Runs after the user submits a prompt. */ + | "userPromptSubmitted" + /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */ + | "userPromptTransformed" + /** Runs when a session starts. */ + | "sessionStart" + /** Runs when a session ends. */ + | "sessionEnd" + /** Runs after an agent result is produced. */ + | "postResult" + /** Runs before a pull request description is generated. */ + | "prePRDescription" + /** Runs when the agent encounters an error. */ + | "errorOccurred" + /** Runs when the agent stops. */ + | "agentStop" + /** Runs when a subagent starts. */ + | "subagentStart" + /** Runs when a subagent stops. */ + | "subagentStop" + /** Runs before conversation context is compacted. */ + | "preCompact" + /** Runs when the agent requests permission. */ + | "permissionRequest" + /** Runs when the agent emits a notification. */ + | "notification"; +/** + * Configuration tier that contributed a discovered hook action. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HookOrigin". + */ +/** @experimental */ +export type HookOrigin = + /** Hook loaded from user settings or the user's hook directory. */ + | "user" + /** Hook loaded from repository settings or the repository hook directory. */ + | "repository" + /** Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. */ + | "plugin" + /** Hook enforced by centrally managed policy. */ + | "policy"; /** * Server transport type: stdio, http, sse (deprecated), or memory * @@ -1127,6 +1191,16 @@ export type FactoryRunFailure = * Factory failure variant discriminator. */ type: "factory_accounting_incomplete"; + } + | { + /** + * Factory run identifier. + */ + runId: string; + /** + * Factory failure variant discriminator. + */ + type: "factory_provider_disconnected"; }; /** * Cumulative resource ceiling that stopped a factory run. @@ -1332,49 +1406,6 @@ export type HistoryRewindOutcome = | "checkpoint-cleanup-failed" /** Files and conversation were rewound, but obsolete file snapshots could not be removed; only conversation-and-files rewinds produce this. */ | "snapshot-prune-failed"; -/** - * Hook event name dispatched through the SDK callback transport. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HookType". - */ -/** @experimental */ -/** @internal */ -export type HookType = - /** Runs before a tool is invoked. */ - | "preToolUse" - /** Runs before an MCP tool is invoked. */ - | "preMcpToolCall" - /** Runs after a tool completes successfully. */ - | "postToolUse" - /** Runs after a tool fails. */ - | "postToolUseFailure" - /** Runs after the user submits a prompt. */ - | "userPromptSubmitted" - /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */ - | "userPromptTransformed" - /** Runs when a session starts. */ - | "sessionStart" - /** Runs when a session ends. */ - | "sessionEnd" - /** Runs after an agent result is produced. */ - | "postResult" - /** Runs before a pull request description is generated. */ - | "prePRDescription" - /** Runs when the agent encounters an error. */ - | "errorOccurred" - /** Runs when the agent stops. */ - | "agentStop" - /** Runs when a subagent starts. */ - | "subagentStart" - /** Runs when a subagent stops. */ - | "subagentStop" - /** Runs before conversation context is compacted. */ - | "preCompact" - /** Runs when the agent requests permission. */ - | "permissionRequest" - /** Runs when the agent emits a notification. */ - | "notification"; /** * Source for direct repo installs (when marketplace is empty) * @@ -4573,7 +4604,7 @@ export interface AgentGetCurrentResult { agent?: AgentInfo | null; } /** - * Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. + * Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "AgentInfo". @@ -4613,6 +4644,11 @@ export interface AgentInfo { * Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. */ model?: string; + /** + * Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + */ + models?: string[]; + modelPolicy?: AgentModelPolicy; /** * MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. * @@ -5487,6 +5523,7 @@ export interface CanvasProviderUnregisterRequest { */ /** @experimental */ export interface CapiSessionOptions { + autoTier?: AutoTier; /** * Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. */ @@ -5833,6 +5870,10 @@ export interface CatalogNetworkFailureError { * HTTP status code, when the failure was a rejected response. */ statusCode?: number; + /** + * Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + */ + retryAfterSeconds?: number; /** * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ @@ -5884,7 +5925,7 @@ export interface CatalogPolicyRejectedError { export interface CatalogSearchRequest { contract: CatalogClientContract; /** - * Free-text search query. Never written to logs or telemetry. + * Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. */ query: string; /** @@ -6820,6 +6861,37 @@ export interface DiscoveredExtensionsEnableRequest { */ ids: string[]; } +/** + * One server-discovered hook action from user, repository, plugin, or managed-policy configuration. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredHook". + */ +/** @experimental */ +export interface DiscoveredHook { + /** + * Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + */ + id: string; + hookType: HookType; + origin: HookOrigin; + /** + * Human-readable source label, such as a hook file path, settings source, or plugin name. + */ + source?: string; + /** + * Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + */ + projectPath?: string; + /** + * Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + */ + enabled: boolean; + /** + * Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + */ + disableKey?: string; +} /** * MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state. * @@ -6859,6 +6931,10 @@ export interface EnqueueCommandParams { * Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. */ command: string; + /** + * Optional user-facing text for the queue row. The command string is shown when omitted. + */ + displayText?: string | null; } /** * Indicates whether the command was accepted into the local execution queue. @@ -8151,6 +8227,10 @@ export interface FactoryRunResult { * Factory run identifier. */ runId: string; + /** + * One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + */ + attempt?: number; status: FactoryRunStatus; /** * Completed factory result. @@ -8953,6 +9033,44 @@ export interface HookInvokeRequest { export interface HookInvokeResponse { output?: JsonValue; } +/** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HooksDiscoverRequest". + */ +/** @experimental */ +export interface HooksDiscoverRequest { + /** + * Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + */ + projectPaths?: string[]; + /** + * When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + */ + excludeHostHooks?: boolean; +} +/** + * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HooksDiscoverResult". + */ +/** @experimental */ +export interface HooksDiscoverResult { + /** + * All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + */ + hooks: DiscoveredHook[]; + /** + * Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + */ + warnings: string[]; + /** + * Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + */ + errors: string[]; +} /** * Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. * @@ -12351,6 +12469,10 @@ export interface ModelBillingPromo { * Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. */ message?: string; + /** + * Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + */ + showBanner?: boolean; } /** * Service-published warning text that hosts should display when presenting a model. @@ -12398,6 +12520,10 @@ export interface ModelApplyStartupOverlayRequest { * Model required by server-managed policy, when configured. */ serverManagedModel?: string; + /** + * 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. + */ + policyHelperModel?: string; /** * Model selected by repository settings, when configured. */ @@ -15837,6 +15963,10 @@ export interface QueuePendingItems { * Stable opaque id for the canonical queued item. Batch rows share one id. */ id: string; + /** + * Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + */ + messageId?: string; kind: QueuePendingItemsKind; /** * Human-readable text to display for this queue entry in the UI @@ -16042,7 +16172,7 @@ export interface RegisterExtensionToolsParams { */ sessionId: string; /** - * In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, extension discovery/launch moves entirely into the runtime — the CLI passes pure config (search paths, disabled ids) via SessionOptions instead. + * In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK surface. * * @internal * @@ -16060,7 +16190,7 @@ export interface RegisterExtensionToolsParams { /** @experimental */ export interface SessionsRegisterExtensionToolsOnSessionOptions { /** - * In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: replaced by runtime-side enable/disable RPCs in the SDK migration. + * In-process `() => boolean` gating callback used only by the CLI. * * @internal */ @@ -16076,7 +16206,7 @@ export interface SessionsRegisterExtensionToolsOnSessionOptions { /** @internal */ export interface RegisterExtensionToolsResult { /** - * In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration. + * In-process unsubscribe function used only by the CLI. * * @internal * @@ -16523,7 +16653,7 @@ export interface SandboxConfigUserPolicyNetwork { /** @experimental */ export interface SandboxConfigUserPolicyNetworkProxy { /** - * Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + * Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ url: string; /** @@ -16588,6 +16718,27 @@ export interface SandboxConfigAuth { */ gh?: boolean; } +/** + * Managed sandbox enforcement state for a session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxEnforcementStatus". + */ +/** @experimental */ +export interface SandboxEnforcementStatus { + /** + * Whether the effective managed policy requires an available sandbox backend. + */ + required: boolean; + /** + * Whether an enforcement failure has permanently blocked the session. + */ + blocked: boolean; + /** + * The first sandbox enforcement failure that blocked the session. + */ + reason?: string; +} /** * Register an absolute-time scheduled prompt. * @@ -18275,6 +18426,17 @@ export interface SessionOpenOptions { * Additional directories to search for skills. */ skillDirectories?: string[]; + /** + * Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. + */ + enableSkills?: boolean; + /** + * Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. + * + * @internal + * @experimental + */ + hasSkillProvider?: boolean; /** * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. */ @@ -18634,7 +18796,7 @@ export interface SessionsOpenCloud { owner?: string; options?: SessionOpenOptions; /** - * In-process callback invoked when the cloud task is created (before connection). Marked internal because a function reference cannot cross the JSON-RPC boundary. Disappears in the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. + * In-process callback invoked when the cloud task is created, before connection. Internal because function references cannot cross the JSON-RPC boundary. * * @internal */ @@ -19438,6 +19600,28 @@ export interface SessionsPruneOldRequest { */ excludeSessionIds?: string[]; } +/** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsReadPersistedEventsRequest". + */ +/** @experimental */ +export interface SessionsReadPersistedEventsRequest { + /** + * Session ID whose persisted event journal should be read. + */ + sessionId: string; + /** + * Opaque cursor returned by a previous persisted-event read. Omit on the first call. + */ + cursor?: string; + /** + * Maximum number of events to return in this batch (1–1000, default 200). + */ + max?: number; + direction?: EventsReadDirection; +} /** * Session ID whose in-use lock should be released. * @@ -19808,7 +19992,7 @@ export interface SessionUpdateOptionsParams { */ enableSessionStore?: boolean; /** - * Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + * Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. */ enableSkills?: boolean; contextTier?: OptionsUpdateContextTier; @@ -20029,6 +20213,83 @@ export interface SkillList { */ skills: Skill[]; } +/** + * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderDescriptor". + */ +/** @experimental */ +export interface SkillProviderDescriptor { + /** + * Invocation and display name. + */ + name: string; + /** + * Description used in skill catalogs without fetching content. + */ + description: string; + /** + * Whether users may invoke the skill directly. Defaults to true. + */ + userInvocable?: boolean; + /** + * Whether model invocation is disabled. Defaults to false. + */ + disableModelInvocation?: boolean; + /** + * Optional freeform argument hint used by slash-command catalogs. + */ + argumentHint?: string; +} +/** + * Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderListResult". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderListResult { + /** + * Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. + * + * @maxItems 1024 + */ + skills: SkillProviderDescriptor[]; +} +/** + * Identifies one SDK-provided skill by invocation name. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderReadRequest". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderReadRequest { + /** + * Target session identifier + */ + sessionId: string; + /** + * Invocation name of the skill to read. + */ + name: string; +} +/** + * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderReadResult". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderReadResult { + /** + * Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + */ + markdown: string; +} /** * Skill names to mark as disabled in global configuration, replacing any previous list. * @@ -20149,7 +20410,7 @@ export interface SkillsInvokedSkill { */ name: string; /** - * Path to the SKILL.md file + * Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity */ path: string; /** @@ -20160,6 +20421,10 @@ export interface SkillsInvokedSkill { * Tools that should be auto-approved when this skill is active, captured at invocation time */ allowedTools?: string[]; + /** + * Whether model invocation was disabled when this skill was invoked + */ + disableModelInvocation?: boolean; /** * Turn number when the skill was invoked */ @@ -20261,6 +20526,7 @@ export interface SlashCommandCompletedResult { * Optional user-facing message describing the completed command */ message?: string; + mode?: SessionMode; /** * True when the invocation mutated user runtime settings; consumers caching settings should refresh */ @@ -20446,6 +20712,7 @@ export interface SubagentSettingsEntry { * Model override for matching subagents */ model?: string; + modelPolicy?: AgentModelPolicy; /** * Reasoning effort override for matching subagents */ @@ -21618,13 +21885,13 @@ export interface UIEphemeralQueryRequest { */ question: string; /** - * In-process streaming callback `(text) => void` invoked with each token as the model emits it. Marked internal: excluded from the public SDK surface. In a process-separated SDK this is replaced by a streaming RPC that yields chunks and a final answer. + * In-process streaming callback `(text) => void` invoked with each token as the model emits it. Internal and excluded from the public SDK surface. * * @internal */ onChunk?: OpaqueInProcessValue; /** - * In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Marked internal: excluded from the public SDK surface. Replaced by an explicit cancellation token + cancel RPC in the SDK migration. + * In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Internal and excluded from the public SDK surface. * * @internal */ @@ -22764,6 +23031,19 @@ export interface SessionLimitPredictionPredictRequest { modelId?: string; clientType?: SessionLimitPredictionClientType; } +/** + * Identifies the target session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderListRequest". + */ +/** @experimental */ +export interface SkillProviderListRequest { + /** + * Target session identifier + */ + sessionId: string; +} /** * Identifies the target session. * @@ -22793,6 +23073,18 @@ export function createServerRpc(connection: MessageConnection) { ping: async (params: PingRequest): Promise => connection.sendRequest("ping", params), /** @experimental */ + hooks: { + /** + * Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + * + * @param params Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @returns Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + */ + discover: async (params: HooksDiscoverRequest): Promise => + connection.sendRequest("hooks.discover", params), + }, + /** @experimental */ models: { /** * Lists Copilot models available to the authenticated user. @@ -22975,7 +23267,7 @@ export function createServerRpc(connection: MessageConnection) { connection.sendRequest("extensions.disable", params), }, /** - * Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility. + * 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. * * @experimental */ @@ -23318,6 +23610,15 @@ export function createServerRpc(connection: MessageConnection) { */ list: async (params: SessionsListRequest): Promise => connection.sendRequest("sessions.list", params), + /** + * Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + * + * @param params Pagination options for reading an inactive or active local session's persisted event journal. + * + * @returns Batch of session events returned by a read, with cursor and continuation metadata. + */ + readPersistedEvents: async (params: SessionsReadPersistedEventsRequest): Promise => + connection.sendRequest("sessions.readPersistedEvents", params), /** * Finds the local session bound to a GitHub task ID, if any. * @@ -23625,6 +23926,16 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ sendMessages: async (params: SendMessagesRequest): Promise => connection.sendRequest("session.sendMessages", { sessionId, ...params }), + /** @experimental */ + sandbox: { + /** + * Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. + * + * @returns Managed sandbox enforcement state for a session. + */ + getEnforcementStatus: async (): Promise => + connection.sendRequest("session.sandbox.getEnforcementStatus", { sessionId }), + }, /** * Aborts the current agent turn. * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 9075379982..7d734bf491 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -24,6 +24,7 @@ export type SessionEvent = | WarningEvent | ModelChangeEvent | ModeChangedEvent + | ModeNoticeDeliveredEvent | SessionLimitsChangedEvent | PermissionsChangedEvent | PlanChangedEvent @@ -40,6 +41,7 @@ export type SessionEvent = | CompactionStartEvent | CompactionCompleteEvent | TaskCompleteEvent + | CompletionReceiptEvent | FusionRouteStartedEvent | FusionRouteFailedEvent | FusionResolvedEvent @@ -49,6 +51,7 @@ export type SessionEvent = | AssistantTurnStartEvent | AssistantIntentEvent | AssistantFusionPhaseStartedEvent + | AssistantFusionPhaseActivityEvent | AssistantFusionPhaseCompletedEvent | AssistantFusionPhaseFailedEvent | AssistantServerToolProgressEvent @@ -135,6 +138,16 @@ export type SessionEvent = | CanvasRemovedEvent | ExtensionsAttachmentsPushedEvent | McpAppToolCallCompleteEvent; +/** + * Routing preference used when the session model is `auto`. + */ +export type AutoTier = + /** Optimize for efficiency. */ + | "efficiency" + /** Balance efficiency and intelligence. */ + | "balance" + /** Optimize for intelligence. */ + | "intelligence"; /** * Hosting platform type of the repository (github or ado) */ @@ -306,6 +319,30 @@ export type TaskCompletionOutcome = | "continue" /** Completion cannot proceed without intervention; the active objective is paused when one is identified. */ | "blocked"; +/** + * Structured terminal status from a tool completion event. + */ +export type CompletionReceiptToolStatus = + /** The tool completed successfully. */ + | "success" + /** The tool failed without a more specific structured status. */ + | "failure" + /** The tool exceeded its time budget. */ + | "timeout" + /** The user rejected the tool call. */ + | "rejected" + /** The permissions service denied the tool call. */ + | "denied"; +/** + * Runtime reason the completion decision was accepted. + */ +export type CompletionReceiptStopReason = + /** The model reached a natural terminal response. */ + | "natural" + /** A terminal tool ended the interaction. */ + | "terminal_tool" + /** The configured agentStop continuation limit was reached. */ + | "agent_stop_block_limit"; /** * Kind of turn for which HydraFusion routing is running. */ @@ -335,6 +372,34 @@ export type FusionPattern = | "cascade" /** Run a primary draft, a read-only critique, and a revision. */ | "critique"; +/** + * HydraFusion phase kind. + */ +/** @experimental */ +export type FusionPhaseKind = + /** Primary solver phase. */ + | "primary" + /** Read-only cascade judge phase. */ + | "judge" + /** Cascade repair phase. */ + | "repair" + /** Initial critique-pattern draft phase. */ + | "draft" + /** Read-only critique phase. */ + | "critic" + /** Critique-pattern revision phase. */ + | "revision" + /** Follow-up phase continuing from the resolved model. */ + | "follow_up"; +/** + * Conversation scope in which a HydraFusion phase executes. + */ +/** @experimental */ +export type FusionConversationScope = + /** Canonical root conversation history. */ + | "root" + /** Isolated read-only review history that does not enter the root conversation. */ + | "review"; /** * The agent mode that was active when this message was sent */ @@ -395,33 +460,16 @@ export type UserMessageDelivery = /** Enqueued while the agent was busy; processed as its own run afterward. */ | "queued"; /** - * Conversation scope in which a HydraFusion phase executes. + * Content-safe activity observed while a HydraFusion phase is running. */ /** @experimental */ -export type FusionConversationScope = - /** Canonical root conversation history. */ - | "root" - /** Isolated read-only review history that does not enter the root conversation. */ - | "review"; -/** - * HydraFusion phase kind. - */ -/** @experimental */ -export type FusionPhaseKind = - /** Primary solver phase. */ - | "primary" - /** Read-only cascade judge phase. */ - | "judge" - /** Cascade repair phase. */ - | "repair" - /** Initial critique-pattern draft phase. */ - | "draft" - /** Read-only critique phase. */ - | "critic" - /** Critique-pattern revision phase. */ - | "revision" - /** Follow-up phase continuing from the resolved model. */ - | "follow_up"; +export type FusionPhaseActivityKind = + /** The provider produced additional private output bytes. */ + | "model_output" + /** A tool began executing inside the phase. */ + | "tool_started" + /** A tool finished executing inside the phase. */ + | "tool_completed"; /** * How a durable phase checkpoint contributes its exact message to canonical root history. */ @@ -469,6 +517,10 @@ export type CitationProvider = */ /** @experimental */ export type CitationLocation = CitationLocationChar | CitationLocationPage | CitationLocationBlock; +/** + * Hosted program caller type + */ +export type AssistantMessageToolRequestCallerType = "program"; /** * API endpoint used for this model call, matching CAPI supported_endpoints vocabulary */ @@ -933,7 +985,9 @@ export type ManagedSettingsResolvedSource = | "device" /** Only session-local SDK-host injection contributed. */ | "client" - /** More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. */ + /** A policy helper registered by device or server policy contributed. Device registration takes priority when present. */ + | "policyHelper" + /** More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. */ | "mixed" /** No managed policy is in force (no channel contributed). */ | "none"; @@ -984,7 +1038,7 @@ export type FactoryRunSettledStatus = /** The run failed, with `failureType` carrying the class when it has one. */ | "error"; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) */ export type SkillSource = /** Skill defined in the current project's skill directories. */ @@ -1000,7 +1054,17 @@ export type SkillSource = /** Skill loaded from a configured custom skill directory. */ | "custom" /** Skill bundled with the runtime. */ - | "builtin"; + | "builtin" + /** Pathless skill supplied lazily by an SDK skill provider. */ + | "sdk"; +/** + * Whether configured models are advisory preferences or required constraints + */ +export type AgentModelPolicy = + /** Treat the authored models as advisory preferences that callers may override. */ + | "preferred" + /** Require subagent execution to use one of the authored models. */ + | "required"; /** * Configuration source: user, workspace, plugin, or builtin */ @@ -1106,6 +1170,7 @@ export interface StartData { * Whether the session was already in use by another client at start time */ alreadyInUse?: boolean; + autoTier?: AutoTier; context?: WorkingDirectoryContext; /** * Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) @@ -1258,6 +1323,7 @@ export interface ResumeData { * Whether the session was already in use by another client at resume time */ alreadyInUse?: boolean; + autoTier?: AutoTier; context?: WorkingDirectoryContext; /** * Context tier currently selected at resume time; null when no tier is active @@ -1885,6 +1951,46 @@ export interface ModeChangedData { newMode: SessionMode; previousMode: SessionMode; } +/** + * Session event "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + */ +export interface ModeNoticeDeliveredEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: ModeNoticeDeliveredData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.mode_notice_delivered". + */ + type: "session.mode_notice_delivered"; +} +/** + * Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + */ +export interface ModeNoticeDeliveredData { + /** + * Model-visible transition notice persisted for a mid-turn delivery + */ + content?: string; + mode: SessionMode; +} /** * Session event "session.session_limits_changed". Session limits update details. Null clears the limits. */ @@ -3009,6 +3115,97 @@ export interface TaskCompleteData { */ summary?: string; } +/** + * Session event "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + */ +/** @experimental */ +export interface CompletionReceiptEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: CompletionReceiptData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.completion_receipt". + */ + type: "session.completion_receipt"; +} +/** + * Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + */ +/** @experimental */ +export interface CompletionReceiptData { + /** + * One-based accepted completion receipt ordinal in the durable session history. + */ + attempt: number; + eventRange: CompletionReceiptEventRange; + /** + * Number of failed structured tool completions in the covered range. + */ + failedToolCount: number; + finalTool?: CompletionReceiptFinalTool; + /** + * Version of the completion receipt payload. + */ + schemaVersion: number; + /** + * Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + */ + sourceEventId: string; + stopReason: CompletionReceiptStopReason; + /** + * Number of successful structured tool completions in the covered range. + */ + successfulToolCount: number; +} +/** + * Inclusive durable event range summarized by a completion receipt. + */ +export interface CompletionReceiptEventRange { + /** + * Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + */ + endEventId: string; + /** + * Identifier of the user message that starts the covered exchange. + */ + startEventId: string; +} +/** + * Final structured tool completion in the covered event range. + */ +export interface CompletionReceiptFinalTool { + /** + * Process exit code from a structured shell result, when available. + */ + exitCode?: number; + status: CompletionReceiptToolStatus; + /** + * Unique identifier of the completed tool call. + */ + toolCallId: string; + /** + * Tool name from the matching tool execution start event, when available. + */ + toolName?: string; +} /** * Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. */ @@ -3182,6 +3379,12 @@ export interface FusionResolvedData { */ modelUniverseVersion?: string; pattern: FusionPattern; + /** + * Presentation-neutral phase plan for clients that render workflow progress. + * + * @experimental + */ + phasePlan?: FusionPhasePlanStep[]; /** * Version of the validated execution-plan format. */ @@ -3240,6 +3443,22 @@ export interface FusionFollowUpRecommendation { compactionTurn: FusionFollowUpAction; userTurn: FusionFollowUpAction; } +/** + * Presentation-neutral phase planned for a HydraFusion turn. + */ +/** @experimental */ +export interface FusionPhasePlanStep { + /** + * Whether the phase executes only when an earlier phase requests it. + */ + conditional: boolean; + kind: FusionPhaseKind; + /** + * Semantic role assigned to the phase. + */ + role: string; + scope: FusionConversationScope; +} /** * Validated HydraFusion routing capability scores. */ @@ -3420,6 +3639,10 @@ export interface UserMessageData { * True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. */ isAutopilotContinuation?: boolean; + /** + * Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + */ + messageId?: string; /** * Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit */ @@ -4059,6 +4282,67 @@ export interface FusionPhaseStartedData { */ role: string; } +/** + * Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. + */ +/** @experimental */ +export interface AssistantFusionPhaseActivityEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionPhaseActivityData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "assistant.fusion_phase_activity". + */ + type: "assistant.fusion_phase_activity"; +} +/** + * Experimental content-safe activity signal for a running HydraFusion phase. + */ +/** @experimental */ +export interface FusionPhaseActivityData { + activity: FusionPhaseActivityKind; + conversationScope: FusionConversationScope; + /** + * Identifier of the HydraFusion turn containing the phase. + */ + fusionId: string; + pattern: FusionPattern; + /** + * Stable identifier for the concrete phase. + */ + phaseId: string; + phaseKind: FusionPhaseKind; + /** + * Semantic role assigned to the phase. + */ + role: string; + /** + * Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + */ + toolCallId?: string; + /** + * Cumulative private response bytes observed for this model call. The event never includes response text. + */ + totalResponseSizeBytes?: number; +} /** * Session event "assistant.fusion_phase_completed". Experimental durable HydraFusion phase output and lossless replay checkpoint. */ @@ -4799,7 +5083,7 @@ export interface FusionAttribution { /** @experimental */ export interface AssistantMessageReasoningBlocks { /** - * Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + * Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. */ blocks?: JsonValue[]; /** @@ -4843,6 +5127,7 @@ export interface AssistantMessageToolRequest { * Arguments to pass to the tool, format depends on the tool */ arguments?: JsonValue; + caller?: AssistantMessageToolRequestCaller; /** * Resolved intention summary describing what this specific call does */ @@ -4869,6 +5154,16 @@ export interface AssistantMessageToolRequest { toolTitle?: string; type?: AssistantMessageToolRequestType; } +/** + * Hosted program that requested this client tool call + */ +export interface AssistantMessageToolRequestCaller { + /** + * Provider-assigned identifier for the hosted caller. + */ + callerId: string; + type: AssistantMessageToolRequestCallerType; +} /** * Session event "assistant.message_start". Streaming assistant message start metadata */ @@ -6542,6 +6837,10 @@ export interface SkillInvokedData { * Description of the skill from its SKILL.md frontmatter */ description?: string; + /** + * Whether model invocation is disabled for this skill + */ + disableModelInvocation?: boolean; /** * Model identifier active when the skill was invoked, when known */ @@ -6551,7 +6850,7 @@ export interface SkillInvokedData { */ name: string; /** - * File path to the SKILL.md definition + * File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity */ path: string; /** @@ -6563,7 +6862,7 @@ export interface SkillInvokedData { */ pluginVersion?: string; /** - * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) */ source?: string; trigger?: SkillInvokedTrigger; @@ -6768,6 +7067,10 @@ export interface SubagentCompletedData { * Model used by the sub-agent */ model?: string; + /** + * Why an explicit task-call model did not become the effective model + */ + modelOverrideReason?: string; /** * Tool call ID of the parent tool invocation that spawned this sub-agent */ @@ -6855,6 +7158,10 @@ export interface SubagentFailedData { * Model selected for the sub-agent, when known */ model?: string; + /** + * Why an explicit task-call model did not become the effective model + */ + modelOverrideReason?: string; /** * Tool call ID of the parent tool invocation that spawned this sub-agent */ @@ -6992,7 +7299,7 @@ export interface HookStartData { */ hookType: string; /** - * Input data passed to the hook + * Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. */ input?: JsonValue; /** @@ -7519,6 +7826,7 @@ export interface PermissionRequestedEvent { * Permission request notification requiring client approval with request details */ export interface PermissionRequestedData { + agentMode?: SessionMode; permissionRequest: PermissionRequest; promptRequest?: PermissionPromptRequest; /** @@ -10041,7 +10349,7 @@ export interface AutoModeResolvedData { stickyOverride?: boolean; } /** - * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. */ /** @experimental */ export interface ManagedSettingsResolvedEvent { @@ -10072,7 +10380,7 @@ export interface ManagedSettingsResolvedEvent { type: "session.managed_settings_resolved"; } /** - * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. */ /** @experimental */ export interface ManagedSettingsResolvedData { @@ -10100,6 +10408,10 @@ export interface ManagedSettingsResolvedData { * Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. */ permissionsAllowIntersected?: boolean; + /** + * Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + */ + policyHelperManaged?: boolean; /** * Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. */ @@ -10724,7 +11036,7 @@ export interface CustomAgentsUpdatedData { warnings: string[]; } /** - * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. + * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. */ export interface CustomAgentsUpdatedAgent { /** @@ -10743,6 +11055,11 @@ export interface CustomAgentsUpdatedAgent { * Model override for this agent, if set */ model?: string; + modelPolicy?: AgentModelPolicy; + /** + * Authored model ids in priority order, if configured + */ + models?: string[]; /** * Internal name of the agent */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 9d55ab1d10..bf6f2195f9 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -53,6 +53,7 @@ export { // surface for those six identifiers is preserved unchanged. export type * from "./generated/session-events.js"; export type { + AskUserVariant, CommandContext, CommandDefinition, CommandHandler, @@ -68,6 +69,7 @@ export type { UserPromptTransformedHandler, UserPromptTransformedHookInput, UserPromptTransformedHookOutput, + CopilotClientInfo, CopilotClientMode, CopilotClientOptions, CopilotExpAssignmentResponse, @@ -119,6 +121,7 @@ export type { ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, + AutoTier, CapiSessionOptions, ModelCapabilities, ModelCapabilitiesOverride, @@ -209,6 +212,8 @@ export type { FactoryRunResult, FactoryRunStatus, FactoryRunSummary, + FactoryListRunsOptions, + FactoryRunsPage, FactoryRunDetail, FactoryProgressPage, FactoryProgressLine, diff --git a/nodejs/src/runtimeArtifacts.ts b/nodejs/src/runtimeArtifacts.ts new file mode 100644 index 0000000000..19d9926250 --- /dev/null +++ b/nodejs/src/runtimeArtifacts.ts @@ -0,0 +1,183 @@ +import { createHash } from "node:crypto"; +import { + chmodSync, + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readdirSync, + renameSync, + rmSync, + statSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join, relative, sep } from "node:path"; + +export interface RuntimeArtifactSources { + packageRoot: string; + platform: string; +} + +const EXCLUDED_TOP_LEVEL = new Set([ + "app.js", + "assets", + "changelog.json", + "copilot", + "copilot.exe", + "copilot-sdk", + "foundry-local-sdk", + "index.js", + "LICENSE.md", + "napi-oop-runtime", + "npm-loader.js", + "package.json", + "preloads", + "pvrecorder", + "queries", + "README.md", + "sdk", + "sea-loader.js", + "webview", +]); + +interface RuntimeAsset { + source: string; + relativePath: string; +} + +function validateFile(path: string, label: string): void { + if (!existsSync(path)) { + throw new Error(`${label} not found at ${path}.`); + } + if (statSync(path).size === 0) { + throw new Error(`${label} at ${path} is empty.`); + } +} + +function validateRuntimeBundle(wrapper: string, runtimeNode: string): void { + validateFile(wrapper, "Copilot runtime wrapper"); + validateFile(runtimeNode, "Copilot runtime.node"); +} + +function isExcluded(relativePath: string): boolean { + const parts = relativePath.split(sep); + const topLevel = parts[0]; + const fileName = parts.at(-1) ?? ""; + return ( + EXCLUDED_TOP_LEVEL.has(topLevel) || + /^tree-sitter.*\.wasm$/.test(topLevel) || + /^voice-.*\.js$/.test(topLevel) || + fileName === "cli-native.node" || + parts.includes("mediaremote-adapter") || + fileName.startsWith("copilot-runtime-bin") + ); +} + +function collectRuntimeAssets(sources: RuntimeArtifactSources): RuntimeAsset[] { + const assets: RuntimeAsset[] = []; + const visit = (directory: string): void => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const source = join(directory, entry.name); + const sourceRelative = relative(sources.packageRoot, source); + if (isExcluded(sourceRelative)) { + continue; + } + if (entry.isDirectory()) { + visit(source); + continue; + } + if (!entry.isFile() && !entry.isSymbolicLink()) { + continue; + } + + const parts = sourceRelative.split(sep); + let relativePath = sourceRelative; + if (parts[0] === "prebuilds") { + if (parts[1] !== sources.platform || parts.length < 3) { + continue; + } + relativePath = parts.slice(2).join(sep); + } + assets.push({ source, relativePath }); + } + }; + visit(sources.packageRoot); + return assets.sort((left, right) => left.relativePath.localeCompare(right.relativePath)); +} + +function sourceFingerprint(assets: RuntimeAsset[]): string { + const hash = createHash("sha256"); + for (const asset of assets) { + const stat = lstatSync(asset.source); + hash.update(asset.relativePath).update("\0"); + hash.update(`${stat.size}:${stat.mtimeMs}`).update("\0"); + } + return hash.digest("hex").slice(0, 20); +} + +function makeExecutable(path: string): void { + if (process.platform === "win32") { + return; + } + const mode = statSync(path).mode; + if ((mode & 0o111) === 0) { + chmodSync(path, mode | 0o111); + } +} + +export function defaultRuntimeCacheRoot( + platform = process.platform, + home = homedir(), + environment: NodeJS.ProcessEnv = process.env +): string { + const cacheDirectory = + platform === "win32" + ? (environment.LOCALAPPDATA ?? join(home, "AppData", "Local")) + : platform === "darwin" + ? join(home, "Library", "Caches") + : (environment.XDG_CACHE_HOME ?? join(home, ".cache")); + return join(cacheDirectory, "github-copilot-sdk", "runtime"); +} + +export function materializeRuntimeBundle( + sources: RuntimeArtifactSources, + cacheRoot = defaultRuntimeCacheRoot() +): string { + const assets = collectRuntimeAssets(sources); + const wrapperName = process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; + const sourceWrapper = assets.find((asset) => asset.relativePath === wrapperName)?.source; + const sourceRuntimeNode = assets.find((asset) => asset.relativePath === "runtime.node")?.source; + validateRuntimeBundle(sourceWrapper ?? "", sourceRuntimeNode ?? ""); + + const installDir = join(cacheRoot, `${sources.platform}-${sourceFingerprint(assets)}`); + const installedWrapper = join(installDir, wrapperName); + const installedRuntimeNode = join(installDir, "runtime.node"); + if (existsSync(installDir)) { + validateRuntimeBundle(installedWrapper, installedRuntimeNode); + makeExecutable(installedWrapper); + return installedWrapper; + } + + mkdirSync(cacheRoot, { recursive: true }); + const stagingDir = mkdtempSync(join(cacheRoot, ".runtime-")); + try { + for (const asset of assets) { + const destination = join(stagingDir, asset.relativePath); + mkdirSync(dirname(destination), { recursive: true }); + copyFileSync(asset.source, destination); + } + const stagedWrapper = join(stagingDir, wrapperName); + makeExecutable(stagedWrapper); + renameSync(stagingDir, installDir); + } catch (error) { + if (!existsSync(installDir)) { + throw error; + } + validateRuntimeBundle(installedWrapper, installedRuntimeNode); + } finally { + rmSync(stagingDir, { recursive: true, force: true }); + } + + return installedWrapper; +} diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 65ff00921c..eb4c6b7561 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -71,6 +71,7 @@ import { FactoryResumeError, isFactoryRunTerminal, type FactoryResumeErrorCode, + type FactoryListRunsOptions, type FactoryRunResult, type FactoryAgentOptions, type RunOptions, @@ -462,6 +463,8 @@ export class CopilotSession { if (options?.resumeFromRunId !== undefined) { return this.factory.resume(options.resumeFromRunId, { limits: options.limits, + notifyOnComplete: options.notifyOnComplete, + logPhaseNames: options.logPhaseNames, }); } const envelope = await this.rpc.factory.run({ @@ -469,6 +472,8 @@ export class CopilotSession { args: options?.args === undefined ? {} : options.args, options: { limits: options?.limits, + notifyOnComplete: options?.notifyOnComplete, + logPhaseNames: options?.logPhaseNames, }, }); @@ -481,6 +486,8 @@ export class CopilotSession { response = await this.rpc.factory.resume({ runId, limits: options?.limits, + notifyOnComplete: options?.notifyOnComplete, + logPhaseNames: options?.logPhaseNames, }); } catch (error) { if ( @@ -499,7 +506,10 @@ export class CopilotSession { }) as SessionFactoryApi["resume"], getRun: async (runId) => this.rpc.factory.getRun({ runId }), waitForRun: (runId, options) => this.waitForFactoryRun(runId, options?.signal), - listRuns: async () => (await this.rpc.factory.listRuns({})).runs, + listRuns: (async (options?: FactoryListRunsOptions) => { + const page = await this.rpc.factory.listRuns(options ?? {}); + return options === undefined ? page.runs : page; + }) as SessionFactoryApi["listRuns"], getRunDetail: (runId) => this.rpc.factory.getRunDetail({ runId }), getRunProgress: (runId, options = {}) => this.rpc.factory.getRunProgress({ runId, ...options }), diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 616e15a467..128ced3d68 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -11,6 +11,7 @@ import type { Canvas } from "./canvas.js"; import type { SessionFsProvider } from "./sessionFsProvider.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; import type { + AutoTier, PermissionRequest as GeneratedPermissionRequest, PermissionRequestedData as GeneratedPermissionRequestedData, PermissionRequestedEvent as GeneratedPermissionRequestedEvent, @@ -72,7 +73,7 @@ export type { export type SessionEvent = | Exclude | PermissionRequestedEvent; -export type { ReasoningSummary } from "./generated/session-events.js"; +export type { AutoTier, ReasoningSummary } from "./generated/session-events.js"; export type { SessionFsProvider } from "./sessionFsProvider.js"; export { createSessionFsAdapter } from "./sessionFsProvider.js"; export type { SessionFsFileInfo } from "./sessionFsProvider.js"; @@ -306,6 +307,37 @@ export type InternalRuntimeConnection = RuntimeConnection | ParentProcessRuntime */ export type CopilotClientMode = "empty" | "copilot-cli"; +/** + * Identity of the integrating application, declared once on the `server.connect` + * handshake so the telemetry the runtime emits on this connection is attributed + * to a single, consistent surface rather than to the runtime's own build. + * + * All fields are optional; omit any of them (or the whole object) to keep the + * runtime's default attribution. Version fields are ignored by the runtime + * unless they look like a version string. + */ +export interface CopilotClientInfo { + /** + * Name of the application using the SDK, e.g. `"acme-developer-portal"`. + */ + applicationName?: string; + + /** + * Version of the application using the SDK, e.g. `"2.4.0"`. + */ + applicationVersion?: string; + + /** + * Optional name of a specific integration within the application, such as an extension or plugin. + */ + integrationName?: string; + + /** + * Optional version of the integration identified by `integrationName`. + */ + integrationVersion?: string; +} + export interface CopilotClientOptions { /** * How to connect to the Copilot runtime. When omitted, defaults to @@ -477,6 +509,16 @@ export interface CopilotClientOptions { */ enableRemoteSessions?: boolean; + /** + * Identity of the integrating application, forwarded to the runtime on the + * `server.connect` handshake. Declaring it lets the telemetry the runtime + * emits on this connection be attributed to a single, consistent surface + * (e.g. the application and its Copilot integration) instead of the + * runtime's own build. All fields are optional; omit it to keep the default + * attribution. + */ + clientInfo?: CopilotClientInfo; + /** * @internal Hook used by `joinSession()` to construct a client that talks * to its parent process over stdio. Not part of the public API. @@ -1265,7 +1307,7 @@ export const defaultJoinSessionPermissionHandler: PermissionHandler = // ============================================================================ /** - * Request for user input from the agent (enables ask_user tool) + * Legacy question-and-answer request from the `ask_user` tool. */ export interface UserInputRequest { /** @@ -2129,6 +2171,17 @@ export interface FactoryMeta { * provider-level choices are conceptually per-provider rather than global. */ export interface CapiSessionOptions { + /** + * Routing preference used when the session model is `auto`. + * Requires a runtime with Auto tier support and V2 Auto routing. + * + * When omitted on create, the runtime uses its default routing behavior. + * The runtime persists this preference across cold resume; an explicit tier + * on cold resume overrides the persisted value. For an already-resident + * session, omission preserves the current tier and a different tier is rejected. + */ + autoTier?: AutoTier; + /** * Whether to use the WebSocket transport for the CAPI Responses API. * @@ -2245,6 +2298,9 @@ export interface ManagedSettings { permissions?: ManagedSettingsPermissions; } +/** Selects the model-facing shape of the built-in `ask_user` tool. */ +export type AskUserVariant = "legacy" | "elicitation"; + /** * Shared configuration fields used by both {@link SessionConfig} (for * creating a new session) and {@link ResumeSessionConfig} (for resuming @@ -2556,10 +2612,20 @@ export interface SessionConfigBase { /** * Handler for user input requests from the agent. - * When provided, enables the ask_user tool allowing the agent to ask questions. + * When provided with the default `legacy` {@link AskUserVariant}, enables the + * question-and-answer form of the `ask_user` tool. */ onUserInputRequest?: UserInputHandler; + /** + * Selects the model-facing shape of the built-in `ask_user` tool. + * + * The default is `"legacy"`. To use `"elicitation"`, also provide + * {@link onElicitationRequest} so the host can answer structured forms. + * The runtime resolves this option when it creates or cold-resumes the session. + */ + askUserVariant?: AskUserVariant; + /** * Handler for elicitation requests from the agent. * When provided, the server calls back to this client for form-based UI dialogs. @@ -2873,6 +2939,12 @@ export interface SessionConfigBase { */ createSessionFsProvider?: (session: CopilotSession) => SessionFsProvider; + /** + * Feature-flag values resolved by the host for this session. + * Re-supply them when resuming after a runtime restart. + */ + featureFlags?: Record; + /** * ExP assignment ("flight") data injected by a trusted integrator, in the * same JSON shape the Copilot CLI fetches from the experimentation service diff --git a/nodejs/test/client-api-codegen.test.ts b/nodejs/test/client-api-codegen.test.ts new file mode 100644 index 0000000000..9331ad7689 --- /dev/null +++ b/nodejs/test/client-api-codegen.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from "vitest"; + +import { emitClientSessionApiRegistration as emitGoClientSessionApiRegistration } from "../../scripts/codegen/go.ts"; +import { emitClientSessionApiRegistration as emitPythonClientSessionApiRegistration } from "../../scripts/codegen/python.ts"; +import { emitClientSessionApiRegistration as emitTypeScriptClientSessionApiRegistration } from "../../scripts/codegen/typescript.ts"; + +const clientSessionSchema: Record = { + mixed: { + visible: { + rpcMethod: "mixed.visible", + params: { + type: "object", + title: "VisibleRequest", + properties: { + sessionId: { type: "string" }, + }, + required: ["sessionId"], + }, + result: { + type: "object", + title: "VisibleResult", + properties: {}, + }, + }, + secret: { + rpcMethod: "mixed.secret", + visibility: "internal", + params: { + $ref: "#/definitions/InternalRequest", + }, + result: { + $ref: "#/definitions/InternalResult", + }, + }, + }, + internalOnly: { + hidden: { + rpcMethod: "internalOnly.hidden", + visibility: "internal", + params: { + $ref: "#/definitions/InternalRequest", + }, + result: { + $ref: "#/definitions/InternalResult", + }, + }, + }, +}; + +const allInternalClientSessionSchema: Record = { + internalOnly: clientSessionSchema.internalOnly, +}; + +function expectOnlyPublicClientSessionHandlers(code: string): void { + expect(code).toContain("mixed.visible"); + expect(code).not.toContain("mixed.secret"); + expect(code).not.toContain("internalOnly.hidden"); + expect(code).not.toContain("InternalRequest"); + expect(code).not.toContain("InternalResult"); +} + +describe("client-session API codegen", () => { + it("excludes internal methods from TypeScript handlers", () => { + const code = emitTypeScriptClientSessionApiRegistration(clientSessionSchema).join("\n"); + const allInternalCode = emitTypeScriptClientSessionApiRegistration( + allInternalClientSessionSchema + ).join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("export interface ClientSessionApiHandlers {"); + expect(allInternalCode).toContain("export function registerClientSessionApiHandlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + }); + + it("excludes internal methods from Go handlers", () => { + const lines: string[] = []; + emitGoClientSessionApiRegistration(lines, clientSessionSchema, (name) => name, new Map()); + const code = lines.join("\n"); + const allInternalLines: string[] = []; + emitGoClientSessionApiRegistration( + allInternalLines, + allInternalClientSessionSchema, + (name) => name, + new Map() + ); + const allInternalCode = allInternalLines.join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("type ClientSessionAPIHandlers struct {"); + expect(allInternalCode).toContain("func RegisterClientSessionAPIHandlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).not.toContain("clientSessionHandlerError"); + }); + + it("excludes internal methods from Python handlers", () => { + const lines: string[] = []; + emitPythonClientSessionApiRegistration(lines, clientSessionSchema, (name) => name); + const code = lines.join("\n"); + const allInternalLines: string[] = []; + emitPythonClientSessionApiRegistration( + allInternalLines, + allInternalClientSessionSchema, + (name) => name + ); + const allInternalCode = allInternalLines.join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("class ClientSessionApiHandlers:"); + expect(allInternalCode).toContain("def register_client_session_api_handlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + }); +}); diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 3ffda2fa71..bd1cf9812f 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -12,6 +12,8 @@ import { createCanvas, DisableBypassPermissionsModes, RuntimeConnection, + type CapiSessionOptions, + type CopilotClientOptions, type GitHubTelemetryNotification, type ManagedSettings, type ModelInfo, @@ -60,6 +62,28 @@ describe("approveAll", () => { }); describe("CopilotClient", () => { + it.each([ + { + source: "connection path", + connection: RuntimeConnection.forStdio({ path: "/explicit/copilot" }), + env: {}, + expected: "/explicit/copilot", + }, + { + source: "COPILOT_CLI_PATH", + connection: RuntimeConnection.forStdio(), + env: { COPILOT_CLI_PATH: "/environment/copilot" }, + expected: "/environment/copilot", + }, + ])( + "preserves explicit child-process override from $source", + ({ connection, env, expected }) => { + const client = new CopilotClient({ connection, env }); + + expect((client as any).resolvedCliPath).toBe(expected); + } + ); + async function startWithMockConnection( builtinPluginDirectories?: readonly string[] ): Promise> { @@ -303,6 +327,39 @@ describe("CopilotClient", () => { }); }); + it("forwards the ask-user variant on create and cold resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + const onElicitationRequest = async () => ({ action: "decline" as const }); + + const session = await client.createSession({ + askUserVariant: "elicitation", + onElicitationRequest, + }); + await client.resumeSession(session.sessionId, { + askUserVariant: "elicitation", + onElicitationRequest, + }); + + expect(spy.mock.calls.find(([method]) => method === "session.create")![1]).toMatchObject({ + askUserVariant: "elicitation", + requestElicitation: true, + }); + expect(spy.mock.calls.find(([method]) => method === "session.resume")![1]).toMatchObject({ + askUserVariant: "elicitation", + requestElicitation: true, + }); + }); + it("omits GitHub MCP tool config when unset", async () => { const client = new CopilotClient(); await client.start(); @@ -1276,7 +1333,7 @@ describe("CopilotClient", () => { expect(resumePayload.expAssignments).toBeUndefined(); }); - it("forwards capi options in session.create and session.resume", async () => { + it("forwards featureFlags in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => stopClient(client)); @@ -1288,14 +1345,15 @@ describe("CopilotClient", () => { if (method === "session.resume") return { sessionId: params.sessionId }; throw new Error(`Unexpected method: ${method}`); }); + const featureFlags = { ENABLED_TEST_FLAG: true, DISABLED_TEST_FLAG: false }; const session = await client.createSession({ onPermissionRequest: approveAll, - capi: { enableWebSocketResponses: false }, + featureFlags, }); await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll, - capi: { enableWebSocketResponses: false }, + featureFlags, }); const createPayload = spy.mock.calls.find( @@ -1304,10 +1362,55 @@ describe("CopilotClient", () => { const resumePayload = spy.mock.calls.find( ([method]) => method === "session.resume" )![1] as any; - expect(createPayload.capi).toEqual({ enableWebSocketResponses: false }); - expect(resumePayload.capi).toEqual({ enableWebSocketResponses: false }); + expect(createPayload.featureFlags).toEqual(featureFlags); + expect(resumePayload.featureFlags).toEqual(featureFlags); }); + it.each([ + undefined, + {}, + { enableWebSocketResponses: false }, + { enableWebSocketResponses: true }, + { autoTier: "efficiency" }, + { autoTier: "balance" }, + { autoTier: "intelligence" }, + { autoTier: "balance", enableWebSocketResponses: false }, + ] satisfies (CapiSessionOptions | undefined)[])( + "forwards capi options %j in session.create and session.resume", + async (capi) => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "auto", + capi, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + capi, + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(JSON.parse(JSON.stringify(createPayload)).capi).toEqual(capi); + expect(JSON.parse(JSON.stringify(resumePayload)).capi).toEqual(capi); + } + ); + it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); @@ -4142,3 +4245,81 @@ describe("managedSettings serialization", () => { }); }); }); + +describe("connect handshake clientInfo", () => { + // Drives verifyProtocolVersion() against a stubbed connection so we can + // observe the `connect` params without spawning a runtime. `connect` maps to + // connection.sendRequest("connect", params) in the generated internal RPC. + async function captureConnectParams( + options: Partial> = {} + ): Promise> { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("localhost:1234"), + ...options, + }); + const sendRequest = vi.fn(async (method: string, _params?: unknown) => { + if (method === "connect") return { protocolVersion: 3 }; + throw new Error(`Unexpected method: ${method}`); + }); + (client as any).connection = { sendRequest }; + + await (client as any).verifyProtocolVersion(); + + const connectCall = sendRequest.mock.calls.find(([method]) => method === "connect"); + expect(connectCall, "connect was not called").toBeTruthy(); + return connectCall![1] as Record; + } + + it("forwards a declared client identity on the connect handshake", async () => { + const clientInfo = { + applicationName: "acme-developer-portal", + applicationVersion: "2.4.0", + integrationName: "copilot-assistant", + integrationVersion: "1.5.0", + }; + + const params = await captureConnectParams({ clientInfo }); + + expect(params.clientInfo).toEqual({ + editorName: "acme-developer-portal", + editorVersion: "2.4.0", + extensionName: "copilot-assistant", + extensionVersion: "1.5.0", + }); + }); + + it("omits clientInfo from the handshake when the host declares none", async () => { + const params = await captureConnectParams(); + + expect(params).not.toHaveProperty("clientInfo"); + }); + + it("drops empty fields and omits an all-empty identity", async () => { + const allEmpty = await captureConnectParams({ + clientInfo: { + applicationName: "", + applicationVersion: "", + integrationName: "", + integrationVersion: "", + }, + }); + expect(allEmpty).not.toHaveProperty("clientInfo"); + + const partial = await captureConnectParams({ + clientInfo: { applicationName: "example-app", applicationVersion: "" }, + }); + expect(partial.clientInfo).toEqual({ editorName: "example-app" }); + }); + + it("keeps telemetry forwarding alongside a declared identity", async () => { + const params = await captureConnectParams({ + clientInfo: { applicationName: "example-app" }, + onGitHubTelemetry: () => {}, + }); + + expect(params).toMatchObject({ + clientInfo: { editorName: "example-app" }, + enableGitHubTelemetryForwarding: true, + }); + }); +}); diff --git a/nodejs/test/e2e/builtin_tools.e2e.test.ts b/nodejs/test/e2e/builtin_tools.e2e.test.ts index 36b70ea195..39900bc7d6 100644 --- a/nodejs/test/e2e/builtin_tools.e2e.test.ts +++ b/nodejs/test/e2e/builtin_tools.e2e.test.ts @@ -130,6 +130,19 @@ describe("Built-in Tools", async () => { async () => { await writeFile(join(workDir, "data.txt"), "apple\nbanana\napricot\ncherry\n"); const session = await client.createSession({ onPermissionRequest: approveAll }); + let grepToolCallId: string | undefined; + let grepCompletedSuccessfully = false; + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "grep") { + grepToolCallId = event.data.toolCallId; + } else if ( + event.type === "tool.execution_complete" && + event.data.toolCallId === grepToolCallId && + event.data.success + ) { + grepCompletedSuccessfully = true; + } + }); const msg = await session.sendAndWait( { prompt: "Search for lines starting with 'ap' in the file 'data.txt'. Tell me which lines matched.", @@ -138,6 +151,7 @@ describe("Built-in Tools", async () => { ); expect(msg?.data.content).toContain("apple"); expect(msg?.data.content).toContain("apricot"); + expect(grepCompletedSuccessfully).toBe(true); }, TEST_TIMEOUT_MS ); diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index 35e7440766..bc3421bfa1 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -184,21 +184,15 @@ describe("Client", () => { await client.stop(); }); - it("should report error with stderr when CLI fails to start", async () => { + it.skipIf(isInProcessTransport)("should report error when CLI fails to start", async () => { const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ args: ["--nonexistent-flag-for-testing"] }), + connection: RuntimeConnection.forStdio({ + args: ["--nonexistent-flag-for-testing"], + }), }); onTestFinishedStop(client); - let initialError: Error | undefined; - try { - await client.start(); - expect.fail("Expected start() to throw an error"); - } catch (error) { - initialError = error as Error; - expect(initialError.message).toContain("stderr"); - expect(initialError.message).toContain("nonexistent"); - } + await expect(client.start()).rejects.toBeInstanceOf(Error); // Verify subsequent calls also fail (don't hang) try { @@ -206,7 +200,7 @@ describe("Client", () => { await session.send("test"); expect.fail("Expected send() to throw an error after CLI exit"); } catch (error) { - expect((error as Error).message).toContain("Connection is closed"); + expect(error).toBeInstanceOf(Error); } }); }); diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index e3dc41343b..2f1dee6cb5 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -467,7 +467,7 @@ describe("Client options", async () => { }); const session = await client.createSession({ clientName: "advanced-create-client", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", reasoningEffort: "medium", reasoningSummary: "detailed", contextTier: "long_context", @@ -532,7 +532,7 @@ describe("Client options", async () => { provider: "create-provider", id: "create-model", name: "Create Model", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "create-wire-model", maxContextWindowTokens: 12_000, maxPromptTokens: 10_000, @@ -544,7 +544,7 @@ describe("Client options", async () => { const createRequest = getCapturedRequest(capturePath, "session.create"); expect(createRequest.clientName).toBe("advanced-create-client"); - expect(createRequest.model).toBe("claude-sonnet-4.5"); + expect(createRequest.model).toBe("claude-sonnet-5"); expect(createRequest.reasoningEffort).toBe("medium"); expect(createRequest.reasoningSummary).toBe("detailed"); expect(createRequest.contextTier).toBe("long_context"); @@ -609,7 +609,7 @@ describe("Client options", async () => { await client.start(); const session = await client.createSession({ - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "azure", wireApi: "responses", @@ -619,7 +619,7 @@ describe("Client options", async () => { bearerToken: "provider-bearer-token", azure: { apiVersion: "2024-02-15-preview" }, headers: { "X-Provider-Wire": "yes" }, - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "azure-deployment", maxPromptTokens: 8192, maxOutputTokens: 1024, @@ -636,7 +636,7 @@ describe("Client options", async () => { expect(provider.bearerToken).toBe("provider-bearer-token"); expect(getObject(provider.azure).apiVersion).toBe("2024-02-15-preview"); expect(getObject(provider.headers)["X-Provider-Wire"]).toBe("yes"); - expect(provider.modelId).toBe("claude-sonnet-4.5"); + expect(provider.modelId).toBe("claude-sonnet-5"); expect(provider.wireModel).toBe("azure-deployment"); expect(provider.maxPromptTokens).toBe(8192); expect(provider.maxOutputTokens).toBe(1024); diff --git a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts index 69bacd4f6e..b0af6524a1 100644 --- a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts @@ -56,8 +56,8 @@ function serveNonInference(url: string): Response { const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -65,7 +65,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { diff --git a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts index 309250d852..04bccb7e08 100644 --- a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts @@ -42,8 +42,8 @@ async function startFakeUpstream(): Promise<{ sendJson(res, 200, { data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -52,7 +52,7 @@ async function startFakeUpstream(): Promise<{ supported_endpoints: ["/responses", "ws:/responses"], capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, diff --git a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts index bd070c20ca..9b0dbbb3bc 100644 --- a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts @@ -171,7 +171,7 @@ const CHAT_COMPLETION_STREAM_EVENTS: string[] = (() => { id: "chatcmpl-stub-1", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }; return [ `data: ${JSON.stringify({ @@ -210,7 +210,7 @@ const BUFFERED_CHAT_COMPLETION_JSON = JSON.stringify({ id: "chatcmpl-stub-1", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -224,8 +224,8 @@ const BUFFERED_CHAT_COMPLETION_JSON = JSON.stringify({ const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -233,7 +233,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { @@ -300,14 +300,14 @@ describe("CopilotRequestHandler threads the runtime session id (CAPI + BYOK)", a const session = await client.createSession({ onPermissionRequest: approveAll, // BYOK providers require an explicit model id. - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "openai", wireApi: "responses", baseUrl: "https://byok.invalid/v1", apiKey: "byok-secret", - modelId: "claude-sonnet-4.5", - wireModel: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", + wireModel: "claude-sonnet-5", }, }); const byokSessionId = session.sessionId; diff --git a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts index 611898f11d..8478f93288 100644 --- a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts +++ b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts @@ -154,7 +154,7 @@ const CHAT_COMPLETION_STREAM = [ id: "persisted-session", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -167,7 +167,7 @@ const CHAT_COMPLETION_STREAM = [ id: "persisted-session", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [{ index: 0, delta: {}, finish_reason: "stop" }], }, ] @@ -179,7 +179,7 @@ const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({ id: "persisted-session", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -193,8 +193,8 @@ const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({ const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -202,7 +202,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { streaming: true, tool_calls: true, parallel_tool_calls: true }, diff --git a/nodejs/test/e2e/extension_env_access.e2e.test.ts b/nodejs/test/e2e/extension_env_access.e2e.test.ts index f034db9051..38210a22fb 100644 --- a/nodejs/test/e2e/extension_env_access.e2e.test.ts +++ b/nodejs/test/e2e/extension_env_access.e2e.test.ts @@ -14,9 +14,9 @@ import { StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node.js"; -import { approveAll } from "../../src/index.js"; +import { approveAll, RuntimeConnection } from "../../src/index.js"; import { getSdkProtocolVersion } from "../../src/sdkProtocolVersion.js"; -import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, getLegacyCliPathForTests } from "./harness/sdkTestContext.js"; import { retry } from "./harness/sdkTestHelper.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -184,56 +184,47 @@ it("ignores a granted variable the extension never requested", async () => { expect(run.postjoin).toBe("E2E_SDK_TOKEN=granted-token\nE2E_SDK_SMUGGLED="); }); -const cliObservations = isInProcessTransport - ? "" - : mkdtempSync(join(tmpdir(), "copilot-env-access-cli-")); +const cliObservations = mkdtempSync(join(tmpdir(), "copilot-env-access-cli-")); const cliResultFile = join(cliObservations, "result"); -const cliContext = isInProcessTransport - ? undefined - : await createSdkTestContext({ - copilotClientOptions: { - env: { - COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", - EXTENSION_ENV_REQUEST: "E2E_SDK_TOKEN", - EXTENSION_RESULT_FILE: cliResultFile, - EXTENSION_PREJOIN_FILE: join(cliObservations, "prejoin"), - EXTENSION_POSTJOIN_FILE: join(cliObservations, "postjoin"), - }, - }, - }); +const cliContext = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + env: { + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", + EXTENSION_ENV_REQUEST: "E2E_SDK_TOKEN", + EXTENSION_RESULT_FILE: cliResultFile, + EXTENSION_PREJOIN_FILE: join(cliObservations, "prejoin"), + EXTENSION_POSTJOIN_FILE: join(cliObservations, "postjoin"), + }, + }, +}); // The released CLI ignores `requestedEnvironmentVariables`, so this covers the // half a real CLI can prove today: asking for variables does not break the join. // It becomes the grant test once `@github/copilot` carries the host half. -it.skipIf(isInProcessTransport)( - "joins a real CLI that does not support environment requests", - async () => { - if (!cliContext) { - throw new Error("Extension E2E requires an out-of-process transport"); - } - const { workDir, copilotClient } = cliContext; - const extensionDir = join(workDir, ".github", "extensions", "env-access"); - await rm(join(workDir, ".github"), { recursive: true, force: true }); - await rm(cliResultFile, { force: true }); - await mkdir(extensionDir, { recursive: true }); - await copyFile(FIXTURE, join(extensionDir, "extension.mjs")); - execFileSync("git", ["init", "--quiet"], { cwd: workDir }); - - await using _session = await copilotClient.createSession({ - requestExtensions: true, - extensionSdkPath: DIST_DIR, - onPermissionRequest: approveAll, - }); +it("joins a real CLI that does not support environment requests", async () => { + const { workDir, copilotClient } = cliContext; + const extensionDir = join(workDir, ".github", "extensions", "env-access"); + await rm(join(workDir, ".github"), { recursive: true, force: true }); + await rm(cliResultFile, { force: true }); + await mkdir(extensionDir, { recursive: true }); + await copyFile(FIXTURE, join(extensionDir, "extension.mjs")); + execFileSync("git", ["init", "--quiet"], { cwd: workDir }); + + await using _session = await copilotClient.createSession({ + requestExtensions: true, + extensionSdkPath: DIST_DIR, + onPermissionRequest: approveAll, + }); - await retry( - "wait for the env-access extension to join the session", - async () => { - expect(existsSync(cliResultFile)).toBe(true); - }, - 300, - 100 - ); + await retry( + "wait for the env-access extension to join the session", + async () => { + expect(existsSync(cliResultFile)).toBe(true); + }, + 300, + 100 + ); - expect(readFileSync(cliResultFile, "utf-8")).toBe("joined"); - } -); + expect(readFileSync(cliResultFile, "utf-8")).toBe("joined"); +}); diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/factory.e2e.test.ts index cddd8e47b0..2bf3ff17fb 100644 --- a/nodejs/test/e2e/factory.e2e.test.ts +++ b/nodejs/test/e2e/factory.e2e.test.ts @@ -4,30 +4,25 @@ import { copyFile, mkdir, rm } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { expect, it, vi } from "vitest"; -import { approveAll, FactoryResumeError } from "../../src/index.js"; +import { approveAll, FactoryResumeError, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN, - isInProcessTransport, + getLegacyCliPathForTests, } from "./harness/sdkTestContext.js"; import { retry } from "./harness/sdkTestHelper.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const factoryTestContext = isInProcessTransport - ? undefined - : await createSdkTestContext({ - copilotClientOptions: { - env: { - COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES", - }, - }, - }); +const factoryTestContext = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + env: { + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES", + }, + }, +}); async function setupFactoryExtension(workDir: string, onPermissionRequest = approveAll) { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { copilotClient, openAiEndpoint } = factoryTestContext; const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); const readyFile = join(extensionDir, "ready"); @@ -73,25 +68,20 @@ async function setupFactoryExtension(workDir: string, onPermissionRequest = appr return session; } -it.skipIf(isInProcessTransport)( - "runs an extension-authored factory across the SDK process boundary", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("argument-echo", { - args: { source: "sdk-e2e", count: 11 }, - }); - - expect(result).toMatchObject({ - status: "completed", - result: { source: "sdk-e2e", count: 11 }, - }); - } -); +it("runs an extension-authored factory across the SDK process boundary", async () => { + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const result = await session.factory.run("argument-echo", { + args: { source: "sdk-e2e", count: 11 }, + notifyOnComplete: false, + }); + + expect(result).toMatchObject({ + status: "completed", + result: { source: "sdk-e2e", count: 11 }, + }); +}); // TODO(cli-1.0.81-2): the subagent request is rejected downstream under CLI 1.0.81-2, so the // fixture reports didThrow: true. Re-enable once the runtime fix ships. @@ -113,199 +103,271 @@ it.skip("forwards every declared subagent option to the runtime", async () => { }); }, 60_000); -it.skipIf(isInProcessTransport)( - "throws FactoryResumeError with not_found for an unknown run", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const error = await session.factory - .resume("00000000-0000-0000-0000-000000000000") - .catch((caught: unknown) => caught); - - expect(error).toBeInstanceOf(FactoryResumeError); - expect((error as FactoryResumeError).code).toBe("not_found"); +it("throws FactoryResumeError with not_found for an unknown run", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "throws FactoryResumeError with non_resumable for a completed run", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const run = await session.factory.run("argument-echo"); - const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught); - - expect(error).toBeInstanceOf(FactoryResumeError); - expect((error as FactoryResumeError).code).toBe("non_resumable"); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const error = await session.factory + .resume("00000000-0000-0000-0000-000000000000") + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(FactoryResumeError); + expect((error as FactoryResumeError).code).toBe("not_found"); +}); + +it("throws FactoryResumeError with non_resumable for a completed run", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "runs a factory when its session denies every permission request", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); - await using session = await setupFactoryExtension(workDir, denyPermissions); - - await expect(session.factory.run("argument-echo")).resolves.toMatchObject({ - status: "completed", - }); - expect(denyPermissions).not.toHaveBeenCalled(); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const run = await session.factory.run("argument-echo", { notifyOnComplete: false }); + const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(FactoryResumeError); + expect((error as FactoryResumeError).code).toBe("non_resumable"); +}); + +it("forwards factory runtime controls across the SDK process boundary", async () => { + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const suppressed = await session.factory.run("phased", { + notifyOnComplete: false, + logPhaseNames: false, + }); + + expect(suppressed).toMatchObject({ + status: "completed", + result: "finished", + }); + const progress = await session.factory.getRunProgress(suppressed.runId); + expect(progress.records).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "phase", text: "Collect" }), + expect.objectContaining({ kind: "log", text: "Collected" }), + expect.objectContaining({ kind: "phase", text: "Summarize" }), + expect.objectContaining({ kind: "log", text: "Summarized" }), + ]) + ); + + const events = await session.getEvents(); + expect( + events.some( + (event) => + event.type === "system.notification" && + event.data.kind.type === "factory_completed" && + event.data.kind.runId === suppressed.runId + ) + ).toBe(false); + expect( + events.filter( + (event) => event.type === "session.info" && event.data.infoType === "factory_phase" + ) + ).toEqual([]); +}); + +it("pages factory runs and returns cursor metadata", async () => { + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const first = await session.factory.run("argument-echo", { + args: { ordinal: 1 }, + notifyOnComplete: false, + }); + const second = await session.factory.run("argument-echo", { + args: { ordinal: 2 }, + notifyOnComplete: false, + }); + const third = await session.factory.run("argument-echo", { + args: { ordinal: 3 }, + notifyOnComplete: false, + }); + + const newest = await session.factory.listRuns({ limit: 1 }); + expect(newest).toMatchObject({ + runs: [expect.objectContaining({ runId: third.runId })], + hasMoreNewer: false, + omittedOlder: 2, + }); + expect(newest.oldestSeq).toBe(newest.newestSeq); + expect(newest.oldestSeq).not.toBeNull(); + + const older = await session.factory.listRuns({ + beforeSeq: newest.oldestSeq!, + limit: 1, + }); + expect(older).toMatchObject({ + runs: [expect.objectContaining({ runId: second.runId })], + hasMoreNewer: true, + omittedOlder: 1, + }); + + const oldest = await session.factory.listRuns({ + beforeSeq: older.oldestSeq!, + limit: 1, + }); + expect(oldest).toMatchObject({ + runs: [expect.objectContaining({ runId: first.runId })], + hasMoreNewer: true, + omittedOlder: 0, + }); +}); + +it("runs a factory when its session denies every permission request", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "resumes a failed factory when its session denies every permission request", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); - await using session = await setupFactoryExtension(workDir, denyPermissions); - - const failedRun = await session.factory.run("fails-once"); - expect(failedRun).toMatchObject({ - status: "error", - }); - - await expect(session.factory.resume(failedRun.runId)).resolves.toMatchObject({ - status: "completed", - result: "resumed", - }); - expect(denyPermissions).not.toHaveBeenCalled(); + const { workDir } = factoryTestContext; + const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); + await using session = await setupFactoryExtension(workDir, denyPermissions); + + await expect( + session.factory.run("argument-echo", { notifyOnComplete: false }) + ).resolves.toMatchObject({ + status: "completed", + }); + expect(denyPermissions).not.toHaveBeenCalled(); +}); + +it("resumes a failed factory when its session denies every permission request", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "refuses a factory started through the context session from a factory body", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("starts-from-context-session"); - - expect(result).toMatchObject({ - status: "completed", - result: expect.stringContaining("factory.run and factory.resume"), - }); - expect((result as { result: string }).result).toContain("factory body"); + const { workDir } = factoryTestContext; + const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); + await using session = await setupFactoryExtension(workDir, denyPermissions); + + const failedRun = await session.factory.run("fails-once", { notifyOnComplete: false }); + expect(failedRun).toMatchObject({ + status: "error", + }); + + await expect( + session.factory.resume(failedRun.runId, { + notifyOnComplete: false, + logPhaseNames: false, + }) + ).resolves.toMatchObject({ + status: "completed", + result: "resumed", + }); + expect(denyPermissions).not.toHaveBeenCalled(); +}); + +it("refuses a factory started through the context session from a factory body", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "refuses a factory started through the module session from a factory body", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("starts-from-module-session"); - - expect(result).toMatchObject({ - status: "completed", - result: expect.stringContaining("factory.run and factory.resume"), - }); - expect((result as { result: string }).result).toContain("factory body"); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const result = await session.factory.run("starts-from-context-session", { + notifyOnComplete: false, + }); + + expect(result).toMatchObject({ + status: "completed", + result: expect.stringContaining("factory.run and factory.resume"), + }); + expect((result as { result: string }).result).toContain("factory body"); +}); + +it("refuses a factory started through the module session from a factory body", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "allows a module-level extension watcher to start a factory while another body is parked", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); - await using session = await setupFactoryExtension(workDir); - - const parked = session.factory.run("parked"); - await retry( - "wait for the parked factory to enter its body", - async () => { - expect(existsSync(join(extensionDir, "entered"))).toBe(true); - }, - 100, - 100 - ); - - writeFileSync(join(extensionDir, "start-b"), "start"); - const bResultFile = join(extensionDir, "b-result"); - await retry( - "wait for the module-level watcher factory run to succeed", - async () => { - expect(existsSync(bResultFile)).toBe(true); - expect(JSON.parse(readFileSync(bResultFile, "utf8"))).toMatchObject({ - status: "success", - result: { - status: "completed", - result: { source: "module-watcher" }, - }, - }); - }, - 100, - 100 - ); - - writeFileSync(join(extensionDir, "release"), "release"); - await expect(parked).resolves.toMatchObject({ - status: "completed", - result: "released", - }); - }, - 60_000 -); - -it.skipIf(isInProcessTransport)( - "returns an array result from an extension-authored factory", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const result = await session.factory.run("array-result"); - - expect(result).toMatchObject({ - status: "completed", - result: [1, "two", false], - }); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const result = await session.factory.run("starts-from-module-session", { + notifyOnComplete: false, + }); + + expect(result).toMatchObject({ + status: "completed", + result: expect.stringContaining("factory.run and factory.resume"), + }); + expect((result as { result: string }).result).toContain("factory body"); +}); + +it("allows a module-level extension watcher to start a factory while another body is parked", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); - -it.skipIf(isInProcessTransport)( - "passes array factory arguments across the SDK process boundary", - async () => { - if (!factoryTestContext) { - throw new Error("Factory E2E requires the stdio transport"); - } - const { workDir } = factoryTestContext; - await using session = await setupFactoryExtension(workDir); - - const args = [1, "two", false]; - const result = await session.factory.run("argument-echo", { args }); - - expect(result).toMatchObject({ - status: "completed", - result: args, - }); + const { workDir } = factoryTestContext; + const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); + await using session = await setupFactoryExtension(workDir); + + const parked = session.factory.run("parked", { notifyOnComplete: false }); + await retry( + "wait for the parked factory to enter its body", + async () => { + expect(existsSync(join(extensionDir, "entered"))).toBe(true); + }, + 100, + 100 + ); + + writeFileSync(join(extensionDir, "start-b"), "start"); + const bResultFile = join(extensionDir, "b-result"); + await retry( + "wait for the module-level watcher factory run to succeed", + async () => { + expect(existsSync(bResultFile)).toBe(true); + expect(JSON.parse(readFileSync(bResultFile, "utf8"))).toMatchObject({ + status: "success", + result: { + status: "completed", + result: { source: "module-watcher" }, + }, + }); + }, + 100, + 100 + ); + + writeFileSync(join(extensionDir, "release"), "release"); + await expect(parked).resolves.toMatchObject({ + status: "completed", + result: "released", + }); +}, 60_000); + +it("returns an array result from an extension-authored factory", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); } -); + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const result = await session.factory.run("array-result", { notifyOnComplete: false }); + + expect(result).toMatchObject({ + status: "completed", + result: [1, "two", false], + }); +}); + +it("passes array factory arguments across the SDK process boundary", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); + } + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const args = [1, "two", false]; + const result = await session.factory.run("argument-echo", { + args, + notifyOnComplete: false, + }); + + expect(result).toMatchObject({ + status: "completed", + result: args, + }); +}); diff --git a/nodejs/test/e2e/fixtures/factory-extension.mjs b/nodejs/test/e2e/fixtures/factory-extension.mjs index 45227a1bea..fb344b4863 100644 --- a/nodejs/test/e2e/fixtures/factory-extension.mjs +++ b/nodejs/test/e2e/fixtures/factory-extension.mjs @@ -40,6 +40,21 @@ const arrayResult = defineFactory({ run: async () => [1, "two", false], }); +const phased = defineFactory({ + meta: { + name: "phased", + description: "Record named phases and ordinary progress.", + phases: [{ title: "Collect" }, { title: "Summarize" }], + }, + run: async ({ phase, log }) => { + phase("Collect"); + log("Collected"); + phase("Summarize"); + log("Summarized"); + return "finished"; + }, +}); + const forwardsSubagentOptions = defineFactory({ meta: { name: "forwards-subagent-options", @@ -141,6 +156,7 @@ session = await joinSession({ factories: [ argumentEcho, arrayResult, + phased, forwardsSubagentOptions, startsFromContextSession, startsFromModuleSession, @@ -153,6 +169,7 @@ void waitForMarker("start-b", 30_000) .then(async () => { const result = await session.factory.run("argument-echo", { args: { source: "module-watcher" }, + notifyOnComplete: false, }); writeFileSync(marker("b-result"), JSON.stringify({ status: "success", result })); }) diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index bf62db4826..58c275c800 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -50,6 +50,29 @@ function getCliPathForTests(): string | undefined { return undefined; } +function getCliPlatformPackageNames(): string[] { + const variants = + process.platform === "linux" + ? process.report?.getReport().header.glibcVersionRuntime + ? ["linux", "linuxmusl"] + : ["linuxmusl", "linux"] + : [process.platform]; + return variants.map((variant) => `@github/copilot-${variant}-${process.arch}`); +} + +/** Resolves the legacy SEA only for tests that explicitly exercise Node-hosted features. */ +export function getLegacyCliPathForTests(): string { + const cliName = process.platform === "win32" ? "copilot.exe" : "copilot"; + const githubModules = resolve(__dirname, "../../../node_modules/@github"); + for (const packageName of getCliPlatformPackageNames()) { + const cliPath = join(githubModules, packageName.slice("@github/".length), cliName); + if (fs.existsSync(cliPath)) { + return cliPath; + } + } + throw new Error("Legacy Copilot CLI binary not found in the installed platform package."); +} + export async function createSdkTestContext({ logLevel, useStdio, @@ -286,8 +309,13 @@ export async function createSdkTestContext({ process.chdir(restoreCwd); restoreCwd = undefined; } - // Empty directories but leave them in place for next test - await rimraf([join(homeDir, "*"), join(workDir, "*")], { glob: true }); + // The in-process runtime retains open state files until afterAll shuts it down. + // Keep its isolated home intact while it is alive; removing open files on POSIX + // can leave later tests using unlinked database state. + const cleanupPaths = isInProcess + ? [join(workDir, "*")] + : [join(homeDir, "*"), join(workDir, "*")]; + await rimraf(cleanupPaths, { glob: true }); }); afterAll(async () => { diff --git a/nodejs/test/e2e/inprocess_ffi.e2e.test.ts b/nodejs/test/e2e/inprocess_ffi.e2e.test.ts index af879ea77b..e3b5f75ee4 100644 --- a/nodejs/test/e2e/inprocess_ffi.e2e.test.ts +++ b/nodejs/test/e2e/inprocess_ffi.e2e.test.ts @@ -11,9 +11,8 @@ describe("In-process FFI transport", () => { // exercised by the full E2E suite running under the `inprocess` CI matrix cell, // not a dedicated test. it("should start and connect over in-process FFI", async () => { - // In-process FFI hosting resolves the CLI entrypoint (COPILOT_CLI_PATH or the - // bundled platform package) and its sibling native runtime library itself. If - // neither is available, start() throws and the test fails hard. + // In-process FFI hosting loads runtime.node directly from the bundled runtime. + // If it is unavailable, start() throws and the test fails hard. const client = new CopilotClient({ connection: RuntimeConnection.forInProcess() }); await client.start(); diff --git a/nodejs/test/e2e/pending_work_resume.e2e.test.ts b/nodejs/test/e2e/pending_work_resume.e2e.test.ts index 85abc3a900..7c2906c7b9 100644 --- a/nodejs/test/e2e/pending_work_resume.e2e.test.ts +++ b/nodejs/test/e2e/pending_work_resume.e2e.test.ts @@ -13,6 +13,7 @@ import type { PermissionRequestResult, } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; const PENDING_WORK_TIMEOUT_MS = 60_000; const TEST_TIMEOUT_MS = 180_000; @@ -516,7 +517,46 @@ describe("Pending work resume", async () => { ).toBe("beta"); if (scenario.disconnectOriginalClient) { - await suspendedClient.forceStop(); + const lockObserver = new CopilotClient({ + workingDirectory: workDir, + env, + gitHubToken: DEFAULT_GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), + }); + try { + await lockObserver.start(); + await waitForCondition( + async () => { + const result = await lockObserver.rpc.sessions.checkInUse({ + sessionIds: [sessionId], + }); + return result.inUse.includes(sessionId); + }, + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: `Timed out waiting for session '${sessionId}' to acquire its lock.`, + } + ); + + await suspendedClient.forceStop(); + + await waitForCondition( + async () => { + const result = await lockObserver.rpc.sessions.checkInUse({ + sessionIds: [sessionId], + }); + return !result.inUse.includes(sessionId); + }, + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: `Timed out waiting for session '${sessionId}' to release its lock.`, + } + ); + } finally { + await lockObserver.forceStop(); + } } const resumedClient = createConnectingClient(cliUrl); diff --git a/nodejs/test/e2e/rewind.e2e.test.ts b/nodejs/test/e2e/rewind.e2e.test.ts index 49c2b3b8f0..7fdfee94c8 100644 --- a/nodejs/test/e2e/rewind.e2e.test.ts +++ b/nodejs/test/e2e/rewind.e2e.test.ts @@ -2,13 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; const FILE_NAME = "rewind-sdk.txt"; +const ORIGINAL_FILE_CONTENT = "Original rewind content"; +const PREPARED_FILE_CONTENT = "Prepared rewind content"; const FILE_CONTENT = "SDK rewind content"; function expectSamePath(actual: string, expected: string): void { @@ -24,66 +26,73 @@ function expectSamePath(actual: string, expected: string): void { describe("Rewind", async () => { const { copilotClient: client, workDir } = await createSdkTestContext(); - // TODO(cli-1.0.81): Re-enable when Windows file-change tracking records built-in create tool writes. - it.skipIf(process.platform === "win32")( - "should restore tracked file and conversation", - async () => { - const filePath = join(workDir, FILE_NAME); - const session = await client.createSession({ - model: "claude-sonnet-4.5", - enableFileChangeTracking: true, - onPermissionRequest: approveAll, + it("should restore tracked file and conversation", async () => { + const filePath = join(workDir, FILE_NAME); + writeFileSync(filePath, ORIGINAL_FILE_CONTENT); + const session = await client.createSession({ + model: "claude-sonnet-5", + enableFileChangeTracking: true, + onPermissionRequest: approveAll, + }); + + try { + const ready = await session.sendAndWait({ + prompt: `Use the edit tool to replace the exact contents of ${FILE_NAME} from ${ORIGINAL_FILE_CONTENT} to ${PREPARED_FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_READY.`, }); + expect(ready?.data.content).toBe("SDK_REWIND_READY"); + expect(readFileSync(filePath, "utf8")).toBe(PREPARED_FILE_CONTENT); - try { - const response = await session.sendAndWait({ - prompt: `Use the create tool to create ${FILE_NAME} containing exactly ${FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_DONE.`, - }); + const response = await session.sendAndWait({ + prompt: `Use the edit tool to replace the exact contents of ${FILE_NAME} from ${PREPARED_FILE_CONTENT} to ${FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_DONE.`, + }); - expect(response?.data.content).toBe("SDK_REWIND_DONE"); - expect(existsSync(filePath)).toBe(true); - expect(readFileSync(filePath, "utf8")).toBe(FILE_CONTENT); + expect(response?.data.content).toBe("SDK_REWIND_DONE"); + expect(existsSync(filePath)).toBe(true); + expect(readFileSync(filePath, "utf8")).toBe(FILE_CONTENT); - let rewindPoints = await session.rpc.history.listRewindPoints(); - const deadline = Date.now() + 30_000; - while ( - Date.now() < deadline && - (rewindPoints.unavailableReason !== undefined || - !rewindPoints.points[0]?.canRestoreFiles) - ) { - await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); - rewindPoints = await session.rpc.history.listRewindPoints(); - } + let rewindPoints = await session.rpc.history.listRewindPoints(); + const deadline = Date.now() + 30_000; + while ( + Date.now() < deadline && + (rewindPoints.unavailableReason !== undefined || + rewindPoints.points.length !== 2 || + !rewindPoints.points[1]?.turnChangedFiles || + !rewindPoints.points[1]?.canRestoreFiles) + ) { + await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); + rewindPoints = await session.rpc.history.listRewindPoints(); + } - expect(rewindPoints.unavailableReason).toBeUndefined(); - expect(rewindPoints.fileChangeTrackingEnabled).toBe(true); - expect(rewindPoints.points).toHaveLength(1); - const rewindPoint = rewindPoints.points[0]; - expect(rewindPoint.canRestoreFiles).toBe(true); - expect(rewindPoint.fileCount).toBe(1); + expect(rewindPoints.unavailableReason).toBeUndefined(); + expect(rewindPoints.fileChangeTrackingEnabled).toBe(true); + expect(rewindPoints.points).toHaveLength(2); + const rewindPoint = rewindPoints.points[1]; + expect(rewindPoint.turnChangedFiles).toBe(true); + expect(rewindPoint.canRestoreFiles).toBe(true); + expect(rewindPoint.fileCount).toBe(1); - const preview = await session.rpc.history.previewRewind({ - eventId: rewindPoint.eventId, - }); - expect(preview.available).toBe(true); - expect(preview.files).toHaveLength(1); - expectSamePath(preview.files[0].path, filePath); + const preview = await session.rpc.history.previewRewind({ + eventId: rewindPoint.eventId, + }); + expect(preview.available).toBe(true); + expect(preview.files).toHaveLength(1); + expectSamePath(preview.files[0].path, filePath); - const rewind = await session.rpc.history.rewind({ - eventId: rewindPoint.eventId, - mode: "conversation-and-files", - }); - expect(rewind.outcome).toBe("success"); - expect(rewind.eventsRemoved).toBeGreaterThan(0); - expect(rewind.restoredFiles).toHaveLength(1); - expectSamePath(rewind.restoredFiles[0], filePath); - expect(existsSync(filePath)).toBe(false); + const rewind = await session.rpc.history.rewind({ + eventId: rewindPoint.eventId, + mode: "conversation-and-files", + }); + expect(rewind.outcome).toBe("success"); + expect(rewind.eventsRemoved).toBeGreaterThan(0); + expect(rewind.restoredFiles).toHaveLength(1); + expectSamePath(rewind.restoredFiles[0], filePath); + expect(existsSync(filePath)).toBe(true); + expect(readFileSync(filePath, "utf8")).toBe(PREPARED_FILE_CONTENT); - const events = await session.getEvents(); - expect(events.some((event) => event.id === rewindPoint.eventId)).toBe(false); - } finally { - await session.disconnect(); - } + const events = await session.getEvents(); + expect(events.some((event) => event.id === rewindPoint.eventId)).toBe(false); + } finally { + await session.disconnect(); } - ); + }); }); diff --git a/nodejs/test/e2e/rpc.e2e.test.ts b/nodejs/test/e2e/rpc.e2e.test.ts index f90547da9b..4e93fdbe44 100644 --- a/nodejs/test/e2e/rpc.e2e.test.ts +++ b/nodejs/test/e2e/rpc.e2e.test.ts @@ -73,7 +73,7 @@ describe("Session RPC", async () => { it.skip("should call session.rpc.model.getCurrent", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const result = await session.rpc.model.getCurrent(); @@ -85,7 +85,7 @@ describe("Session RPC", async () => { it.skip("should call session.rpc.model.switchTo", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); // Get initial model diff --git a/nodejs/test/e2e/rpc_server.e2e.test.ts b/nodejs/test/e2e/rpc_server.e2e.test.ts index 5075ae68d9..13a63875e9 100644 --- a/nodejs/test/e2e/rpc_server.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server.e2e.test.ts @@ -144,7 +144,7 @@ describe("Server-scoped RPC", async () => { const result = await authClient.listModels(); expect(Array.isArray(result)).toBe(true); - expect(result.some((m) => m.id === "claude-sonnet-4.5")).toBe(true); + expect(result.some((m) => m.id === "claude-sonnet-5")).toBe(true); for (const model of result) { expect(model.name).toBeTruthy(); } diff --git a/nodejs/test/e2e/rpc_session_state.e2e.test.ts b/nodejs/test/e2e/rpc_session_state.e2e.test.ts index 5164f99232..aab08b3bc5 100644 --- a/nodejs/test/e2e/rpc_session_state.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state.e2e.test.ts @@ -40,7 +40,7 @@ describe("Session-scoped RPC", async () => { it("should call session rpc model getcurrent", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const result = await session.rpc.model.getCurrent(); @@ -65,7 +65,7 @@ describe("Session-scoped RPC", async () => { it("should call session rpc model switchto", async () => { const session = await switchClient.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const before = await session.rpc.model.getCurrent(); @@ -315,14 +315,14 @@ describe("Session-scoped RPC", async () => { const branch = `rpc-context-${randomUUID()}`; const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", workingDirectory: firstDirectory, }); try { const initialSnapshot = await session.rpc.metadata.snapshot(); expect(initialSnapshot.sessionId).toBe(session.sessionId); expect(initialSnapshot.currentMode).toBe("interactive"); - expect(initialSnapshot.selectedModel).toBe("claude-sonnet-4.5"); + expect(initialSnapshot.selectedModel).toBe("claude-sonnet-5"); expect(initialSnapshot.isRemote).toBe(false); expect(initialSnapshot.alreadyInUse).toBe(false); expect(Date.parse(initialSnapshot.startTime)).not.toBeNaN(); @@ -446,7 +446,7 @@ describe("Session-scoped RPC", async () => { it("should set reasoning effort and auto name", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); try { const reasoning = await session.rpc.model.setReasoningEffort({ @@ -455,7 +455,7 @@ describe("Session-scoped RPC", async () => { expect(reasoning.reasoningEffort).toBe("high"); const currentModel = await session.rpc.model.getCurrent(); - expect(currentModel.modelId).toBe("claude-sonnet-4.5"); + expect(currentModel.modelId).toBe("claude-sonnet-5"); expect(currentModel.reasoningEffort).toBe("high"); const autoName = `Auto Session ${randomUUID()}`; @@ -734,11 +734,11 @@ describe("Session-scoped RPC", async () => { const contextInfo = await session.rpc.metadata.contextInfo({ promptTokenLimit: 128_000, outputTokenLimit: 4_096, - selectedModel: "claude-sonnet-4.5", + selectedModel: "claude-sonnet-5", }); expect(contextInfo.contextInfo).not.toBeNull(); if (contextInfo.contextInfo) { - expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-4.5"); + expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-5"); expect(contextInfo.contextInfo.promptTokenLimit).toBe(128_000); expect(contextInfo.contextInfo.limit).toBeGreaterThanOrEqual( contextInfo.contextInfo.promptTokenLimit @@ -755,7 +755,7 @@ describe("Session-scoped RPC", async () => { } const recomputed = await session.rpc.metadata.recomputeContextTokens({ - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", }); expect(recomputed.systemTokenCount).toBeGreaterThan(0); expect(recomputed.messagesTokenCount).toBeGreaterThan(0); diff --git a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts index 6111809914..7b88af7e2d 100644 --- a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts @@ -70,7 +70,7 @@ describe("Session-scoped state extras RPC", async () => { try { await authClient.start(); session = await authClient.createSession({ - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", onPermissionRequest: approveAll, }); @@ -79,7 +79,7 @@ describe("Session-scoped state extras RPC", async () => { expect(Array.isArray(result.list)).toBe(true); expect(result.list.length).toBeGreaterThan(0); expect( - result.list.some((model) => JSON.stringify(model).includes("claude-sonnet-4.5")) + result.list.some((model) => JSON.stringify(model).includes("claude-sonnet-5")) ).toBe(true); } finally { await disconnect(session); @@ -126,7 +126,7 @@ describe("Session-scoped state extras RPC", async () => { provider: providerName, id: modelId, name: "SDK Runtime Model", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "wire-sdk-runtime-model", maxContextWindowTokens: 4096, maxPromptTokens: 3072, diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index b89221998a..bab0efc687 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -98,7 +98,7 @@ describe("Sessions", () => { it("should create and disconnect sessions", async () => { await using session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); @@ -107,7 +107,7 @@ describe("Sessions", () => { expect(sessionStartEvents).toMatchObject([ { type: "session.start", - data: { sessionId: session.sessionId, selectedModel: "claude-sonnet-4.5" }, + data: { sessionId: session.sessionId, selectedModel: "claude-sonnet-5" }, }, ]); diff --git a/nodejs/test/e2e/session_config.e2e.test.ts b/nodejs/test/e2e/session_config.e2e.test.ts index 85137e0ff9..8d041f1ec8 100644 --- a/nodejs/test/e2e/session_config.e2e.test.ts +++ b/nodejs/test/e2e/session_config.e2e.test.ts @@ -119,6 +119,7 @@ describe("Session Configuration", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, + model: "claude-sonnet-5", modelCapabilities: { supports: { vision: false } }, }); @@ -129,7 +130,7 @@ describe("Session Configuration", async () => { expect(hasImageUrlContent(t1Messages)).toBe(false); // Switch vision on (re-specify same model with updated capabilities) - await session.setModel("claude-sonnet-4.5", { + await session.setModel("claude-sonnet-5", { modelCapabilities: { supports: { vision: true } }, }); @@ -149,6 +150,7 @@ describe("Session Configuration", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, + model: "claude-sonnet-5", modelCapabilities: { supports: { vision: true } }, }); @@ -159,7 +161,7 @@ describe("Session Configuration", async () => { expect(hasImageUrlContent(t1Messages)).toBe(true); // Switch vision off - await session.setModel("claude-sonnet-4.5", { + await session.setModel("claude-sonnet-5", { modelCapabilities: { supports: { vision: false } }, }); @@ -342,7 +344,7 @@ describe("Session Configuration", async () => { id: "msg_stub_1", type: "message", role: "assistant", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", content: [], stop_reason: null, stop_sequence: null, @@ -384,8 +386,8 @@ describe("Session Configuration", async () => { return json({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -393,7 +395,7 @@ describe("Session Configuration", async () => { model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { @@ -423,7 +425,7 @@ describe("Session Configuration", async () => { id: "msg_stub_1", type: "message", role: "assistant", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", content: [{ type: "text", text: "OK from the synthetic stream." }], stop_reason: "end_turn", stop_sequence: null, @@ -434,7 +436,7 @@ describe("Session Configuration", async () => { id: "chatcmpl-stub-1", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -462,8 +464,8 @@ describe("Session Configuration", async () => { type: "anthropic" as const, baseUrl: "https://anthropic-citations.invalid/v1", apiKey: "test-provider-key", - modelId: "claude-sonnet-4.5", - wireModel: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", + wireModel: "claude-sonnet-5", }; } @@ -563,7 +565,7 @@ describe("Session Configuration", async () => { try { const session = await citationClient.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", enableCitations: true, provider: createAnthropicProvider(), }); @@ -607,7 +609,7 @@ describe("Session Configuration", async () => { try { const session2 = await resumeClient.resumeSession(session1.sessionId, { onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", enableCitations: true, provider: createAnthropicProvider(), }); @@ -711,7 +713,7 @@ describe("Session Configuration", async () => { it("should forward custom provider headers on create", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: createProxyProvider("create-provider-header"), }); @@ -734,7 +736,7 @@ describe("Session Configuration", async () => { const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: createProxyProvider("resume-provider-header"), }); @@ -762,7 +764,7 @@ describe("Session Configuration", async () => { // tests for serialization coverage). const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "openai", baseUrl: openAiEndpoint.url, @@ -791,7 +793,7 @@ describe("Session Configuration", async () => { type: "openai", baseUrl: openAiEndpoint.url, apiKey: "test-provider-key", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", }, }); @@ -799,7 +801,7 @@ describe("Session Configuration", async () => { const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges.length).toBe(1); - expect(exchanges[0].request.model).toBe("claude-sonnet-4.5"); + expect(exchanges[0].request.model).toBe("claude-sonnet-5"); await session.disconnect(); }); diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index 2e85dd5af2..6366db36cb 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -5,7 +5,11 @@ import { afterAll, describe, expect, it } from "vitest"; import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; -import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; +import { + createSdkTestContext, + getLegacyCliPathForTests, + isInProcessTransport, +} from "./harness/sdkTestContext.js"; describe("UI Elicitation", async () => { const { copilotClient: client } = await createSdkTestContext(); @@ -38,6 +42,36 @@ describe("UI Elicitation Callback", async () => { } ); + // In-process sessions do not expose current tool metadata for introspection. + it.skipIf(isInProcessTransport)( + "session created with the elicitation ask-user variant exposes the structured tool", + { timeout: 60_000 }, + async () => { + const legacyClient = ctx.createClient({ + connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + }); + try { + const session = await legacyClient.createSession({ + onPermissionRequest: approveAll, + askUserVariant: "elicitation", + onElicitationRequest: async () => ({ action: "accept", content: {} }), + }); + + await session.rpc.tools.initializeAndValidate(); + const { tools } = await session.rpc.tools.getCurrentMetadata(); + const askUserSchema = tools?.find((tool) => tool.name === "ask_user") + ?.input_schema as { properties?: Record } | undefined; + + expect(askUserSchema?.properties).toHaveProperty("message"); + expect(askUserSchema?.properties).toHaveProperty("requestedSchema"); + expect(askUserSchema?.properties).not.toHaveProperty("question"); + await session.disconnect(); + } finally { + await legacyClient.stop(); + } + } + ); + it( "session created without onElicitationRequest reports no elicitation capability", { timeout: 60_000 }, diff --git a/nodejs/test/factory.test.ts b/nodejs/test/factory.test.ts index dcf434616a..c9b8f65074 100644 --- a/nodejs/test/factory.test.ts +++ b/nodejs/test/factory.test.ts @@ -435,6 +435,7 @@ describe("factories", () => { const guide = readFileSync(new URL("../docs/factories.md", import.meta.url), "utf8"); const publicApi = readFileSync(new URL("../src/factory.ts", import.meta.url), "utf8"); const listRunsPagingWording = "newest default page of this session's durable factory runs"; + const listRunsMetadata = ["oldestSeq", "newestSeq", "hasMoreNewer", "omittedOlder"]; const resumeCodes = [ "not_found", "non_resumable", @@ -458,6 +459,9 @@ describe("factories", () => { for (const document of [normalizedGuide, normalizedPublicApi]) { expect(document).toContain(listRunsPagingWording); + for (const field of listRunsMetadata) { + expect(document).toContain(field); + } } expect(normalizedGuide).toContain( @@ -1500,14 +1504,31 @@ describe("factories", () => { revision: 4, }; const detail = { ...summary, phases: [], agents: [], progress }; + const runsPage = { + runs: [summary], + oldestSeq: 11, + newestSeq: 12, + hasMoreNewer: true, + omittedOlder: 10, + }; const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.listRuns") return { runs: [summary] }; + if (method === "session.factory.listRuns") return runsPage; if (method === "session.factory.getRunDetail") return detail; return progress; }); const session = new CopilotSession("session-observe", { sendRequest } as never); await expect(session.factory.listRuns()).resolves.toEqual([summary]); + const listedPage = await session.factory.listRuns({ + afterSeq: 10, + beforeSeq: 20, + limit: 50, + }); + expect(listedPage).toEqual(runsPage); + expect(listedPage.oldestSeq).toBe(11); + expect(listedPage.newestSeq).toBe(12); + expect(listedPage.hasMoreNewer).toBe(true); + expect(listedPage.omittedOlder).toBe(10); await expect(session.factory.getRunDetail("run-observe")).resolves.toEqual(detail); await expect( session.factory.getRunProgress("run-observe", { @@ -1519,11 +1540,17 @@ describe("factories", () => { expect(sendRequest).toHaveBeenNthCalledWith(1, "session.factory.listRuns", { sessionId: session.sessionId, }); - expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.getRunDetail", { + expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.listRuns", { + sessionId: session.sessionId, + afterSeq: 10, + beforeSeq: 20, + limit: 50, + }); + expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunDetail", { sessionId: session.sessionId, runId: "run-observe", }); - expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunProgress", { + expect(sendRequest).toHaveBeenNthCalledWith(4, "session.factory.getRunProgress", { sessionId: session.sessionId, runId: "run-observe", phaseId: "p0", @@ -2132,6 +2159,8 @@ describe("factories", () => { await expect( session.factory.resume("run-prior", { limits: { maxTotalSubagents: 7 }, + notifyOnComplete: true, + logPhaseNames: true, }) ).resolves.toMatchObject({ status: "completed", @@ -2141,13 +2170,20 @@ describe("factories", () => { session.factory.run("by-name", { args: { value: 1 }, limits: { maxTotalSubagents: 7 }, + notifyOnComplete: false, + logPhaseNames: true, resumeFromRunId: "run-prior", }) ).resolves.toMatchObject({ status: "completed", result: { name: "stored-name", persistedArgs: true }, }); - await expect(session.factory.run(factory)).resolves.toMatchObject({ + await expect( + session.factory.run(factory, { + notifyOnComplete: true, + logPhaseNames: false, + }) + ).resolves.toMatchObject({ status: "completed", result: { name: "friendly-run" }, }); @@ -2155,17 +2191,25 @@ describe("factories", () => { sessionId: session.sessionId, runId: "run-prior", limits: { maxTotalSubagents: 7 }, + notifyOnComplete: true, + logPhaseNames: true, }); expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.resume", { sessionId: session.sessionId, runId: "run-prior", limits: { maxTotalSubagents: 7 }, + notifyOnComplete: false, + logPhaseNames: true, }); expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.run", { sessionId: session.sessionId, name: "friendly-run", args: {}, - options: { limits: undefined }, + options: { + limits: undefined, + notifyOnComplete: true, + logPhaseNames: false, + }, }); }); diff --git a/nodejs/test/runtimeArtifacts.test.ts b/nodejs/test/runtimeArtifacts.test.ts new file mode 100644 index 0000000000..4a58789e6e --- /dev/null +++ b/nodejs/test/runtimeArtifacts.test.ts @@ -0,0 +1,109 @@ +import { existsSync, mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { defaultRuntimeCacheRoot, materializeRuntimeBundle } from "../src/runtimeArtifacts.js"; + +describe("defaultRuntimeCacheRoot", () => { + it.each([ + [ + "darwin", + "/home/test", + {}, + join("/home/test", "Library", "Caches", "github-copilot-sdk", "runtime"), + ], + ["linux", "/home/test", {}, join("/home/test", ".cache", "github-copilot-sdk", "runtime")], + [ + "linux", + "/home/test", + { XDG_CACHE_HOME: "/cache" }, + join("/cache", "github-copilot-sdk", "runtime"), + ], + [ + "win32", + "C:\\Users\\test", + { LOCALAPPDATA: "C:\\Users\\test\\AppData\\Local" }, + join("C:\\Users\\test\\AppData\\Local", "github-copilot-sdk", "runtime"), + ], + ])("uses the %s user cache directory", (platform, home, environment, expected) => { + expect(defaultRuntimeCacheRoot(platform, home, environment)).toBe(expected); + }); +}); + +describe("materializeRuntimeBundle", () => { + afterEach(() => vi.unstubAllEnvs()); + + it("materializes an adjacent pair from an absent cache with a stripped environment", () => { + const sourceDir = mkdtempSync(join(tmpdir(), "copilot-runtime-source-")); + const cacheRoot = join(sourceDir, "absent-cache"); + const emptyPath = join(sourceDir, "empty-path"); + mkdirSync(emptyPath); + const wrapperName = + process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; + const prebuilds = join(sourceDir, "prebuilds", "test-platform"); + const wrapper = join(prebuilds, wrapperName); + const runtimeNode = join(prebuilds, "runtime.node"); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(wrapper, "wrapper"); + writeFileSync(runtimeNode, "runtime"); + mkdirSync(join(sourceDir, "ripgrep", "bin", "test-platform"), { recursive: true }); + writeFileSync(join(sourceDir, "ripgrep", "bin", "test-platform", "rg"), "ripgrep"); + mkdirSync(join(sourceDir, "definitions"), { recursive: true }); + writeFileSync(join(sourceDir, "definitions", "future.json"), "{}"); + writeFileSync(join(sourceDir, "app.js"), "excluded"); + writeFileSync(join(sourceDir, "copilot"), "excluded"); + writeFileSync(join(sourceDir, "copilot.exe"), "excluded"); + writeFileSync(join(sourceDir, "LICENSE.md"), "excluded"); + writeFileSync(join(sourceDir, "README.md"), "excluded"); + + vi.stubEnv("PATH", emptyPath); + vi.stubEnv("COPILOT_CLI_PATH", undefined); + vi.stubEnv("COPILOT_RUNTIME_HOST_COMMAND", undefined); + vi.stubEnv("COPILOT_RUNTIME_PROVIDER_LIB", undefined); + + expect(process.env.COPILOT_CLI_PATH).toBeUndefined(); + expect(process.env.COPILOT_RUNTIME_HOST_COMMAND).toBeUndefined(); + expect(process.env.COPILOT_RUNTIME_PROVIDER_LIB).toBeUndefined(); + + const installedWrapper = materializeRuntimeBundle( + { packageRoot: sourceDir, platform: "test-platform" }, + cacheRoot + ); + const installDir = dirname(installedWrapper); + + expect(readFileSync(installedWrapper, "utf8")).toBe("wrapper"); + expect(readFileSync(join(installDir, "runtime.node"), "utf8")).toBe("runtime"); + expect( + readFileSync(join(installDir, "ripgrep", "bin", "test-platform", "rg"), "utf8") + ).toBe("ripgrep"); + expect(existsSync(join(installDir, "app.js"))).toBe(false); + expect(existsSync(join(installDir, "copilot"))).toBe(false); + expect(existsSync(join(installDir, "copilot.exe"))).toBe(false); + expect(existsSync(join(installDir, "LICENSE.md"))).toBe(false); + expect(existsSync(join(installDir, "README.md"))).toBe(false); + if (process.platform !== "win32") { + expect(statSync(installedWrapper).mode & 0o111).not.toBe(0); + } + }); + + it("fails clearly when the package has no runtime.node", () => { + const sourceDir = mkdtempSync(join(tmpdir(), "copilot-runtime-missing-node-")); + const wrapperName = + process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; + const prebuilds = join(sourceDir, "prebuilds", "test-platform"); + const wrapper = join(prebuilds, wrapperName); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(wrapper, "wrapper"); + + expect(() => + materializeRuntimeBundle( + { + packageRoot: sourceDir, + platform: "test-platform", + }, + join(sourceDir, "cache") + ) + ).toThrow(/Copilot runtime\.node not found/); + }); +}); diff --git a/nodejs/test/session-event-codegen.test.ts b/nodejs/test/session-event-codegen.test.ts index c84d9bc082..2dad8f4f18 100644 --- a/nodejs/test/session-event-codegen.test.ts +++ b/nodejs/test/session-event-codegen.test.ts @@ -3,10 +3,37 @@ import { describe, expect, it } from "vitest"; import { generateSessionEventsCode as generateCSharpSessionEventsCode } from "../../scripts/codegen/csharp.ts"; import { generateGoSessionEventsCode } from "../../scripts/codegen/go.ts"; -import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts"; +import { + generatePythonSessionEventsCode, + postProcessExternalRefsForPython, +} from "../../scripts/codegen/python.ts"; import { generateSessionEventsCode as generateRustSessionEventsCode } from "../../scripts/codegen/rust.ts"; describe("session event codegen", () => { + it("replaces external reference placeholders regardless of acronym casing", () => { + const code = `@dataclass +class ExternalRefMCPOauthHTTPResponse: + external_ref_marker_external_ref_mcp_oauth_http_response: str + + @staticmethod + def from_dict(obj: Any) -> 'ExternalRefMCPOauthHTTPResponse': + value = obj.get("__externalRefMarker___ExternalRef_McpOauthHttpResponse") + return ExternalRefMCPOauthHTTPResponse(value) + +@dataclass +class ProbeResult: + response: ExternalRefMCPOauthHTTPResponse +`; + + const processed = postProcessExternalRefsForPython( + code, + new Map([["__ExternalRef_McpOauthHttpResponse", "McpOauthHttpResponse"]]) + ); + + expect(processed).toContain("response: McpOauthHttpResponse"); + expect(processed).not.toContain("class ExternalRefMCPOauthHTTPResponse"); + }); + it("maps special schema formats to the expected Python types", () => { const schema: JSONSchema7 = { definitions: { diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 5c41f2216a..93edebfc80 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -21,6 +21,8 @@ import type { FactoryAgentOptions as WireFactoryAgentOptions } from "../src/gene import type { // The aggregate union; must still resolve via the package root. SessionEvent, + AutoTier, + CapiSessionOptions, PermissionRequest, PermissionRequestedData, PermissionRequestedEvent, @@ -128,6 +130,32 @@ type _PermissionRequestedEventStaysAlignedWithSessionEventUnion = _AssertEqual< const _permissionRequestedEventAlignmentCheck: _PermissionRequestedEventStaysAlignedWithSessionEventUnion = true; describe("Session event type exports (#1156)", () => { + it.each(["efficiency", "balance", "intelligence", undefined] satisfies ( + | AutoTier + | undefined + )[])("exposes Auto tier %s on start and resume data", (autoTier) => { + const start: StartData = { + copilotVersion: "1.0.82-1", + producer: "copilot-agent", + sessionId: "session-1", + startTime: "2026-08-28T00:00:00Z", + version: 1, + autoTier, + }; + const resume: ResumeData = { + eventCount: 1, + resumeTime: "2026-08-28T00:01:00Z", + autoTier, + }; + const capi: CapiSessionOptions = { autoTier: start.autoTier }; + expect(capi.autoTier).toBe(autoTier); + expect(resume.autoTier).toBe(autoTier); + if (autoTier === undefined) { + expect(JSON.parse(JSON.stringify(start))).not.toHaveProperty("autoTier"); + expect(JSON.parse(JSON.stringify(resume))).not.toHaveProperty("autoTier"); + } + }); + it("exposes the headline ToolExecutionStartData type with a usable shape", () => { // This is the specific type called out in issue #1156. The annotation // is the compile-time API-surface check; these assertions only validate diff --git a/python/README.md b/python/README.md index 61608c16a0..359026df41 100644 --- a/python/README.md +++ b/python/README.md @@ -29,8 +29,9 @@ runtime: python -m copilot download-runtime ``` -This caches the runtime binary locally. If you skip this step, the SDK will -attempt to download it automatically on first use as a fallback. +This caches `copilot-runtime`, its adjacent `runtime.node`, and the compatible +`copilot` host locally. If you skip this step, the SDK downloads the bundle +automatically on first managed stdio/TCP use. To pre-provision the native library required by the in-process (FFI) transport (see [In-process (FFI) transport](#in-process-ffi-transport)), pass `--in-process`: @@ -39,15 +40,15 @@ To pre-provision the native library required by the in-process (FFI) transport python -m copilot download-runtime --in-process ``` -This additionally fetches the native runtime library into the versioned runtime -cache. Stdio/TCP users never download it. When omitted, it is downloaded -lazily on first use of the in-process transport. +This instead provisions the compatible CLI artifact and native runtime library +used by in-process hosting. When omitted, they are downloaded lazily on first +use of the in-process transport. | Platform | Cache path | |----------|-----------| -| Linux | `~/.cache/github-copilot-sdk/cli//copilot` | -| macOS | `~/Library/Caches/github-copilot-sdk/cli//copilot` | -| Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\copilot.exe` | +| Linux | `~/.cache/github-copilot-sdk/cli//prebuilds//` | +| macOS | `~/Library/Caches/github-copilot-sdk/cli//prebuilds//` | +| Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\prebuilds\\` | ### Environment variables @@ -56,7 +57,8 @@ lazily on first use of the in-process transport. | `COPILOT_CLI_PATH` | Use this specific binary instead of downloading | | `COPILOT_CLI_EXTRACT_DIR` | Override the cache directory (binary placed directly here) | | `COPILOT_SKIP_CLI_DOWNLOAD` | Set to `1` to disable auto-download | -| `COPILOT_CLI_DOWNLOAD_BASE_URL` | Override the GitHub Releases download URL | +| `COPILOT_NPM_REGISTRY_URL` | Override the npm registry used for managed out-of-process and in-process runtime downloads | +| `COPILOT_CLI_DOWNLOAD_BASE_URL` | Override the GitHub Releases download URL used for the root CLI | ## Run the Sample @@ -223,6 +225,10 @@ All options are kw-only parameters: - `RuntimeConnection.for_uri(url, connection_token=None)` — connect to an existing CLI server (e.g. `"localhost:8080"`). - `RuntimeConnection.for_inprocess()` — host the runtime in-process via its native C ABI (FFI). See [In-process (FFI) transport](#in-process-ffi-transport). +Managed stdio and TCP connections use the downloaded `copilot-runtime` +executable with adjacent `runtime.node` by default. An explicit connection +path or `COPILOT_CLI_PATH` overrides the downloaded runtime. + Child-process connections (`for_stdio`/`for_tcp`) also expose a per-connection `env` field for the spawned process. Set it on the returned connection instead of the client-level `env` — setting both raises: @@ -272,6 +278,7 @@ finally: These are passed as keyword arguments to `create_session()`: - `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** +- `capi` (CapiSessionOptions): Copilot API options. With `model="auto"`, set `auto_tier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics. - `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh", "max"). Use `list_models()` to check which models support this option. - `session_id` (str): Custom session ID - `tools` (list): Custom tools exposed to the CLI. Tools with `handler=None` are declaration-only and must be resolved via pending tool-call RPCs. @@ -283,7 +290,8 @@ These are passed as keyword arguments to `create_session()`: - `enable_session_store` (bool): Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled. - `github_token_provider` (callable): Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `github_token`. - `on_permission_request` (callable): 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.approve_all` approves requests when managed settings are disabled and raises an error when `enable_managed_settings` is true. Custom handlers can inspect `managed_approval_required` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. -- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. +- `on_user_input_request` (callable): Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. +- `ask_user_variant` (`"legacy"` | `"elicitation"`): Selects the model-facing shape of the `ask_user` tool. Defaults to `"legacy"`; use `"elicitation"` with `on_elicitation_request`. Re-supply this option when cold-resuming a session. - `hooks` (SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. ```python @@ -899,7 +907,7 @@ To let a specific custom tool bypass the permission prompt entirely, set `skip_p ## User Input Requests -Enable the agent to ask questions to the user using the `ask_user` tool by providing an `on_user_input_request` handler: +Enable the legacy question-and-answer `ask_user` tool by providing an `on_user_input_request` handler: ```python async def handle_user_input(request, invocation): diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 608dacf253..5d9e3f9500 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -29,8 +29,11 @@ OpenCanvasInstance, ) from .client import ( + AskUserVariant, + AutoTier, CapiSessionOptions, ChildProcessRuntimeConnection, + ClientInfo, CloudSessionOptions, CloudSessionRepository, CopilotClient, @@ -229,6 +232,8 @@ "AutoModeSwitchHandler", "AutoModeSwitchRequest", "AutoModeSwitchResponse", + "AskUserVariant", + "AutoTier", "BUILTIN_TOOLS_ISOLATED", "CanvasAction", "CanvasDeclaration", @@ -240,6 +245,7 @@ "CanvasProviderIdentity", "CapiSessionOptions", "ChildProcessRuntimeConnection", + "ClientInfo", "CloudSessionOptions", "CloudSessionRepository", "CommandContext", diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py index b831e072ad..4477fcfff3 100644 --- a/python/copilot/_cli_download.py +++ b/python/copilot/_cli_download.py @@ -27,7 +27,7 @@ import tempfile import time import zipfile -from pathlib import Path +from pathlib import Path, PurePosixPath from urllib.error import HTTPError, URLError from urllib.request import urlopen @@ -373,6 +373,164 @@ def _extract_runtime_node(data: bytes, npm_platform: str) -> bytes: raise RuntimeError(f"'{target}' not found in runtime package for {npm_platform}.") +def _extract_runtime_wrapper(data: bytes, npm_platform: str) -> bytes: + """Extract the SDK out-of-process wrapper from an npm platform tarball.""" + wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" + target = f"package/prebuilds/{npm_platform}/{wrapper_name}" + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf: + for name in tf.getnames(): + if name == target or name.endswith(f"/prebuilds/{npm_platform}/{wrapper_name}"): + member = tf.getmember(name) + extracted = tf.extractfile(member) + if extracted is not None: + return extracted.read() + raise RuntimeError(f"'{target}' not found in runtime package for {npm_platform}.") + + +_HOSTLESS_EXCLUDED_TOP_LEVEL = { + "app.js", + "assets", + "changelog.json", + "copilot", + "copilot.exe", + "copilot-sdk", + "foundry-local-sdk", + "index.js", + "LICENSE.md", + "napi-oop-runtime", + "npm-loader.js", + "package.json", + "preloads", + "pvrecorder", + "queries", + "README.md", + "sdk", + "sea-loader.js", + "webview", +} + + +def _hostless_runtime_path(member_name: str, npm_platform: str) -> Path | None: + parts = PurePosixPath(member_name).parts + if not parts or parts[0] != "package" or len(parts) < 2: + return None + relative = parts[1:] + top_level = relative[0] + file_name = relative[-1] + if ( + top_level in _HOSTLESS_EXCLUDED_TOP_LEVEL + or (top_level.startswith("tree-sitter") and top_level.endswith(".wasm")) + or (top_level.startswith("voice-") and top_level.endswith(".js")) + or file_name == "cli-native.node" + or "mediaremote-adapter" in relative + or file_name.startswith("copilot-runtime-bin") + ): + return None + if top_level == "prebuilds": + if len(relative) < 3 or relative[1] != npm_platform: + return None + relative = relative[2:] + destination = Path(*relative) + if destination.is_absolute() or ".." in destination.parts: + raise RuntimeError(f"Unsafe runtime package path: {member_name}") + return destination + + +def _extract_runtime_bundle(data: bytes, npm_platform: str, destination: Path) -> None: + """Extract the hostless runtime tree, retaining unknown package assets by default.""" + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: + for member in archive: + relative = _hostless_runtime_path(member.name, npm_platform) + if relative is None or member.isdir(): + continue + if not member.isfile(): + raise RuntimeError(f"Unsupported runtime package entry: {member.name}") + extracted = archive.extractfile(member) + if extracted is None: + raise RuntimeError(f"Failed to read runtime package entry: {member.name}") + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(extracted.read()) + if sys.platform != "win32": + target.chmod(member.mode & 0o777) + + +def ensure_runtime_wrapper(version: str | None = None, force: bool = False) -> str: + """Provision the runtime pair and its retained npm package assets.""" + ver = version or CLI_VERSION + if not ver: + raise RuntimeError("No runtime version is pinned.") + npm_platform = get_npm_platform() + wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" + pair_dir = get_cache_dir(ver) / "prebuilds" / npm_platform + wrapper_path = pair_dir / wrapper_name + runtime_path = pair_dir / "runtime.node" + assets_marker = pair_dir / ".hostless-runtime-assets-v2" + + wrapper_exists = wrapper_path.is_file() and wrapper_path.stat().st_size > 0 + runtime_exists = runtime_path.is_file() and runtime_path.stat().st_size > 0 + if wrapper_exists and runtime_exists and assets_marker.is_file() and not force: + return str(wrapper_path) + if not force and wrapper_exists != runtime_exists: + raise RuntimeError( + f"Incomplete Copilot runtime bundle in {pair_dir}: " + f"{wrapper_name} and runtime.node are required." + ) + if _should_skip_download(): + raise RuntimeError( + f"Copilot runtime bundle is not cached in {pair_dir} " + "and automatic downloads are disabled." + ) + + data = _fetch_url_bytes(get_runtime_lib_url(ver, npm_platform), timeout=600) + integrity = _fetch_runtime_integrity(npm_platform, ver) + if not integrity: + raise RuntimeError( + "No Subresource Integrity value available for the Copilot runtime " + f"package ({npm_platform}@{ver}); refusing to stage unverified native code." + ) + _verify_integrity(data, integrity) + import shutil + + pair_dir.parent.mkdir(parents=True, exist_ok=True) + staging_dir = Path(tempfile.mkdtemp(dir=pair_dir.parent, prefix=".runtime-bundle-")) + try: + _extract_runtime_bundle(data, npm_platform, staging_dir) + staged_wrapper = staging_dir / wrapper_name + staged_runtime = staging_dir / "runtime.node" + if ( + not staged_wrapper.is_file() + or staged_wrapper.stat().st_size == 0 + or not staged_runtime.is_file() + or staged_runtime.stat().st_size == 0 + ): + raise RuntimeError("Copilot runtime wrapper and runtime.node must both be non-empty.") + if sys.platform != "win32": + staged_wrapper.chmod( + staged_wrapper.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) + (staging_dir / assets_marker.name).write_text("1\n", encoding="ascii") + try: + if pair_dir.exists() and (force or not assets_marker.is_file()): + shutil.rmtree(pair_dir, ignore_errors=True) + staging_dir.replace(pair_dir) + except OSError: + if ( + wrapper_path.is_file() + and wrapper_path.stat().st_size > 0 + and runtime_path.is_file() + and runtime_path.stat().st_size > 0 + and assets_marker.is_file() + ): + return str(wrapper_path) + raise + finally: + if staging_dir.exists(): + shutil.rmtree(staging_dir, ignore_errors=True) + + return str(wrapper_path) + + def ensure_runtime_library(cli_path: str, version: str | None = None) -> str | None: """Ensure the native in-process (FFI) runtime library sits next to ``cli_path``. @@ -536,7 +694,10 @@ def main() -> None: print(f"Downloading Copilot runtime v{ver}...") try: - path = download_cli(ver, force=args.force) + if args.in_process: + path = download_cli(ver, force=args.force) + else: + path = ensure_runtime_wrapper(ver, force=args.force) print(f"Runtime cached at: {path}") if args.in_process: print("Downloading in-process (FFI) runtime library...") diff --git a/python/copilot/_ffi_runtime_host.py b/python/copilot/_ffi_runtime_host.py index e04d1655e6..98aa776600 100644 --- a/python/copilot/_ffi_runtime_host.py +++ b/python/copilot/_ffi_runtime_host.py @@ -3,9 +3,9 @@ Instead of spawning the Copilot CLI as a child process and talking JSON-RPC over stdio/TCP, the in-process transport loads the runtime's native shared library (``runtime.node`` — a Rust ``cdylib``) into this process and drives JSON-RPC over -its C ABI (FFI). The native ``host_start`` export spawns the residual worker -itself, so the SDK never launches the worker directly; it only pumps opaque LSP -``Content-Length:``-framed JSON-RPC bytes across the boundary: +its C ABI (FFI). The native ``host_start`` export constructs the Rust server +synchronously; the SDK only pumps opaque LSP ``Content-Length:``-framed JSON-RPC +bytes across the boundary: - client → server frames go to ``copilot_runtime_connection_write`` - server → client frames arrive on a native callback that feeds a thread-safe @@ -114,8 +114,8 @@ def _natural_library_name() -> str: return "libcopilot_runtime.so" -def resolve_library_path(cli_entrypoint: str) -> str | None: - """Resolve the native runtime library next to the given CLI entrypoint. +def resolve_library_path(runtime_entrypoint: str) -> str | None: + """Resolve the native runtime library next to the given runtime entrypoint. Checks, in order: @@ -125,7 +125,7 @@ def resolve_library_path(cli_entrypoint: str) -> str | None: Returns the absolute path, or ``None`` when neither exists. """ - directory = Path(cli_entrypoint).resolve().parent + directory = Path(runtime_entrypoint).resolve().parent flat = directory / _natural_library_name() if flat.is_file(): @@ -327,15 +327,15 @@ def wait(self, timeout: float | None = None) -> int: # noqa: ARG002 class FfiRuntimeHost: """Hosts the Copilot runtime in-process via its native C ABI. - Construct with :meth:`create`, then :meth:`start` to spawn the worker and open - the FFI connection. Expose :attr:`process` to :class:`JsonRpcClient`, and call - :meth:`dispose` to tear everything down. + Construct with :meth:`create`, then :meth:`start` to start the native engine + and open the FFI connection. Expose :attr:`process` to + :class:`JsonRpcClient`, and call :meth:`dispose` to tear everything down. """ def __init__( self, library_path: str, - cli_entrypoint: str, + cli_entrypoint: str | None, environment: dict[str, str] | None = None, args: Sequence[str] = (), ) -> None: @@ -367,31 +367,30 @@ def process(self) -> _FfiProcessAdapter: @staticmethod def create( - cli_entrypoint: str, + library_path: str, + cli_entrypoint: str | None = None, environment: dict[str, str] | None = None, args: Sequence[str] = (), ) -> FfiRuntimeHost: - """Resolve the cdylib next to the CLI entrypoint and prepare the host. + """Load the runtime cdylib and prepare the host. Raises: RuntimeError: If the native runtime library cannot be found. """ - full_entrypoint = str(Path(cli_entrypoint).resolve()) - library_path = resolve_library_path(full_entrypoint) - if library_path is None: + full_library_path = str(Path(library_path).resolve()) + if not Path(full_library_path).is_file(): raise RuntimeError( - "In-process FFI runtime library not found next to " - f"'{full_entrypoint}'. Download it with " - "`python -m copilot download-runtime --in-process`, or set " - "COPILOT_CLI_PATH to a runtime package that ships it." + f"In-process FFI runtime library not found at '{full_library_path}'." ) - return FfiRuntimeHost(library_path, full_entrypoint, environment, args) + full_entrypoint = ( + str(Path(cli_entrypoint).resolve()) if cli_entrypoint is not None else None + ) + return FfiRuntimeHost(full_library_path, full_entrypoint, environment, args) def _build_argv(self) -> bytes: - # A `.js` entrypoint (dev) is launched via node; the packaged single-file - # CLI embeds its own Node and is invoked directly. `--no-auto-update` - # pins the worker to the runtime package matching the loaded cdylib. - if self._cli_entrypoint.lower().endswith(".js"): + if self._cli_entrypoint is None: + argv: list[str] = [] + elif self._cli_entrypoint.lower().endswith(".js"): argv = ["node", self._cli_entrypoint, "--embedded-host", "--no-auto-update"] else: argv = [self._cli_entrypoint, "--embedded-host", "--no-auto-update"] @@ -407,11 +406,9 @@ def _build_env(self) -> bytes | None: return json.dumps(obj).encode("utf-8") def start_blocking(self) -> None: - """Spawn the worker and open the FFI connection (blocks up to ~30s). + """Start the native engine and open the FFI connection. - Must be run off the event loop (e.g. via :func:`asyncio.to_thread`); - ``host_start`` blocks until the worker connects back and signals - readiness. + Must be run off the event loop (e.g. via :func:`asyncio.to_thread`). """ argv = self._build_argv() env = self._build_env() @@ -419,8 +416,7 @@ def start_blocking(self) -> None: self._server_id = self._lib.host_start(argv, len(argv), env, len(env) if env else 0) if not self._server_id: raise RuntimeError( - f"copilot_runtime_host_start failed (library '{self._library_path}', " - f"entrypoint '{self._cli_entrypoint}')." + f"copilot_runtime_host_start failed (library '{self._library_path}')." ) self._outbound_callback = _OutboundCallback(self._on_outbound) diff --git a/python/copilot/client.py b/python/copilot/client.py index 271fad626c..929194c9b7 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -28,6 +28,7 @@ from collections.abc import Awaitable, Callable, Mapping, Sequence from dataclasses import dataclass, field from datetime import UTC, datetime +from pathlib import Path from types import TracebackType from typing import Any, ClassVar, Literal, NotRequired, TypedDict, cast, overload @@ -177,6 +178,8 @@ class GitHubTokenCancelledResult(TypedDict): _ConnectionState = Literal["disconnected", "connecting", "connected", "error"] LogLevel = Literal["none", "error", "warning", "info", "debug", "all"] +AskUserVariant = Literal["legacy", "elicitation"] +"""Model-facing shape of the runtime's built-in ``ask_user`` tool.""" @dataclass @@ -260,9 +263,23 @@ def _exp_assignment_response_to_dict( return wire +AutoTier = Literal["efficiency", "balance", "intelligence"] +"""Routing preference used when the session model is ``auto``.""" + + class CapiSessionOptions(TypedDict, total=False): """Provider-scoped Copilot API (CAPI) session options.""" + auto_tier: AutoTier + """Routing preference used when the session model is ``auto``. + + Requires a runtime with Auto tier support and V2 Auto routing. When omitted + on create, the runtime uses its default routing behavior. The runtime persists + this preference across cold resume; an explicit tier on cold resume overrides + the persisted value. For an already-resident session, omission preserves the + current tier and a different tier is rejected. + """ + enable_web_socket_responses: bool """Whether to use WebSocket transport for the CAPI Responses API. @@ -289,6 +306,8 @@ def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, An def _capi_session_options_to_wire(options: CapiSessionOptions) -> dict[str, Any]: wire: dict[str, Any] = {} + if "auto_tier" in options: + wire["autoTier"] = options["auto_tier"] if "enable_web_socket_responses" in options: wire["enableWebSocketResponses"] = options["enable_web_socket_responses"] return wire @@ -488,6 +507,46 @@ class TelemetryConfig(TypedDict, total=False): """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501 +class ClientInfo(TypedDict, total=False): + """Identity of the integrating application, declared on ``server.connect``. + + Declaring it lets the telemetry the runtime emits on this connection be + attributed to a single, consistent surface (the application and its Copilot + integration) instead of the runtime's own build. All fields are optional; + omit any of them (or the whole object) to keep the default attribution. + """ + + application_name: str + """Name of the application using the SDK, e.g. ``"acme-developer-portal"``.""" + application_version: str + """Version of the application using the SDK, e.g. ``"2.4.0"``.""" + integration_name: str + """Optional name of an application integration, such as an extension or plugin.""" + integration_version: str + """Optional version of the integration named by ``integration_name``.""" + + +def _client_info_to_wire(client_info: ClientInfo | None) -> dict[str, str] | None: + """Map a snake_case :class:`ClientInfo` onto the camelCase connect wire shape. + + Empty fields are dropped. Returns ``None`` when no field carries a non-empty + value so the caller omits the ``clientInfo`` field entirely and keeps the + runtime's default attribution. + """ + if not client_info: + return None + wire: dict[str, str] = {} + if client_info.get("application_name"): + wire["editorName"] = client_info["application_name"] + if client_info.get("application_version"): + wire["editorVersion"] = client_info["application_version"] + if client_info.get("integration_name"): + wire["extensionName"] = client_info["integration_name"] + if client_info.get("integration_version"): + wire["extensionVersion"] = client_info["integration_version"] + return wire or None + + @dataclass class RuntimeConnection: """Discriminated config describing how to reach the Copilot runtime. @@ -757,6 +816,7 @@ class _CopilotClientOptions: request_handler: CopilotRequestHandler | None = None session_idle_timeout_seconds: int | None = None enable_remote_sessions: bool = False + client_info: ClientInfo | None = None on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None on_github_telemetry: Callable[[GitHubTelemetryNotification], None | Awaitable[None]] | None = ( None @@ -1354,25 +1414,6 @@ def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent: _CLI_PROCESS_EXIT_TIMEOUT_SECONDS = 5 -def _get_or_download_cli(*, include_runtime_lib: bool = False) -> str | None: - """Get the cached CLI binary, downloading if necessary. - - Returns the path to the CLI binary, or None if unavailable (dev install - with no pinned version, or auto-download disabled). - - When ``include_runtime_lib`` is set, also ensures the native in-process FFI - runtime is available (downloading it on first use). - """ - from ._cli_download import get_or_download_cli - - cli_path = get_or_download_cli() - if cli_path and include_runtime_lib: - from ._cli_download import ensure_runtime_library - - ensure_runtime_library(cli_path) - return cli_path - - def _extract_transform_callbacks( system_message: SystemMessageConfig | dict[str, Any] | None, ) -> tuple[dict[str, Any] | None, dict[str, SectionTransformFn] | None]: @@ -1530,6 +1571,7 @@ def __init__( request_handler: CopilotRequestHandler | None = None, session_idle_timeout_seconds: int | None = None, enable_remote_sessions: bool = False, + client_info: ClientInfo | None = None, on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None, on_github_telemetry: Callable[[GitHubTelemetryNotification], None | Awaitable[None]] | None = None, @@ -1579,6 +1621,11 @@ def __init__( Control integration). When ``True``, sessions in a GitHub repository working directory are accessible from GitHub web and mobile. + client_info: Identity of the integrating application, forwarded to the + runtime on the ``server.connect`` handshake. Declaring it lets + the telemetry the runtime emits on this connection be attributed + to a consistent surface instead of the runtime's own build. All + fields are optional; omit it to keep the default attribution. on_list_models: Custom handler for :meth:`list_models`. When provided, the handler is called instead of querying the runtime server. @@ -1616,6 +1663,7 @@ def __init__( request_handler=request_handler, session_idle_timeout_seconds=session_idle_timeout_seconds, enable_remote_sessions=enable_remote_sessions, + client_info=client_info, on_list_models=on_list_models, on_github_telemetry=on_github_telemetry, mode=mode, @@ -1649,6 +1697,7 @@ def __init__( self._cli_path_source: str | None = None self._ffi_host: FfiRuntimeHost | None = None self._inprocess_runtime_path: str | None = None + self._inprocess_cli_entrypoint: str | None = None if isinstance(connection, UriRuntimeConnection): if connection.connection_token is not None and len(connection.connection_token) == 0: @@ -1660,9 +1709,7 @@ def __init__( # In-process (FFI): no child process and no per-connection token. self._runtime_port = None self._effective_connection_token = None - self._inprocess_runtime_path = self._resolve_runtime_entrypoint( - None, include_runtime_lib=True - ) + self._inprocess_runtime_path = self._resolve_inprocess_runtime() if options.use_logged_in_user is None: options.use_logged_in_user = not bool(options.github_token) else: @@ -1683,7 +1730,7 @@ def __init__( else: self._effective_connection_token = None - # Resolve CLI path: explicit > COPILOT_CLI_PATH env var > downloaded binary. + # Resolve runtime path: explicit CLI > COPILOT_CLI_PATH > downloaded runtime. # Select the environment by identity, not truthiness, so an intentionally # empty per-connection or client env stays authoritative (the spawned child # receives that empty mapping) instead of falling back to os.environ and @@ -1728,52 +1775,47 @@ def _resolve_runtime_entrypoint( path: str | None, *, env: Mapping[str, str] | None = None, - include_runtime_lib: bool = False, ) -> str: """Resolve the runtime executable path (explicit > env > downloaded). Sets ``self._cli_path_source`` for diagnostics. When - ``include_runtime_lib`` is set (in-process transport), also ensures the - native runtime library is downloaded alongside the CLI. - Raises: RuntimeError: If no runtime path can be resolved. """ if path is not None: self._cli_path_source = "explicit" - return self._ensure_runtime_lib(path) if include_runtime_lib else path + return path lookup = env if env is not None else os.environ env_cli_path = lookup.get("COPILOT_CLI_PATH") if env_cli_path: self._cli_path_source = "environment" - return self._ensure_runtime_lib(env_cli_path) if include_runtime_lib else env_cli_path - - downloaded_path = _get_or_download_cli(include_runtime_lib=include_runtime_lib) - if downloaded_path: - self._cli_path_source = "downloaded" - return downloaded_path - - raise RuntimeError( - "Copilot CLI not found. Install a published wheel (which " - "auto-downloads the CLI on first use), set COPILOT_CLI_PATH, " - "or pass an explicit path via " - "RuntimeConnection.for_stdio(path=...) / " - "RuntimeConnection.for_tcp(path=...)." - ) + return env_cli_path - @staticmethod - def _ensure_runtime_lib(cli_path: str) -> str: - """Ensure the in-process runtime library sits next to a user-supplied CLI. + from ._cli_download import ensure_runtime_wrapper - For explicit/``COPILOT_CLI_PATH`` entrypoints, the native library may - already be bundled (dev ``prebuilds`` layout); otherwise it is fetched on - first use. Returns ``cli_path`` unchanged. - """ - from ._cli_download import ensure_runtime_library + self._cli_path_source = "downloaded" + return ensure_runtime_wrapper() - ensure_runtime_library(cli_path) - return cli_path + def _resolve_inprocess_runtime(self) -> str: + explicit_cli = os.environ.get("COPILOT_CLI_PATH") + if explicit_cli: + from ._cli_download import ensure_runtime_library + + runtime_path = ensure_runtime_library(explicit_cli) + if runtime_path is None: + raise RuntimeError( + f"In-process runtime library not found next to '{explicit_cli}'." + ) + self._cli_path_source = "environment" + self._inprocess_cli_entrypoint = explicit_cli + return runtime_path + + from ._cli_download import ensure_runtime_wrapper + + wrapper_path = Path(ensure_runtime_wrapper()) + self._cli_path_source = "downloaded" + return str(wrapper_path.with_name("runtime.node")) @property def rpc(self) -> ServerRpc: @@ -2209,6 +2251,7 @@ async def create_session( available_tools: list[str] | ToolSet | None = None, excluded_tools: list[str] | ToolSet | None = None, on_user_input_request: UserInputHandler | None = None, + ask_user_variant: AskUserVariant | None = None, hooks: SessionHooks | None = None, working_directory: str | None = None, additional_directories: list[str] | None = None, @@ -2271,6 +2314,7 @@ async def create_session( extension_info: ExtensionInfo | None = None, canvas_provider: CanvasProviderIdentity | None = None, canvas_handler: CanvasHandler | None = None, + feature_flags: dict[str, bool] | None = None, exp_assignments: CopilotExpAssignmentResponse | None = None, enable_managed_settings: bool | None = None, github_mcp_tool_config: GitHubMcpToolConfig | None = None, @@ -2311,10 +2355,16 @@ async def create_session( including custom tools registered via ``tools=``. Ignored if ``available_tools`` is set. on_user_input_request: Handler for user input requests. + ask_user_variant: Model-facing shape of the ``ask_user`` tool. + Accepted values are ``"legacy"`` and ``"elicitation"``. The + default is ``"legacy"``. To use ``"elicitation"``, also provide + ``on_elicitation_request`` so the host can answer structured forms. hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. - capi: CAPI provider-scoped options. WebSocket transport is the + capi: CAPI provider-scoped options. Set ``auto_tier`` to ``efficiency``, + ``balance``, or ``intelligence`` to select an Auto routing preference + on a runtime with Auto tier support. WebSocket transport is the default for the CAPI Responses API whenever the model advertises the ``ws:/responses`` endpoint. Set ``enable_web_socket_responses=False`` to force the HTTP @@ -2410,6 +2460,9 @@ async def create_session( on its own and has no effect unless MCP Apps are enabled for the session (see ``enable_mcp_apps``). Omitted from the wire payload entirely when None. + feature_flags: Feature-flag values resolved by the host for this + session. Re-supply them when resuming after a runtime restart. + Sent on the wire as ``featureFlags``. exp_assignments: ExP assignment ("flight") data injected by a trusted integrator, in the same JSON shape the Copilot CLI fetches from the experimentation service @@ -2465,6 +2518,8 @@ async def create_session( raise ValueError("on_permission_request must be callable when provided.") if github_token is not None and github_token_provider is not None: raise ValueError("github_token and github_token_provider are mutually exclusive") + if ask_user_variant not in (None, "legacy", "elicitation"): + raise ValueError('ask_user_variant must be "legacy" or "elicitation"') if not self._client: await self.start() @@ -2552,6 +2607,8 @@ async def create_session( # Enable user input request callback if handler provided if on_user_input_request: payload["requestUserInput"] = True + if ask_user_variant is not None: + payload["askUserVariant"] = ask_user_variant # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) @@ -2584,6 +2641,9 @@ async def create_session( if cloud is not None: payload["cloud"] = _cloud_session_options_to_dict(cloud) + if feature_flags is not None: + payload["featureFlags"] = feature_flags + # Add ExP assignment data if provided (trusted integrator) if exp_assignments is not None: payload["expAssignments"] = _exp_assignment_response_to_dict(exp_assignments) @@ -2970,6 +3030,7 @@ async def resume_session( available_tools: list[str] | ToolSet | None = None, excluded_tools: list[str] | ToolSet | None = None, on_user_input_request: UserInputHandler | None = None, + ask_user_variant: AskUserVariant | None = None, hooks: SessionHooks | None = None, working_directory: str | None = None, additional_directories: list[str] | None = None, @@ -3033,6 +3094,7 @@ async def resume_session( canvas_provider: CanvasProviderIdentity | None = None, canvas_handler: CanvasHandler | None = None, open_canvases: list[OpenCanvasInstance] | None = None, + feature_flags: dict[str, bool] | None = None, exp_assignments: CopilotExpAssignmentResponse | None = None, enable_managed_settings: bool | None = None, github_mcp_tool_config: GitHubMcpToolConfig | None = None, @@ -3073,10 +3135,17 @@ async def resume_session( including custom tools registered via ``tools=``. Ignored if ``available_tools`` is set. on_user_input_request: Handler for user input requests. + ask_user_variant: Model-facing shape of the ``ask_user`` tool. + Accepted values are ``"legacy"`` and ``"elicitation"``. The + default is ``"legacy"``. To use ``"elicitation"``, also provide + ``on_elicitation_request`` so the host can answer structured forms. hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. - capi: CAPI provider-scoped options. WebSocket transport is the + capi: CAPI provider-scoped options. Omit ``auto_tier`` to preserve the + current or persisted Auto routing preference. An explicit tier + overrides it on cold resume, but cannot change it on an + already-resident session. WebSocket transport is the default for the CAPI Responses API whenever the model advertises the ``ws:/responses`` endpoint. Set ``enable_web_socket_responses=False`` to force the HTTP @@ -3174,6 +3243,8 @@ async def resume_session( tool calls or permission prompts that were still pending when the session was last suspended. When False (the default), the runtime treats pending work as interrupted on resume. + feature_flags: Feature-flag values resolved by the host to apply + on resume. Sent on the wire as ``featureFlags``. exp_assignments: ExP assignment ("flight") data injected by a trusted integrator, in the same JSON shape the Copilot CLI fetches from the experimentation service @@ -3226,6 +3297,8 @@ async def resume_session( raise ValueError("on_permission_request must be callable when provided.") if github_token is not None and github_token_provider is not None: raise ValueError("github_token and github_token_provider are mutually exclusive") + if ask_user_variant not in (None, "legacy", "elicitation"): + raise ValueError('ask_user_variant must be "legacy" or "elicitation"') if not self._client: await self.start() @@ -3341,6 +3414,8 @@ async def resume_session( if on_user_input_request: payload["requestUserInput"] = True + if ask_user_variant is not None: + payload["askUserVariant"] = ask_user_variant # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) @@ -3368,6 +3443,9 @@ async def resume_session( if remote_session is not None: payload["remoteSession"] = remote_session.value + if feature_flags is not None: + payload["featureFlags"] = feature_flags + # Add ExP assignment data if provided (trusted integrator) if exp_assignments is not None: payload["expAssignments"] = _exp_assignment_response_to_dict(exp_assignments) @@ -4024,6 +4102,12 @@ async def _verify_protocol_version(self) -> None: # event is forwarded). Also sent on session.create/resume for older CLIs. if self._on_github_telemetry is not None: connect_params["enableGitHubTelemetryForwarding"] = True + # Declare the integrating application's identity so the runtime attributes + # the telemetry it emits on this connection to a consistent surface + # instead of its own build. Omitted when the app didn't supply it. + client_info = _client_info_to_wire(self._options.client_info) + if client_info is not None: + connect_params["clientInfo"] = client_info connect_result = _ConnectResult.from_dict( await self._client.request("connect", connect_params) ) @@ -4286,7 +4370,6 @@ async def _start_cli_server(self) -> None: env = dict(os.environ) else: env = dict(opts.env) - # Set auth token in environment if provided if opts.github_token: env["COPILOT_SDK_AUTH_TOKEN"] = opts.github_token @@ -4437,6 +4520,7 @@ async def _start_inprocess_ffi(self) -> None: host = FfiRuntimeHost.create( runtime_path, + cli_entrypoint=self._inprocess_cli_entrypoint, environment=environment or None, args=tuple(args), ) diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 1d59a55f4a..b393fb6eed 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -6,7 +6,7 @@ from typing import ClassVar, TYPE_CHECKING -from .session_events import AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity +from .session_events import AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity if TYPE_CHECKING: from .._jsonrpc import JsonRpcClient @@ -1051,6 +1051,11 @@ def to_dict(self) -> dict: class CapiSessionOptions: """Options scoped to the built-in CAPI (Copilot API) provider.""" + auto_tier: AutoTier | None = None + """Routing preference used when the session model is `auto`. The runtime persists the + preference across cold resume. When omitted, the default routing behavior is used. + Resuming an already-resident session cannot change its preference. + """ enable_web_socket_responses: bool | None = None """Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses @@ -1062,11 +1067,14 @@ class CapiSessionOptions: @staticmethod def from_dict(obj: Any) -> 'CapiSessionOptions': assert isinstance(obj, dict) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) enable_web_socket_responses = from_union([from_bool, from_none], obj.get("enableWebSocketResponses")) - return CapiSessionOptions(enable_web_socket_responses) + return CapiSessionOptions(auto_tier, enable_web_socket_responses) def to_dict(self) -> dict: result: dict = {} + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) if self.enable_web_socket_responses is not None: result["enableWebSocketResponses"] = from_union([from_bool, from_none], self.enable_web_socket_responses) return result @@ -1323,8 +1331,11 @@ class CatalogNetworkFailureReason(Enum): DNS = "dns" HTTP_STATUS = "http-status" OFFLINE = "offline" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" RESPONSE_TOO_LARGE = "response-too-large" + SERVICE_UNAVAILABLE = "service-unavailable" TIMEOUT = "timeout" TLS = "tls" @@ -1419,12 +1430,15 @@ class CatalogSearchResultReason(Enum): NO_CREDENTIAL = "no-credential" OFFLINE = "offline" PLANNING_UNAVAILABLE = "planning-unavailable" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" PROXY_REJECTED = "proxy-rejected" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" REDIRECT_TO_BLOCKED_ADDRESS = "redirect-to-blocked-address" RESPONSE_TOO_LARGE = "response-too-large" SCHEMA_VIOLATION = "schema-violation" SEARCH_UNAVAILABLE = "search-unavailable" + SERVICE_UNAVAILABLE = "service-unavailable" SIZE_LIMIT_EXCEEDED = "size-limit-exceeded" TIMEOUT = "timeout" TLS = "tls" @@ -2484,6 +2498,42 @@ def to_dict(self) -> dict: result["ids"] = from_list(from_str, self.ids) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class HookType(Enum): + """Hook event that invokes this action. + + Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally + support callback-only events. + """ + AGENT_STOP = "agentStop" + ERROR_OCCURRED = "errorOccurred" + NOTIFICATION = "notification" + PERMISSION_REQUEST = "permissionRequest" + POST_RESULT = "postResult" + POST_TOOL_USE = "postToolUse" + POST_TOOL_USE_FAILURE = "postToolUseFailure" + PRE_COMPACT = "preCompact" + PRE_MCP_TOOL_CALL = "preMcpToolCall" + PRE_PR_DESCRIPTION = "prePRDescription" + PRE_TOOL_USE = "preToolUse" + SESSION_END = "sessionEnd" + SESSION_START = "sessionStart" + SUBAGENT_START = "subagentStart" + SUBAGENT_STOP = "subagentStop" + USER_PROMPT_SUBMITTED = "userPromptSubmitted" + USER_PROMPT_TRANSFORMED = "userPromptTransformed" + +# Experimental: this type is part of an experimental API and may change or be removed. +class HookOrigin(Enum): + """Configuration tier that contributed this hook action. + + Configuration tier that contributed a discovered hook action. + """ + PLUGIN = "plugin" + POLICY = "policy" + REPOSITORY = "repository" + USER = "user" + # Experimental: this type is part of an experimental API and may change or be removed. class DiscoveredMCPServerType(Enum): """Server transport type: stdio, http, sse (deprecated), or memory""" @@ -2502,16 +2552,21 @@ class EnqueueCommandParams: """Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. """ + display_text: str | None = None + """Optional user-facing text for the queue row. The command string is shown when omitted.""" @staticmethod def from_dict(obj: Any) -> 'EnqueueCommandParams': assert isinstance(obj, dict) command = from_str(obj.get("command")) - return EnqueueCommandParams(command) + display_text = from_union([from_none, from_str], obj.get("displayText")) + return EnqueueCommandParams(command, display_text) def to_dict(self) -> dict: result: dict = {} result["command"] = from_str(self.command) + if self.display_text is not None: + result["displayText"] = from_union([from_none, from_str], self.display_text) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -2566,6 +2621,9 @@ class EventsReadDirection(Enum): cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. + + Direction to page through persisted history. Forward starts at the beginning; backward + starts with the newest events. Events in each page remain chronological. """ BACKWARD = "backward" FORWARD = "forward" @@ -3342,6 +3400,7 @@ class FactoryRunFailureType(Enum): FACTORY_ACCOUNTING_INCOMPLETE = "factory_accounting_incomplete" FACTORY_DURABLE_FAILURE = "factory_durable_failure" FACTORY_LIMIT_REACHED = "factory_limit_reached" + FACTORY_PROVIDER_DISCONNECTED = "factory_provider_disconnected" FACTORY_RESUME_DECLINED = "factory_resume_declined" # Experimental: this type is part of an experimental API and may change or be removed. @@ -3989,28 +4048,6 @@ def to_dict(self) -> dict: class HMACAuthInfoType(Enum): HMAC = "hmac" -# Internal: this type is an internal SDK API and is not part of the public surface. -class _HookType(Enum): - """Hook event name dispatched through the SDK callback transport.""" - - AGENT_STOP = "agentStop" - ERROR_OCCURRED = "errorOccurred" - NOTIFICATION = "notification" - PERMISSION_REQUEST = "permissionRequest" - POST_RESULT = "postResult" - POST_TOOL_USE = "postToolUse" - POST_TOOL_USE_FAILURE = "postToolUseFailure" - PRE_COMPACT = "preCompact" - PRE_MCP_TOOL_CALL = "preMcpToolCall" - PRE_PR_DESCRIPTION = "prePRDescription" - PRE_TOOL_USE = "preToolUse" - SESSION_END = "sessionEnd" - SESSION_START = "sessionStart" - SUBAGENT_START = "subagentStart" - SUBAGENT_STOP = "subagentStop" - USER_PROMPT_SUBMITTED = "userPromptSubmitted" - USER_PROMPT_TRANSFORMED = "userPromptTransformed" - # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass class _HookInvokeResponse: @@ -4030,6 +4067,38 @@ def to_dict(self) -> dict: result["output"] = self.output return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HooksDiscoverRequest: + """Optional project paths and host-exclusion behavior for server-scoped hook discovery.""" + + exclude_host_hooks: bool | None = None + """When true, omit host-owned user and plugin hook rows and their diagnostics. + Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks + still contribute to each remaining row's effective enabled state. This filters sources + rather than simulating a host with no settings. + """ + project_paths: list[str] | None = None + """Optional project directory paths whose trusted repository and project-expanded plugin + hooks should be discovered. When omitted or empty, user, managed-policy, and globally + enabled installed or explicit plugin hooks are returned without project expansion. + """ + + @staticmethod + def from_dict(obj: Any) -> 'HooksDiscoverRequest': + assert isinstance(obj, dict) + exclude_host_hooks = from_union([from_bool, from_none], obj.get("excludeHostHooks")) + project_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("projectPaths")) + return HooksDiscoverRequest(exclude_host_hooks, project_paths) + + def to_dict(self) -> dict: + result: dict = {} + if self.exclude_host_hooks is not None: + result["excludeHostHooks"] = from_union([from_bool, from_none], self.exclude_host_hooks) + if self.project_paths is not None: + result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) + return result + class InstalledPluginSourceURLSource(Enum): GITHUB = "github" LOCAL = "local" @@ -5576,28 +5645,6 @@ def to_dict(self) -> dict: result["serverName"] = from_str(self.server_name) return result -@dataclass -class ExternalRefMCPOauthHTTPResponse: - """HTTP response returned by the server. - - HTTP 401 or 403 response returned by the server. - - HTTP response returned by the server, when the probe reached the server and captured the - complete response. - """ - external_ref_marker_external_ref_mcp_oauth_http_response: str - - @staticmethod - def from_dict(obj: Any) -> 'ExternalRefMCPOauthHTTPResponse': - assert isinstance(obj, dict) - external_ref_marker_external_ref_mcp_oauth_http_response = from_str(obj.get("__externalRefMarker___ExternalRef_McpOauthHttpResponse")) - return ExternalRefMCPOauthHTTPResponse(external_ref_marker_external_ref_mcp_oauth_http_response) - - def to_dict(self) -> dict: - result: dict = {} - result["__externalRefMarker___ExternalRef_McpOauthHttpResponse"] = from_str(self.external_ref_marker_external_ref_mcp_oauth_http_response) - return result - class Status(Enum): AUTHENTICATED = "authenticated" FAILED = "failed" @@ -5745,7 +5792,9 @@ class MCPPlanInstallResultReason(Enum): OFFLINE = "offline" PLANNING_UNAVAILABLE = "planning-unavailable" POLICY_FORBIDS = "policy-forbids" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" PROXY_REJECTED = "proxy-rejected" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" REDIRECT_TO_BLOCKED_ADDRESS = "redirect-to-blocked-address" REMOTE_ENUMERATION_UNAVAILABLE = "remote-enumeration-unavailable" @@ -5753,6 +5802,7 @@ class MCPPlanInstallResultReason(Enum): RESPONSE_TOO_LARGE = "response-too-large" SCHEMA_VIOLATION = "schema-violation" SEARCH_UNAVAILABLE = "search-unavailable" + SERVICE_UNAVAILABLE = "service-unavailable" SIZE_LIMIT_EXCEEDED = "size-limit-exceeded" STALE = "stale" TIMEOUT = "timeout" @@ -6605,6 +6655,13 @@ class ModelBillingPromo: """Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. """ + show_banner: bool | None = None + """Whether the service asked hosts to give this promotion a prominent surface, such as a + dedicated banner, in addition to listing it with the model. `true` requests that surface + and `false` asks for the model list only. Absent means the service expressed no + preference — for example a response that predates the field — so hosts should apply their + own default rather than read it as `false`. + """ @staticmethod def from_dict(obj: Any) -> 'ModelBillingPromo': @@ -6613,7 +6670,8 @@ def from_dict(obj: Any) -> 'ModelBillingPromo': ends_at = from_union([from_str, from_none], obj.get("endsAt")) id = from_union([from_str, from_none], obj.get("id")) message = from_union([from_str, from_none], obj.get("message")) - return ModelBillingPromo(discount_percent, ends_at, id, message) + show_banner = from_union([from_bool, from_none], obj.get("showBanner")) + return ModelBillingPromo(discount_percent, ends_at, id, message, show_banner) def to_dict(self) -> dict: result: dict = {} @@ -6625,6 +6683,8 @@ def to_dict(self) -> dict: result["id"] = from_union([from_str, from_none], self.id) if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) + if self.show_banner is not None: + result["showBanner"] = from_union([from_bool, from_none], self.show_banner) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -9184,9 +9244,7 @@ class SessionsRegisterExtensionToolsOnSessionOptions: # Internal: this field is an internal SDK API and is not part of the public surface. enabled: Any = None - """In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: - replaced by runtime-side enable/disable RPCs in the SDK migration. - """ + """In-process `() => boolean` gating callback used only by the CLI.""" @staticmethod def from_dict(obj: Any) -> 'SessionsRegisterExtensionToolsOnSessionOptions': @@ -9200,6 +9258,26 @@ def to_dict(self) -> dict: result["enabled"] = self.enabled return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _RegisterExtensionToolsResult: + """Handle for releasing the extension tool registration.""" + + unsubscribe: Any + """In-process unsubscribe function used only by the CLI.""" + + @staticmethod + def from_dict(obj: Any) -> '_RegisterExtensionToolsResult': + assert isinstance(obj, dict) + unsubscribe = obj.get("unsubscribe") + return _RegisterExtensionToolsResult(unsubscribe) + + def to_dict(self) -> dict: + result: dict = {} + result["unsubscribe"] = self.unsubscribe + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ReleaseEventInterestParams: @@ -9579,22 +9657,25 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SandboxConfigUserPolicyNetworkProxy: - """HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - Credentials go in the separate `username`/`password` fields. A credential-free http:// - loopback proxy URL is routed through the localhost proxy automatically; an https:// or - authenticated loopback URL is used as-is. + """HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + accepted and routed through the IPv4 gateway), and does not support proxy credentials. + macOS relies on applications honoring proxy environment variables. Windows also + configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + networking stack. Configure supported credentials in the separate `username` and + `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + while an https:// or authenticated loopback URL uses the URL form. HTTP proxy configuration for sandboxed traffic. """ url: str """Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the - scheme's standard port when omitted. Credentials must not be embedded here — a - `user:pass@` authority is rejected; put them in the separate `username`/`password` - fields. A credential-free http:// loopback URL is routed through the localhost proxy - automatically; loopback covers localhost and any *.localhost subdomain, the whole - 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or - one with a username/password set, is used as-is. + scheme's standard port when omitted; an explicit port must be between 1 and 65535. + Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in + the separate `username`/`password` fields. A credential-free http:// loopback proxy URL + is routed through the localhost proxy automatically; loopback covers localhost and any + *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback + (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. """ password: str | None = None """Optional password for proxy authentication, combined with the URL at spawn time. The @@ -9664,6 +9745,36 @@ class _SandboxConfigSource(Enum): USER_DISABLED = "user_disabled" USER_ENABLED = "user_enabled" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxEnforcementStatus: + """Managed sandbox enforcement state for a session.""" + + blocked: bool + """Whether an enforcement failure has permanently blocked the session.""" + + required: bool + """Whether the effective managed policy requires an available sandbox backend.""" + + reason: str | None = None + """The first sandbox enforcement failure that blocked the session.""" + + @staticmethod + def from_dict(obj: Any) -> 'SandboxEnforcementStatus': + assert isinstance(obj, dict) + blocked = from_bool(obj.get("blocked")) + required = from_bool(obj.get("required")) + reason = from_union([from_str, from_none], obj.get("reason")) + return SandboxEnforcementStatus(blocked, required, reason) + + def to_dict(self) -> dict: + result: dict = {} + result["blocked"] = from_bool(self.blocked) + result["required"] = from_bool(self.required) + if self.reason is not None: + result["reason"] = from_union([from_str, from_none], self.reason) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ScheduleAddAtRequest: @@ -12614,6 +12725,114 @@ class SkillDiscoveryScope(Enum): PERSONAL_COPILOT = "personal-copilot" PROJECT = "project" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillProviderDescriptor: + """Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched + separately and lazily. + """ + description: str + """Description used in skill catalogs without fetching content.""" + + name: str + """Invocation and display name.""" + + argument_hint: str | None = None + """Optional freeform argument hint used by slash-command catalogs.""" + + disable_model_invocation: bool | None = None + """Whether model invocation is disabled. Defaults to false.""" + + user_invocable: bool | None = None + """Whether users may invoke the skill directly. Defaults to true.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillProviderDescriptor': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + name = from_str(obj.get("name")) + argument_hint = from_union([from_str, from_none], obj.get("argumentHint")) + disable_model_invocation = from_union([from_bool, from_none], obj.get("disableModelInvocation")) + user_invocable = from_union([from_bool, from_none], obj.get("userInvocable")) + return SkillProviderDescriptor(description, name, argument_hint, disable_model_invocation, user_invocable) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["name"] = from_str(self.name) + if self.argument_hint is not None: + result["argumentHint"] = from_union([from_str, from_none], self.argument_hint) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_bool, from_none], self.disable_model_invocation) + if self.user_invocable is not None: + result["userInvocable"] = from_union([from_bool, from_none], self.user_invocable) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillProviderListRequest: + """Identifies the target session.""" + + session_id: str + """Target session identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillProviderListRequest': + assert isinstance(obj, dict) + session_id = from_str(obj.get("sessionId")) + return SkillProviderListRequest(session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderReadRequest: + """Identifies one SDK-provided skill by invocation name.""" + + name: str + """Invocation name of the skill to read.""" + + session_id: str + """Target session identifier""" + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderReadRequest': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + session_id = from_str(obj.get("sessionId")) + return _SkillProviderReadRequest(name, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderReadResult: + """Complete text-only SKILL.md content returned by an SDK session's skill provider. Related + files and assets are not supported. + """ + markdown: str + """Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit.""" + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderReadResult': + assert isinstance(obj, dict) + markdown = from_str(obj.get("markdown")) + return _SkillProviderReadResult(markdown) + + def to_dict(self) -> dict: + result: dict = {} + result["markdown"] = from_str(self.markdown) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SkillsConfigSetSkillDisabledRequest: @@ -13697,6 +13916,42 @@ class UIElicitationSchemaPropertyNumberType(Enum): INTEGER = "integer" NUMBER = "number" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class UIEphemeralQueryRequest: + """Transient question to answer without adding it to conversation history.""" + + question: str + """Question to answer from the current conversation context.""" + + # Internal: this field is an internal SDK API and is not part of the public surface. + abort_signal: Any = None + """In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. + Internal and excluded from the public SDK surface. + """ + # Internal: this field is an internal SDK API and is not part of the public surface. + on_chunk: Any = None + """In-process streaming callback `(text) => void` invoked with each token as the model emits + it. Internal and excluded from the public SDK surface. + """ + + @staticmethod + def from_dict(obj: Any) -> 'UIEphemeralQueryRequest': + assert isinstance(obj, dict) + question = from_str(obj.get("question")) + abort_signal = obj.get("abortSignal") + on_chunk = obj.get("onChunk") + return UIEphemeralQueryRequest(question, abort_signal, on_chunk) + + def to_dict(self) -> dict: + result: dict = {} + result["question"] = from_str(self.question) + if self.abort_signal is not None: + result["abortSignal"] = self.abort_signal + if self.on_chunk is not None: + result["onChunk"] = self.on_chunk + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class UIEphemeralQueryResult: @@ -15037,8 +15292,9 @@ class CatalogSearchRequest: """Protocol version and capabilities the caller requires.""" query: str - """Free-text search query. Never written to logs or telemetry.""" - + """Free-text search query. Persisted as tool input for session continuity, but omitted from + telemetry. + """ kinds: list[CatalogCandidateKind] | None = None """Restrict results to these candidate kinds. When omitted, every kind the runtime supports is searched. @@ -15327,6 +15583,11 @@ class CatalogNetworkFailureError: reason: CatalogNetworkFailureReason """Categorised failure, low cardinality so it can be aggregated without carrying a URL.""" + retry_after_seconds: int | None = None + """Bounded cooldown in seconds before another catalog request should be attempted, when the + authority supplied a numeric Retry-After value or the runtime applied its documented + fallback. + """ status_code: int | None = None """HTTP status code, when the failure was a rejected response.""" @@ -15335,14 +15596,17 @@ def from_dict(obj: Any) -> 'CatalogNetworkFailureError': assert isinstance(obj, dict) message = from_str(obj.get("message")) reason = CatalogNetworkFailureReason(obj.get("reason")) + retry_after_seconds = from_union([from_int, from_none], obj.get("retryAfterSeconds")) status_code = from_union([from_int, from_none], obj.get("statusCode")) - return CatalogNetworkFailureError(message, reason, status_code) + return CatalogNetworkFailureError(message, reason, retry_after_seconds, status_code) def to_dict(self) -> dict: result: dict = {} result["kind"] = self.kind result["message"] = from_str(self.message) result["reason"] = to_enum(CatalogNetworkFailureReason, self.reason) + if self.retry_after_seconds is not None: + result["retryAfterSeconds"] = from_union([from_int, from_none], self.retry_after_seconds) if self.status_code is not None: result["statusCode"] = from_union([from_int, from_none], self.status_code) return result @@ -16223,6 +16487,132 @@ def to_dict(self) -> dict: result["plugin"] = from_union([lambda x: to_class(DiscoveredExtensionPlugin, x), from_none], self.plugin) return result +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _HookInvokeRequest: + """Runtime-owned wire payload for a server-to-client hook callback invocation.""" + + hook_type: HookType + input: Any + session_id: str + + @staticmethod + def from_dict(obj: Any) -> '_HookInvokeRequest': + assert isinstance(obj, dict) + hook_type = HookType(obj.get("hookType")) + input = obj.get("input") + session_id = from_str(obj.get("sessionId")) + return _HookInvokeRequest(hook_type, input, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["hookType"] = to_enum(HookType, self.hook_type) + result["input"] = self.input + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredHook: + """One server-discovered hook action from user, repository, plugin, or managed-policy + configuration. + """ + enabled: bool + """Whether this action is enabled under the server-side discovery settings. Concrete + sessions may differ because they can add session-specific directories, plugins, or trust. + False when its disable key is present in the user's disabled-hooks setting or disable-all + settings suppress the action. + """ + hook_type: HookType + """Hook event that invokes this action.""" + + id: str + """Deterministic identifier for this server-discovered action row. It remains stable while + the project, origin, source, event, action content, and duplicate ordinal are unchanged. + This is row identity, not the key persisted in disabledHooks. + """ + origin: HookOrigin + """Configuration tier that contributed this hook action.""" + + disable_key: str | None = None + """Durable content hash used by hook enablement. Identical actions may intentionally share + this key. Omitted when changing the user's disabled-hooks setting cannot change the + action's current server-discovered state, including managed-policy hooks, session-start + prompt actions, actions suppressed by disable-all settings, and projectless plugin + actions that require project-directory expansion. + """ + project_path: str | None = None + """Input project path for which this server-side action was resolved. Set on every row + returned for project-scoped discovery, including repeated user and policy actions. + """ + source: str | None = None + """Human-readable source label, such as a hook file path, settings source, or plugin name.""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredHook': + assert isinstance(obj, dict) + enabled = from_bool(obj.get("enabled")) + hook_type = HookType(obj.get("hookType")) + id = from_str(obj.get("id")) + origin = HookOrigin(obj.get("origin")) + disable_key = from_union([from_str, from_none], obj.get("disableKey")) + project_path = from_union([from_str, from_none], obj.get("projectPath")) + source = from_union([from_str, from_none], obj.get("source")) + return DiscoveredHook(enabled, hook_type, id, origin, disable_key, project_path, source) + + def to_dict(self) -> dict: + result: dict = {} + result["enabled"] = from_bool(self.enabled) + result["hookType"] = to_enum(HookType, self.hook_type) + result["id"] = from_str(self.id) + result["origin"] = to_enum(HookOrigin, self.origin) + if self.disable_key is not None: + result["disableKey"] = from_union([from_str, from_none], self.disable_key) + if self.project_path is not None: + result["projectPath"] = from_union([from_str, from_none], self.project_path) + if self.source is not None: + result["source"] = from_union([from_str, from_none], self.source) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionsReadPersistedEventsRequest: + """Pagination options for reading an inactive or active local session's persisted event + journal. + """ + session_id: str + """Session ID whose persisted event journal should be read.""" + + cursor: str | None = None + """Opaque cursor returned by a previous persisted-event read. Omit on the first call.""" + + direction: EventsReadDirection | None = None + """Direction to page through persisted history. Forward starts at the beginning; backward + starts with the newest events. Events in each page remain chronological. + """ + max: int | None = None + """Maximum number of events to return in this batch (1–1000, default 200).""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionsReadPersistedEventsRequest': + assert isinstance(obj, dict) + session_id = from_str(obj.get("sessionId")) + cursor = from_union([from_str, from_none], obj.get("cursor")) + direction = from_union([EventsReadDirection, from_none], obj.get("direction")) + max = from_union([from_int, from_none], obj.get("max")) + return SessionsReadPersistedEventsRequest(session_id, cursor, direction, max) + + def to_dict(self) -> dict: + result: dict = {} + result["sessionId"] = from_str(self.session_id) + if self.cursor is not None: + result["cursor"] = from_union([from_str, from_none], self.cursor) + if self.direction is not None: + result["direction"] = from_union([lambda x: to_enum(EventsReadDirection, x), from_none], self.direction) + if self.max is not None: + result["max"] = from_union([from_int, from_none], self.max) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class EventLogReadRequest: @@ -16894,9 +17284,12 @@ class FactoryRunFailure: Machine-readable factory run failure. - Machine-readable failure details for an errored run. + Machine-readable failure details for a halted or errored run. The run stopped because its usage accounting could not be completed. + + The extension that owns the factory disconnected while the run was executing, so the host + halted it. The run's journaled subagent results are preserved so a resume can reuse them. """ run_id: str """Factory run identifier. @@ -17498,30 +17891,6 @@ def to_dict(self) -> dict: result["mode"] = to_enum(HistoryRewindMode, self.mode) return result -# Internal: this type is an internal SDK API and is not part of the public surface. -@dataclass -class _HookInvokeRequest: - """Runtime-owned wire payload for a server-to-client hook callback invocation.""" - - hook_type: _HookType - input: Any - session_id: str - - @staticmethod - def from_dict(obj: Any) -> '_HookInvokeRequest': - assert isinstance(obj, dict) - hook_type = _HookType(obj.get("hookType")) - input = obj.get("input") - session_id = from_str(obj.get("sessionId")) - return _HookInvokeRequest(hook_type, input, session_id) - - def to_dict(self) -> dict: - result: dict = {} - result["hookType"] = to_enum(_HookType, self.hook_type) - result["input"] = self.input - result["sessionId"] = from_str(self.session_id) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class InstalledPluginSource: @@ -22278,6 +22647,11 @@ class QueuePendingItems: kind: QueuePendingItemsKind """Whether this item is a queued user message or a queued slash command / model change""" + message_id: str | None = None + """Stable identity of the queued user message. Present for message rows and absent for slash + commands and model changes. + """ + @staticmethod def from_dict(obj: Any) -> 'QueuePendingItems': assert isinstance(obj, dict) @@ -22285,7 +22659,8 @@ def from_dict(obj: Any) -> 'QueuePendingItems': display_text = from_str(obj.get("displayText")) id = from_str(obj.get("id")) kind = QueuePendingItemsKind(obj.get("kind")) - return QueuePendingItems(agent_mode, display_text, id, kind) + message_id = from_union([from_str, from_none], obj.get("messageId")) + return QueuePendingItems(agent_mode, display_text, id, kind, message_id) def to_dict(self) -> dict: result: dict = {} @@ -22293,6 +22668,8 @@ def to_dict(self) -> dict: result["displayText"] = from_str(self.display_text) result["id"] = from_str(self.id) result["kind"] = to_enum(QueuePendingItemsKind, self.kind) + if self.message_id is not None: + result["messageId"] = from_union([from_str, from_none], self.message_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -22302,10 +22679,8 @@ class _RegisterExtensionToolsParams: """Params to attach an extension loader's tools to a session.""" loader: Any - """In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is - excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, - extension discovery/launch moves entirely into the runtime — the CLI passes pure config - (search paths, disabled ids) via SessionOptions instead. + """In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK + surface. """ session_id: str """Session to register extension tools on.""" @@ -22557,11 +22932,14 @@ class SandboxConfigUserPolicyNetwork: """Whether outbound network traffic is allowed at all.""" proxy: SandboxConfigUserPolicyNetworkProxy | None = None - """HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - Credentials go in the separate `username`/`password` fields. A credential-free http:// - loopback proxy URL is routed through the localhost proxy automatically; an https:// or - authenticated loopback URL is used as-is. + """HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + accepted and routed through the IPv4 gateway), and does not support proxy credentials. + macOS relies on applications honoring proxy environment variables. Windows also + configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + networking stack. Configure supported credentials in the separate `username` and + `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + while an https:// or authenticated loopback URL uses the URL form. """ @staticmethod @@ -23364,7 +23742,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AgentInfo: - """Agent metadata, including identifiers, display details, source, tools, model, MCP + """Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. The newly selected custom agent @@ -23391,6 +23769,13 @@ class AgentInfo: """Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. """ + model_policy: AgentModelPolicy | None = None + """Whether authored models are preferences or required constraints.""" + + models: list[str] | None = None + """Authored preferred model ids for this agent, in priority order. Runtime model selection + chooses the first available model; omitted means no authored preference. + """ path: str | None = None """Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. @@ -23422,13 +23807,15 @@ def from_dict(obj: Any) -> 'AgentInfo': name = from_str(obj.get("name")) mcp_servers = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("mcpServers")) model = from_union([from_str, from_none], obj.get("model")) + model_policy = from_union([AgentModelPolicy, from_none], obj.get("modelPolicy")) + models = from_union([lambda x: from_list(from_str, x), from_none], obj.get("models")) path = from_union([from_str, from_none], obj.get("path")) prompt = from_union([from_str, from_none], obj.get("prompt")) skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("skills")) source = from_union([AgentInfoSource, from_none], obj.get("source")) tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tools")) user_invocable = from_union([from_bool, from_none], obj.get("userInvocable")) - return AgentInfo(description, display_name, id, name, mcp_servers, model, path, prompt, skills, source, tools, user_invocable) + return AgentInfo(description, display_name, id, name, mcp_servers, model, model_policy, models, path, prompt, skills, source, tools, user_invocable) def to_dict(self) -> dict: result: dict = {} @@ -23440,6 +23827,10 @@ def to_dict(self) -> dict: result["mcpServers"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.mcp_servers) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([lambda x: to_enum(AgentModelPolicy, x), from_none], self.model_policy) + if self.models is not None: + result["models"] = from_union([lambda x: from_list(from_str, x), from_none], self.models) if self.path is not None: result["path"] = from_union([from_str, from_none], self.path) if self.prompt is not None: @@ -23507,11 +23898,15 @@ class SkillsInvokedSkill: """Unique identifier for the skill""" path: str - """Path to the SKILL.md file""" - + """Path to the SKILL.md file, or an empty string for an SDK-provided skill without a + filesystem identity + """ allowed_tools: list[str] | None = None """Tools that should be auto-approved when this skill is active, captured at invocation time""" + disable_model_invocation: bool | None = None + """Whether model invocation was disabled when this skill was invoked""" + @staticmethod def from_dict(obj: Any) -> 'SkillsInvokedSkill': assert isinstance(obj, dict) @@ -23520,7 +23915,8 @@ def from_dict(obj: Any) -> 'SkillsInvokedSkill': name = from_str(obj.get("name")) path = from_str(obj.get("path")) allowed_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("allowedTools")) - return SkillsInvokedSkill(content, invoked_at_turn, name, path, allowed_tools) + disable_model_invocation = from_union([from_bool, from_none], obj.get("disableModelInvocation")) + return SkillsInvokedSkill(content, invoked_at_turn, name, path, allowed_tools, disable_model_invocation) def to_dict(self) -> dict: result: dict = {} @@ -23530,6 +23926,8 @@ def to_dict(self) -> dict: result["path"] = from_str(self.path) if self.allowed_tools is not None: result["allowedTools"] = from_union([lambda x: from_list(from_str, x), from_none], self.allowed_tools) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_bool, from_none], self.disable_model_invocation) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -23570,6 +23968,29 @@ def to_dict(self) -> dict: result["projectPath"] = from_union([from_str, from_none], self.project_path) return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderListResult: + """Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to + 1024 descriptors and 1 MiB of aggregate metadata. + """ + skills: list[SkillProviderDescriptor] + """Skill descriptors in provider order. Invocation names must be unique under + case-insensitive comparison. + """ + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderListResult': + assert isinstance(obj, dict) + skills = from_list(SkillProviderDescriptor.from_dict, obj.get("skills")) + return _SkillProviderListResult(skills) + + def to_dict(self) -> dict: + result: dict = {} + result["skills"] = from_list(lambda x: to_class(SkillProviderDescriptor, x), self.skills) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandAddTimelineEntryResult: @@ -23664,6 +24085,9 @@ class SlashCommandCompletedResult: message: str | None = None """Optional user-facing message describing the completed command""" + mode: SessionMode | None = None + """Optional target session mode applied without submitting an agent prompt""" + runtime_settings_changed: bool | None = None """True when the invocation mutated user runtime settings; consumers caching settings should refresh @@ -23673,14 +24097,17 @@ class SlashCommandCompletedResult: def from_dict(obj: Any) -> 'SlashCommandCompletedResult': assert isinstance(obj, dict) message = from_union([from_str, from_none], obj.get("message")) + mode = from_union([SessionMode, from_none], obj.get("mode")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandCompletedResult(message, runtime_settings_changed) + return SlashCommandCompletedResult(message, mode, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} result["kind"] = self.kind if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode) if self.runtime_settings_changed is not None: result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) return result @@ -25495,6 +25922,43 @@ def to_dict(self) -> dict: result["mode"] = to_enum(DiscoveredExtensionMode, self.mode) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HooksDiscoverResult: + """Server-discovered hook actions and partial-load diagnostics from user, repository, + plugin, and managed-policy sources. Concrete sessions may include additional + session-specific hook sources. + """ + errors: list[str] + """Errors for hook sources or actions that could not be loaded, making the result partially + incomplete. Other valid actions are still returned. Project-resolution and + repository-settings errors are prefixed with their project path. + """ + hooks: list[DiscoveredHook] + """All discovered hook actions. Byte-identical actions remain separate rows even when they + share a disable key. + """ + warnings: list[str] + """Non-fatal source-loading warnings. Discovery remains complete for the affected source, + although the source had a recoverable issue. Repository-settings warnings are prefixed + with their project path when attribution is available. + """ + + @staticmethod + def from_dict(obj: Any) -> 'HooksDiscoverResult': + assert isinstance(obj, dict) + errors = from_list(from_str, obj.get("errors")) + hooks = from_list(DiscoveredHook.from_dict, obj.get("hooks")) + warnings = from_list(from_str, obj.get("warnings")) + return HooksDiscoverResult(errors, hooks, warnings) + + def to_dict(self) -> dict: + result: dict = {} + result["errors"] = from_list(from_str, self.errors) + result["hooks"] = from_list(lambda x: to_class(DiscoveredHook, x), self.hooks) + result["warnings"] = from_list(from_str, self.warnings) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ExtensionList: @@ -25967,11 +26431,15 @@ class FactoryRunResult: status: FactoryRunStatus """Current or terminal factory run status.""" + attempt: int | None = None + """One-based execution attempt represented by this envelope. Absent before the first attempt + starts or when returned by an older runtime. + """ error: str | None = None """Error message for an errored run.""" failure: FactoryRunFailure | None = None - """Machine-readable failure details for an errored run.""" + """Machine-readable failure details for a halted or errored run.""" reason: str | None = None """Reason for a halted or cancelled run.""" @@ -25987,17 +26455,20 @@ def from_dict(obj: Any) -> 'FactoryRunResult': assert isinstance(obj, dict) run_id = from_str(obj.get("runId")) status = FactoryRunStatus(obj.get("status")) + attempt = from_union([from_int, from_none], obj.get("attempt")) error = from_union([from_str, from_none], obj.get("error")) failure = from_union([FactoryRunFailure.from_dict, from_none], obj.get("failure")) reason = from_union([from_str, from_none], obj.get("reason")) result = obj.get("result") snapshot = obj.get("snapshot") - return FactoryRunResult(run_id, status, error, failure, reason, result, snapshot) + return FactoryRunResult(run_id, status, attempt, error, failure, reason, result, snapshot) def to_dict(self) -> dict: result: dict = {} result["runId"] = from_str(self.run_id) result["status"] = to_enum(FactoryRunStatus, self.status) + if self.attempt is not None: + result["attempt"] = from_union([from_int, from_none], self.attempt) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) if self.failure is not None: @@ -30840,6 +31311,10 @@ class SessionOpenOptions: enable_script_safety: bool | None = None """Whether shell-script safety heuristics are enabled.""" + enable_skills: bool | None = None + """Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by + default. + """ enable_streaming: bool | None = None """Whether model responses stream as delta events.""" @@ -30870,6 +31345,14 @@ class SessionOpenOptions: feature_flags: dict[str, bool] | None = None """Feature-flag values resolved by the host.""" + # Internal: this field is an internal SDK API and is not part of the public surface. + has_skill_provider: bool | None = None + """Whether the requesting SDK session has a skill provider. The provider remains ephemeral + and is never persisted in session options or history. When enableSkills is false, it + remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw + sessions.open flows reject it because they cannot safely pre-register the callback + handler. + """ included_builtin_agents: list[str] | None = None """Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the @@ -31018,6 +31501,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': enable_managed_settings = from_union([from_bool, from_none], obj.get("enableManagedSettings")) enable_on_demand_instruction_discovery = from_union([from_bool, from_none], obj.get("enableOnDemandInstructionDiscovery")) enable_script_safety = from_union([from_bool, from_none], obj.get("enableScriptSafety")) + enable_skills = from_union([from_bool, from_none], obj.get("enableSkills")) enable_streaming = from_union([from_bool, from_none], obj.get("enableStreaming")) env_value_mode = from_union([MCPSetEnvValueModeDetails, from_none], obj.get("envValueMode")) events_log_directory = from_union([from_str, from_none], obj.get("eventsLogDirectory")) @@ -31026,6 +31510,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': excluded_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("excludedTools")) exp_assignments = obj.get("expAssignments") feature_flags = from_union([lambda x: from_dict(from_bool, x), from_none], obj.get("featureFlags")) + has_skill_provider = from_union([from_bool, from_none], obj.get("hasSkillProvider")) included_builtin_agents = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinAgents")) included_builtin_skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinSkills")) installed_plugins = from_union([lambda x: from_list(InstalledPlugin.from_dict, x), from_none], obj.get("installedPlugins")) @@ -31062,7 +31547,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) working_directory_context = from_union([SessionContext.from_dict, from_none], obj.get("workingDirectoryContext")) - return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) + return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_skills, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, has_skill_provider, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) def to_dict(self) -> dict: result: dict = {} @@ -31116,6 +31601,8 @@ def to_dict(self) -> dict: result["enableOnDemandInstructionDiscovery"] = from_union([from_bool, from_none], self.enable_on_demand_instruction_discovery) if self.enable_script_safety is not None: result["enableScriptSafety"] = from_union([from_bool, from_none], self.enable_script_safety) + if self.enable_skills is not None: + result["enableSkills"] = from_union([from_bool, from_none], self.enable_skills) if self.enable_streaming is not None: result["enableStreaming"] = from_union([from_bool, from_none], self.enable_streaming) if self.env_value_mode is not None: @@ -31132,6 +31619,8 @@ def to_dict(self) -> dict: result["expAssignments"] = self.exp_assignments if self.feature_flags is not None: result["featureFlags"] = from_union([lambda x: from_dict(from_bool, x), from_none], self.feature_flags) + if self.has_skill_provider is not None: + result["hasSkillProvider"] = from_union([from_bool, from_none], self.has_skill_provider) if self.included_builtin_agents is not None: result["includedBuiltinAgents"] = from_union([lambda x: from_list(from_str, x), from_none], self.included_builtin_agents) if self.included_builtin_skills is not None: @@ -31279,8 +31768,9 @@ class SessionUpdateOptionsParams: """Whether to enable cross-session store writes and reads.""" enable_skills: bool | None = None - """Whether to enable skill directory scanning and loading. Falls back to - enableConfigDiscovery when unset. + """Whether skill loading is enabled. Explicit false disables every source, including a bound + SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, + creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. """ enable_streaming: bool | None = None """Whether to stream model responses.""" @@ -32942,7 +33432,7 @@ class MCPOauthProbeResult: status: Status """Probe outcome variant discriminator.""" - http_response: ExternalRefMCPOauthHTTPResponse | None = None + http_response: McpOauthHttpResponse | None = None """HTTP response returned by the server. HTTP 401 or 403 response returned by the server. @@ -32963,7 +33453,7 @@ class MCPOauthProbeResult: def from_dict(obj: Any) -> 'MCPOauthProbeResult': assert isinstance(obj, dict) status = Status(obj.get("status")) - http_response = from_union([ExternalRefMCPOauthHTTPResponse.from_dict, from_none], obj.get("httpResponse")) + http_response = from_union([McpOauthHttpResponse.from_dict, from_none], obj.get("httpResponse")) reason = from_union([MCPOauthProbeNeedsAuthReason, from_none], obj.get("reason")) www_authenticate_params = from_union([McpOauthWWWAuthenticateParams.from_dict, from_none], obj.get("wwwAuthenticateParams")) error = from_union([from_str, from_none], obj.get("error")) @@ -32973,7 +33463,7 @@ def to_dict(self) -> dict: result: dict = {} result["status"] = to_enum(Status, self.status) if self.http_response is not None: - result["httpResponse"] = from_union([lambda x: to_class(ExternalRefMCPOauthHTTPResponse, x), from_none], self.http_response) + result["httpResponse"] = from_union([lambda x: to_class(McpOauthHttpResponse, x), from_none], self.http_response) if self.reason is not None: result["reason"] = from_union([lambda x: to_enum(MCPOauthProbeNeedsAuthReason, x), from_none], self.reason) if self.www_authenticate_params is not None: @@ -33891,6 +34381,11 @@ class ModelApplyStartupOverlayRequest: device_managed_model: str | None = None """Model required by device-managed policy, when configured.""" + policy_helper_model: str | None = None + """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. + """ repo_context_tier: str | None = None """Context tier selected by repository settings, when configured.""" @@ -33909,11 +34404,12 @@ def from_dict(obj: Any) -> 'ModelApplyStartupOverlayRequest': cli_model = from_union([from_str, from_none], obj.get("cliModel")) deferred_resume = from_union([from_bool, from_none], obj.get("deferredResume")) device_managed_model = from_union([from_str, from_none], obj.get("deviceManagedModel")) + policy_helper_model = from_union([from_str, from_none], obj.get("policyHelperModel")) repo_context_tier = from_union([from_str, from_none], obj.get("repoContextTier")) repo_model = from_union([from_str, from_none], obj.get("repoModel")) repo_reasoning_effort = from_union([from_str, from_none], obj.get("repoReasoningEffort")) server_managed_model = from_union([from_str, from_none], obj.get("serverManagedModel")) - return ModelApplyStartupOverlayRequest(cli_model, deferred_resume, device_managed_model, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model) + return ModelApplyStartupOverlayRequest(cli_model, deferred_resume, device_managed_model, policy_helper_model, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model) def to_dict(self) -> dict: result: dict = {} @@ -33923,6 +34419,8 @@ def to_dict(self) -> dict: result["deferredResume"] = from_union([from_bool, from_none], self.deferred_resume) if self.device_managed_model is not None: result["deviceManagedModel"] = from_union([from_str, from_none], self.device_managed_model) + if self.policy_helper_model is not None: + result["policyHelperModel"] = from_union([from_str, from_none], self.policy_helper_model) if self.repo_context_tier is not None: result["repoContextTier"] = from_union([from_str, from_none], self.repo_context_tier) if self.repo_model is not None: @@ -34006,8 +34504,8 @@ class ModelSwitchToRequest: """When true, evaluate context-window compaction policy before applying the switch.""" source: ModelChangeSource | None = None - """Origin to record on the effective `session.model_change` event. Defaults to `sdk` when - omitted. + """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. """ verbosity: Verbosity | None = None """Output verbosity level to request for supported models""" @@ -34202,28 +34700,6 @@ def to_dict(self) -> dict: result["title"] = from_union([from_str, from_none], self.title) return result -# Experimental: this type is part of an experimental API and may change or be removed. -# Internal: this type is an internal SDK API and is not part of the public surface. -@dataclass -class _RegisterExtensionToolsResult: - """Handle for releasing the extension tool registration.""" - - unsubscribe: Any - """In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an - explicit `extensions.unregister` RPC in the SDK migration. - """ - - @staticmethod - def from_dict(obj: Any) -> '_RegisterExtensionToolsResult': - assert isinstance(obj, dict) - unsubscribe = obj.get("unsubscribe") - return _RegisterExtensionToolsResult(unsubscribe) - - def to_dict(self) -> dict: - result: dict = {} - result["unsubscribe"] = self.unsubscribe - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionLimitPredictionDetails: @@ -34484,10 +34960,8 @@ class SessionsOpenCloud: # Internal: this field is an internal SDK API and is not part of the public surface. on_task_created: Any = None - """In-process callback invoked when the cloud task is created (before connection). Marked - internal because a function reference cannot cross the JSON-RPC boundary. Disappears in - the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from - 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. + """In-process callback invoked when the cloud task is created, before connection. Internal + because function references cannot cross the JSON-RPC boundary. """ options: SessionOpenOptions | None = None """Session options for cloud session creation.""" @@ -34756,13 +35230,17 @@ class SubagentSettingsEntry: model: str | None = None """Model override for matching subagents""" + model_policy: AgentModelPolicy | None = None + """Whether the configured model strategy is preferred or required""" + @staticmethod def from_dict(obj: Any) -> 'SubagentSettingsEntry': assert isinstance(obj, dict) context_tier = from_union([SubagentSettingsEntryContextTier, from_none], obj.get("contextTier")) effort_level = from_union([from_str, from_none], obj.get("effortLevel")) model = from_union([from_str, from_none], obj.get("model")) - return SubagentSettingsEntry(context_tier, effort_level, model) + model_policy = from_union([AgentModelPolicy, from_none], obj.get("modelPolicy")) + return SubagentSettingsEntry(context_tier, effort_level, model, model_policy) def to_dict(self) -> dict: result: dict = {} @@ -34772,6 +35250,8 @@ def to_dict(self) -> dict: result["effortLevel"] = from_union([from_str, from_none], self.effort_level) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([lambda x: to_enum(AgentModelPolicy, x), from_none], self.model_policy) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -34949,44 +35429,6 @@ def to_dict(self) -> dict: result["tools"] = from_list(lambda x: to_class(ProtocolExternalToolDefinition, x), self.tools) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class UIEphemeralQueryRequest: - """Transient question to answer without adding it to conversation history.""" - - question: str - """Question to answer from the current conversation context.""" - - # Internal: this field is an internal SDK API and is not part of the public surface. - abort_signal: Any = None - """In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. - Marked internal: excluded from the public SDK surface. Replaced by an explicit - cancellation token + cancel RPC in the SDK migration. - """ - # Internal: this field is an internal SDK API and is not part of the public surface. - on_chunk: Any = None - """In-process streaming callback `(text) => void` invoked with each token as the model emits - it. Marked internal: excluded from the public SDK surface. In a process-separated SDK - this is replaced by a streaming RPC that yields chunks and a final answer. - """ - - @staticmethod - def from_dict(obj: Any) -> 'UIEphemeralQueryRequest': - assert isinstance(obj, dict) - question = from_str(obj.get("question")) - abort_signal = obj.get("abortSignal") - on_chunk = obj.get("onChunk") - return UIEphemeralQueryRequest(question, abort_signal, on_chunk) - - def to_dict(self) -> dict: - result: dict = {} - result["question"] = from_str(self.question) - if self.abort_signal is not None: - result["abortSignal"] = self.abort_signal - if self.on_chunk is not None: - result["onChunk"] = self.on_chunk - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class UpdateSubagentSettingsRequest: @@ -35226,6 +35668,7 @@ class RPC: discovered_extensions_disable_request: DiscoveredExtensionsDisableRequest discovered_extensions_enable_request: DiscoveredExtensionsEnableRequest discovered_extension_source: DiscoveredExtensionSource + discovered_hook: DiscoveredHook discovered_mcp_server: DiscoveredMCPServer discovered_mcp_server_type: DiscoveredMCPServerType enqueue_command_params: EnqueueCommandParams @@ -35348,7 +35791,10 @@ class RPC: hmac_auth_info: HMACAuthInfo hook_invoke_request: _HookInvokeRequest hook_invoke_response: _HookInvokeResponse - hook_type: _HookType + hook_origin: HookOrigin + hooks_discover_request: HooksDiscoverRequest + hooks_discover_result: HooksDiscoverResult + hook_type: HookType installed_plugin: InstalledPlugin installed_plugin_info: InstalledPluginInfo installed_plugin_source: InstalledPluginSource | str @@ -35849,6 +36295,7 @@ class RPC: sandbox_config_user_policy_network: SandboxConfigUserPolicyNetwork sandbox_config_user_policy_network_proxy: SandboxConfigUserPolicyNetworkProxy sandbox_config_user_policy_seatbelt: SandboxConfigUserPolicySeatbelt + sandbox_enforcement_status: SandboxEnforcementStatus schedule_add_at_request: ScheduleAddAtRequest schedule_add_cron_request: ScheduleAddCronRequest schedule_add_request: ScheduleAddRequest @@ -36020,6 +36467,7 @@ class RPC: sessions_open_status: SessionsOpenStatus session_source: SessionSource sessions_prune_old_request: SessionsPruneOldRequest + sessions_read_persisted_events_request: SessionsReadPersistedEventsRequest sessions_register_extension_tools_on_session_options: SessionsRegisterExtensionToolsOnSessionOptions sessions_release_lock_request: SessionsReleaseLockRequest sessions_release_lock_result: SessionsReleaseLockResult @@ -36059,6 +36507,11 @@ class RPC: skill_discovery_path_list: SkillDiscoveryPathList skill_discovery_scope: SkillDiscoveryScope skill_list: SkillList + skill_provider_descriptor: SkillProviderDescriptor + skill_provider_list_request: SkillProviderListRequest + skill_provider_list_result: _SkillProviderListResult + skill_provider_read_request: _SkillProviderReadRequest + skill_provider_read_result: _SkillProviderReadResult skills_config_set_disabled_skills_request: SkillsConfigSetDisabledSkillsRequest skills_config_set_skill_disabled_request: SkillsConfigSetSkillDisabledRequest skills_disable_request: SkillsDisableRequest @@ -36409,6 +36862,7 @@ def from_dict(obj: Any) -> 'RPC': discovered_extensions_disable_request = DiscoveredExtensionsDisableRequest.from_dict(obj.get("DiscoveredExtensionsDisableRequest")) discovered_extensions_enable_request = DiscoveredExtensionsEnableRequest.from_dict(obj.get("DiscoveredExtensionsEnableRequest")) discovered_extension_source = DiscoveredExtensionSource(obj.get("DiscoveredExtensionSource")) + discovered_hook = DiscoveredHook.from_dict(obj.get("DiscoveredHook")) discovered_mcp_server = DiscoveredMCPServer.from_dict(obj.get("DiscoveredMcpServer")) discovered_mcp_server_type = DiscoveredMCPServerType(obj.get("DiscoveredMcpServerType")) enqueue_command_params = EnqueueCommandParams.from_dict(obj.get("EnqueueCommandParams")) @@ -36531,7 +36985,10 @@ def from_dict(obj: Any) -> 'RPC': hmac_auth_info = HMACAuthInfo.from_dict(obj.get("HMACAuthInfo")) hook_invoke_request = _HookInvokeRequest.from_dict(obj.get("HookInvokeRequest")) hook_invoke_response = _HookInvokeResponse.from_dict(obj.get("HookInvokeResponse")) - hook_type = _HookType(obj.get("HookType")) + hook_origin = HookOrigin(obj.get("HookOrigin")) + hooks_discover_request = HooksDiscoverRequest.from_dict(obj.get("HooksDiscoverRequest")) + hooks_discover_result = HooksDiscoverResult.from_dict(obj.get("HooksDiscoverResult")) + hook_type = HookType(obj.get("HookType")) installed_plugin = InstalledPlugin.from_dict(obj.get("InstalledPlugin")) installed_plugin_info = InstalledPluginInfo.from_dict(obj.get("InstalledPluginInfo")) installed_plugin_source = from_union([InstalledPluginSource.from_dict, from_str], obj.get("InstalledPluginSource")) @@ -37032,6 +37489,7 @@ def from_dict(obj: Any) -> 'RPC': sandbox_config_user_policy_network = SandboxConfigUserPolicyNetwork.from_dict(obj.get("SandboxConfigUserPolicyNetwork")) sandbox_config_user_policy_network_proxy = SandboxConfigUserPolicyNetworkProxy.from_dict(obj.get("SandboxConfigUserPolicyNetworkProxy")) sandbox_config_user_policy_seatbelt = SandboxConfigUserPolicySeatbelt.from_dict(obj.get("SandboxConfigUserPolicySeatbelt")) + sandbox_enforcement_status = SandboxEnforcementStatus.from_dict(obj.get("SandboxEnforcementStatus")) schedule_add_at_request = ScheduleAddAtRequest.from_dict(obj.get("ScheduleAddAtRequest")) schedule_add_cron_request = ScheduleAddCronRequest.from_dict(obj.get("ScheduleAddCronRequest")) schedule_add_request = ScheduleAddRequest.from_dict(obj.get("ScheduleAddRequest")) @@ -37203,6 +37661,7 @@ def from_dict(obj: Any) -> 'RPC': sessions_open_status = SessionsOpenStatus(obj.get("SessionsOpenStatus")) session_source = SessionSource(obj.get("SessionSource")) sessions_prune_old_request = SessionsPruneOldRequest.from_dict(obj.get("SessionsPruneOldRequest")) + sessions_read_persisted_events_request = SessionsReadPersistedEventsRequest.from_dict(obj.get("SessionsReadPersistedEventsRequest")) sessions_register_extension_tools_on_session_options = SessionsRegisterExtensionToolsOnSessionOptions.from_dict(obj.get("SessionsRegisterExtensionToolsOnSessionOptions")) sessions_release_lock_request = SessionsReleaseLockRequest.from_dict(obj.get("SessionsReleaseLockRequest")) sessions_release_lock_result = SessionsReleaseLockResult.from_dict(obj.get("SessionsReleaseLockResult")) @@ -37242,6 +37701,11 @@ def from_dict(obj: Any) -> 'RPC': skill_discovery_path_list = SkillDiscoveryPathList.from_dict(obj.get("SkillDiscoveryPathList")) skill_discovery_scope = SkillDiscoveryScope(obj.get("SkillDiscoveryScope")) skill_list = SkillList.from_dict(obj.get("SkillList")) + skill_provider_descriptor = SkillProviderDescriptor.from_dict(obj.get("SkillProviderDescriptor")) + skill_provider_list_request = SkillProviderListRequest.from_dict(obj.get("SkillProviderListRequest")) + skill_provider_list_result = _SkillProviderListResult.from_dict(obj.get("SkillProviderListResult")) + skill_provider_read_request = _SkillProviderReadRequest.from_dict(obj.get("SkillProviderReadRequest")) + skill_provider_read_result = _SkillProviderReadResult.from_dict(obj.get("SkillProviderReadResult")) skills_config_set_disabled_skills_request = SkillsConfigSetDisabledSkillsRequest.from_dict(obj.get("SkillsConfigSetDisabledSkillsRequest")) skills_config_set_skill_disabled_request = SkillsConfigSetSkillDisabledRequest.from_dict(obj.get("SkillsConfigSetSkillDisabledRequest")) skills_disable_request = SkillsDisableRequest.from_dict(obj.get("SkillsDisableRequest")) @@ -37410,7 +37874,7 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_external_tool_defer, protocol_external_tool_definition, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) + return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_external_tool_defer, protocol_external_tool_definition, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -37592,6 +38056,7 @@ def to_dict(self) -> dict: result["DiscoveredExtensionsDisableRequest"] = to_class(DiscoveredExtensionsDisableRequest, self.discovered_extensions_disable_request) result["DiscoveredExtensionsEnableRequest"] = to_class(DiscoveredExtensionsEnableRequest, self.discovered_extensions_enable_request) result["DiscoveredExtensionSource"] = to_enum(DiscoveredExtensionSource, self.discovered_extension_source) + result["DiscoveredHook"] = to_class(DiscoveredHook, self.discovered_hook) result["DiscoveredMcpServer"] = to_class(DiscoveredMCPServer, self.discovered_mcp_server) result["DiscoveredMcpServerType"] = to_enum(DiscoveredMCPServerType, self.discovered_mcp_server_type) result["EnqueueCommandParams"] = to_class(EnqueueCommandParams, self.enqueue_command_params) @@ -37714,7 +38179,10 @@ def to_dict(self) -> dict: result["HMACAuthInfo"] = to_class(HMACAuthInfo, self.hmac_auth_info) result["HookInvokeRequest"] = to_class(_HookInvokeRequest, self.hook_invoke_request) result["HookInvokeResponse"] = to_class(_HookInvokeResponse, self.hook_invoke_response) - result["HookType"] = to_enum(_HookType, self.hook_type) + result["HookOrigin"] = to_enum(HookOrigin, self.hook_origin) + result["HooksDiscoverRequest"] = to_class(HooksDiscoverRequest, self.hooks_discover_request) + result["HooksDiscoverResult"] = to_class(HooksDiscoverResult, self.hooks_discover_result) + result["HookType"] = to_enum(HookType, self.hook_type) result["InstalledPlugin"] = to_class(InstalledPlugin, self.installed_plugin) result["InstalledPluginInfo"] = to_class(InstalledPluginInfo, self.installed_plugin_info) result["InstalledPluginSource"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str], self.installed_plugin_source) @@ -38215,6 +38683,7 @@ def to_dict(self) -> dict: result["SandboxConfigUserPolicyNetwork"] = to_class(SandboxConfigUserPolicyNetwork, self.sandbox_config_user_policy_network) result["SandboxConfigUserPolicyNetworkProxy"] = to_class(SandboxConfigUserPolicyNetworkProxy, self.sandbox_config_user_policy_network_proxy) result["SandboxConfigUserPolicySeatbelt"] = to_class(SandboxConfigUserPolicySeatbelt, self.sandbox_config_user_policy_seatbelt) + result["SandboxEnforcementStatus"] = to_class(SandboxEnforcementStatus, self.sandbox_enforcement_status) result["ScheduleAddAtRequest"] = to_class(ScheduleAddAtRequest, self.schedule_add_at_request) result["ScheduleAddCronRequest"] = to_class(ScheduleAddCronRequest, self.schedule_add_cron_request) result["ScheduleAddRequest"] = to_class(ScheduleAddRequest, self.schedule_add_request) @@ -38386,6 +38855,7 @@ def to_dict(self) -> dict: result["SessionsOpenStatus"] = to_enum(SessionsOpenStatus, self.sessions_open_status) result["SessionSource"] = to_enum(SessionSource, self.session_source) result["SessionsPruneOldRequest"] = to_class(SessionsPruneOldRequest, self.sessions_prune_old_request) + result["SessionsReadPersistedEventsRequest"] = to_class(SessionsReadPersistedEventsRequest, self.sessions_read_persisted_events_request) result["SessionsRegisterExtensionToolsOnSessionOptions"] = to_class(SessionsRegisterExtensionToolsOnSessionOptions, self.sessions_register_extension_tools_on_session_options) result["SessionsReleaseLockRequest"] = to_class(SessionsReleaseLockRequest, self.sessions_release_lock_request) result["SessionsReleaseLockResult"] = to_class(SessionsReleaseLockResult, self.sessions_release_lock_result) @@ -38425,6 +38895,11 @@ def to_dict(self) -> dict: result["SkillDiscoveryPathList"] = to_class(SkillDiscoveryPathList, self.skill_discovery_path_list) result["SkillDiscoveryScope"] = to_enum(SkillDiscoveryScope, self.skill_discovery_scope) result["SkillList"] = to_class(SkillList, self.skill_list) + result["SkillProviderDescriptor"] = to_class(SkillProviderDescriptor, self.skill_provider_descriptor) + result["SkillProviderListRequest"] = to_class(SkillProviderListRequest, self.skill_provider_list_request) + result["SkillProviderListResult"] = to_class(_SkillProviderListResult, self.skill_provider_list_result) + result["SkillProviderReadRequest"] = to_class(_SkillProviderReadRequest, self.skill_provider_read_request) + result["SkillProviderReadResult"] = to_class(_SkillProviderReadResult, self.skill_provider_read_result) result["SkillsConfigSetDisabledSkillsRequest"] = to_class(SkillsConfigSetDisabledSkillsRequest, self.skills_config_set_disabled_skills_request) result["SkillsConfigSetSkillDisabledRequest"] = to_class(SkillsConfigSetSkillDisabledRequest, self.skills_config_set_skill_disabled_request) result["SkillsDisableRequest"] = to_class(SkillsDisableRequest, self.skills_disable_request) @@ -39054,6 +39529,17 @@ def _patch_model_capabilities(data: dict) -> dict: return data +# Experimental: this API group is experimental and may change or be removed. +class ServerHooksApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def discover(self, params: HooksDiscoverRequest, *, timeout: float | None = None) -> HooksDiscoverResult: + "Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources.\n\nArgs:\n params: Optional project paths and host-exclusion behavior for server-scoped hook discovery.\n\nReturns:\n Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return HooksDiscoverResult.from_dict(await self._client.request("hooks.discover", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerModelsApi: def __init__(self, client: "JsonRpcClient"): @@ -39466,6 +39952,11 @@ async def list(self, params: SessionsListRequest, *, timeout: float | None = Non params_dict = {k: v for k, v in params.to_dict().items() if v is not None} return SessionList.from_dict(await self._client.request("sessions.list", params_dict, **_timeout_kwargs(timeout))) + async def read_persisted_events(self, params: SessionsReadPersistedEventsRequest, *, timeout: float | None = None) -> EventsReadResult: + "Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session.\n\nArgs:\n params: Pagination options for reading an inactive or active local session's persisted event journal.\n\nReturns:\n Batch of session events returned by a read, with cursor and continuation metadata." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return EventsReadResult.from_dict(await self._client.request("sessions.readPersistedEvents", params_dict, **_timeout_kwargs(timeout))) + async def find_by_task_id(self, params: SessionsFindByTaskIDRequest, *, timeout: float | None = None) -> SessionsFindByTaskIDResult: "Finds the local session bound to a GitHub task ID, if any.\n\nArgs:\n params: GitHub task ID to look up.\n\nReturns:\n ID of the local session bound to the given GitHub task, or omitted when none." params_dict = {k: v for k, v in params.to_dict().items() if v is not None} @@ -39575,6 +40066,7 @@ class ServerRpc: """Typed server-scoped RPC methods.""" def __init__(self, client: "JsonRpcClient"): self._client = client + self.hooks = ServerHooksApi(client) self.models = ServerModelsApi(client) self.tools = ServerToolsApi(client) self.account = ServerAccountApi(client) @@ -39601,7 +40093,7 @@ async def ping(self, params: PingRequest, *, timeout: float | None = None) -> Pi return PingResult.from_dict(await self._client.request("ping", params_dict, **_timeout_kwargs(timeout))) async def register_extension_launch_provider(self, *, timeout: float | None = None) -> None: - "Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility.\n\n.. warning:: This API is experimental and may change or be removed in future versions." + "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.\n\n.. warning:: This API is experimental and may change or be removed in future versions." await self._client.request("registerExtensionLaunchProvider", {}, **_timeout_kwargs(timeout)) @@ -39663,6 +40155,17 @@ async def _connect(self, params: _ConnectRequest, *, timeout: float | None = Non return _ConnectResult.from_dict(await self._client.request("connect", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class SandboxApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def get_enforcement_status(self, *, timeout: float | None = None) -> SandboxEnforcementStatus: + "Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session.\n\nReturns:\n Managed sandbox enforcement state for a session." + return SandboxEnforcementStatus.from_dict(await self._client.request("session.sandbox.getEnforcementStatus", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class GitHubAuthApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -41139,6 +41642,7 @@ class SessionRpc: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id + self.sandbox = SandboxApi(client, session_id) self.git_hub_auth = GitHubAuthApi(client, session_id) self.debug = DebugApi(client, session_id) self.canvas = CanvasApi(client, session_id) @@ -42030,6 +42534,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "DiscoveredExtensions", "DiscoveredExtensionsDisableRequest", "DiscoveredExtensionsEnableRequest", + "DiscoveredHook", "DiscoveredMCPServer", "DiscoveredMCPServerType", "EnqueueCommandParams", @@ -42061,7 +42566,6 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ExtensionsApi", "ExtensionsDisableRequest", "ExtensionsEnableRequest", - "ExternalRefMCPOauthHTTPResponse", "ExternalToolResult", "ExternalToolTextResultForLlm", "ExternalToolTextResultForLlmBinaryResultsForLlm", @@ -42174,6 +42678,10 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "HistorySummarizeForHandoffResult", "HistoryTruncateRequest", "HistoryTruncateResult", + "HookOrigin", + "HookType", + "HooksDiscoverRequest", + "HooksDiscoverResult", "HooksHandler", "Host", "HostType", @@ -42766,6 +43274,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "RemoteSessionMode", "RemoteSessionRepository", "RunOptions", + "SandboxApi", "SandboxConfig", "SandboxConfigAuth", "SandboxConfigUserPolicy", @@ -42775,6 +43284,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SandboxConfigUserPolicyNetwork", "SandboxConfigUserPolicyNetworkProxy", "SandboxConfigUserPolicySeatbelt", + "SandboxEnforcementStatus", "Saved", "ScheduleAddAtRequest", "ScheduleAddCronRequest", @@ -42806,6 +43316,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ServerCatalogApi", "ServerCommandsApi", "ServerExtensionsApi", + "ServerHooksApi", "ServerInstructionSourceList", "ServerInstructionsApi", "ServerLlmInferenceApi", @@ -42991,6 +43502,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionsOpenResumeLastKind", "SessionsOpenStatus", "SessionsPruneOldRequest", + "SessionsReadPersistedEventsRequest", "SessionsRegisterExtensionToolsOnSessionOptions", "SessionsReleaseLockRequest", "SessionsReleaseLockResult", @@ -43027,6 +43539,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SkillDiscoveryPathList", "SkillDiscoveryScope", "SkillList", + "SkillProviderDescriptor", + "SkillProviderListRequest", "SkillsApi", "SkillsConfigSetDisabledSkillsRequest", "SkillsConfigSetSkillDisabledRequest", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index c22f9f24fc..51e719bcf9 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -137,6 +137,7 @@ class SessionEventType(Enum): SESSION_WARNING = "session.warning" SESSION_MODEL_CHANGE = "session.model_change" SESSION_MODE_CHANGED = "session.mode_changed" + SESSION_MODE_NOTICE_DELIVERED = "session.mode_notice_delivered" SESSION_SESSION_LIMITS_CHANGED = "session.session_limits_changed" # Experimental: this event is part of an experimental API and may change or be removed. SESSION_PERMISSIONS_CHANGED = "session.permissions_changed" @@ -155,6 +156,8 @@ class SessionEventType(Enum): SESSION_COMPACTION_COMPLETE = "session.compaction_complete" SESSION_TASK_COMPLETE = "session.task_complete" # Experimental: this event is part of an experimental API and may change or be removed. + SESSION_COMPLETION_RECEIPT = "session.completion_receipt" + # Experimental: this event is part of an experimental API and may change or be removed. SESSION_FUSION_ROUTE_STARTED = "session.fusion_route_started" # Experimental: this event is part of an experimental API and may change or be removed. SESSION_FUSION_ROUTE_FAILED = "session.fusion_route_failed" @@ -171,6 +174,8 @@ class SessionEventType(Enum): # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_STARTED = "assistant.fusion_phase_started" # Experimental: this event is part of an experimental API and may change or be removed. + ASSISTANT_FUSION_PHASE_ACTIVITY = "assistant.fusion_phase_activity" + # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_COMPLETED = "assistant.fusion_phase_completed" # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_FAILED = "assistant.fusion_phase_failed" @@ -397,6 +402,60 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AssistantFusionPhaseActivityData: + "Experimental content-safe activity signal for a running HydraFusion phase." + activity: FusionPhaseActivityKind + conversation_scope: FusionConversationScope + fusion_id: str + pattern: FusionPattern + phase_id: str + phase_kind: FusionPhaseKind + role: str + tool_call_id: str | None = None + total_response_size_bytes: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "AssistantFusionPhaseActivityData": + assert isinstance(obj, dict) + activity = parse_enum(FusionPhaseActivityKind, obj.get("activity")) + conversation_scope = parse_enum(FusionConversationScope, obj.get("conversationScope")) + fusion_id = from_str(obj.get("fusionId")) + pattern = parse_enum(FusionPattern, obj.get("pattern")) + phase_id = from_str(obj.get("phaseId")) + phase_kind = parse_enum(FusionPhaseKind, obj.get("phaseKind")) + role = from_str(obj.get("role")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + total_response_size_bytes = from_union([from_none, from_int], obj.get("totalResponseSizeBytes")) + return AssistantFusionPhaseActivityData( + activity=activity, + conversation_scope=conversation_scope, + fusion_id=fusion_id, + pattern=pattern, + phase_id=phase_id, + phase_kind=phase_kind, + role=role, + tool_call_id=tool_call_id, + total_response_size_bytes=total_response_size_bytes, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["activity"] = to_enum(FusionPhaseActivityKind, self.activity) + result["conversationScope"] = to_enum(FusionConversationScope, self.conversation_scope) + result["fusionId"] = from_str(self.fusion_id) + result["pattern"] = to_enum(FusionPattern, self.pattern) + result["phaseId"] = from_str(self.phase_id) + result["phaseKind"] = to_enum(FusionPhaseKind, self.phase_kind) + result["role"] = from_str(self.role) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + if self.total_response_size_bytes is not None: + result["totalResponseSizeBytes"] = from_union([from_none, to_int], self.total_response_size_bytes) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AssistantFusionPhaseCompletedData: @@ -1201,6 +1260,38 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class FusionPhasePlanStep: + "Presentation-neutral phase planned for a HydraFusion turn." + conditional: bool + kind: FusionPhaseKind + role: str + scope: FusionConversationScope + + @staticmethod + def from_dict(obj: Any) -> "FusionPhasePlanStep": + assert isinstance(obj, dict) + conditional = from_bool(obj.get("conditional")) + kind = parse_enum(FusionPhaseKind, obj.get("kind")) + role = from_str(obj.get("role")) + scope = parse_enum(FusionConversationScope, obj.get("scope")) + return FusionPhasePlanStep( + conditional=conditional, + kind=kind, + role=role, + scope=scope, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["conditional"] = from_bool(self.conditional) + result["kind"] = to_enum(FusionPhaseKind, self.kind) + result["role"] = from_str(self.role) + result["scope"] = to_enum(FusionConversationScope, self.scope) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class FusionPhaseUsage: @@ -1677,6 +1768,55 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionCompletionReceiptData: + "Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted." + attempt: int + event_range: CompletionReceiptEventRange + failed_tool_count: int + schema_version: int + source_event_id: str + stop_reason: CompletionReceiptStopReason + successful_tool_count: int + final_tool: CompletionReceiptFinalTool | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionCompletionReceiptData": + assert isinstance(obj, dict) + attempt = from_int(obj.get("attempt")) + event_range = CompletionReceiptEventRange.from_dict(obj.get("eventRange")) + failed_tool_count = from_int(obj.get("failedToolCount")) + schema_version = from_int(obj.get("schemaVersion")) + source_event_id = from_str(obj.get("sourceEventId")) + stop_reason = parse_enum(CompletionReceiptStopReason, obj.get("stopReason")) + successful_tool_count = from_int(obj.get("successfulToolCount")) + final_tool = from_union([from_none, CompletionReceiptFinalTool.from_dict], obj.get("finalTool")) + return SessionCompletionReceiptData( + attempt=attempt, + event_range=event_range, + failed_tool_count=failed_tool_count, + schema_version=schema_version, + source_event_id=source_event_id, + stop_reason=stop_reason, + successful_tool_count=successful_tool_count, + final_tool=final_tool, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["attempt"] = to_int(self.attempt) + result["eventRange"] = to_class(CompletionReceiptEventRange, self.event_range) + result["failedToolCount"] = to_int(self.failed_tool_count) + result["schemaVersion"] = to_int(self.schema_version) + result["sourceEventId"] = from_str(self.source_event_id) + result["stopReason"] = to_enum(CompletionReceiptStopReason, self.stop_reason) + result["successfulToolCount"] = to_int(self.successful_tool_count) + if self.final_tool is not None: + result["finalTool"] = from_union([from_none, lambda x: to_class(CompletionReceiptFinalTool, x)], self.final_tool) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFusionCompletedData: @@ -1782,6 +1922,8 @@ class SessionFusionResolvedData: turn_id: str follow_up: FusionFollowUpRecommendation | None = None model_universe_version: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + phase_plan: list[FusionPhasePlanStep] | None = None plan_version: str | None = None policy_version: str | None = None route_source: str | None = None @@ -1806,6 +1948,7 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData": turn_id = from_str(obj.get("turnId")) follow_up = from_union([from_none, FusionFollowUpRecommendation.from_dict], obj.get("followUp")) model_universe_version = from_union([from_none, from_str], obj.get("modelUniverseVersion")) + phase_plan = from_union([from_none, lambda x: from_list(FusionPhasePlanStep.from_dict, x)], obj.get("phasePlan")) plan_version = from_union([from_none, from_str], obj.get("planVersion")) policy_version = from_union([from_none, from_str], obj.get("policyVersion")) route_source = from_union([from_none, from_str], obj.get("routeSource")) @@ -1827,6 +1970,7 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData": turn_id=turn_id, follow_up=follow_up, model_universe_version=model_universe_version, + phase_plan=phase_plan, plan_version=plan_version, policy_version=policy_version, route_source=route_source, @@ -1853,6 +1997,8 @@ def to_dict(self) -> dict: result["followUp"] = from_union([from_none, lambda x: to_class(FusionFollowUpRecommendation, x)], self.follow_up) if self.model_universe_version is not None: result["modelUniverseVersion"] = from_union([from_none, from_str], self.model_universe_version) + if self.phase_plan is not None: + result["phasePlan"] = from_union([from_none, lambda x: from_list(lambda x: to_class(FusionPhasePlanStep, x), x)], self.phase_plan) if self.plan_version is not None: result["planVersion"] = from_union([from_none, from_str], self.plan_version) if self.policy_version is not None: @@ -1992,7 +2138,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionManagedSettingsResolvedData: - "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes." + "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes." bypass_permissions_disabled: bool device_managed: bool fail_closed: bool @@ -2001,6 +2147,7 @@ class SessionManagedSettingsResolvedData: source: ManagedSettingsResolvedSource client_managed: bool | None = None permissions_allow_intersected: bool | None = None + policy_helper_managed: bool | None = None sandbox_enabled_by_undetermined_policy: bool | None = None settings: Any = None @@ -2015,6 +2162,7 @@ def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData": source = parse_enum(ManagedSettingsResolvedSource, obj.get("source")) client_managed = from_union([from_none, from_bool], obj.get("clientManaged")) permissions_allow_intersected = from_union([from_none, from_bool], obj.get("permissionsAllowIntersected")) + policy_helper_managed = from_union([from_none, from_bool], obj.get("policyHelperManaged")) sandbox_enabled_by_undetermined_policy = from_union([from_none, from_bool], obj.get("sandboxEnabledByUndeterminedPolicy")) settings = obj.get("settings") return SessionManagedSettingsResolvedData( @@ -2026,6 +2174,7 @@ def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData": source=source, client_managed=client_managed, permissions_allow_intersected=permissions_allow_intersected, + policy_helper_managed=policy_helper_managed, sandbox_enabled_by_undetermined_policy=sandbox_enabled_by_undetermined_policy, settings=settings, ) @@ -2042,6 +2191,8 @@ def to_dict(self) -> dict: result["clientManaged"] = from_union([from_none, from_bool], self.client_managed) if self.permissions_allow_intersected is not None: result["permissionsAllowIntersected"] = from_union([from_none, from_bool], self.permissions_allow_intersected) + if self.policy_helper_managed is not None: + result["policyHelperManaged"] = from_union([from_none, from_bool], self.policy_helper_managed) if self.sandbox_enabled_by_undetermined_policy is not None: result["sandboxEnabledByUndeterminedPolicy"] = from_union([from_none, from_bool], self.sandbox_enabled_by_undetermined_policy) if self.settings is not None: @@ -2450,6 +2601,7 @@ class AssistantMessageToolRequest: name: str tool_call_id: str arguments: Any = None + caller: AssistantMessageToolRequestCaller | None = None intention_summary: str | None = None mcp_server_name: str | None = None mcp_tool_name: str | None = None @@ -2462,6 +2614,7 @@ def from_dict(obj: Any) -> "AssistantMessageToolRequest": name = from_str(obj.get("name")) tool_call_id = from_str(obj.get("toolCallId")) arguments = obj.get("arguments") + caller = from_union([from_none, AssistantMessageToolRequestCaller.from_dict], obj.get("caller")) intention_summary = from_union([from_none, from_str], obj.get("intentionSummary")) mcp_server_name = from_union([from_none, from_str], obj.get("mcpServerName")) mcp_tool_name = from_union([from_none, from_str], obj.get("mcpToolName")) @@ -2471,6 +2624,7 @@ def from_dict(obj: Any) -> "AssistantMessageToolRequest": name=name, tool_call_id=tool_call_id, arguments=arguments, + caller=caller, intention_summary=intention_summary, mcp_server_name=mcp_server_name, mcp_tool_name=mcp_tool_name, @@ -2484,6 +2638,8 @@ def to_dict(self) -> dict: result["toolCallId"] = from_str(self.tool_call_id) if self.arguments is not None: result["arguments"] = self.arguments + if self.caller is not None: + result["caller"] = from_union([from_none, lambda x: to_class(AssistantMessageToolRequestCaller, x)], self.caller) if self.intention_summary is not None: result["intentionSummary"] = from_union([from_none, from_str], self.intention_summary) if self.mcp_server_name is not None: @@ -2497,6 +2653,29 @@ def to_dict(self) -> dict: return result +@dataclass +class AssistantMessageToolRequestCaller: + "Hosted program that requested this client tool call" + caller_id: str + type: AssistantMessageToolRequestCallerType + + @staticmethod + def from_dict(obj: Any) -> "AssistantMessageToolRequestCaller": + assert isinstance(obj, dict) + caller_id = from_str(obj.get("callerId")) + type = parse_enum(AssistantMessageToolRequestCallerType, obj.get("type")) + return AssistantMessageToolRequestCaller( + caller_id=caller_id, + type=type, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["callerId"] = from_str(self.caller_id) + result["type"] = to_enum(AssistantMessageToolRequestCallerType, self.type) + return result + + @dataclass class AssistantReasoningData: "Assistant reasoning content for timeline display with complete thinking text" @@ -4075,9 +4254,65 @@ def to_dict(self) -> dict: return result +@dataclass +class CompletionReceiptEventRange: + "Inclusive durable event range summarized by a completion receipt." + end_event_id: str + start_event_id: str + + @staticmethod + def from_dict(obj: Any) -> "CompletionReceiptEventRange": + assert isinstance(obj, dict) + end_event_id = from_str(obj.get("endEventId")) + start_event_id = from_str(obj.get("startEventId")) + return CompletionReceiptEventRange( + end_event_id=end_event_id, + start_event_id=start_event_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["endEventId"] = from_str(self.end_event_id) + result["startEventId"] = from_str(self.start_event_id) + return result + + +@dataclass +class CompletionReceiptFinalTool: + "Final structured tool completion in the covered event range." + status: CompletionReceiptToolStatus + tool_call_id: str + exit_code: int | None = None + tool_name: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "CompletionReceiptFinalTool": + assert isinstance(obj, dict) + status = parse_enum(CompletionReceiptToolStatus, obj.get("status")) + tool_call_id = from_str(obj.get("toolCallId")) + exit_code = from_union([from_none, from_int], obj.get("exitCode")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return CompletionReceiptFinalTool( + status=status, + tool_call_id=tool_call_id, + exit_code=exit_code, + tool_name=tool_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(CompletionReceiptToolStatus, self.status) + result["toolCallId"] = from_str(self.tool_call_id) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, to_int], self.exit_code) + if self.tool_name is not None: + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + + @dataclass class CustomAgentsUpdatedAgent: - "A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override." + "A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration." description: str display_name: str id: str @@ -4086,6 +4321,8 @@ class CustomAgentsUpdatedAgent: tools: list[str] | None user_invocable: bool model: str | None = None + model_policy: AgentModelPolicy | None = None + models: list[str] | None = None @staticmethod def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": @@ -4098,6 +4335,8 @@ def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": tools = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("tools")) user_invocable = from_bool(obj.get("userInvocable")) model = from_union([from_none, from_str], obj.get("model")) + model_policy = from_union([from_none, lambda x: parse_enum(AgentModelPolicy, x)], obj.get("modelPolicy")) + models = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("models")) return CustomAgentsUpdatedAgent( description=description, display_name=display_name, @@ -4107,6 +4346,8 @@ def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": tools=tools, user_invocable=user_invocable, model=model, + model_policy=model_policy, + models=models, ) def to_dict(self) -> dict: @@ -4120,6 +4361,10 @@ def to_dict(self) -> dict: result["userInvocable"] = from_bool(self.user_invocable) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([from_none, lambda x: to_enum(AgentModelPolicy, x)], self.model_policy) + if self.models is not None: + result["models"] = from_union([from_none, lambda x: from_list(from_str, x)], self.models) return result @@ -7044,6 +7289,7 @@ class PermissionRequestedData: "Permission request notification requiring client approval with request details" permission_request: PermissionRequest request_id: str + agent_mode: SessionMode | None = None prompt_request: PermissionPromptRequest | None = None resolved_by_hook: bool | None = None risk_assessment: Any = None @@ -7053,12 +7299,14 @@ def from_dict(obj: Any) -> "PermissionRequestedData": assert isinstance(obj, dict) permission_request = _load_PermissionRequest(obj.get("permissionRequest")) request_id = from_str(obj.get("requestId")) + agent_mode = from_union([from_none, lambda x: parse_enum(SessionMode, x)], obj.get("agentMode")) prompt_request = from_union([from_none, _load_PermissionPromptRequest], obj.get("promptRequest")) resolved_by_hook = from_union([from_none, from_bool], obj.get("resolvedByHook")) risk_assessment = obj.get("riskAssessment") return PermissionRequestedData( permission_request=permission_request, request_id=request_id, + agent_mode=agent_mode, prompt_request=prompt_request, resolved_by_hook=resolved_by_hook, risk_assessment=risk_assessment, @@ -7068,6 +7316,8 @@ def to_dict(self) -> dict: result: dict = {} result["permissionRequest"] = self.permission_request.to_dict() result["requestId"] = from_str(self.request_id) + if self.agent_mode is not None: + result["agentMode"] = from_union([from_none, lambda x: to_enum(SessionMode, x)], self.agent_mode) if self.prompt_request is not None: result["promptRequest"] = from_union([from_none, lambda x: x.to_dict()], self.prompt_request) if self.resolved_by_hook is not None: @@ -8109,6 +8359,30 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionModeNoticeDeliveredData: + "Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume." + mode: SessionMode + content: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionModeNoticeDeliveredData": + assert isinstance(obj, dict) + mode = parse_enum(SessionMode, obj.get("mode")) + content = from_union([from_none, from_str], obj.get("content")) + return SessionModeNoticeDeliveredData( + mode=mode, + content=content, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["mode"] = to_enum(SessionMode, self.mode) + if self.content is not None: + result["content"] = from_union([from_none, from_str], self.content) + return result + + @dataclass class SessionModelChangeData: "Model change details including previous and new model identifiers" @@ -8222,6 +8496,7 @@ class SessionResumeData: event_count: int resume_time: datetime already_in_use: bool | None = None + auto_tier: AutoTier | None = None context: WorkingDirectoryContext | None = None context_tier: ContextTier | None = None continue_pending_work: bool | None = None @@ -8240,6 +8515,7 @@ def from_dict(obj: Any) -> "SessionResumeData": event_count = from_int(obj.get("eventCount")) resume_time = from_datetime(obj.get("resumeTime")) already_in_use = from_union([from_none, from_bool], obj.get("alreadyInUse")) + auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("autoTier")) context = from_union([from_none, WorkingDirectoryContext.from_dict], obj.get("context")) context_tier = from_union([from_none, lambda x: parse_enum(ContextTier, x)], obj.get("contextTier")) continue_pending_work = from_union([from_none, from_bool], obj.get("continuePendingWork")) @@ -8255,6 +8531,7 @@ def from_dict(obj: Any) -> "SessionResumeData": event_count=event_count, resume_time=resume_time, already_in_use=already_in_use, + auto_tier=auto_tier, context=context, context_tier=context_tier, continue_pending_work=continue_pending_work, @@ -8274,6 +8551,8 @@ def to_dict(self) -> dict: result["resumeTime"] = to_datetime(self.resume_time) if self.already_in_use is not None: result["alreadyInUse"] = from_union([from_none, from_bool], self.already_in_use) + if self.auto_tier is not None: + result["autoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.auto_tier) if self.context is not None: result["context"] = from_union([from_none, lambda x: to_class(WorkingDirectoryContext, x)], self.context) if self.context_tier is not None: @@ -8566,6 +8845,7 @@ class SessionStartData: start_time: datetime version: int already_in_use: bool | None = None + auto_tier: AutoTier | None = None context: WorkingDirectoryContext | None = None context_tier: ContextTier | None = None detached_from_spawning_parent_session_id: str | None = None @@ -8586,6 +8866,7 @@ def from_dict(obj: Any) -> "SessionStartData": start_time = from_datetime(obj.get("startTime")) version = from_int(obj.get("version")) already_in_use = from_union([from_none, from_bool], obj.get("alreadyInUse")) + auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("autoTier")) context = from_union([from_none, WorkingDirectoryContext.from_dict], obj.get("context")) context_tier = from_union([from_none, lambda x: parse_enum(ContextTier, x)], obj.get("contextTier")) detached_from_spawning_parent_session_id = from_union([from_none, from_str], obj.get("detachedFromSpawningParentSessionId")) @@ -8603,6 +8884,7 @@ def from_dict(obj: Any) -> "SessionStartData": start_time=start_time, version=version, already_in_use=already_in_use, + auto_tier=auto_tier, context=context, context_tier=context_tier, detached_from_spawning_parent_session_id=detached_from_spawning_parent_session_id, @@ -8624,6 +8906,8 @@ def to_dict(self) -> dict: result["version"] = to_int(self.version) if self.already_in_use is not None: result["alreadyInUse"] = from_union([from_none, from_bool], self.already_in_use) + if self.auto_tier is not None: + result["autoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.auto_tier) if self.context is not None: result["context"] = from_union([from_none, lambda x: to_class(WorkingDirectoryContext, x)], self.context) if self.context_tier is not None: @@ -9126,6 +9410,7 @@ class SkillInvokedData: path: str allowed_tools: list[str] | None = None description: str | None = None + disable_model_invocation: bool | None = None model: str | None = None plugin_name: str | None = None plugin_version: str | None = None @@ -9140,6 +9425,7 @@ def from_dict(obj: Any) -> "SkillInvokedData": path = from_str(obj.get("path")) allowed_tools = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("allowedTools")) description = from_union([from_none, from_str], obj.get("description")) + disable_model_invocation = from_union([from_none, from_bool], obj.get("disableModelInvocation")) model = from_union([from_none, from_str], obj.get("model")) plugin_name = from_union([from_none, from_str], obj.get("pluginName")) plugin_version = from_union([from_none, from_str], obj.get("pluginVersion")) @@ -9151,6 +9437,7 @@ def from_dict(obj: Any) -> "SkillInvokedData": path=path, allowed_tools=allowed_tools, description=description, + disable_model_invocation=disable_model_invocation, model=model, plugin_name=plugin_name, plugin_version=plugin_version, @@ -9167,6 +9454,8 @@ def to_dict(self) -> dict: result["allowedTools"] = from_union([from_none, lambda x: from_list(from_str, x)], self.allowed_tools) if self.description is not None: result["description"] = from_union([from_none, from_str], self.description) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_none, from_bool], self.disable_model_invocation) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) if self.plugin_name is not None: @@ -9244,6 +9533,7 @@ class SubagentCompletedData: explicit_model_override: str | None = None first_dispatched_model: str | None = None model: str | None = None + model_override_reason: str | None = None total_tokens: int | None = None total_tool_calls: int | None = None @@ -9261,6 +9551,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData": explicit_model_override = from_union([from_none, from_str], obj.get("explicitModelOverride")) first_dispatched_model = from_union([from_none, from_str], obj.get("firstDispatchedModel")) model = from_union([from_none, from_str], obj.get("model")) + model_override_reason = from_union([from_none, from_str], obj.get("modelOverrideReason")) total_tokens = from_union([from_none, from_int], obj.get("totalTokens")) total_tool_calls = from_union([from_none, from_int], obj.get("totalToolCalls")) return SubagentCompletedData( @@ -9275,6 +9566,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData": explicit_model_override=explicit_model_override, first_dispatched_model=first_dispatched_model, model=model, + model_override_reason=model_override_reason, total_tokens=total_tokens, total_tool_calls=total_tool_calls, ) @@ -9300,6 +9592,8 @@ def to_dict(self) -> dict: result["firstDispatchedModel"] = from_union([from_none, from_str], self.first_dispatched_model) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_override_reason is not None: + result["modelOverrideReason"] = from_union([from_none, from_str], self.model_override_reason) if self.total_tokens is not None: result["totalTokens"] = from_union([from_none, to_int], self.total_tokens) if self.total_tool_calls is not None: @@ -9366,6 +9660,7 @@ class SubagentFailedData: explicit_model_override: str | None = None first_dispatched_model: str | None = None model: str | None = None + model_override_reason: str | None = None total_tokens: int | None = None total_tool_calls: int | None = None @@ -9383,6 +9678,7 @@ def from_dict(obj: Any) -> "SubagentFailedData": explicit_model_override = from_union([from_none, from_str], obj.get("explicitModelOverride")) first_dispatched_model = from_union([from_none, from_str], obj.get("firstDispatchedModel")) model = from_union([from_none, from_str], obj.get("model")) + model_override_reason = from_union([from_none, from_str], obj.get("modelOverrideReason")) total_tokens = from_union([from_none, from_int], obj.get("totalTokens")) total_tool_calls = from_union([from_none, from_int], obj.get("totalToolCalls")) return SubagentFailedData( @@ -9397,6 +9693,7 @@ def from_dict(obj: Any) -> "SubagentFailedData": explicit_model_override=explicit_model_override, first_dispatched_model=first_dispatched_model, model=model, + model_override_reason=model_override_reason, total_tokens=total_tokens, total_tool_calls=total_tool_calls, ) @@ -9421,6 +9718,8 @@ def to_dict(self) -> dict: result["firstDispatchedModel"] = from_union([from_none, from_str], self.first_dispatched_model) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_override_reason is not None: + result["modelOverrideReason"] = from_union([from_none, from_str], self.model_override_reason) if self.total_tokens is not None: result["totalTokens"] = from_union([from_none, to_int], self.total_tokens) if self.total_tool_calls is not None: @@ -10949,6 +11248,7 @@ class UserMessageData: delivery: UserMessageDelivery | None = None interaction_id: str | None = None is_autopilot_continuation: bool | None = None + message_id: str | None = None native_document_path_fallback_paths: list[str] | None = None parent_agent_task_id: str | None = None source: str | None = None @@ -10965,6 +11265,7 @@ def from_dict(obj: Any) -> "UserMessageData": delivery = from_union([from_none, lambda x: parse_enum(UserMessageDelivery, x)], obj.get("delivery")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_autopilot_continuation = from_union([from_none, from_bool], obj.get("isAutopilotContinuation")) + message_id = from_union([from_none, from_str], obj.get("messageId")) native_document_path_fallback_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("nativeDocumentPathFallbackPaths")) parent_agent_task_id = from_union([from_none, from_str], obj.get("parentAgentTaskId")) source = from_union([from_none, from_str], obj.get("source")) @@ -10978,6 +11279,7 @@ def from_dict(obj: Any) -> "UserMessageData": delivery=delivery, interaction_id=interaction_id, is_autopilot_continuation=is_autopilot_continuation, + message_id=message_id, native_document_path_fallback_paths=native_document_path_fallback_paths, parent_agent_task_id=parent_agent_task_id, source=source, @@ -10999,6 +11301,8 @@ def to_dict(self) -> dict: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) if self.is_autopilot_continuation is not None: result["isAutopilotContinuation"] = from_union([from_none, from_bool], self.is_autopilot_continuation) + if self.message_id is not None: + result["messageId"] = from_union([from_none, from_str], self.message_id) if self.native_document_path_fallback_paths is not None: result["nativeDocumentPathFallbackPaths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.native_document_path_fallback_paths) if self.parent_agent_task_id is not None: @@ -11522,6 +11826,17 @@ class FusionPattern(Enum): CRITIQUE = "critique" +# Experimental: this enum is part of an experimental API and may change or be removed. +class FusionPhaseActivityKind(Enum): + "Content-safe activity observed while a HydraFusion phase is running." + # The provider produced additional private output bytes. + MODEL_OUTPUT = "model_output" + # A tool began executing inside the phase. + TOOL_STARTED = "tool_started" + # A tool finished executing inside the phase. + TOOL_COMPLETED = "tool_completed" + + # Experimental: this enum is part of an experimental API and may change or be removed. class FusionPhaseKind(Enum): "HydraFusion phase kind." @@ -11637,6 +11952,19 @@ class AgentInterruptedCancelPhase(Enum): MID_STREAM = "mid_stream" +class AgentModelPolicy(Enum): + "Whether configured models are advisory preferences or required constraints" + # Treat the authored models as advisory preferences that callers may override. + PREFERRED = "preferred" + # Require subagent execution to use one of the authored models. + REQUIRED = "required" + + +class AssistantMessageToolRequestCallerType(Enum): + "Hosted program caller type" + PROGRAM = "program" + + class AssistantMessageToolRequestType(Enum): "Tool call type: \"function\" for standard tool calls, \"custom\" for grammar-based tool calls. Defaults to \"function\" when absent." # Standard function-style tool call. @@ -11695,6 +12023,16 @@ class AutoModeSwitchResponse(Enum): NO = "no" +class AutoTier(Enum): + "Routing preference used when the session model is `auto`." + # Optimize for efficiency. + EFFICIENCY = "efficiency" + # Balance efficiency and intelligence. + BALANCE = "balance" + # Optimize for intelligence. + INTELLIGENCE = "intelligence" + + class AutopilotObjectiveChangedOperation(Enum): "The type of operation performed on the autopilot objective state file" # Autopilot objective state file was created for a new objective. @@ -11747,6 +12085,30 @@ class CompactionTrigger(Enum): MODEL_SWITCH = "model_switch" +class CompletionReceiptStopReason(Enum): + "Runtime reason the completion decision was accepted." + # The model reached a natural terminal response. + NATURAL = "natural" + # A terminal tool ended the interaction. + TERMINAL_TOOL = "terminal_tool" + # The configured agentStop continuation limit was reached. + AGENT_STOP_BLOCK_LIMIT = "agent_stop_block_limit" + + +class CompletionReceiptToolStatus(Enum): + "Structured terminal status from a tool completion event." + # The tool completed successfully. + SUCCESS = "success" + # The tool failed without a more specific structured status. + FAILURE = "failure" + # The tool exceeded its time budget. + TIMEOUT = "timeout" + # The user rejected the tool call. + REJECTED = "rejected" + # The permissions service denied the tool call. + DENIED = "denied" + + class ContextTier(Enum): "Allowed values for the `ContextTier` enumeration." # Default context tier with standard context window size. @@ -11867,7 +12229,9 @@ class ManagedSettingsResolvedSource(Enum): DEVICE = "device" # Only session-local SDK-host injection contributed. CLIENT = "client" - # More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + # A policy helper registered by device or server policy contributed. Device registration takes priority when present. + POLICY_HELPER = "policyHelper" + # More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. MIXED = "mixed" # No managed policy is in force (no channel contributed). NONE = "none" @@ -12154,7 +12518,7 @@ class SkillInvokedTrigger(Enum): class SkillSource(Enum): - "Source location type (e.g., project, personal-copilot, plugin, builtin)" + "Source location type (e.g., project, personal-copilot, plugin, builtin, sdk)" # Skill defined in the current project's skill directories. PROJECT = "project" # Skill discovered from a parent directory in the current workspace tree. @@ -12169,6 +12533,8 @@ class SkillSource(Enum): CUSTOM = "custom" # Skill bundled with the runtime. BUILTIN = "builtin" + # Pathless skill supplied lazily by an SDK skill provider. + SDK = "sdk" class SystemMessageRole(Enum): @@ -12281,7 +12647,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -12321,6 +12687,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_WARNING: data = SessionWarningData.from_dict(data_obj) case SessionEventType.SESSION_MODEL_CHANGE: data = SessionModelChangeData.from_dict(data_obj) case SessionEventType.SESSION_MODE_CHANGED: data = SessionModeChangedData.from_dict(data_obj) + case SessionEventType.SESSION_MODE_NOTICE_DELIVERED: data = SessionModeNoticeDeliveredData.from_dict(data_obj) case SessionEventType.SESSION_SESSION_LIMITS_CHANGED: data = SessionSessionLimitsChangedData.from_dict(data_obj) case SessionEventType.SESSION_PERMISSIONS_CHANGED: data = SessionPermissionsChangedData.from_dict(data_obj) case SessionEventType.SESSION_PLAN_CHANGED: data = SessionPlanChangedData.from_dict(data_obj) @@ -12337,6 +12704,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_COMPACTION_START: data = SessionCompactionStartData.from_dict(data_obj) case SessionEventType.SESSION_COMPACTION_COMPLETE: data = SessionCompactionCompleteData.from_dict(data_obj) case SessionEventType.SESSION_TASK_COMPLETE: data = SessionTaskCompleteData.from_dict(data_obj) + case SessionEventType.SESSION_COMPLETION_RECEIPT: data = SessionCompletionReceiptData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_ROUTE_STARTED: data = SessionFusionRouteStartedData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_ROUTE_FAILED: data = SessionFusionRouteFailedData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_RESOLVED: data = SessionFusionResolvedData.from_dict(data_obj) @@ -12348,6 +12716,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.AGENT_INTERRUPTED: data = AgentInterruptedData.from_dict(data_obj) case SessionEventType.ASSISTANT_INTENT: data = AssistantIntentData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_STARTED: data = AssistantFusionPhaseStartedData.from_dict(data_obj) + case SessionEventType.ASSISTANT_FUSION_PHASE_ACTIVITY: data = AssistantFusionPhaseActivityData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_COMPLETED: data = AssistantFusionPhaseCompletedData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_FAILED: data = AssistantFusionPhaseFailedData.from_dict(data_obj) case SessionEventType.ASSISTANT_SERVER_TOOL_PROGRESS: data = AssistantServerToolProgressData.from_dict(data_obj) @@ -12476,6 +12845,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AgentInterruptedActivity", "AgentInterruptedCancelPhase", "AgentInterruptedData", + "AgentModelPolicy", + "AssistantFusionPhaseActivityData", "AssistantFusionPhaseCompletedData", "AssistantFusionPhaseFailedData", "AssistantFusionPhaseStartedData", @@ -12487,6 +12858,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AssistantMessageServerTools", "AssistantMessageStartData", "AssistantMessageToolRequest", + "AssistantMessageToolRequestCaller", + "AssistantMessageToolRequestCallerType", "AssistantMessageToolRequestType", "AssistantReasoningData", "AssistantReasoningDeltaData", @@ -12530,6 +12903,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AutoModeSwitchCompletedData", "AutoModeSwitchRequestedData", "AutoModeSwitchResponse", + "AutoTier", "AutopilotObjectiveChangedOperation", "AutopilotObjectiveChangedStatus", "BinaryAssetReference", @@ -12557,6 +12931,10 @@ def session_event_to_dict(x: SessionEvent) -> Any: "CompactionCompleteCompactionTokensUsed", "CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail", "CompactionTrigger", + "CompletionReceiptEventRange", + "CompletionReceiptFinalTool", + "CompletionReceiptStopReason", + "CompletionReceiptToolStatus", "ContextTier", "CustomAgentsUpdatedAgent", "Data", @@ -12586,7 +12964,9 @@ def session_event_to_dict(x: SessionEvent) -> Any: "FusionFollowUpAction", "FusionFollowUpRecommendation", "FusionPattern", + "FusionPhaseActivityKind", "FusionPhaseKind", + "FusionPhasePlanStep", "FusionPhaseStatus", "FusionPhaseUsage", "FusionScores", @@ -12712,6 +13092,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SessionCanvasUnavailableData", "SessionCompactionCompleteData", "SessionCompactionStartData", + "SessionCompletionReceiptData", "SessionContextChangedData", "SessionContextClearedData", "SessionCustomAgentsUpdatedData", @@ -12740,6 +13121,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SessionMcpServersLoadedData", "SessionMode", "SessionModeChangedData", + "SessionModeNoticeDeliveredData", "SessionModelChangeData", "SessionPermissionsChangedData", "SessionPlanChangedData", diff --git a/python/copilot/session.py b/python/copilot/session.py index 78afdde139..3c6d3d54a4 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -504,7 +504,7 @@ class McpAuthContext(TypedDict): class UserInputRequest(TypedDict, total=False): - """Request for user input from the agent (enables ask_user tool)""" + """Legacy question-and-answer request from the ask_user tool.""" question: str choices: list[str] diff --git a/python/e2e/_copilot_request_helpers.py b/python/e2e/_copilot_request_helpers.py index 2d91bc9bc2..d4073dd197 100644 --- a/python/e2e/_copilot_request_helpers.py +++ b/python/e2e/_copilot_request_helpers.py @@ -58,8 +58,8 @@ def _wants_stream(body: bytes) -> bool: def model_catalog(supported_endpoints: list[str] | None = None) -> dict: """The synthetic ``/models`` catalog payload.""" model: dict = { - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -67,7 +67,7 @@ def model_catalog(supported_endpoints: list[str] | None = None) -> dict: "model_picker_enabled": True, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": {"max_context_window_tokens": 200000, "max_output_tokens": 8192}, "supports": { @@ -191,7 +191,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", } chunks = [ { @@ -234,7 +234,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [], "stop_reason": None, "stop_sequence": None, @@ -283,7 +283,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [{"type": "text", "text": text}], "stop_reason": "end_turn", "stop_sequence": None, @@ -300,7 +300,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [ { "index": 0, diff --git a/python/e2e/conftest.py b/python/e2e/conftest.py index f441097f32..a789b2b567 100644 --- a/python/e2e/conftest.py +++ b/python/e2e/conftest.py @@ -1,10 +1,14 @@ """Shared pytest fixtures for e2e tests.""" +import json import os +from pathlib import Path import pytest import pytest_asyncio +import copilot._cli_download as cli_download + from .testharness import E2ETestContext, is_inprocess_transport # Host-side auth resolution ranks HMAC above the GitHub token, so an ambient @@ -15,9 +19,16 @@ # .NET's InProcessEnvIsolation [ModuleInitializer] and Node's module-init guard. # Out-of-process children resolve auth in their own process where the token already # outranks HMAC. See https://github.com/github/copilot-sdk/issues/1934. +if not cli_download.CLI_VERSION: + package_lock = json.loads( + (Path(__file__).parents[2] / "nodejs" / "package-lock.json").read_text() + ) + cli_download.CLI_VERSION = package_lock["packages"]["node_modules/@github/copilot"]["version"] + if is_inprocess_transport(): os.environ.pop("COPILOT_HMAC_KEY", None) os.environ.pop("CAPI_HMAC_KEY", None) + os.environ.pop("COPILOT_CLI_PATH", None) @pytest.hookimpl(tryfirst=True, hookwrapper=True) diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py index fe1ed54820..4bceabcb5e 100644 --- a/python/e2e/test_client_options_e2e.py +++ b/python/e2e/test_client_options_e2e.py @@ -396,7 +396,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( await client.start() session = await client.create_session( client_name="advanced-create-client", - model="claude-sonnet-4.5", + model="claude-sonnet-5", reasoning_effort="medium", reasoning_summary="detailed", context_tier="long_context", @@ -470,7 +470,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( "provider": "create-provider", "id": "create-model", "name": "Create Model", - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", "wire_model": "create-wire-model", "max_context_window_tokens": 12_000, "max_prompt_tokens": 10_000, @@ -482,7 +482,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( try: params = _get_captured_request(capture_path, "session.create") assert params["clientName"] == "advanced-create-client" - assert params["model"] == "claude-sonnet-4.5" + assert params["model"] == "claude-sonnet-5" assert params["reasoningEffort"] == "medium" assert params["reasoningSummary"] == "detailed" assert params["contextTier"] == "long_context" @@ -538,7 +538,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( try: await client.start() session = await client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "azure", "wire_api": "responses", @@ -548,7 +548,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( "bearer_token": "provider-bearer-token", "azure": {"api_version": "2024-02-15-preview"}, "headers": {"X-Provider-Wire": "yes"}, - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", "wire_model": "azure-deployment", "max_prompt_tokens": 8192, "max_output_tokens": 1024, @@ -565,7 +565,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( assert provider["bearerToken"] == "provider-bearer-token" assert provider["azure"]["apiVersion"] == "2024-02-15-preview" assert provider["headers"]["X-Provider-Wire"] == "yes" - assert provider["modelId"] == "claude-sonnet-4.5" + assert provider["modelId"] == "claude-sonnet-5" assert provider["wireModel"] == "azure-deployment" assert provider["maxPromptTokens"] == 8192 assert provider["maxOutputTokens"] == 1024 diff --git a/python/e2e/test_copilot_request_session_id_e2e.py b/python/e2e/test_copilot_request_session_id_e2e.py index 81624d73d0..75bf15f2a4 100644 --- a/python/e2e/test_copilot_request_session_id_e2e.py +++ b/python/e2e/test_copilot_request_session_id_e2e.py @@ -105,14 +105,14 @@ async def test_threads_session_id_into_byok_session(self, session_id_client): baseline = len(handler.records) session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "openai", "wire_api": "responses", "base_url": "https://byok.invalid/v1", "api_key": "byok-secret", - "model_id": "claude-sonnet-4.5", - "wire_model": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", + "wire_model": "claude-sonnet-5", }, ) byok_session_id = session.session_id diff --git a/python/e2e/test_inprocess_ffi_e2e.py b/python/e2e/test_inprocess_ffi_e2e.py index c119c4ea4e..ea82037b7a 100644 --- a/python/e2e/test_inprocess_ffi_e2e.py +++ b/python/e2e/test_inprocess_ffi_e2e.py @@ -15,20 +15,14 @@ from copilot import CopilotClient, RuntimeConnection from .testharness import E2ETestContext -from .testharness.context import get_cli_path_for_tests pytestmark = pytest.mark.asyncio(loop_scope="module") class TestInProcessFfi: - async def test_should_start_and_connect_over_in_process_ffi( - self, ctx: E2ETestContext, monkeypatch: pytest.MonkeyPatch - ): - # In-process hosting loads the runtime cdylib next to the resolved CLI - # entrypoint and lets the native host spawn the worker. ``ping`` is a - # purely local RPC round-trip, so no auth or replay proxy is involved. - # If the native library is unavailable, start() raises and the test fails. - monkeypatch.setenv("COPILOT_CLI_PATH", get_cli_path_for_tests()) + async def test_should_start_and_connect_over_in_process_ffi(self, ctx: E2ETestContext): + # In-process hosting loads runtime.node directly. ``ping`` is a purely local + # RPC round-trip, so no auth or replay proxy is involved. client = CopilotClient(connection=RuntimeConnection.for_inprocess()) await client.start() diff --git a/python/e2e/test_mode_handlers_e2e.py b/python/e2e/test_mode_handlers_e2e.py index f6173a4a5e..d5182c453d 100644 --- a/python/e2e/test_mode_handlers_e2e.py +++ b/python/e2e/test_mode_handlers_e2e.py @@ -6,6 +6,7 @@ import pytest +from copilot.rpc import ModeSetRequest from copilot.session import PermissionHandler from copilot.session_events import ( AutoModeSwitchCompletedData, @@ -15,6 +16,7 @@ ExitPlanModeCompletedData, ExitPlanModeRequestedData, SessionIdleData, + SessionMode, SessionModelChangeData, ) @@ -111,10 +113,8 @@ async def on_exit_plan_mode_request(request, invocation): ) ) - response = await session.send_and_wait( - PLAN_PROMPT, - agent_mode="plan", - ) + await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN)) + response = await session.send_and_wait(PLAN_PROMPT) assert len(exit_plan_mode_requests) == 1 request = exit_plan_mode_requests[0] diff --git a/python/e2e/test_rewind_e2e.py b/python/e2e/test_rewind_e2e.py index 10e7bc4dd9..cf7627c1a3 100644 --- a/python/e2e/test_rewind_e2e.py +++ b/python/e2e/test_rewind_e2e.py @@ -4,7 +4,6 @@ import asyncio import os -import sys from pathlib import Path import pytest @@ -22,6 +21,8 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") FILE_NAME = "rewind-sdk.txt" +ORIGINAL_FILE_CONTENT = "Original rewind content" +PREPARED_FILE_CONTENT = "Prepared rewind content" FILE_CONTENT = "SDK rewind content" @@ -31,19 +32,27 @@ def _same_path(left: str | Path, right: str | Path) -> bool: class TestRewind: async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestContext): - if sys.platform == "win32": - pytest.skip("blocked on CLI 1.0.81 file-change tracking regression on Windows") - file_path = Path(ctx.work_dir) / FILE_NAME + file_path.write_text(ORIGINAL_FILE_CONTENT, encoding="utf-8") session = await ctx.client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_file_change_tracking=True, on_permission_request=PermissionHandler.approve_all, ) try: + ready = await session.send_and_wait( + f"Use the edit tool to replace the exact contents of {FILE_NAME} " + f"from {ORIGINAL_FILE_CONTENT} to {PREPARED_FILE_CONTENT}. " + "After the tool succeeds, reply with exactly SDK_REWIND_READY." + ) + assert ready is not None + assert ready.data.content == "SDK_REWIND_READY" + assert file_path.read_text(encoding="utf-8") == PREPARED_FILE_CONTENT + response = await session.send_and_wait( - f"Use the create tool to create {FILE_NAME} containing exactly {FILE_CONTENT}. " + f"Use the edit tool to replace the exact contents of {FILE_NAME} " + f"from {PREPARED_FILE_CONTENT} to {FILE_CONTENT}. " "After the tool succeeds, reply with exactly SDK_REWIND_DONE." ) @@ -59,16 +68,18 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo deadline = asyncio.get_running_loop().time() + 30 while asyncio.get_running_loop().time() < deadline and not ( rewind_points.unavailable_reason is None - and rewind_points.points - and rewind_points.points[0].can_restore_files + and len(rewind_points.points) == 2 + and rewind_points.points[1].turn_changed_files + and rewind_points.points[1].can_restore_files ): await asyncio.sleep(0.1) rewind_points = await session.rpc.history.list_rewind_points() assert rewind_points.unavailable_reason is None assert rewind_points.file_change_tracking_enabled - assert len(rewind_points.points) == 1 - rewind_point = rewind_points.points[0] + assert len(rewind_points.points) == 2 + rewind_point = rewind_points.points[1] + assert rewind_point.turn_changed_files assert rewind_point.can_restore_files assert rewind_point.file_count == 1 @@ -89,7 +100,7 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo assert rewind.events_removed is not None and rewind.events_removed > 0 assert len(rewind.restored_files) == 1 assert _same_path(rewind.restored_files[0], file_path) - assert not file_path.exists() + assert file_path.read_text(encoding="utf-8") == PREPARED_FILE_CONTENT events = await session.get_events() assert all(str(event.id) != rewind_point.event_id for event in events) diff --git a/python/e2e/test_rpc_e2e.py b/python/e2e/test_rpc_e2e.py index 4440635727..c9f08742cd 100644 --- a/python/e2e/test_rpc_e2e.py +++ b/python/e2e/test_rpc_e2e.py @@ -82,7 +82,7 @@ class TestSessionRpc: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): """Test calling session.rpc.model.getCurrent""" session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) result = await session.rpc.model.get_current() @@ -96,7 +96,7 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext from copilot.rpc import ModelSwitchToRequest session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) # Get initial model diff --git a/python/e2e/test_rpc_server_e2e.py b/python/e2e/test_rpc_server_e2e.py index e7c4a446ce..fdff3b8004 100644 --- a/python/e2e/test_rpc_server_e2e.py +++ b/python/e2e/test_rpc_server_e2e.py @@ -183,7 +183,7 @@ async def test_should_call_rpc_models_list_with_typed_result(self, authed_ctx: E await client.start() result = await client.rpc.models.list(ModelsListRequest()) assert result.models is not None - assert any(model.id == "claude-sonnet-4.5" for model in result.models) + assert any(model.id == "claude-sonnet-5" for model in result.models) assert all((model.name or "").strip() for model in result.models) finally: try: diff --git a/python/e2e/test_rpc_session_state_e2e.py b/python/e2e/test_rpc_session_state_e2e.py index f4b03d2e65..622192cfe9 100644 --- a/python/e2e/test_rpc_session_state_e2e.py +++ b/python/e2e/test_rpc_session_state_e2e.py @@ -104,7 +104,7 @@ class TestRpcSessionState: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: result = await session.rpc.model.get_current() @@ -128,7 +128,7 @@ async def test_should_call_session_rpc_model_switchto(self, ctx: E2ETestContext) ) session = await isolated_ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: before = await session.rpc.model.get_current() @@ -264,13 +264,13 @@ async def test_should_call_metadata_snapshot_set_working_directory_and_record_co session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", working_directory=first_dir, ) try: snapshot = await session.rpc.metadata.snapshot() assert snapshot.session_id == session.session_id - assert snapshot.selected_model == "claude-sonnet-4.5" + assert snapshot.selected_model == "claude-sonnet-5" assert snapshot.is_remote is False assert snapshot.already_in_use is False assert _path_equals(first_dir, snapshot.working_directory) @@ -395,7 +395,7 @@ async def snapshot_updated() -> bool: async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: reasoning = await session.rpc.model.set_reasoning_effort( @@ -403,7 +403,7 @@ async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestConte ) assert reasoning.reasoning_effort == "high" current = await session.rpc.model.get_current() - assert current.model_id == "claude-sonnet-4.5" + assert current.model_id == "claude-sonnet-5" assert current.reasoning_effort == "high" auto_name = f"Auto Session {uuid.uuid4().hex}" @@ -637,12 +637,12 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC MetadataContextInfoRequest( prompt_token_limit=128_000, output_token_limit=4_096, - selected_model="claude-sonnet-4.5", + selected_model="claude-sonnet-5", ) ) if context_info.context_info is not None: context = context_info.context_info - assert context.model_name == "claude-sonnet-4.5" + assert context.model_name == "claude-sonnet-5" assert context.prompt_token_limit == 128_000 assert context.limit >= context.prompt_token_limit assert context.total_tokens > 0 @@ -657,7 +657,7 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC ) recomputed = await session.rpc.metadata.recompute_context_tokens( - MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-4.5") + MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-5") ) assert recomputed.system_token_count > 0 assert recomputed.messages_token_count > 0 diff --git a/python/e2e/test_rpc_session_state_extras_e2e.py b/python/e2e/test_rpc_session_state_extras_e2e.py index 7523059c7b..02ee0cd790 100644 --- a/python/e2e/test_rpc_session_state_extras_e2e.py +++ b/python/e2e/test_rpc_session_state_extras_e2e.py @@ -77,7 +77,7 @@ async def test_should_list_models_for_session(self, ctx: E2ETestContext): client = _make_authed_client(ctx, token) try: async with await client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", on_permission_request=PermissionHandler.approve_all, github_token=token, ) as session: @@ -86,8 +86,7 @@ async def test_should_list_models_for_session(self, ctx: E2ETestContext): assert result.list is not None assert len(result.list) > 0 assert any( - "claude-sonnet-4.5" in json.dumps(model, sort_keys=True) - for model in result.list + "claude-sonnet-5" in json.dumps(model, sort_keys=True) for model in result.list ) finally: await _stop_client(client) @@ -126,7 +125,7 @@ async def test_should_add_byok_provider_and_model_at_runtime(self, ctx: E2ETestC provider=provider_name, id=model_id, name="SDK Runtime Model", - model_id="claude-sonnet-4.5", + model_id="claude-sonnet-5", wire_model="wire-sdk-runtime-model", max_context_window_tokens=4096, max_prompt_tokens=3072, diff --git a/python/e2e/test_session_config_e2e.py b/python/e2e/test_session_config_e2e.py index 62dc671893..4fc78e645d 100644 --- a/python/e2e/test_session_config_e2e.py +++ b/python/e2e/test_session_config_e2e.py @@ -167,8 +167,8 @@ def _create_anthropic_provider() -> dict: "type": "anthropic", "base_url": "https://anthropic-citations.invalid/v1", "api_key": "test-provider-key", - "model_id": "claude-sonnet-4.5", - "wire_model": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", + "wire_model": "claude-sonnet-5", } @@ -201,6 +201,7 @@ async def test_vision_disabled_then_enabled_via_setmodel(self, ctx: E2ETestConte session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, + model="claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=False) ), @@ -213,7 +214,7 @@ async def test_vision_disabled_then_enabled_via_setmodel(self, ctx: E2ETestConte # Switch vision on await session.set_model( - "claude-sonnet-4.5", + "claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=True) ), @@ -234,6 +235,7 @@ async def test_vision_enabled_then_disabled_via_setmodel(self, ctx: E2ETestConte session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, + model="claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=True) ), @@ -246,7 +248,7 @@ async def test_vision_enabled_then_disabled_via_setmodel(self, ctx: E2ETestConte # Switch vision off await session.set_model( - "claude-sonnet-4.5", + "claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=False) ), @@ -295,7 +297,7 @@ async def test_should_forward_clientname_in_useragent(self, ctx: E2ETestContext) async def test_should_forward_custom_provider_headers_on_create(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider=_make_proxy_provider(ctx.proxy_url, "create-provider-header"), ) @@ -319,7 +321,7 @@ async def test_should_forward_custom_provider_headers_on_resume(self, ctx: E2ETe session2 = await ctx.client.resume_session( session_id, on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider=_make_proxy_provider(ctx.proxy_url, "resume-provider-header"), ) @@ -345,7 +347,7 @@ async def test_should_forward_provider_wire_model(self, ctx: E2ETestContext): # it directly (see unit tests for serialization coverage). session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "openai", "base_url": ctx.proxy_url, @@ -374,7 +376,7 @@ async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestCont "type": "openai", "base_url": ctx.proxy_url, "api_key": "test-provider-key", - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", }, ) @@ -382,7 +384,7 @@ async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestCont exchanges = await ctx.get_exchanges() assert len(exchanges) == 1 - assert exchanges[0]["request"]["model"] == "claude-sonnet-4.5" + assert exchanges[0]["request"]["model"] == "claude-sonnet-5" await session.disconnect() @@ -473,7 +475,7 @@ async def test_should_enable_citations_for_anthropic_file_attachments_on_create( try: session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_citations=True, provider=_create_anthropic_provider(), ) @@ -521,7 +523,7 @@ async def test_should_enable_citations_for_anthropic_file_attachments_on_resume( session2 = await resume_client.resume_session( session1.session_id, on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_citations=True, provider=_create_anthropic_provider(), ) diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index 08413f228f..a126eabc40 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -25,7 +25,7 @@ class TestSessions: async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) assert session.session_id @@ -33,7 +33,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): assert len(messages) > 0 assert messages[0].type.value == "session.start" assert messages[0].data.session_id == session.session_id - assert messages[0].data.selected_model == "claude-sonnet-4.5" + assert messages[0].data.selected_model == "claude-sonnet-5" await session.disconnect() diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index 2171e25f2d..8eaecf6244 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -191,7 +191,6 @@ def _apply_inprocess_environment(self) -> None: { "GH_TOKEN": DEFAULT_GITHUB_TOKEN, "GITHUB_TOKEN": DEFAULT_GITHUB_TOKEN, - "COPILOT_CLI_PATH": self.cli_path, "COPILOT_HMAC_KEY": "", "CAPI_HMAC_KEY": "", } @@ -274,11 +273,13 @@ async def configure_for_test(self, test_file: str, test_name: str): if self._proxy: await self._proxy.configure(abs_snapshot_path, self.work_dir) - # Clear temp directories between tests (but leave them in place) - # Use ignore_errors=True / suppress(OSError) to handle race conditions - # where files (e.g., SQLite session-store.db on Windows) may still be - # held open by a background process during cleanup. - for base_dir in (self.home_dir, self.work_dir): + # Keep the in-process runtime's isolated home intact until teardown stops + # the runtime. Removing its open state files on POSIX can leave later tests + # using unlinked database state. + cleanup_dirs = ( + (self.work_dir,) if self._client_inprocess else (self.home_dir, self.work_dir) + ) + for base_dir in cleanup_dirs: base_path = Path(base_dir) base_path.mkdir(parents=True, exist_ok=True) for item in base_path.iterdir(): diff --git a/python/test_cli_download.py b/python/test_cli_download.py index 36952919df..a5a20dce0d 100644 --- a/python/test_cli_download.py +++ b/python/test_cli_download.py @@ -4,6 +4,9 @@ import base64 import hashlib +import io +import os +import tarfile from unittest.mock import patch import pytest @@ -16,6 +19,28 @@ def _integrity(data: bytes, algo: str = "sha512") -> str: return f"{algo}-{base64.b64encode(digest).decode('ascii')}" +def _runtime_package(npm_platform: str) -> bytes: + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + members = { + f"package/prebuilds/{npm_platform}/{wrapper_name}": b"wrapper", + f"package/prebuilds/{npm_platform}/runtime.node": b"runtime", + "package/copilot": b"excluded", + "package/copilot.exe": b"excluded", + f"package/ripgrep/bin/{npm_platform}/rg": b"ripgrep", + "package/definitions/future.json": b"{}", + "package/app.js": b"excluded", + "package/LICENSE.md": b"excluded", + "package/README.md": b"excluded", + } + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w:gz") as archive: + for name, content in members.items(): + info = tarfile.TarInfo(name) + info.size = len(content) + archive.addfile(info, io.BytesIO(content)) + return buffer.getvalue() + + class TestVerifyIntegrity: def test_accepts_matching_checksum(self): data = b"native-library-bytes" @@ -51,3 +76,92 @@ def test_raises_when_integrity_unavailable(self, tmp_path): # The library bytes must never be extracted/written when verification is impossible. extract.assert_not_called() + + +class TestEnsureRuntimeWrapper: + def test_materializes_pair_from_absent_cache_with_stripped_environment( + self, tmp_path, monkeypatch + ): + npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + data = _runtime_package(npm_platform) + cache_dir = tmp_path / "cache" + empty_path = tmp_path / "empty-path" + empty_path.mkdir() + assert not cache_dir.exists() + + for name in ( + "COPILOT_CLI_PATH", + "COPILOT_RUNTIME_HOST_COMMAND", + "COPILOT_RUNTIME_PROVIDER_LIB", + ): + monkeypatch.delenv(name, raising=False) + monkeypatch.setenv("PATH", str(empty_path)) + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), + patch.object(_cli_download, "_should_skip_download", return_value=False), + patch.object(_cli_download, "_fetch_url_bytes", return_value=data), + patch.object( + _cli_download, + "_fetch_runtime_integrity", + return_value=_integrity(data), + ), + ): + wrapper = _cli_download.ensure_runtime_wrapper(version="1.2.3") + + install_dir = cache_dir / "prebuilds" / npm_platform + assert wrapper == str(install_dir / wrapper_name) + assert (install_dir / wrapper_name).read_bytes() == b"wrapper" + assert (install_dir / "runtime.node").read_bytes() == b"runtime" + assert (install_dir / "ripgrep" / "bin" / npm_platform / "rg").read_bytes() == b"ripgrep" + assert (install_dir / "definitions" / "future.json").read_bytes() == b"{}" + assert not (install_dir / "app.js").exists() + assert not (install_dir / "copilot").exists() + assert not (install_dir / "copilot.exe").exists() + assert (install_dir / ".hostless-runtime-assets-v2").is_file() + if os.name != "nt": + assert (install_dir / wrapper_name).stat().st_mode & 0o111 + + def test_rejects_cached_wrapper_without_runtime_node(self, tmp_path): + npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + cache_dir = tmp_path / "cache" + install_dir = cache_dir / "prebuilds" / npm_platform + install_dir.mkdir(parents=True) + (install_dir / wrapper_name).write_bytes(b"wrapper") + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), + ): + with pytest.raises(RuntimeError, match="Incomplete Copilot runtime bundle"): + _cli_download.ensure_runtime_wrapper(version="1.2.3") + + def test_upgrades_pair_only_cache_with_retained_assets(self, tmp_path): + npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + cache_dir = tmp_path / "cache" + install_dir = cache_dir / "prebuilds" / npm_platform + install_dir.mkdir(parents=True) + (install_dir / wrapper_name).write_bytes(b"old-wrapper") + (install_dir / "runtime.node").write_bytes(b"old-runtime") + (install_dir / "copilot").write_bytes(b"legacy-sea") + (install_dir / ".hostless-runtime-assets-v1").write_text("1\n", encoding="ascii") + data = _runtime_package(npm_platform) + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), + patch.object(_cli_download, "_should_skip_download", return_value=False), + patch.object(_cli_download, "_fetch_url_bytes", return_value=data), + patch.object(_cli_download, "_fetch_runtime_integrity", return_value=_integrity(data)), + ): + wrapper = _cli_download.ensure_runtime_wrapper(version="1.2.3") + + assert wrapper == str(install_dir / wrapper_name) + assert (install_dir / wrapper_name).read_bytes() == b"wrapper" + assert not (install_dir / "copilot").exists() + assert (install_dir / ".hostless-runtime-assets-v2").is_file() + assert (install_dir / "ripgrep" / "bin" / npm_platform / "rg").is_file() diff --git a/python/test_client.py b/python/test_client.py index a33f0ecd60..bf8de113ae 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -59,6 +59,27 @@ def test_inprocess_connection_has_no_child_process_options(): assert not hasattr(connection, "args") +def test_explicit_child_process_path_does_not_require_runtime_bundle(tmp_path): + explicit = tmp_path / "copilot" + connection = RuntimeConnection.for_stdio(path=str(explicit)) + + CopilotClient(connection=connection, env={"PATH": str(tmp_path)}) + + assert connection.path == str(explicit) + + +def test_copilot_cli_path_does_not_require_runtime_bundle(tmp_path): + explicit = tmp_path / "copilot" + connection = RuntimeConnection.for_stdio() + + CopilotClient( + connection=connection, + env={"PATH": str(tmp_path), "COPILOT_CLI_PATH": str(explicit)}, + ) + + assert connection.path == str(explicit) + + class TestBuiltinPluginDirectories: @staticmethod async def _start_client(paths=None): @@ -211,6 +232,61 @@ async def test_resume_session_allows_none_permission_handler(self): class TestCreateSessionConfig: + @pytest.mark.asyncio + async def test_ask_user_variant_forwarded_on_create_and_cold_resume(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: list[tuple[str, dict]] = [] + + async def mock_request(method, params, **kwargs): + captured.append((method, params)) + result = {"sessionId": params["sessionId"], "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + + client._client.request = mock_request + await client.create_session( + session_id="ask-user-create", + ask_user_variant="elicitation", + ) + await client.resume_session( + "ask-user-cold-resume", + ask_user_variant="legacy", + ) + await client.create_session(session_id="ask-user-default-create") + await client.resume_session("ask-user-default-cold-resume") + + payloads = {(method, params["sessionId"]): params for method, params in captured} + assert ( + payloads[("session.create", "ask-user-create")]["askUserVariant"] == "elicitation" + ) + assert ( + payloads[("session.resume", "ask-user-cold-resume")]["askUserVariant"] == "legacy" + ) + assert "askUserVariant" not in payloads[("session.create", "ask-user-default-create")] + assert ( + "askUserVariant" not in payloads[("session.resume", "ask-user-default-cold-resume")] + ) + finally: + await client.force_stop() + + @pytest.mark.asyncio + @pytest.mark.parametrize("method", ["create", "resume"]) + async def test_ask_user_variant_rejects_unknown_values(self, method): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + + with pytest.raises(ValueError, match="ask_user_variant"): + if method == "create": + await client.create_session(ask_user_variant="unknown") # type: ignore[arg-type] + else: + await client.resume_session( + "ask-user-cold-resume", + ask_user_variant="unknown", # type: ignore[arg-type] + ) + @pytest.mark.asyncio async def test_additional_directories_forwarded_on_create_and_resume(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) @@ -1071,7 +1147,56 @@ async def mock_request(method, params, **kwargs): await client.force_stop() @pytest.mark.asyncio - async def test_create_and_resume_session_forward_capi_options(self): + @pytest.mark.parametrize( + ("create_capi", "resume_capi", "expected_create", "expected_resume"), + [ + (None, None, None, None), + ({}, {}, {}, {}), + ( + {"enable_web_socket_responses": False}, + {"enable_web_socket_responses": True}, + {"enableWebSocketResponses": False}, + {"enableWebSocketResponses": True}, + ), + ( + {"enable_web_socket_responses": True}, + {"enable_web_socket_responses": False}, + {"enableWebSocketResponses": True}, + {"enableWebSocketResponses": False}, + ), + ( + {"auto_tier": "efficiency"}, + {"auto_tier": "efficiency"}, + {"autoTier": "efficiency"}, + {"autoTier": "efficiency"}, + ), + ( + {"auto_tier": "balance"}, + {"auto_tier": "balance"}, + {"autoTier": "balance"}, + {"autoTier": "balance"}, + ), + ( + {"auto_tier": "intelligence"}, + {"auto_tier": "intelligence"}, + {"autoTier": "intelligence"}, + {"autoTier": "intelligence"}, + ), + ( + {"auto_tier": "balance", "enable_web_socket_responses": False}, + {"auto_tier": "balance", "enable_web_socket_responses": True}, + {"autoTier": "balance", "enableWebSocketResponses": False}, + {"autoTier": "balance", "enableWebSocketResponses": True}, + ), + ], + ) + async def test_create_and_resume_session_forward_capi_options( + self, + create_capi: CapiSessionOptions | None, + resume_capi: CapiSessionOptions | None, + expected_create: dict[str, object] | None, + expected_resume: dict[str, object] | None, + ): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -1088,11 +1213,9 @@ async def mock_request(method, params, **kwargs): return {} client._client.request = mock_request - create_capi: CapiSessionOptions = {"enable_web_socket_responses": False} - resume_capi: CapiSessionOptions = {"enable_web_socket_responses": True} - session = await client.create_session( on_permission_request=PermissionHandler.approve_all, + model="auto", capi=create_capi, ) await client.resume_session( @@ -1101,12 +1224,14 @@ async def mock_request(method, params, **kwargs): capi=resume_capi, ) - assert captured["session.create"]["capi"] == { - "enableWebSocketResponses": False, - } - assert captured["session.resume"]["capi"] == { - "enableWebSocketResponses": True, - } + for method, expected in ( + ("session.create", expected_create), + ("session.resume", expected_resume), + ): + if expected is None: + assert "capi" not in captured[method] + else: + assert captured[method]["capi"] == expected finally: await client.force_stop() @@ -1336,6 +1461,41 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_feature_flags(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + feature_flags = {"ENABLED_TEST_FLAG": True, "DISABLED_TEST_FLAG": False} + + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + feature_flags=feature_flags, + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + feature_flags=feature_flags, + ) + + assert captured["session.create"]["featureFlags"] == feature_flags + assert captured["session.resume"]["featureFlags"] == feature_flags + finally: + await client.force_stop() + class TestURLParsing: def test_parse_port_only_url(self): @@ -2991,6 +3151,105 @@ async def request(self, method, params, **kwargs): await client._verify_protocol_version() assert "enableGitHubTelemetryForwarding" not in captured["connect"] + @pytest.mark.asyncio + async def test_connect_forwards_client_info(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={ + "application_name": "acme-developer-portal", + "application_version": "2.4.0", + "integration_name": "copilot-assistant", + "integration_version": "1.5.0", + }, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == { + "editorName": "acme-developer-portal", + "editorVersion": "2.4.0", + "extensionName": "copilot-assistant", + "extensionVersion": "1.5.0", + } + + @pytest.mark.asyncio + async def test_connect_omits_client_info_when_unset(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert "clientInfo" not in captured["connect"] + + @pytest.mark.asyncio + async def test_connect_forwards_partial_client_info_with_forwarding(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={"application_name": "example-app"}, + on_github_telemetry=lambda _notification: None, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == {"editorName": "example-app"} + assert captured["connect"]["enableGitHubTelemetryForwarding"] is True + + @pytest.mark.asyncio + async def test_connect_drops_empty_client_info_fields(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={"application_name": "example-app", "application_version": ""}, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == {"editorName": "example-app"} + + @pytest.mark.asyncio + async def test_connect_omits_all_empty_client_info(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={ + "application_name": "", + "application_version": "", + "integration_name": "", + "integration_version": "", + }, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert "clientInfo" not in captured["connect"] + @pytest.mark.asyncio async def test_event_routes_to_handler(self): from copilot.generated.rpc import GitHubTelemetryNotification diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 2e8015a97d..a42b9994fc 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -14,6 +14,7 @@ from copilot.session_events import ( AttachmentGitHubReferenceType, + AutoTier, Data, ElicitationCompletedAction, ElicitationRequestedMode, @@ -24,6 +25,8 @@ PermissionRequestMemoryAction, SessionEventType, SessionManagedSettingsResolvedData, + SessionResumeData, + SessionStartData, SessionTaskCompleteData, UserMessageAgentMode, session_event_from_dict, @@ -34,6 +37,40 @@ class TestEventForwardCompatibility: """Test forward compatibility for unknown event types.""" + @pytest.mark.parametrize("event_type", ["session.start", "session.resume"]) + @pytest.mark.parametrize("tier", ["efficiency", "balance", "intelligence", None]) + def test_auto_tier_lifecycle_events_round_trip(self, event_type, tier): + timestamp = "2026-08-28T00:00:00Z" + data = ( + { + "copilotVersion": "1.0.82-1", + "producer": "copilot-agent", + "sessionId": str(uuid4()), + "startTime": timestamp, + "version": 1, + } + if event_type == "session.start" + else {"eventCount": 1, "resumeTime": timestamp} + ) + if tier is not None: + data["autoTier"] = tier + event = session_event_from_dict( + { + "id": str(uuid4()), + "timestamp": timestamp, + "parentId": None, + "type": event_type, + "data": data, + } + ) + assert isinstance(event.data, (SessionStartData, SessionResumeData)) + assert event.data.auto_tier == (AutoTier(tier) if tier is not None else None) + serialized = session_event_to_dict(event)["data"] + if tier is None: + assert "autoTier" not in serialized + else: + assert serialized["autoTier"] == tier + def test_session_usage_info_is_recognized(self): """The session.usage_info event type should be in the enum.""" assert SessionEventType.SESSION_USAGE_INFO.value == "session.usage_info" @@ -144,6 +181,7 @@ def test_managed_settings_client_provenance_round_trips(self): "server", "device", "client", + "policyHelper", "mixed", "none", ] diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8de6797989..b91eebd06c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -454,6 +454,7 @@ dependencies = [ "tracing", "ureq", "uuid", + "windows-sys 0.61.2", "zip", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0f18a9b159..c7a704fd50 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -70,6 +70,12 @@ tokio-tungstenite = { version = "0.24", default-features = false, features = ["c [target.'cfg(windows)'.dependencies] zip = { version = "2", default-features = false, features = ["deflate"], optional = true } +windows-sys = { version = "0.61", default-features = false, features = [ + "Win32_Foundation", + "Win32_System_Diagnostics_ToolHelp", + "Win32_System_JobObjects", + "Win32_System_Threading", +] } [dev-dependencies] rusqlite = { version = "0.35", features = ["bundled"] } @@ -90,6 +96,28 @@ required-features = ["test-support"] name = "protocol_version_test" required-features = ["test-support"] +[[test]] +name = "extension_launch_provider_test" +required-features = ["test-support"] + +[[test]] +name = "extension_launch_provider_runtime_test" +required-features = ["test-support"] + +[[bin]] +name = "copilot-extension-test-fixture" +path = "tests/fixtures/extension_fixture.rs" +required-features = ["test-support"] +test = false +bench = false + +[[bin]] +name = "copilot-host-crash-fixture" +path = "tests/fixtures/host_crash_fixture.rs" +required-features = ["test-support"] +test = false +bench = false + [build-dependencies] base64 = "0.22" dirs = "5" diff --git a/rust/README.md b/rust/README.md index 323d525d37..b9a1f5b1bc 100644 --- a/rust/README.md +++ b/rust/README.md @@ -101,8 +101,63 @@ transports. | `env_remove` | `Vec` | Environment variables to remove | | `extra_args` | `Vec` | Extra CLI flags | | `transport` | `Transport` | `Default`, `Stdio`, `InProcess`, `Tcp`, or `External` | +| `extension_launch_provider` | `Option>` | Connection-global extension launch resolver | -With the default `CliProgram::Resolve`, `Client::start()` resolves the CLI in this order: an explicit `CliProgram::Path(path)`, the `COPILOT_CLI_PATH` env var, then the bundled CLI that was embedded at build time. There is no PATH scanning — if you've opted out of bundling (`default-features = false`) you must supply either `CliProgram::Path` or `COPILOT_CLI_PATH`. +With the default `CliProgram::Resolve`, managed stdio and TCP transports resolve an explicit `CliProgram::Path(path)`, `COPILOT_CLI_PATH`, then the bundled `copilot-runtime` wrapper and adjacent `runtime.node`. In-process transport retains its CLI-entrypoint resolution. There is no PATH scanning. + +#### Extension launch provider + +Hosts that own legacy extension process assets can supply a typed, asynchronous +launch resolver: + +```rust,ignore +use std::collections::HashMap; + +use async_trait::async_trait; +use github_copilot_sdk::extension_launch_provider::{ + ExtensionLaunchProfile, ExtensionLaunchProvider, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use github_copilot_sdk::{Client, ClientOptions, Result}; + +struct AppExtensionLaunchProvider; + +#[async_trait] +impl ExtensionLaunchProvider for AppExtensionLaunchProvider { + async fn resolve( + &self, + request: ExtensionLaunchProviderResolveRequest, + ) -> Result { + Ok(ExtensionLaunchProviderResolveResult { + launch: Some(ExtensionLaunchProfile { + executable: "/app/copilot".to_string(), + args: vec!["/app/preloads/extension_bootstrap.mjs".to_string()], + env: HashMap::from([ + ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()), + ("EXTENSION_PATH".to_string(), request.module_path), + ]), + }), + }) + } +} + +let client = Client::start( + ClientOptions::new().with_extension_launch_provider(AppExtensionLaunchProvider), +).await?; +``` + +`Client::start` registers the provider before returning, and reverse requests +are routed at the connection level rather than through a session. The SDK +forwards the returned executable, arguments, and environment unchanged; it +does not discover or bundle an executable or bootstrap. The runtime owns and +overrides `COPILOT_SDK_PATH`, `SESSION_ID`, and +`COPILOT_EXTENSION_PARENT_PID`. + +`COPILOT_CLI_DIST_DIR` is only appropriate when the host supplies a complete +CLI distribution containing `index.js` and its matching preloads. When the +executable is a version-matched standalone Copilot binary, omit that variable +and set `COPILOT_AUTO_UPDATE=false` so its embedded distribution remains +selected. ### Session @@ -274,6 +329,11 @@ let config = SessionConfig { let session = client.create_session(config).await?; ``` +Use `with_ask_user_variant(AskUserVariant::Elicitation)` together with +`with_elicitation_handler(...)` to expose the structured form-based `ask_user` +tool. The default remains `AskUserVariant::Legacy`. Re-supply the option and +handler through `ResumeSessionConfig` on a cold resume. + For rotating per-session GitHub credentials, install a `GitHubTokenProvider` instead of setting `github_token`: @@ -302,6 +362,30 @@ 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. +### Auto routing tiers + +Use `CapiSessionOptions::with_auto_tier` to select `AutoTier::Efficiency`, +`AutoTier::Balance`, or `AutoTier::Intelligence`. This option is meaningful only +with model `auto` (Auto mode V2). +It requires a runtime version that supports `capi.autoTier`. + +```rust +use github_copilot_sdk::{AutoTier, CapiSessionOptions, SessionConfig}; + +let config = SessionConfig::default() + .with_model("auto") + .with_capi(CapiSessionOptions::new().with_auto_tier(AutoTier::Balance)); +``` + +The same options work with `ResumeSessionConfig::with_capi` and can be combined +with `with_enable_web_socket_responses(false)`. The SDK omits an unset tier: +the runtime chooses its default on create and preserves the persisted/current +tier on resume. An explicit tier overrides the persisted tier on cold resume; +the runtime rejects a conflicting tier when the session is already resident +in memory. The SDK does not choose a default or manage tier persistence. +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) +for the lifecycle rules. + ### Session Hooks Hooks intercept CLI behavior at lifecycle points — tool use, prompt submission, session start/end, and errors. Install a `SessionHooks` impl with [`SessionConfig::with_hooks`] — the SDK auto-enables `hooks` in `SessionConfig` when one is set. @@ -521,6 +605,7 @@ impl ElicitationHandler for MyElicitation { let config = SessionConfig::default() .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_ask_user_variant(AskUserVariant::Elicitation) .with_elicitation_handler(Arc::new(MyElicitation)); ``` @@ -820,6 +905,7 @@ none of them are scheduled for removal. | File | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | | `lib.rs` | `Client`, `ClientOptions`, `CliProgram`, `Transport`, `Error` | +| `extension_launch_provider.rs` | Connection-global `ExtensionLaunchProvider` trait and launch profile DTOs | | `session.rs` | `Session` struct, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session` | | `subscription.rs` | `EventSubscription` / `LifecycleSubscription` (`Stream`-able observer handles for `subscribe()` / `subscribe_lifecycle()`) | | `handler.rs` | `PermissionHandler`, `ElicitationHandler`, `UserInputHandler`, `ExitPlanModeHandler`, `AutoModeSwitchHandler` traits; `ApproveAllHandler`, `DenyAllHandler` | @@ -829,13 +915,15 @@ none of them are scheduled for removal. | `types.rs` | CLI protocol types (`SessionId`, `SessionEvent`, `SessionConfig`, `Tool`, etc.) | | `resolve.rs` | Bundled-CLI resolution (`copilot_binary`) | | `embeddedcli.rs` | Embedded CLI extraction (gated on the default `bundled-cli` feature) | -| `router.rs` | Internal per-session event demux | +| `router.rs` | Internal connection-global request dispatch and per-session event demux | | `jsonrpc.rs` | Internal Content-Length framed JSON-RPC transport | -## Embedded CLI +## Bundled runtime artifacts The SDK provisions its runtime at build time. By default the `bundled-cli` -feature embeds the verified child-process runtime in your compiled crate. +feature embeds the verified `copilot-runtime` wrapper and adjacent +`runtime.node` in your compiled crate. The compatible CLI artifact remains +available separately for `install_bundled_cli` and in-process hosting. Enable `bundled-in-process` to additionally embed the native runtime library and use `Transport::InProcess`: @@ -853,15 +941,11 @@ For builds that prefer a smaller artifact, disable the `bundled-cli` feature: github-copilot-sdk = { version = "0.1", default-features = false } ``` -> **You become responsible for supplying the CLI at runtime.** With -> `bundled-cli` disabled, the produced binary does not contain the CLI -> and will not search the system for one. You must point it at a -> compatible CLI via [`CliProgram::Path`] (on `ClientOptions`) or the -> `COPILOT_CLI_PATH` environment variable, and you are responsible for -> guaranteeing the supplied CLI version is compatible with this SDK -> release. Do **not** assume that whatever CLI happens to be installed -> on the target system will work — the SDK and CLI are versioned -> together. +> **You become responsible for supplying the runtime at deployment.** With +> `bundled-cli` disabled, the produced binary does not contain these artifacts +> and will not search the system for them. For managed child-process transports, +> supply a compatible wrapper pair via an explicit [`CliProgram::Path`]. +> `COPILOT_CLI_PATH` remains a direct program override. > > **Convenience on the build machine only.** As a special case, > `build.rs` downloads and integrity-verifies the compatible CLI version and @@ -870,8 +954,8 @@ github-copilot-sdk = { version = "0.1", default-features = false } > makes local development and CI ergonomic, but it does **not** carry > over when you copy the built binary to another machine — distributed > builds (release artifacts, signed installers, container images, etc.) -> must either keep `bundled-cli` enabled or ship the CLI alongside and -> set `CliProgram::Path` / `COPILOT_CLI_PATH`. +> must either keep `bundled-cli` enabled or ship the runtime pair and set +> `CliProgram::Path`. ### How it works @@ -884,17 +968,17 @@ github-copilot-sdk = { version = "0.1", default-features = false } 2. **Build time:** `build.rs` downloads the platform-specific npm package and verifies its `sha512` integrity against the lockfile or publish snapshot. Then: - - **`bundled-cli` on (default):** creates and embeds a minimal archive containing only the CLI executable. - - **`bundled-in-process` on:** the minimal archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`); no other npm package files are embedded. - - **`bundled-cli` off:** extracts the binary directly into the platform cache (staging file + atomic rename), idempotent across rebuilds. If the extracted binary is already present at the expected path, the download is skipped entirely — the extracted binary *is* the cache. + - **`bundled-cli` on (default):** creates and embeds a minimal archive containing the CLI executable, `copilot-runtime[.exe]`, and `runtime.node`. + - **`bundled-in-process` on:** the archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`). + - **`bundled-cli` off:** extracts the same artifacts directly into the platform cache using staging files and atomic renames. -3. **Runtime:** in both modes the binary lives at: +3. **Runtime:** in both modes the artifacts share one versioned directory: | OS | Path | |----|------| - | macOS | `~/Library/Caches/github-copilot-sdk/cli//copilot` | - | Linux | `${XDG_CACHE_HOME:-~/.cache}/github-copilot-sdk/cli//copilot` | - | Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\copilot.exe` | + | macOS | `~/Library/Caches/github-copilot-sdk/cli//` | + | Linux | `${XDG_CACHE_HOME:-~/.cache}/github-copilot-sdk/cli//` | + | Windows | `%LOCALAPPDATA%\github-copilot-sdk\cli\\` | Old version directories accumulate in siblings; clean them up at your leisure. @@ -923,18 +1007,20 @@ COPILOT_CLI_EXTRACT_DIR = { value = "vendor/copilot", relative = true, force = t ### Skipping the bundle entirely -Set `COPILOT_SKIP_CLI_DOWNLOAD=1` at build time to disable the entire download / bundle / cache mechanism — `build.rs` returns immediately without touching the network. Use this when you always supply the CLI at runtime via `ClientOptions::program = CliProgram::Path(...)` or `COPILOT_CLI_PATH`. Works regardless of the `bundled-cli` feature state; runtime resolution falls through to `Error::BinaryNotFound` unless one of those explicit sources resolves. +Set `COPILOT_SKIP_CLI_DOWNLOAD=1` at build time to disable the entire download / bundle / cache mechanism — `build.rs` returns immediately without touching the network. Use this when you always supply the managed runtime via `ClientOptions::program = CliProgram::Path(...)`. Works regardless of the `bundled-cli` feature state; runtime resolution falls through to `Error::BinaryNotFound` unless an applicable explicit source resolves. ### Resolution priority -`Client::start` resolves the CLI in this order: +For managed child-process transports, `Client::start` resolves the program in this order: 1. Explicit `CliProgram::Path(path)` on `ClientOptions::program`. 2. `COPILOT_CLI_PATH` environment variable, if it points at a real file. -3. **`bundled-cli` on:** the embedded archive, lazily extracted on first call. -4. **`bundled-cli` off:** the build-time-extracted binary in the per-user cache, located by recomputing the convention from `COPILOT_SDK_CLI_VERSION` + OS + optional `COPILOT_CLI_EXTRACT_DIR`. +3. **`bundled-cli` on:** the embedded wrapper pair, lazily extracted on first call. +4. **`bundled-cli` off:** the build-time-extracted wrapper pair in the per-user cache. -There is no PATH scanning. If none of the above resolves, `Client::start` returns `Error::BinaryNotFound`. +In-process transport resolves the compatible CLI artifact from +`COPILOT_CLI_PATH`, the embedded archive, or the build-time cache. There is no +PATH scanning. ### Reaching the bundled binary without a `Client` @@ -954,12 +1040,24 @@ if HAS_BUNDLED_CLI { } ``` -This returns the same path `Client::start` would resolve to for -`CliProgram::Resolve` with no `COPILOT_CLI_PATH` override and no -`ClientOptions::bundled_cli_extract_dir` configured. It returns `None` -when `bundled-cli` is off or the target is unsupported, and (unlike the -full resolver) does not fall back to the build-time-extracted dev-cache -path. +This returns the bundled CLI artifact, preserving the public API's original +meaning. Managed child-process transports resolve `copilot-runtime` instead. +The function returns `None` when `bundled-cli` is off or the target is +unsupported and does not fall back to the build-time extraction cache. + +Use [`install_bundled_runtime`] when a health check or intermediate launcher +needs the managed runtime executable: + +```rust,no_run +use github_copilot_sdk::install_bundled_runtime; + +if let Some(path) = install_bundled_runtime() { + println!("bundled runtime at {}", path.display()); +} +``` + +This extracts `copilot-runtime` together with adjacent `runtime.node`, then +returns the wrapper path. ### Download cache (build-time, embed mode) @@ -973,8 +1071,8 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64` | Feature | Default | Description | | ------- | ------- | ----------- | -| `bundled-cli` | ✓ | Embeds only the CLI executable. Disable via `default-features = false` when supplying the CLI via `CliProgram::Path` or `COPILOT_CLI_PATH`. | -| `bundled-in-process` | — | Enables `Transport::InProcess`, implies `bundled-cli`, and additionally embeds only the platform-native runtime library. | +| `bundled-cli` | ✓ | Embeds the managed wrapper pair and compatible CLI artifact. Disable via `default-features = false` when supplying the runtime explicitly. | +| `bundled-in-process` | — | Enables `Transport::InProcess`, implies `bundled-cli`, and additionally embeds the platform-native runtime library. | | `derive` | — | `schema_for::()` for generating JSON Schema from Rust types (adds `schemars`). | ```toml diff --git a/rust/build.rs b/rust/build.rs index d04cf2870b..c01464bb4a 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -1,11 +1,6 @@ -#[cfg(feature = "bundled-in-process")] #[path = "build/in_process.rs"] mod implementation; -#[cfg(not(feature = "bundled-in-process"))] -#[path = "build/out_of_process.rs"] -mod implementation; - fn main() { implementation::main(); } diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs index 5826fbfa76..e6edcf1b90 100644 --- a/rust/build/in_process.rs +++ b/rust/build/in_process.rs @@ -40,7 +40,7 @@ pub(crate) fn main() { // path source resolves first. if std::env::var_os("COPILOT_SKIP_CLI_DOWNLOAD").is_some() { println!( - "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping CLI download/bundle/cache" + "cargo:warning=COPILOT_SKIP_CLI_DOWNLOAD is set — skipping runtime download/bundle/cache" ); return; } @@ -95,38 +95,61 @@ pub(crate) fn main() { if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_some() { let archive = cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); - verify_binary_present_in_archive(&archive, platform.binary_name, &archive_name); + verify_runtime_package(&archive, platform, &archive_name); emit_embedded(out, &archive, platform, include_runtime); println!("cargo:rustc-cfg=has_bundled_cli"); } else { - // With `bundled-cli` off the extracted binary *is* the cache. - // Skip the upstream download entirely when it already exists at - // the expected path. No two separate caches. + // With `bundled-cli` off the extracted runtime pair *is* the cache. + // Skip the upstream download entirely when both files already exist. // - // Runtime resolution (see `src/resolve.rs::extracted_cli_path`) + // Runtime resolution (see `src/resolve.rs::extracted_program`) // recomputes this same path from `COPILOT_SDK_CLI_VERSION` + the // OS-derived binary name + optional `COPILOT_CLI_EXTRACT_DIR`, // so we don't bake an absolute path into the crate. let install_dir = extracted_install_dir(&version); - let final_path = install_dir.join(platform.binary_name); - - // Invalidate build.rs whenever the cached binary disappears (cache GC, - // manual rm, OS reset, switching extract dir). Without this, cargo + let required_paths = [ + install_dir.join(platform.runtime_wrapper_name()), + install_dir.join("runtime.node"), + install_dir.join(".hostless-runtime-assets-v1"), + ]; + let expected_marker = format!("{version}\n{expected_integrity}\n"); + + // Invalidate build.rs whenever either cached artifact disappears (cache + // GC, manual rm, OS reset, switching extract dir). Without this, cargo // replays the saved `has_extracted_cli` cfg from its build-script // output cache even when the file is gone, and runtime resolution // fails with BinaryNotFound. - println!("cargo:rerun-if-changed={}", final_path.display()); + for path in &required_paths { + println!("cargo:rerun-if-changed={}", path.display()); + } - if !final_path.is_file() { + let cache_is_current = required_paths.iter().all(|path| path.is_file()) + && std::fs::read_to_string(&required_paths[2]).ok().as_deref() + == Some(expected_marker.as_str()); + if !cache_is_current { + if install_dir.exists() { + std::fs::remove_dir_all(&install_dir).unwrap_or_else(|e| { + panic!( + "failed to clear stale runtime bundle {}: {e}", + install_dir.display() + ) + }); + } let archive = cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); - verify_binary_present_in_archive(&archive, platform.binary_name, &archive_name); - extract_to_cache(&archive, &install_dir, platform); + verify_runtime_package(&archive, platform, &archive_name); + extract_to_cache( + &archive, + &install_dir, + platform, + include_runtime, + &expected_marker, + ); } // Re-check after potential download+extract above; not an `else` // because we need to verify the extraction actually produced the file. - if final_path.is_file() { + if required_paths.iter().all(|path| path.is_file()) { println!("cargo:rustc-cfg=has_extracted_cli"); } } @@ -176,19 +199,8 @@ fn build_embedded_archive(package: &[u8], platform: Platform, include_runtime: b .mtime(0) .write(Vec::new(), flate2::Compression::default()); let mut archive = tar::Builder::new(encoder); - append_archive_file( - &mut archive, - platform.binary_name, - &extract_binary_bytes(package, platform), - 0o755, - ); + let runtime = append_hostless_runtime_tree(&mut archive, package, platform); if include_runtime { - let runtime = extract_runtime_library_bytes(package).unwrap_or_else(|| { - panic!( - "package `{}` does not contain the native runtime library required by the `bundled-in-process` feature", - platform.package_name - ) - }); append_archive_file( &mut archive, platform.runtime_library_name(), @@ -204,6 +216,103 @@ fn build_embedded_archive(package: &[u8], platform: Platform, include_runtime: b .expect("failed to compress minimal embedded CLI archive") } +fn append_hostless_runtime_tree( + archive: &mut tar::Builder, + package: &[u8], + platform: Platform, +) -> Vec { + let decoder = flate2::read::GzDecoder::new(package); + let mut source = tar::Archive::new(decoder); + let mut runtime = None; + for entry in source + .entries() + .unwrap_or_else(|e| panic!("failed to read npm package entries: {e}")) + { + let mut entry = entry.unwrap_or_else(|e| panic!("failed to read npm package entry: {e}")); + if !entry.header().entry_type().is_file() { + continue; + } + let source_path = entry + .path() + .unwrap_or_else(|e| panic!("failed to read npm package path: {e}")); + let Some(destination) = hostless_runtime_path(&source_path.to_string_lossy(), platform) + else { + continue; + }; + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .unwrap_or_else(|e| panic!("failed to read npm package entry bytes: {e}")); + let mode = entry.header().mode().unwrap_or(0o644); + if destination == Path::new("runtime.node") { + runtime = Some(bytes.clone()); + } + append_archive_file( + archive, + destination + .to_str() + .expect("npm package paths are valid UTF-8"), + &bytes, + mode, + ); + } + runtime.unwrap_or_else(|| { + panic!( + "package `{}` does not contain prebuilds//runtime.node", + platform.package_name + ) + }) +} + +fn hostless_runtime_path(source: &str, platform: Platform) -> Option { + let relative = source.strip_prefix("package/")?; + let parts: Vec<&str> = relative.split('/').collect(); + if parts.iter().any(|part| part.is_empty() || *part == "..") { + return None; + } + let top_level = *parts.first()?; + let file_name = *parts.last()?; + const EXCLUDED_TOP_LEVEL: &[&str] = &[ + "app.js", + "assets", + "changelog.json", + "copilot-sdk", + "foundry-local-sdk", + "index.js", + "LICENSE.md", + "napi-oop-runtime", + "npm-loader.js", + "package.json", + "preloads", + "pvrecorder", + "queries", + "README.md", + "sdk", + "sea-loader.js", + "webview", + ]; + if EXCLUDED_TOP_LEVEL.contains(&top_level) + || (top_level.starts_with("tree-sitter") && top_level.ends_with(".wasm")) + || (top_level.starts_with("voice-") && top_level.ends_with(".js")) + || file_name == "cli-native.node" + || parts.contains(&"mediaremote-adapter") + || file_name.starts_with("copilot-runtime-bin") + { + return None; + } + if top_level == "prebuilds" { + let npm_platform = platform + .package_name + .strip_prefix("copilot-") + .expect("platform package name has copilot- prefix"); + if parts.get(1) != Some(&npm_platform) || parts.len() < 3 { + return None; + } + return Some(parts[2..].iter().copied().collect()); + } + Some(parts.iter().copied().collect()) +} + fn append_archive_file( archive: &mut tar::Builder, path: &str, @@ -315,6 +424,14 @@ struct Platform { } impl Platform { + fn runtime_wrapper_name(&self) -> &'static str { + if self.package_name.contains("win32") { + "copilot-runtime.exe" + } else { + "copilot-runtime" + } + } + fn runtime_library_name(&self) -> &'static str { if self.package_name.contains("win32") { "copilot_runtime.dll" @@ -368,8 +485,8 @@ fn target_platform() -> Option { } } -/// Write the single binary entry from `archive` to -/// `/` and return the resulting path. +/// Write the runtime wrapper pair from `archive` to `install_dir` and return +/// the wrapper path. /// Idempotent — returns the existing path if a previous build already /// populated the target. /// @@ -378,15 +495,13 @@ fn target_platform() -> Option { /// binary. `fs::rename` for files is atomic on both Unix and Windows /// (Windows uses `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING`); for /// directories it is not, which is why we stage at file granularity. -fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> PathBuf { - let final_path = install_dir.join(platform.binary_name); - - // Caller already gated on `final_path.is_file()`; this is a safety - // net for any future caller that forgets. - if final_path.is_file() { - return final_path; - } - +fn extract_to_cache( + archive: &[u8], + install_dir: &Path, + platform: Platform, + include_runtime: bool, + marker: &str, +) -> PathBuf { std::fs::create_dir_all(install_dir).unwrap_or_else(|e| { panic!( "failed to create install dir {}: {e}", @@ -394,8 +509,96 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P ) }); - let bytes = extract_binary_bytes(archive, platform); + let decoder = flate2::read::GzDecoder::new(archive); + let mut source = tar::Archive::new(decoder); + let mut runtime = None; + for entry in source + .entries() + .unwrap_or_else(|e| panic!("failed to read npm package entries: {e}")) + { + let mut entry = entry.unwrap_or_else(|e| panic!("failed to read npm package entry: {e}")); + if !entry.header().entry_type().is_file() { + continue; + } + let source_path = entry + .path() + .unwrap_or_else(|e| panic!("failed to read npm package path: {e}")); + let Some(destination) = hostless_runtime_path(&source_path.to_string_lossy(), platform) + else { + continue; + }; + if destination == Path::new(platform.binary_name) { + continue; + } + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .unwrap_or_else(|e| panic!("failed to read npm package entry bytes: {e}")); + let executable = entry.header().mode().unwrap_or(0o644) & 0o111 != 0; + if destination == Path::new("runtime.node") { + runtime = Some(bytes.clone()); + } + install_cached_file_path(install_dir, &destination, &bytes, executable); + } + let runtime = runtime.expect("verified runtime.node is present"); + if include_runtime { + install_cached_file( + install_dir, + platform.runtime_library_name(), + &runtime, + false, + ); + } + install_cached_file( + install_dir, + ".hostless-runtime-assets-v1", + marker.as_bytes(), + false, + ); + + let final_path = install_dir.join(platform.runtime_wrapper_name()); + println!( + "cargo:warning=Extracted Copilot runtime bundle to {}", + install_dir.display() + ); + final_path +} + +fn install_cached_file(install_dir: &Path, file_name: &str, bytes: &[u8], executable: bool) { + install_cached_file_path(install_dir, Path::new(file_name), bytes, executable); +} +fn install_cached_file_path( + install_dir: &Path, + relative_path: &Path, + bytes: &[u8], + executable: bool, +) { + // `executable` only affects file permissions on Unix (see the `#[cfg(unix)]` + // block below); explicitly mark it used elsewhere so non-Unix targets don't + // warn about an unused parameter under `-D warnings`. + #[cfg(not(unix))] + let _ = executable; + + assert!( + !relative_path.is_absolute() + && !relative_path.components().any(|component| { + matches!( + component, + std::path::Component::Prefix(_) + | std::path::Component::RootDir + | std::path::Component::ParentDir + ) + }), + "unsafe runtime package path: {}", + relative_path.display() + ); + let final_path = install_dir.join(relative_path); + if final_path.is_file() { + return; + } + std::fs::create_dir_all(final_path.parent().expect("runtime asset has parent")) + .unwrap_or_else(|e| panic!("failed to create runtime asset directory: {e}")); // Staging file is a sibling of the final binary so the rename stays // on the same filesystem (cross-fs rename is not atomic). PID + nanos // disambiguate concurrent builds racing on the same cache. @@ -405,7 +608,10 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P .unwrap_or(0); let staging_path = install_dir.join(format!( ".{}.staging-{}-{nanos}", - platform.binary_name, + relative_path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("runtime-asset"), std::process::id(), )); @@ -418,7 +624,7 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P ); }); - if let Err(e) = f.write_all(&bytes) { + if let Err(e) = f.write_all(bytes) { let _ = std::fs::remove_file(&staging_path); panic!( "failed to write staging file {}: {e}", @@ -427,7 +633,7 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P } #[cfg(unix)] - { + if executable { use std::os::unix::fs::PermissionsExt; if let Err(e) = f.set_permissions(std::fs::Permissions::from_mode(0o755)) { let _ = std::fs::remove_file(&staging_path); @@ -472,32 +678,6 @@ fn extract_to_cache(archive: &[u8], install_dir: &Path, platform: Platform) -> P final_path.display() ); } - - // Surface where the binary landed so contributors can find it. Quiet - // on the hot path: the caller's `is_file()` short-circuit (and the - // safety net at the top of this function) means this only fires on a - // true cache miss. - println!( - "cargo:warning=Extracted Copilot CLI to {}", - final_path.display() - ); - - final_path -} - -fn extract_runtime_library_bytes(archive: &[u8]) -> Option> { - let gz = flate2::read::GzDecoder::new(archive); - let mut tar = tar::Archive::new(gz); - for entry in tar.entries().ok()? { - let mut entry = entry.ok()?; - let name = entry.path().ok()?.to_string_lossy().into_owned(); - if name == "runtime.node" || name.ends_with("/runtime.node") { - let mut bytes = Vec::with_capacity(entry.size() as usize); - entry.read_to_end(&mut bytes).ok()?; - return Some(bytes); - } - } - None } /// Replace characters outside `[a-zA-Z0-9._-]` with `_` so the version @@ -514,37 +694,6 @@ fn sanitize_version(version: &str) -> String { .collect() } -/// Extract the single `binary_name` entry from the npm package archive. Reused -/// between embed mode's `verify_binary_present_in_archive` and the -/// `extract_to_cache` path used when `bundled-cli` is off. Panics if the -/// entry isn't found — callers have already invoked -/// `verify_binary_present_in_archive`. -fn extract_binary_bytes(archive: &[u8], platform: Platform) -> Vec { - let gz = flate2::read::GzDecoder::new(archive); - let mut tar = tar::Archive::new(gz); - for entry in tar - .entries() - .unwrap_or_else(|e| panic!("failed to read tar entries: {e}")) - { - let mut entry = entry.unwrap_or_else(|e| panic!("failed to read tar entry: {e}")); - let path = entry - .path() - .unwrap_or_else(|e| panic!("failed to read tar entry path: {e}")); - let name = path.to_string_lossy().into_owned(); - if name == platform.binary_name || name.ends_with(&format!("/{}", platform.binary_name)) { - let mut bytes = Vec::with_capacity(entry.size() as usize); - entry - .read_to_end(&mut bytes) - .unwrap_or_else(|e| panic!("failed to read tar entry bytes: {e}")); - return bytes; - } - } - panic!( - "binary `{}` not found in package `{}`", - platform.binary_name, platform.package_name - ); -} - /// Read a file from the download cache, or download it (with retries) and save /// to cache. Verifies npm integrity on every path. Evicts stale/corrupt cache entries /// automatically. Cache I/O failures are treated as cache misses — they never @@ -682,15 +831,17 @@ fn try_download(url: &str) -> Result, DownloadError> { } } -/// Walks the downloaded archive at build time to confirm an entry matching -/// `binary_name` exists. Panics with a clear message if not. -fn verify_binary_present_in_archive(archive: &[u8], binary_name: &str, package_name: &str) { - let found = archive_contains_tar_entry(archive, binary_name); - if !found { +fn verify_runtime_package(archive: &[u8], platform: Platform, package_name: &str) { + for file_name in [ + platform.binary_name, + "runtime.node", + platform.runtime_wrapper_name(), + ] { + if archive_contains_tar_entry(archive, file_name) { + continue; + } panic!( - "Copilot CLI package `{package_name}` does not contain an entry named `{binary_name}`. \ - The package layout may have changed; runtime extraction would fail. \ - Update `verify_binary_present_in_archive` in build.rs and the matching `extract_binary` in src/embeddedcli.rs." + "Copilot runtime package `{package_name}` does not contain an entry named `{file_name}`" ); } } diff --git a/rust/src/embeddedcli.rs b/rust/src/embeddedcli.rs index 40900a4d22..3cc527a2e2 100644 --- a/rust/src/embeddedcli.rs +++ b/rust/src/embeddedcli.rs @@ -3,10 +3,10 @@ //! feature set). //! //! Normal builds embed the platform release archive from GitHub Releases. -//! Builds with `bundled-in-process` instead embed a minimal archive from the -//! platform npm package containing the CLI executable and native runtime -//! library. Extraction to a real on-disk path is deferred until the first call -//! to [`path`] / [`install_at`]. +//! Builds with `bundled-in-process` instead embed a filtered archive from the +//! platform npm package containing the CLI executable, runtime wrapper, native +//! runtime artifacts, and auxiliary runtime assets. Extraction to a real +//! on-disk path is deferred until the relevant installer is called. //! //! The embedded bytes are part of the consumer's signed binary and therefore //! trusted *as the source of truth* — but the bytes that land on disk are not. @@ -28,7 +28,7 @@ // off but still needs to exercise them. #[cfg(any(has_bundled_cli, test))] use std::fs; -#[cfg(all(has_bundled_cli, any(feature = "bundled-in-process", not(windows))))] +#[cfg(has_bundled_cli)] use std::io::Read; #[cfg(any(has_bundled_cli, test))] use std::io::Write; @@ -65,9 +65,19 @@ const CLI_VERSION: &str = env!("COPILOT_SDK_CLI_VERSION"); const CLI_BINARY_NAME: &str = "copilot.exe"; #[cfg(all(has_bundled_cli, not(windows)))] const CLI_BINARY_NAME: &str = "copilot"; +#[cfg(all(has_bundled_cli, windows))] +const RUNTIME_BINARY_NAME: &str = "copilot-runtime.exe"; +#[cfg(all(has_bundled_cli, not(windows)))] +const RUNTIME_BINARY_NAME: &str = "copilot-runtime"; +#[cfg(has_bundled_cli)] +const RUNTIME_NODE_NAME: &str = "runtime.node"; +#[cfg(has_bundled_cli)] +const RUNTIME_VERSION_MARKER: &str = ".copilot-runtime-version"; #[cfg(feature = "bundled-cli")] static INSTALLED_PATH: OnceLock> = OnceLock::new(); +#[cfg(feature = "bundled-cli")] +static INSTALLED_RUNTIME_PATH: OnceLock> = OnceLock::new(); /// Returns the path to the installed CLI binary, lazily extracting the /// embedded archive on first call. @@ -91,7 +101,7 @@ pub(crate) fn path() -> Option { #[cfg(has_bundled_cli)] { let dir = default_install_dir(CLI_VERSION); - match install(&dir, build_time::CLI_ARCHIVE) { + match install_cli_bundle(&dir, build_time::CLI_ARCHIVE) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed"); return Some(path); @@ -119,7 +129,7 @@ pub(crate) fn path() -> Option { pub(crate) fn install_at(extract_dir: &Path) -> Option { #[cfg(has_bundled_cli)] { - match install(extract_dir, build_time::CLI_ARCHIVE) { + match install_cli_bundle(extract_dir, build_time::CLI_ARCHIVE) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed"); return Some(path); @@ -136,6 +146,93 @@ pub(crate) fn install_at(extract_dir: &Path) -> Option { None } +/// Returns the path to the bundled runtime wrapper, extracting the wrapper and +/// adjacent `runtime.node` on first call. +#[cfg(feature = "bundled-cli")] +pub(crate) fn runtime_path() -> Option { + INSTALLED_RUNTIME_PATH + .get_or_init(|| { + #[cfg(has_bundled_cli)] + { + let dir = default_install_dir(CLI_VERSION); + match install_runtime(&dir, build_time::CLI_ARCHIVE) { + Ok(path) => { + info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed"); + return Some(path); + } + Err(e) => { + warn!(error = %e, "embedded runtime installation failed"); + } + } + } + None + }) + .clone() +} + +/// Installs the bundled runtime wrapper and adjacent `runtime.node` into a +/// caller-specified directory. +#[cfg(feature = "bundled-cli")] +pub(crate) fn install_runtime_at(extract_dir: &Path) -> Option { + #[cfg(has_bundled_cli)] + { + let install_dir = match runtime_install_dir(extract_dir, CLI_VERSION) { + Ok(dir) => dir, + Err(e) => { + warn!(error = %e, "embedded runtime install directory selection failed"); + return None; + } + }; + match install_runtime(&install_dir, build_time::CLI_ARCHIVE) { + Ok(path) => { + info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed"); + return Some(path); + } + Err(e) => { + warn!(error = %e, "embedded runtime installation failed"); + } + } + } + #[cfg(not(has_bundled_cli))] + { + let _ = extract_dir; + } + None +} + +#[cfg(has_bundled_cli)] +fn runtime_install_dir(base_dir: &Path, version: &str) -> Result { + fs::create_dir_all(base_dir) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?; + let marker = base_dir.join(RUNTIME_VERSION_MARKER); + match fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&marker) + { + Ok(mut file) => { + if let Err(error) = file + .write_all(version.as_bytes()) + .and_then(|()| file.sync_all()) + { + drop(file); + let _ = fs::remove_file(&marker); + return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, error)); + } + Ok(base_dir.to_path_buf()) + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + let installed_version = fs::read_to_string(marker).unwrap_or_default(); + if installed_version == version { + Ok(base_dir.to_path_buf()) + } else { + Ok(base_dir.join(version)) + } + } + Err(error) => Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, error)), + } +} + #[cfg(has_bundled_cli)] fn default_install_dir(version: &str) -> PathBuf { let cache = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); @@ -168,34 +265,151 @@ const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.dylib"; const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.so"; #[cfg(has_bundled_cli)] -fn install(install_dir: &Path, archive: &[u8]) -> Result { - let final_path = install_cli(install_dir, archive)?; +fn install_cli_bundle(install_dir: &Path, archive: &[u8]) -> Result { + install_cli(install_dir, archive)?; + install_hostless_assets(install_dir, archive)?; #[cfg(feature = "bundled-in-process")] { install_runtime_library(install_dir, archive)?; } - Ok(final_path) + Ok(install_dir.join(CLI_BINARY_NAME)) +} + +#[cfg(has_bundled_cli)] +fn install_runtime(install_dir: &Path, archive: &[u8]) -> Result { + fs::create_dir_all(install_dir) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?; + install_hostless_assets(install_dir, archive)?; + install_runtime_pair(install_dir, archive)?; + Ok(install_dir.join(RUNTIME_BINARY_NAME)) +} + +#[cfg(has_bundled_cli)] +fn install_hostless_assets(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> { + let gz = flate2::read::GzDecoder::new(archive); + let mut tar = tar::Archive::new(gz); + for entry in tar + .entries() + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))? + { + let mut entry = + entry.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?; + if !entry.header().entry_type().is_file() { + continue; + } + let path = entry + .path() + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))? + .into_owned(); + let file_name = path.file_name().and_then(|name| name.to_str()); + if path == Path::new(CLI_BINARY_NAME) + || matches!( + file_name, + Some("copilot_runtime.dll") + | Some("libcopilot_runtime.dylib") + | Some("libcopilot_runtime.so") + ) + { + continue; + } + if path.is_absolute() + || path.components().any(|component| { + matches!( + component, + std::path::Component::Prefix(_) + | std::path::Component::RootDir + | std::path::Component::ParentDir + ) + }) + { + return Err(EmbeddedCliError::with_message( + EmbeddedCliErrorKind::Archive, + format!("unsafe embedded runtime asset path: {}", path.display()), + )); + } + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?; + let target = install_dir.join(&path); + if fs::read(&target) + .map(|installed| installed == bytes) + .unwrap_or(false) + { + continue; + } + let parent = target.parent().ok_or_else(|| { + EmbeddedCliError::with_message( + EmbeddedCliErrorKind::Archive, + format!("embedded runtime asset has no parent: {}", path.display()), + ) + })?; + fs::create_dir_all(parent) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?; + let tmp = write_temp_file(parent, &bytes)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = entry.header().mode().unwrap_or(0o644) & 0o777; + fs::set_permissions(&tmp, fs::Permissions::from_mode(mode)) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?; + } + if let Err(error) = publish(&tmp, &target) { + let _ = fs::remove_file(&tmp); + return Err(error); + } + } + Ok(()) +} + +#[cfg(has_bundled_cli)] +fn install_runtime_pair(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> { + install_adjacent_file(install_dir, archive, RUNTIME_NODE_NAME, "runtime.node")?; + install_adjacent_file( + install_dir, + archive, + RUNTIME_BINARY_NAME, + "copilot runtime wrapper", + ) } #[cfg(all(has_bundled_cli, feature = "bundled-in-process"))] fn install_runtime_library(install_dir: &Path, archive: &[u8]) -> Result<(), EmbeddedCliError> { - let target = install_dir.join(RUNTIME_LIBRARY_NAME); - if fs::metadata(&target).map(|m| m.len() > 0).unwrap_or(false) { - return Ok(()); - } - let bytes = extract_binary(archive, RUNTIME_LIBRARY_NAME)?; + install_adjacent_file( + install_dir, + archive, + RUNTIME_LIBRARY_NAME, + "in-process FFI runtime library", + ) +} + +#[cfg(has_bundled_cli)] +fn install_adjacent_file( + install_dir: &Path, + archive: &[u8], + file_name: &str, + label: &str, +) -> Result<(), EmbeddedCliError> { + let target = install_dir.join(file_name); + let bytes = extract_binary(archive, file_name)?; if bytes.is_empty() { return Err(EmbeddedCliError::with_message( EmbeddedCliErrorKind::Verification, - "embedded runtime library is empty", + format!("embedded {label} is empty"), )); } + if fs::read(&target) + .map(|installed| installed == bytes) + .unwrap_or(false) + { + return Ok(()); + } let tmp = write_temp_file(install_dir, &bytes)?; if let Err(e) = publish(&tmp, &target) { let _ = fs::remove_file(&tmp); return Err(e); } - tracing::debug!(path = %target.display(), "in-process FFI runtime library installed"); + tracing::debug!(path = %target.display(), %label, "embedded runtime artifact installed"); Ok(()) } @@ -480,7 +694,7 @@ fn read_marker_len(marker_path: &Path) -> Option { .ok() } -#[cfg(all(has_bundled_cli, any(feature = "bundled-in-process", not(windows))))] +#[cfg(has_bundled_cli)] fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> { let gz = flate2::read::GzDecoder::new(archive); let mut tar = tar::Archive::new(gz); @@ -505,26 +719,6 @@ fn extract_binary(archive: &[u8], binary_name: &str) -> Result, Embedded Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into()) } -#[cfg(all(has_bundled_cli, not(feature = "bundled-in-process"), windows))] -fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> { - let cursor = std::io::Cursor::new(archive); - let mut zip = zip::ZipArchive::new(cursor) - .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Zip, e))?; - for i in 0..zip.len() { - let mut entry = zip - .by_index(i) - .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Zip, e))?; - let name = entry.name().to_string(); - if name == binary_name || name.ends_with(&format!("/{binary_name}")) { - let mut bytes = Vec::with_capacity(entry.size() as usize); - std::io::copy(&mut entry, &mut bytes) - .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?; - return Ok(bytes); - } - } - Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into()) -} - #[cfg(has_bundled_cli)] fn sanitize_version(version: &str) -> String { version @@ -541,10 +735,7 @@ fn sanitize_version(version: &str) -> String { #[allow(dead_code)] enum EmbeddedCliErrorKind { CreateDir, - #[cfg(any(feature = "bundled-in-process", not(windows)))] Archive, - #[cfg(all(not(feature = "bundled-in-process"), windows))] - Zip, BinaryNotFoundInArchive, Io, /// Atomically renaming the staged temp file onto the final path failed. @@ -561,10 +752,7 @@ impl std::fmt::Display for EmbeddedCliErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EmbeddedCliErrorKind::CreateDir => f.write_str("failed to create install directory"), - #[cfg(any(feature = "bundled-in-process", not(windows)))] EmbeddedCliErrorKind::Archive => f.write_str("failed to read archive entry"), - #[cfg(all(not(feature = "bundled-in-process"), windows))] - EmbeddedCliErrorKind::Zip => f.write_str("failed to read zip archive"), EmbeddedCliErrorKind::BinaryNotFoundInArchive => { f.write_str("CLI binary not found in embedded archive") } @@ -670,7 +858,7 @@ mod tests { #[cfg(all(has_bundled_cli, feature = "bundled-in-process"))] #[test] - fn embedded_archive_contains_only_expected_files() { + fn embedded_archive_contains_runtime_assets_and_excludes_cli_only_files() { let gz = flate2::read::GzDecoder::new(build_time::CLI_ARCHIVE); let mut archive = tar::Archive::new(gz); let mut names: Vec = archive @@ -687,12 +875,13 @@ mod tests { .collect(); names.sort(); - let mut expected = vec![ - CLI_BINARY_NAME.to_string(), - RUNTIME_LIBRARY_NAME.to_string(), - ]; - expected.sort(); - assert_eq!(names, expected); + assert!(names.contains(&CLI_BINARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_LIBRARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_BINARY_NAME.to_string())); + assert!(names.contains(&RUNTIME_NODE_NAME.to_string())); + assert!(names.iter().any(|name| name.starts_with("ripgrep/"))); + assert!(names.iter().any(|name| name.starts_with("definitions/"))); + assert!(!names.contains(&"app.js".to_string())); } /// Bytes whose header looks like a valid executable image on the host @@ -841,4 +1030,42 @@ mod tests { assert_eq!(mode & 0o777, 0o755, "temp binary should be executable"); } } + + #[cfg(has_bundled_cli)] + #[test] + fn runtime_install_replaces_stale_pair() { + let dir = tempfile::tempdir().expect("tempdir"); + fs::write(dir.path().join(RUNTIME_NODE_NAME), b"stale runtime").expect("seed runtime"); + fs::write(dir.path().join(RUNTIME_BINARY_NAME), b"stale wrapper").expect("seed wrapper"); + + install_runtime(dir.path(), build_time::CLI_ARCHIVE).expect("install runtime"); + + assert_eq!( + fs::read(dir.path().join(RUNTIME_NODE_NAME)).expect("read runtime"), + extract_binary(build_time::CLI_ARCHIVE, RUNTIME_NODE_NAME).expect("extract runtime") + ); + assert_eq!( + fs::read(dir.path().join(RUNTIME_BINARY_NAME)).expect("read wrapper"), + extract_binary(build_time::CLI_ARCHIVE, RUNTIME_BINARY_NAME).expect("extract wrapper") + ); + } + + #[cfg(has_bundled_cli)] + #[test] + fn custom_runtime_install_dir_isolated_by_version() { + let dir = tempfile::tempdir().expect("tempdir"); + + assert_eq!( + runtime_install_dir(dir.path(), "1.0.0").expect("claim directory"), + dir.path() + ); + assert_eq!( + runtime_install_dir(dir.path(), "1.0.0").expect("reuse directory"), + dir.path() + ); + assert_eq!( + runtime_install_dir(dir.path(), "2.0.0").expect("isolate directory"), + dir.path().join("2.0.0") + ); + } } diff --git a/rust/src/extension_launch_provider.rs b/rust/src/extension_launch_provider.rs new file mode 100644 index 0000000000..63e2284144 --- /dev/null +++ b/rust/src/extension_launch_provider.rs @@ -0,0 +1,158 @@ +//! Connection-level extension launch profile resolution. + +use std::sync::{Arc, OnceLock, Weak}; + +use async_trait::async_trait; +use parking_lot::RwLock; +use serde::Serialize; +use serde_json::Value; +use tracing::warn; + +pub use crate::rpc::{ + ExtensionLaunchProfile, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use crate::{ + Client, ClientInner, JsonRpcError, JsonRpcRequest, JsonRpcResponse, Result, error_codes, +}; + +pub(crate) const RESOLVE_METHOD: &str = "extensionLaunchProvider.resolve"; +const MISSING_HANDLER_MESSAGE: &str = "No extensionLaunchProvider client-global handler registered"; + +/// Resolves process launch profiles for extension entrypoints discovered by the runtime. +/// +/// Configure an implementation with +/// [`ClientOptions::with_extension_launch_provider`](crate::ClientOptions::with_extension_launch_provider). +/// The SDK registers the provider before [`Client::start`](crate::Client::start) +/// returns, so extension resolution cannot race session creation. +/// +/// The returned executable, arguments, and environment are forwarded unchanged. +/// The runtime remains authoritative for its reserved `COPILOT_SDK_PATH`, +/// `SESSION_ID`, and `COPILOT_EXTENSION_PARENT_PID` environment variables. +#[async_trait] +pub trait ExtensionLaunchProvider: Send + Sync + 'static { + /// Resolve a launch profile for one discovered extension entrypoint. + /// + /// Return a result with `launch: None` when the provider does not support + /// the entrypoint. + async fn resolve( + &self, + request: ExtensionLaunchProviderResolveRequest, + ) -> Result; +} + +pub(crate) struct ExtensionLaunchProviderDispatcher { + handler: RwLock>>, + client: OnceLock>, +} + +impl ExtensionLaunchProviderDispatcher { + pub(crate) fn new(handler: Option>) -> Self { + Self { + handler: RwLock::new(handler), + client: OnceLock::new(), + } + } + + pub(crate) fn set_client(&self, client: Weak) { + let _ = self.client.set(client); + } + + pub(crate) fn is_configured(&self) -> bool { + self.handler.read().is_some() + } + + pub(crate) fn clear(&self) { + self.handler.write().take(); + } + + pub(crate) async fn dispatch(&self, request: JsonRpcRequest) { + let request_id = request.id; + let Some(handler) = self.handler.read().clone() else { + self.send_error( + request_id, + error_codes::INTERNAL_ERROR, + MISSING_HANDLER_MESSAGE, + ) + .await; + return; + }; + + let params = request + .params + .unwrap_or_else(|| Value::Object(serde_json::Map::new())); + let request = match serde_json::from_value(params) { + Ok(request) => request, + Err(error) => { + self.send_error( + request_id, + error_codes::INVALID_PARAMS, + &format!("invalid params: {error}"), + ) + .await; + return; + } + }; + + match handler.resolve(request).await { + Ok(result) => self.respond(request_id, result).await, + Err(error) => { + self.send_error(request_id, error_codes::INTERNAL_ERROR, &error.to_string()) + .await; + } + } + } + + fn client(&self) -> Option { + self.client + .get() + .and_then(Weak::upgrade) + .map(Client::from_inner) + } + + async fn respond(&self, request_id: u64, result: T) { + let value = match serde_json::to_value(result) { + Ok(value) => value, + Err(error) => { + warn!(error = %error, "failed to serialize extension launch provider response"); + self.send_error( + request_id, + error_codes::INTERNAL_ERROR, + "serialization failure", + ) + .await; + return; + } + }; + + let Some(client) = self.client() else { + return; + }; + let _ = client + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request_id, + result: Some(value), + error: None, + }) + .await; + } + + async fn send_error(&self, request_id: u64, code: i32, message: &str) { + let Some(client) = self.client() else { + return; + }; + let _ = client + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request_id, + result: None, + error: Some(JsonRpcError { + code, + message: message.to_string(), + data: None, + }), + }) + .await; + } +} diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs index f784b1a6d1..a25990062c 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -2,12 +2,11 @@ //! library and speaking JSON-RPC over its C ABI, //! instead of spawning a CLI child process and communicating over stdio/TCP. //! -//! The runtime's `host_start` export spawns the residual TypeScript worker -//! itself — the packaged single-file CLI (`copilot --embedded-host`) or, for -//! dev, `node dist-cli/index.js --embedded-host`. JSON-RPC frames are pumped -//! across the ABI: writes go to `connection_write`; inbound frames arrive on a -//! native callback that feeds an async reader. The framing is unchanged — the -//! same LSP `Content-Length:` frames the stdio transport uses. +//! The runtime's `host_start` export constructs the Rust server synchronously in +//! this process. JSON-RPC frames are pumped across the ABI: writes go to +//! `connection_write`; inbound frames arrive on a native callback that feeds an +//! async reader. The framing is unchanged — the same LSP `Content-Length:` +//! frames the stdio transport uses. use std::collections::HashMap; use std::ffi::c_void; @@ -204,12 +203,11 @@ impl AsyncWrite for FfiWriter { } } -/// Prepared FFI host: the bound cdylib exports plus the spawn arguments needed -/// to start the runtime worker. The cdylib is loaded process-globally and never -/// unloaded (see [`load_library`]). +/// Prepared FFI host. The cdylib is loaded process-globally and never unloaded +/// (see [`load_library`]). pub(crate) struct FfiHost { library_path: PathBuf, - entrypoint: PathBuf, + cli_entrypoint: Option, environment: Vec<(String, String)>, args: Vec, host_start: HostStartFn, @@ -224,30 +222,34 @@ pub(crate) struct FfiHost { unsafe impl Send for FfiHost {} impl FfiHost { - /// Load the cdylib next to `entrypoint` and bind its exports. - /// - /// `entrypoint` is the packaged single-file CLI binary or, for dev, a - /// `.js` file launched via `node`. The native library is resolved relative - /// to the entrypoint directory, supporting both packaged and development - /// layouts. + /// Load the cdylib next to `runtime_entrypoint` and bind its exports. pub(crate) fn create( - entrypoint: &Path, + runtime_entrypoint: &Path, + cli_entrypoint: Option<&Path>, environment: Vec<(String, String)>, args: Vec, ) -> Result { - let entrypoint = std::fs::canonicalize(entrypoint) - .map(path_for_child_process) + let runtime_entrypoint = std::fs::canonicalize(runtime_entrypoint).map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "failed to resolve in-process runtime entrypoint '{}': {e}", + runtime_entrypoint.display() + ), + ) + })?; + let cli_entrypoint = cli_entrypoint + .map(std::fs::canonicalize) + .transpose() .map_err(|e| { Error::with_message( ErrorKind::InvalidConfig, - format!( - "failed to resolve in-process CLI entrypoint '{}': {e}", - entrypoint.display() - ), + format!("failed to resolve explicit in-process CLI entrypoint: {e}"), ) - })?; - let library_path = - std::fs::canonicalize(resolve_library_path(&entrypoint)?).map_err(|e| { + })? + .map(path_for_child_process); + let library_path = std::fs::canonicalize(resolve_library_path(&runtime_entrypoint)?) + .map_err(|e| { Error::with_message( ErrorKind::InvalidConfig, format!("failed to resolve in-process runtime library: {e}"), @@ -267,7 +269,7 @@ impl FfiHost { Ok(Self { library_path, - entrypoint, + cli_entrypoint, environment, args, host_start, @@ -278,11 +280,7 @@ impl FfiHost { }) } - /// Start the runtime worker and open the FFI JSON-RPC connection. - /// - /// `host_start` blocks until the worker connects back and signals - /// readiness (up to ~30s), and must not run on an async executor thread, so - /// the blocking handshake is offloaded to [`tokio::task::spawn_blocking`]. + /// Start the native runtime and open the FFI JSON-RPC connection. pub(crate) async fn start(self) -> Result<(FfiReader, FfiWriter, Arc), Error> { tokio::task::spawn_blocking(move || self.start_blocking()) .await @@ -295,7 +293,7 @@ impl FfiHost { } fn start_blocking(self) -> Result<(FfiReader, FfiWriter, Arc), Error> { - let argv = build_argv_json(&self.entrypoint, &self.args); + let argv = build_argv_json(self.cli_entrypoint.as_deref(), &self.args); let env = build_env_json(&self.environment); let (env_ptr, env_len) = match &env { @@ -309,9 +307,8 @@ impl FfiHost { return Err(Error::with_message( ErrorKind::InvalidConfig, format!( - "copilot_runtime_host_start failed (library '{}', entrypoint '{}')", - self.library_path.display(), - self.entrypoint.display() + "copilot_runtime_host_start failed (library '{}')", + self.library_path.display() ), )); } @@ -440,6 +437,8 @@ pub(crate) fn prebuilds_folder() -> Option { "win32" } else if cfg!(target_os = "macos") { "darwin" + } else if cfg!(all(target_os = "linux", target_env = "musl")) { + "linuxmusl" } else if cfg!(target_os = "linux") { "linux" } else { @@ -472,6 +471,11 @@ fn resolve_library_path(entrypoint: &Path) -> Result { return Ok(flat); } + let adjacent = dir.join("runtime.node"); + if adjacent.is_file() { + return Ok(adjacent); + } + // Development package layout. let prebuilds = prebuilds_folder().map(|folder| dir.join("prebuilds").join(folder).join("runtime.node")); @@ -521,28 +525,23 @@ fn path_for_child_process(path: PathBuf) -> PathBuf { path } -fn build_argv_json(entrypoint: &Path, extra_args: &[String]) -> Vec { - // A `.js` entrypoint (dev / dist-cli) is launched via node; the packaged - // single-file CLI binary embeds its own Node and is invoked directly. - let entrypoint_str = entrypoint.to_string_lossy().into_owned(); - let is_js = entrypoint - .extension() - .and_then(|ext| ext.to_str()) - .is_some_and(|ext| ext.eq_ignore_ascii_case("js")); - let mut argv: Vec = if is_js { - vec![ - "node".to_string(), - entrypoint_str, - "--embedded-host".to_string(), - "--no-auto-update".to_string(), - ] - } else { - vec![ +fn build_argv_json(entrypoint: Option<&Path>, extra_args: &[String]) -> Vec { + let mut argv = Vec::new(); + if let Some(entrypoint) = entrypoint { + let entrypoint_str = entrypoint.to_string_lossy().into_owned(); + if entrypoint + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("js")) + { + argv.push("node".to_string()); + } + argv.extend([ entrypoint_str, "--embedded-host".to_string(), "--no-auto-update".to_string(), - ] - }; + ]); + } argv.extend_from_slice(extra_args); serde_json::to_vec(&argv).expect("argv serializes") } @@ -563,29 +562,20 @@ mod tests { use super::*; #[test] - fn argv_pins_worker_and_appends_client_options() { + fn argv_without_entrypoint_contains_only_client_options() { let argv: Vec = serde_json::from_slice(&build_argv_json( - Path::new("copilot"), + None, &["--log-level".into(), "debug".into()], )) .unwrap(); - assert_eq!( - argv, - [ - "copilot", - "--embedded-host", - "--no-auto-update", - "--log-level", - "debug" - ] - ); + assert_eq!(argv, ["--log-level", "debug"]); } #[test] - fn javascript_entrypoint_uses_node() { + fn explicit_javascript_entrypoint_uses_node() { let argv: Vec = - serde_json::from_slice(&build_argv_json(Path::new("index.js"), &[])).unwrap(); + serde_json::from_slice(&build_argv_json(Some(Path::new("index.js")), &[])).unwrap(); assert_eq!( argv, diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index e22c888f23..68d8b7d054 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -10,10 +10,11 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use super::session_events::{ - AbortReason, ContextTier, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, - McpServerStatus, ModelChangeSource, OmittedBinaryOmittedReason, PermissionMode, - PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionLimitsConfig, SessionMode, - ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity, + AbortReason, AgentModelPolicy, AutoTier, ContextTier, McpOauthHttpResponse, + McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, + OmittedBinaryOmittedReason, PermissionMode, PermissionPromptRequest, PermissionRule, + ReasoningSummary, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, + TaskCompletionOutcome, UserToolSessionApproval, Verbosity, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -23,6 +24,8 @@ pub mod rpc_methods { pub const PING: &str = "ping"; /// `connect` pub const CONNECT: &str = "connect"; + /// `hooks.discover` + pub const HOOKS_DISCOVER: &str = "hooks.discover"; /// `models.list` pub const MODELS_LIST: &str = "models.list"; /// `models.getBuiltInCatalog` @@ -141,6 +144,8 @@ pub mod rpc_methods { pub const SESSIONS_LIST: &str = "sessions.list"; /// `sessions.getMetadata` pub const SESSIONS_GETMETADATA: &str = "sessions.getMetadata"; + /// `sessions.readPersistedEvents` + pub const SESSIONS_READPERSISTEDEVENTS: &str = "sessions.readPersistedEvents"; /// `sessions.listNonEmptySessionIds` pub const SESSIONS_LISTNONEMPTYSESSIONIDS: &str = "sessions.listNonEmptySessionIds"; /// `sessions.findByTaskId` @@ -202,6 +207,8 @@ pub mod rpc_methods { pub const SESSION_SEND: &str = "session.send"; /// `session.sendMessages` pub const SESSION_SENDMESSAGES: &str = "session.sendMessages"; + /// `session.sandbox.getEnforcementStatus` + pub const SESSION_SANDBOX_GETENFORCEMENTSTATUS: &str = "session.sandbox.getEnforcementStatus"; /// `session.sendSystemNotification` pub const SESSION_SENDSYSTEMNOTIFICATION: &str = "session.sendSystemNotification"; /// `session.abort` @@ -734,6 +741,10 @@ pub mod rpc_methods { pub const SESSION_SCHEDULE_REARMSELFPACED: &str = "session.schedule.rearmSelfPaced"; /// `session.schedule.stop` pub const SESSION_SCHEDULE_STOP: &str = "session.schedule.stop"; + /// `skillProvider.list` + pub const SKILLPROVIDER_LIST: &str = "skillProvider.list"; + /// `skillProvider.read` + pub const SKILLPROVIDER_READ: &str = "skillProvider.read"; /// `providerToken.getToken` pub const PROVIDERTOKEN_GETTOKEN: &str = "providerToken.getToken"; /// `factory.execute` @@ -1538,7 +1549,7 @@ pub struct AgentDiscoveryPathList { pub paths: Vec, } -/// Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. +/// Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. /// ///

/// @@ -1568,6 +1579,12 @@ pub struct AgentInfo { /// Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether authored models are preferences or required constraints. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, + /// Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + #[serde(skip_serializing_if = "Option::is_none")] + pub models: Option>, /// Name of the agent. Use `id` as the stable selection identifier. pub name: String, /// Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. @@ -3019,6 +3036,9 @@ pub struct CanvasProviderUnregisterRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CapiSessionOptions { + /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. #[serde(skip_serializing_if = "Option::is_none")] pub enable_web_socket_responses: Option, @@ -3359,6 +3379,9 @@ pub struct CatalogNetworkFailureError { pub message: String, /// Categorised failure, low cardinality so it can be aggregated without carrying a URL. pub reason: CatalogNetworkFailureReason, + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + #[serde(skip_serializing_if = "Option::is_none")] + pub retry_after_seconds: Option, /// HTTP status code, when the failure was a rejected response. #[serde(skip_serializing_if = "Option::is_none")] pub status_code: Option, @@ -3421,7 +3444,7 @@ pub struct CatalogSearchRequest { /// Maximum number of candidates to return. Defaults to 10 when omitted. #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. pub query: String, } @@ -4370,6 +4393,36 @@ pub struct DiscoveredExtensionsEnableRequest { pub ids: Vec, } +/// One server-discovered hook action from user, repository, plugin, or managed-policy configuration. +/// +///
+/// +/// **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 DiscoveredHook { + /// Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_key: Option, + /// Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + pub enabled: bool, + /// Hook event that invokes this action. + pub hook_type: HookType, + /// Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + pub id: String, + /// Configuration tier that contributed this hook action. + pub origin: HookOrigin, + /// Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + #[serde(skip_serializing_if = "Option::is_none")] + pub project_path: Option, + /// Human-readable source label, such as a hook file path, settings source, or plugin name. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + /// MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state. /// ///
@@ -4411,6 +4464,9 @@ pub struct DiscoveredMcpServer { pub struct EnqueueCommandParams { /// Slash-prefixed command string to enqueue, e.g. '/compact' or '/model gpt-4'. Queued FIFO with any in-flight items; if the session is idle, processing kicks off immediately. pub command: String, + /// Optional user-facing text for the queue row. The command string is shown when omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_text: Option, } /// Indicates whether the command was accepted into the local execution queue. @@ -5612,10 +5668,13 @@ pub struct FactoryResumeRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -6444,8 +6503,7 @@ pub struct HistoryTruncateResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct HookInvokeRequest { - #[doc(hidden)] - pub(crate) hook_type: HookType, + pub hook_type: HookType, pub input: serde_json::Value, pub session_id: SessionId, } @@ -6458,6 +6516,44 @@ pub(crate) struct HookInvokeResponse { pub output: Option, } +/// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +/// +///
+/// +/// **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 HooksDiscoverRequest { + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_host_hooks: Option, + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + #[serde(skip_serializing_if = "Option::is_none")] + pub project_paths: Option>, +} + +/// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. +/// +///
+/// +/// **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 HooksDiscoverResult { + /// Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + pub errors: Vec, + /// All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + pub hooks: Vec, + /// Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + pub warnings: Vec, +} + /// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. /// ///
@@ -9877,6 +9973,9 @@ pub struct ModelBillingPromo { /// Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + #[serde(skip_serializing_if = "Option::is_none")] + pub show_banner: Option, } /// Long context tier pricing (available for models with extended context windows) @@ -10203,6 +10302,9 @@ pub struct ModelApplyStartupOverlayRequest { /// Model required by device-managed policy, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub device_managed_model: Option, + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_model: Option, /// Context tier selected by repository settings, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub repo_context_tier: Option, @@ -10498,7 +10600,7 @@ pub struct ModelSwitchToRequest { /// When true, evaluate context-window compaction policy before applying the switch. #[serde(skip_serializing_if = "Option::is_none")] pub run_compaction_preflight: Option, - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// 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. #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// Output verbosity level to request for supported models @@ -13953,6 +14055,9 @@ pub struct QueuePendingItems { pub id: String, /// Whether this item is a queued user message or a queued slash command / model change pub kind: QueuePendingItemsKind, + /// Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + #[serde(skip_serializing_if = "Option::is_none")] + pub message_id: Option, } /// Snapshot of the session's pending queued items and immediate-steering messages. @@ -14164,7 +14269,7 @@ pub struct RegisterEventInterestResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionsRegisterExtensionToolsOnSessionOptions { - /// In-process `() => boolean` gating callback (CLI-only optimization). Marked internal: replaced by runtime-side enable/disable RPCs in the SDK migration. + /// In-process `() => boolean` gating callback used only by the CLI. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) enabled: Option, @@ -14181,7 +14286,7 @@ pub struct SessionsRegisterExtensionToolsOnSessionOptions { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct RegisterExtensionToolsParams { - /// In-process ExtensionLoader handle (CLI-only optimization). Marked internal: this field is excluded from the public SDK surface. When the CLI migrates to a process-separated SDK, extension discovery/launch moves entirely into the runtime — the CLI passes pure config (search paths, disabled ids) via SessionOptions instead. + /// In-process ExtensionLoader handle used only by the CLI and excluded from the public SDK surface. #[doc(hidden)] pub(crate) loader: serde_json::Value, /// Optional registration options. @@ -14202,7 +14307,7 @@ pub(crate) struct RegisterExtensionToolsParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct RegisterExtensionToolsResult { - /// In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration. + /// In-process unsubscribe function used only by the CLI. #[doc(hidden)] pub(crate) unsubscribe: serde_json::Value, } @@ -14659,7 +14764,7 @@ pub struct SandboxConfigUserPolicyNetworkProxy { /// Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, - /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. pub url: String, /// Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. #[serde(skip_serializing_if = "Option::is_none")] @@ -14683,7 +14788,7 @@ pub struct SandboxConfigUserPolicyNetwork { /// Whether outbound network traffic is allowed at all. #[serde(skip_serializing_if = "Option::is_none")] pub allow_outbound: Option, - /// HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. + /// HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the `[::]` dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. #[serde(skip_serializing_if = "Option::is_none")] pub proxy: Option, } @@ -14756,6 +14861,26 @@ pub struct SandboxConfig { pub user_policy: Option, } +/// Managed sandbox enforcement state for a 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 SandboxEnforcementStatus { + /// Whether an enforcement failure has permanently blocked the session. + pub blocked: bool, + /// The first sandbox enforcement failure that blocked the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Whether the effective managed policy requires an available sandbox backend. + pub required: bool, +} + /// Register an absolute-time scheduled prompt. /// ///
@@ -16757,6 +16882,9 @@ pub struct SessionOpenOptions { /// Whether shell-script safety heuristics are enabled. #[serde(skip_serializing_if = "Option::is_none")] pub enable_script_safety: Option, + /// Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_skills: Option, /// Whether model responses stream as delta events. #[serde(skip_serializing_if = "Option::is_none")] pub enable_streaming: Option, @@ -16782,6 +16910,17 @@ pub struct SessionOpenOptions { /// Feature-flag values resolved by the host. #[serde(skip_serializing_if = "Option::is_none")] pub feature_flags: Option>, + /// Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) has_skill_provider: Option, /// Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. #[serde(skip_serializing_if = "Option::is_none")] pub included_builtin_agents: Option>, @@ -17033,7 +17172,7 @@ pub struct SessionsOpenRemote { pub struct SessionsOpenCloud { /// Create a new cloud (coding-agent) session. pub kind: SessionsOpenCloudKind, - /// In-process callback invoked when the cloud task is created (before connection). Marked internal because a function reference cannot cross the JSON-RPC boundary. Disappears in the SDK migration: the field is purely cosmetic (it flips a single CLI phase label from 'creating' to 'connecting') and the wire-clean version just drops the intermediate phase. + /// In-process callback invoked when the cloud task is created, before connection. Internal because function references cannot cross the JSON-RPC boundary. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) on_task_created: Option, @@ -17928,6 +18067,30 @@ pub struct SessionsPruneOldRequest { pub older_than_days: i64, } +/// Pagination options for reading an inactive or active local session's persisted event journal. +/// +///
+/// +/// **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 SessionsReadPersistedEventsRequest { + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + #[serde(skip_serializing_if = "Option::is_none")] + pub direction: Option, + /// Maximum number of events to return in this batch (1–1000, default 200). + #[serde(skip_serializing_if = "Option::is_none")] + pub max: Option, + /// Session ID whose persisted event journal should be read. + pub session_id: SessionId, +} + /// Session ID whose in-use lock should be released. /// ///
@@ -18203,7 +18366,7 @@ pub struct SessionUpdateOptionsParams { /// Whether to enable cross-session store writes and reads. #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_store: Option, - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, /// Whether to stream model responses. @@ -18550,6 +18713,79 @@ pub struct SkillList { pub skills: Vec, } +/// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. +/// +///
+/// +/// **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 SkillProviderDescriptor { + /// Optional freeform argument hint used by slash-command catalogs. + #[serde(skip_serializing_if = "Option::is_none")] + pub argument_hint: Option, + /// Description used in skill catalogs without fetching content. + pub description: String, + /// Whether model invocation is disabled. Defaults to false. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, + /// Invocation and display name. + pub name: String, + /// Whether users may invoke the skill directly. Defaults to true. + #[serde(skip_serializing_if = "Option::is_none")] + pub user_invocable: Option, +} + +/// Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate 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(crate) struct SkillProviderListResult { + /// Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. + pub skills: Vec, +} + +/// Identifies one SDK-provided skill by invocation name. +/// +///
+/// +/// **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 SkillProviderReadRequest { + /// Target session identifier + pub session_id: SessionId, + /// Invocation name of the skill to read. + pub name: String, +} + +/// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. +/// +///
+/// +/// **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 SkillProviderReadResult { + /// Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + pub markdown: String, +} + /// Skill names to mark as disabled in global configuration, replacing any previous list. /// ///
@@ -18669,11 +18905,14 @@ pub struct SkillsInvokedSkill { pub allowed_tools: Option>, /// Full content of the skill file pub content: String, + /// Whether model invocation was disabled when this skill was invoked + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, /// Turn number when the skill was invoked pub invoked_at_turn: i64, /// Unique identifier for the skill pub name: String, - /// Path to the SKILL.md file + /// Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity pub path: String, } @@ -18794,6 +19033,9 @@ pub struct SlashCommandCompletedResult { /// Optional user-facing message describing the completed command #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// Optional target session mode applied without submitting an agent prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, /// True when the invocation mutated user runtime settings; consumers caching settings should refresh #[serde(skip_serializing_if = "Option::is_none")] pub runtime_settings_changed: Option, @@ -18987,6 +19229,9 @@ pub struct SubagentSettingsEntry { /// Model override for matching subagents #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether the configured model strategy is preferred or required + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, } /// Subagent settings to apply, or null to clear the live session override @@ -20202,11 +20447,11 @@ pub struct UIElicitationStringOneOfField { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UIEphemeralQueryRequest { - /// In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Marked internal: excluded from the public SDK surface. Replaced by an explicit cancellation token + cancel RPC in the SDK migration. + /// In-process `AbortSignal` forwarded to the model client to cancel an in-flight request. Internal and excluded from the public SDK surface. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) abort_signal: Option, - /// In-process streaming callback `(text) => void` invoked with each token as the model emits it. Marked internal: excluded from the public SDK surface. In a process-separated SDK this is replaced by a streaming RPC that yields chunks and a final answer. + /// In-process streaming callback `(text) => void` invoked with each token as the model emits it. Internal and excluded from the public SDK surface. #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) on_chunk: Option, @@ -21751,6 +21996,27 @@ pub struct SessionsListResult { pub sessions: Vec, } +/// Batch of session events returned by a read, with cursor and continuation 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 SessionsReadPersistedEventsResult { + /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). + pub cursor: String, + /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. + pub cursor_status: EventsCursorStatus, + /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. + pub events: Vec, + /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. + pub has_more: bool, +} + /// ID of the local session bound to the given GitHub task, or omitted when none. /// ///
@@ -21942,7 +22208,7 @@ pub struct SessionsGetRemoteControlStatusResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SessionsRegisterExtensionToolsOnSessionResult { - /// In-process unsubscribe function (CLI-only optimization). Marked internal: replaced by an explicit `extensions.unregister` RPC in the SDK migration. + /// In-process unsubscribe function used only by the CLI. #[doc(hidden)] pub(crate) unsubscribe: serde_json::Value, } @@ -21992,6 +22258,41 @@ pub struct SessionSendMessagesResult { pub message_ids: 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 SessionSandboxGetEnforcementStatusParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Managed sandbox enforcement state for a 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 SessionSandboxGetEnforcementStatusResult { + /// Whether an enforcement failure has permanently blocked the session. + pub blocked: bool, + /// The first sandbox enforcement failure that blocked the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Whether the effective managed policy requires an available sandbox backend. + pub required: bool, +} + /// Result of aborting the current turn /// ///
@@ -22324,10 +22625,13 @@ pub struct SessionCanvasActionInvokeResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22373,10 +22677,13 @@ pub struct SessionFactoryResumeResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryRunFromToolResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22422,10 +22729,13 @@ pub struct SessionFactoryResumeFromToolResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryGetRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22563,10 +22873,13 @@ pub struct SessionFactoryGetRunProgressResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryCancelResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -26948,6 +27261,21 @@ pub struct SessionScheduleStopResult { pub entry: 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 SkillProviderListParams { + /// Target session identifier + pub session_id: SessionId, +} + /// A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh. /// ///
@@ -28314,7 +28642,16 @@ pub enum CatalogNetworkFailureReason { /// The connection was refused or reset. #[serde(rename = "connection-refused")] ConnectionRefused, - /// The authority returned a status the runtime treats as a failure. + /// The configured proxy returned 407 and requires authentication. + #[serde(rename = "proxy-authentication-required")] + ProxyAuthenticationRequired, + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + #[serde(rename = "rate-limited")] + RateLimited, + /// The authority returned a transient 5xx response. + #[serde(rename = "service-unavailable")] + ServiceUnavailable, + /// The authority returned another status the runtime treats as a failure. #[serde(rename = "http-status")] HttpStatus, /// The response exceeded the permitted size. @@ -28851,6 +29188,101 @@ pub enum DiscoveredExtensionMode { Unknown, } +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +/// +///
+/// +/// **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 HookType { + /// Runs before a tool is invoked. + #[serde(rename = "preToolUse")] + PreToolUse, + /// Runs before an MCP tool is invoked. + #[serde(rename = "preMcpToolCall")] + PreMcpToolCall, + /// Runs after a tool completes successfully. + #[serde(rename = "postToolUse")] + PostToolUse, + /// Runs after a tool fails. + #[serde(rename = "postToolUseFailure")] + PostToolUseFailure, + /// Runs after the user submits a prompt. + #[serde(rename = "userPromptSubmitted")] + UserPromptSubmitted, + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + #[serde(rename = "userPromptTransformed")] + UserPromptTransformed, + /// Runs when a session starts. + #[serde(rename = "sessionStart")] + SessionStart, + /// Runs when a session ends. + #[serde(rename = "sessionEnd")] + SessionEnd, + /// Runs after an agent result is produced. + #[serde(rename = "postResult")] + PostResult, + /// Runs before a pull request description is generated. + #[serde(rename = "prePRDescription")] + PrePRDescription, + /// Runs when the agent encounters an error. + #[serde(rename = "errorOccurred")] + ErrorOccurred, + /// Runs when the agent stops. + #[serde(rename = "agentStop")] + AgentStop, + /// Runs when a subagent starts. + #[serde(rename = "subagentStart")] + SubagentStart, + /// Runs when a subagent stops. + #[serde(rename = "subagentStop")] + SubagentStop, + /// Runs before conversation context is compacted. + #[serde(rename = "preCompact")] + PreCompact, + /// Runs when the agent requests permission. + #[serde(rename = "permissionRequest")] + PermissionRequest, + /// Runs when the agent emits a notification. + #[serde(rename = "notification")] + Notification, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Configuration tier that contributed a discovered hook action. +/// +///
+/// +/// **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 HookOrigin { + /// Hook loaded from user settings or the user's hook directory. + #[serde(rename = "user")] + User, + /// Hook loaded from repository settings or the repository hook directory. + #[serde(rename = "repository")] + Repository, + /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. + #[serde(rename = "plugin")] + Plugin, + /// Hook enforced by centrally managed policy. + #[serde(rename = "policy")] + Policy, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Server transport type: stdio, http, sse (deprecated), or memory /// ///
@@ -29465,66 +29897,6 @@ pub enum HistoryRewindOutcome { Unknown, } -/// Hook event name dispatched through the SDK callback transport. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum HookType { - /// Runs before a tool is invoked. - #[serde(rename = "preToolUse")] - PreToolUse, - /// Runs before an MCP tool is invoked. - #[serde(rename = "preMcpToolCall")] - PreMcpToolCall, - /// Runs after a tool completes successfully. - #[serde(rename = "postToolUse")] - PostToolUse, - /// Runs after a tool fails. - #[serde(rename = "postToolUseFailure")] - PostToolUseFailure, - /// Runs after the user submits a prompt. - #[serde(rename = "userPromptSubmitted")] - UserPromptSubmitted, - /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. - #[serde(rename = "userPromptTransformed")] - UserPromptTransformed, - /// Runs when a session starts. - #[serde(rename = "sessionStart")] - SessionStart, - /// Runs when a session ends. - #[serde(rename = "sessionEnd")] - SessionEnd, - /// Runs after an agent result is produced. - #[serde(rename = "postResult")] - PostResult, - /// Runs before a pull request description is generated. - #[serde(rename = "prePRDescription")] - PrePRDescription, - /// Runs when the agent encounters an error. - #[serde(rename = "errorOccurred")] - ErrorOccurred, - /// Runs when the agent stops. - #[serde(rename = "agentStop")] - AgentStop, - /// Runs when a subagent starts. - #[serde(rename = "subagentStart")] - SubagentStart, - /// Runs when a subagent stops. - #[serde(rename = "subagentStop")] - SubagentStop, - /// Runs before conversation context is compacted. - #[serde(rename = "preCompact")] - PreCompact, - /// Runs when the agent requests permission. - #[serde(rename = "permissionRequest")] - PermissionRequest, - /// Runs when the agent emits a notification. - #[serde(rename = "notification")] - Notification, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Constant value. Always "github". #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum InstalledPluginSourceGitHubSource { diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 4d5b7f1538..d31cea5ce9 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -64,6 +64,13 @@ impl<'a> ClientRpc<'a> { } } + /// `hooks.*` sub-namespace. + pub fn hooks(&self) -> ClientRpcHooks<'a> { + ClientRpcHooks { + client: self.client, + } + } + /// `instructions.*` sub-namespace. pub fn instructions(&self) -> ClientRpcInstructions<'a> { ClientRpcInstructions { @@ -211,7 +218,7 @@ impl<'a> ClientRpc<'a> { Ok(serde_json::from_value(_value)?) } - /// Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime temporarily falls back to its built-in Node launcher for backward compatibility. + /// 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. /// /// Wire method: `registerExtensionLaunchProvider`. /// @@ -656,6 +663,45 @@ impl<'a> ClientRpcExtensions<'a> { } } +/// `hooks.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcHooks<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcHooks<'a> { + /// Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + /// + /// Wire method: `hooks.discover`. + /// + /// # Parameters + /// + /// * `params` - Optional project paths and host-exclusion behavior for server-scoped hook discovery. + /// + /// # Returns + /// + /// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook 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: HooksDiscoverRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::HOOKS_DISCOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `instructions.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcInstructions<'a> { @@ -1886,6 +1932,37 @@ impl<'a> ClientRpcSessions<'a> { Ok(serde_json::from_value(_value)?) } + /// Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + /// + /// Wire method: `sessions.readPersistedEvents`. + /// + /// # Parameters + /// + /// * `params` - Pagination options for reading an inactive or active local session's persisted event journal. + /// + /// # Returns + /// + /// Batch of session events returned by a read, with cursor and continuation metadata. + /// + ///
+ /// + /// **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 read_persisted_events( + &self, + params: SessionsReadPersistedEventsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_READPERSISTEDEVENTS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. /// /// Wire method: `sessions.listNonEmptySessionIds`. @@ -3185,6 +3262,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.sandbox.*` sub-namespace. + pub fn sandbox(&self) -> SessionRpcSandbox<'a> { + SessionRpcSandbox { + session: self.session, + } + } + /// `session.schedule.*` sub-namespace. pub fn schedule(&self) -> SessionRpcSchedule<'a> { SessionRpcSchedule { @@ -9413,6 +9497,42 @@ impl<'a> SessionRpcRemote<'a> { } } +/// `session.sandbox.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcSandbox<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcSandbox<'a> { + /// Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. + /// + /// Wire method: `session.sandbox.getEnforcementStatus`. + /// + /// # Returns + /// + /// Managed sandbox enforcement state for a session. + /// + ///
+ /// + /// **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_enforcement_status(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_SANDBOX_GETENFORCEMENTSTATUS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.schedule.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcSchedule<'a> { diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 79284b1671..eb63f11438 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -39,6 +39,8 @@ pub enum SessionEventType { SessionModelChange, #[serde(rename = "session.mode_changed")] SessionModeChanged, + #[serde(rename = "session.mode_notice_delivered")] + SessionModeNoticeDelivered, #[serde(rename = "session.session_limits_changed")] SessionSessionLimitsChanged, /// @@ -85,6 +87,15 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "session.completion_receipt")] + SessionCompletionReceipt, + /// + ///
+ /// + /// **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.fusion_route_started")] SessionFusionRouteStarted, /// @@ -142,6 +153,15 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "assistant.fusion_phase_activity")] + AssistantFusionPhaseActivity, + /// + ///
+ /// + /// **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 = "assistant.fusion_phase_completed")] AssistantFusionPhaseCompleted, /// @@ -465,6 +485,8 @@ pub enum SessionEventData { SessionModelChange(SessionModelChangeData), #[serde(rename = "session.mode_changed")] SessionModeChanged(SessionModeChangedData), + #[serde(rename = "session.mode_notice_delivered")] + SessionModeNoticeDelivered(SessionModeNoticeDeliveredData), #[serde(rename = "session.session_limits_changed")] SessionSessionLimitsChanged(SessionSessionLimitsChangedData), /// @@ -511,6 +533,15 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "session.completion_receipt")] + SessionCompletionReceipt(SessionCompletionReceiptData), + /// + ///
+ /// + /// **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.fusion_route_started")] SessionFusionRouteStarted(SessionFusionRouteStartedData), /// @@ -568,6 +599,15 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "assistant.fusion_phase_activity")] + AssistantFusionPhaseActivity(AssistantFusionPhaseActivityData), + /// + ///
+ /// + /// **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 = "assistant.fusion_phase_completed")] AssistantFusionPhaseCompleted(AssistantFusionPhaseCompletedData), /// @@ -939,6 +979,9 @@ pub struct SessionStartData { /// Whether the session was already in use by another client at start time #[serde(skip_serializing_if = "Option::is_none")] pub already_in_use: Option, + /// Auto routing preference selected at session creation time + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Working directory and git context at session start #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, @@ -988,6 +1031,9 @@ pub struct SessionResumeData { /// Whether the session was already in use by another client at resume time #[serde(skip_serializing_if = "Option::is_none")] pub already_in_use: Option, + /// Auto routing preference active at resume time + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Updated working directory and git context at resume time #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, @@ -1229,6 +1275,17 @@ pub struct SessionModeChangedData { pub previous_mode: SessionMode, } +/// Session event "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionModeNoticeDeliveredData { + /// Model-visible transition notice persisted for a mid-turn delivery + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + /// Mode established by the delivered transition notice + pub mode: SessionMode, +} + /// Session event "session.session_limits_changed". Session limits update details. Null clears the limits. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1811,6 +1868,62 @@ pub struct SessionTaskCompleteData { pub summary: Option, } +/// Inclusive durable event range summarized by a completion receipt. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionReceiptEventRange { + /// Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + pub end_event_id: String, + /// Identifier of the user message that starts the covered exchange. + pub start_event_id: String, +} + +/// Final structured tool completion in the covered event range. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionReceiptFinalTool { + /// Process exit code from a structured shell result, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub exit_code: Option, + /// Structured success or failure status from the tool completion event. + pub status: CompletionReceiptToolStatus, + /// Unique identifier of the completed tool call. + pub tool_call_id: String, + /// Tool name from the matching tool execution start event, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_name: Option, +} + +/// Session event "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +/// +///
+/// +/// **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 SessionCompletionReceiptData { + /// One-based accepted completion receipt ordinal in the durable session history. + pub attempt: i64, + /// Inclusive durable event range summarized by this receipt. + pub event_range: CompletionReceiptEventRange, + /// Number of failed structured tool completions in the covered range. + pub failed_tool_count: i64, + /// Final structured tool completion in the covered range, when one exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub final_tool: Option, + /// Version of the completion receipt payload. + pub schema_version: i64, + /// Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + pub source_event_id: String, + /// Runtime reason the completion decision was accepted. + pub stop_reason: CompletionReceiptStopReason, + /// Number of successful structured tool completions in the covered range. + pub successful_tool_count: i64, +} + /// Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. /// ///
@@ -1880,6 +1993,27 @@ pub struct FusionFollowUpRecommendation { pub user_turn: FusionFollowUpAction, } +/// Presentation-neutral phase planned for a HydraFusion turn. +/// +///
+/// +/// **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 FusionPhasePlanStep { + /// Whether the phase executes only when an earlier phase requests it. + pub conditional: bool, + /// Kind of phase that may execute. + pub kind: FusionPhaseKind, + /// Semantic role assigned to the phase. + pub role: String, + /// Conversation scope in which the phase executes. + pub scope: FusionConversationScope, +} + /// Validated HydraFusion routing capability scores. /// ///
@@ -1928,6 +2062,16 @@ pub struct SessionFusionResolvedData { pub model_universe_version: Option, /// Validated orchestration pattern selected for the turn. pub pattern: FusionPattern, + /// Presentation-neutral phase plan for clients that render workflow progress. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub phase_plan: Option>, /// Version of the validated execution-plan format. #[serde(skip_serializing_if = "Option::is_none")] pub plan_version: Option, @@ -2035,6 +2179,9 @@ pub struct UserMessageData { /// True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. #[serde(skip_serializing_if = "Option::is_none")] pub is_autopilot_continuation: Option, + /// Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + #[serde(skip_serializing_if = "Option::is_none")] + pub message_id: Option, /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit #[serde(skip_serializing_if = "Option::is_none")] pub native_document_path_fallback_paths: Option>, @@ -2165,6 +2312,39 @@ pub struct AssistantFusionPhaseStartedData { pub role: String, } +/// Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. +/// +///
+/// +/// **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 AssistantFusionPhaseActivityData { + /// Kind of real activity observed. + pub activity: FusionPhaseActivityKind, + /// Conversation scope in which the phase executes. + pub conversation_scope: FusionConversationScope, + /// Identifier of the HydraFusion turn containing the phase. + pub fusion_id: String, + /// HydraFusion orchestration pattern containing the phase. + pub pattern: FusionPattern, + /// Stable identifier for the concrete phase. + pub phase_id: String, + /// Kind of phase currently executing. + pub phase_kind: FusionPhaseKind, + /// Semantic role assigned to the phase. + pub role: String, + /// Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + /// Cumulative private response bytes observed for this model call. The event never includes response text. + #[serde(skip_serializing_if = "Option::is_none")] + pub total_response_size_bytes: Option, +} + /// Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. /// ///
@@ -2493,7 +2673,7 @@ pub struct FusionAttribution { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AssistantMessageReasoningBlocks { - /// Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + /// Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. #[serde(skip_serializing_if = "Option::is_none")] pub blocks: Option>, /// Model provider that produced these reasoning blocks. @@ -2527,6 +2707,16 @@ pub struct AssistantMessageServerTools { pub raw_content_blocks: Option>, } +/// Hosted program that requested this client tool call +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantMessageToolRequestCaller { + /// Provider-assigned identifier for the hosted caller. + pub caller_id: String, + /// Kind of hosted caller that requested the client tool call. + pub r#type: AssistantMessageToolRequestCallerType, +} + /// A tool invocation request from the assistant #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2534,6 +2724,9 @@ pub struct AssistantMessageToolRequest { /// Arguments to pass to the tool, format depends on the tool #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option, + /// Hosted program that requested this client tool call + #[serde(skip_serializing_if = "Option::is_none")] + pub caller: Option, /// Resolved intention summary describing what this specific call does #[serde(skip_serializing_if = "Option::is_none")] pub intention_summary: Option, @@ -3841,12 +4034,15 @@ pub struct SkillInvokedData { /// Description of the skill from its SKILL.md frontmatter #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// Whether model invocation is disabled for this skill + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, /// Model identifier active when the skill was invoked, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Name of the invoked skill pub name: String, - /// File path to the SKILL.md definition + /// File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity pub path: String, /// Name of the plugin this skill originated from, when applicable #[serde(skip_serializing_if = "Option::is_none")] @@ -3854,7 +4050,7 @@ pub struct SkillInvokedData { /// Version of the plugin this skill originated from, when applicable #[serde(skip_serializing_if = "Option::is_none")] pub plugin_version: Option, - /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// What triggered the skill invocation: `user-invoked` (explicit user action, such as via a slash command or UI affordance), `agent-invoked` (agent requested the skill), or `context-load` (loaded as part of another context, such as preloading skills configured on a custom agent or subagent) @@ -3947,6 +4143,9 @@ pub struct SubagentCompletedData { /// Model used by the sub-agent #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Why an explicit task-call model did not become the effective model + #[serde(skip_serializing_if = "Option::is_none")] + pub model_override_reason: Option, /// Tool call ID of the parent tool invocation that spawned this sub-agent pub tool_call_id: String, /// Total tokens (input + output) consumed by the sub-agent @@ -3988,6 +4187,9 @@ pub struct SubagentFailedData { /// Model selected for the sub-agent, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Why an explicit task-call model did not become the effective model + #[serde(skip_serializing_if = "Option::is_none")] + pub model_override_reason: Option, /// Tool call ID of the parent tool invocation that spawned this sub-agent pub tool_call_id: String, /// Total tokens (input + output) consumed before the sub-agent failed @@ -4023,7 +4225,7 @@ pub struct HookStartData { pub hook_invocation_id: String, /// Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") pub hook_type: String, - /// Input data passed to the hook + /// Input data passed to the hook. For postToolUse hooks the retained copy served by session.eventLog.read (and by a resumed session) elides the tool result's inline `contents`/`uiResource` and replaces an over-long `textResultForLlm` with a `[copilot:elided ...]` marker, to keep a multi-megabyte payload out of the durable event log; the live subscription stream still delivers the full value. Read the adjacent tool.execution_complete event for the tool result itself. #[serde(skip_serializing_if = "Option::is_none")] pub input: Option, /// Tool call ID of the parent tool invocation when this event originates from a sub-agent @@ -4996,6 +5198,9 @@ pub struct PermissionPromptRequestExtensionEnvAccess { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PermissionRequestedData { + /// Agent mode captured from the owning turn when permission evaluation began. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_mode: Option, /// Details of the permission being requested pub permission_request: PermissionRequest, /// Derived user-facing permission prompt details for UI consumers @@ -5692,7 +5897,7 @@ pub struct SessionAutoModeResolvedData { pub sticky_override: Option, } -/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. /// ///
/// @@ -5717,6 +5922,9 @@ pub struct SessionManagedSettingsResolvedData { /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. #[serde(skip_serializing_if = "Option::is_none")] pub permissions_allow_intersected: Option, + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_managed: Option, /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_enabled_by_undetermined_policy: Option, @@ -5725,7 +5933,7 @@ pub struct SessionManagedSettingsResolvedData { /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, - /// Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. pub source: ManagedSettingsResolvedSource, } @@ -5929,7 +6137,7 @@ pub struct SkillsLoadedSkill { /// Absolute path to the skill file, if available #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, - /// Source location type (e.g., project, personal-copilot, plugin, builtin) + /// Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) pub source: SkillSource, /// Whether the skill can be invoked by the user as a slash command pub user_invocable: bool, @@ -5943,7 +6151,7 @@ pub struct SessionSkillsLoadedData { pub skills: Vec, } -/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CustomAgentsUpdatedAgent { @@ -5956,6 +6164,12 @@ pub struct CustomAgentsUpdatedAgent { /// Model override for this agent, if set #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether authored models are preferences or required constraints + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, + /// Authored model ids in priority order, if configured + #[serde(skip_serializing_if = "Option::is_none")] + pub models: Option>, /// Internal name of the agent pub name: String, /// Source location: user, project, inherited, remote, or plugin @@ -6321,6 +6535,24 @@ pub struct McpAppToolCallCompleteData { pub tool_name: String, } +/// Routing preference used when the session model is `auto`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +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, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Hosting platform type of the repository (github or ado) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum WorkingDirectoryContextHostType { @@ -6631,6 +6863,48 @@ pub enum TaskCompletionOutcome { Unknown, } +/// Structured terminal status from a tool completion event. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CompletionReceiptToolStatus { + /// The tool completed successfully. + #[serde(rename = "success")] + Success, + /// The tool failed without a more specific structured status. + #[serde(rename = "failure")] + Failure, + /// The tool exceeded its time budget. + #[serde(rename = "timeout")] + Timeout, + /// The user rejected the tool call. + #[serde(rename = "rejected")] + Rejected, + /// The permissions service denied the tool call. + #[serde(rename = "denied")] + Denied, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Runtime reason the completion decision was accepted. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CompletionReceiptStopReason { + /// The model reached a natural terminal response. + #[serde(rename = "natural")] + Natural, + /// A terminal tool ended the interaction. + #[serde(rename = "terminal_tool")] + TerminalTool, + /// The configured agentStop continuation limit was reached. + #[serde(rename = "agent_stop_block_limit")] + AgentStopBlockLimit, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Kind of turn for which HydraFusion routing is running. /// ///
@@ -6700,6 +6974,65 @@ pub enum FusionPattern { Unknown, } +/// HydraFusion phase kind. +/// +///
+/// +/// **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 FusionPhaseKind { + /// Primary solver phase. + #[serde(rename = "primary")] + Primary, + /// Read-only cascade judge phase. + #[serde(rename = "judge")] + Judge, + /// Cascade repair phase. + #[serde(rename = "repair")] + Repair, + /// Initial critique-pattern draft phase. + #[serde(rename = "draft")] + Draft, + /// Read-only critique phase. + #[serde(rename = "critic")] + Critic, + /// Critique-pattern revision phase. + #[serde(rename = "revision")] + Revision, + /// Follow-up phase continuing from the resolved model. + #[serde(rename = "follow_up")] + FollowUp, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Conversation scope in which a HydraFusion phase executes. +/// +///
+/// +/// **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 FusionConversationScope { + /// Canonical root conversation history. + #[serde(rename = "root")] + Root, + /// Isolated read-only review history that does not enter the root conversation. + #[serde(rename = "review")] + Review, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The agent mode that was active when this message was sent #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UserMessageAgentMode { @@ -6790,7 +7123,7 @@ pub enum ModelCallFailureTransport { Unknown, } -/// Conversation scope in which a HydraFusion phase executes. +/// Content-safe activity observed while a HydraFusion phase is running. /// ///
/// @@ -6799,50 +7132,16 @@ pub enum ModelCallFailureTransport { /// ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionConversationScope { - /// Canonical root conversation history. - #[serde(rename = "root")] - Root, - /// Isolated read-only review history that does not enter the root conversation. - #[serde(rename = "review")] - Review, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// HydraFusion phase kind. -/// -///
-/// -/// **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 FusionPhaseKind { - /// Primary solver phase. - #[serde(rename = "primary")] - Primary, - /// Read-only cascade judge phase. - #[serde(rename = "judge")] - Judge, - /// Cascade repair phase. - #[serde(rename = "repair")] - Repair, - /// Initial critique-pattern draft phase. - #[serde(rename = "draft")] - Draft, - /// Read-only critique phase. - #[serde(rename = "critic")] - Critic, - /// Critique-pattern revision phase. - #[serde(rename = "revision")] - Revision, - /// Follow-up phase continuing from the resolved model. - #[serde(rename = "follow_up")] - FollowUp, +pub enum FusionPhaseActivityKind { + /// The provider produced additional private output bytes. + #[serde(rename = "model_output")] + ModelOutput, + /// A tool began executing inside the phase. + #[serde(rename = "tool_started")] + ToolStarted, + /// A tool finished executing inside the phase. + #[serde(rename = "tool_completed")] + ToolCompleted, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -6939,6 +7238,17 @@ pub enum CitationProvider { Unknown, } +/// Hosted program caller type +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AssistantMessageToolRequestCallerType { + #[serde(rename = "program")] + Program, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AssistantUsageApiEndpoint { @@ -8110,7 +8420,10 @@ pub enum ManagedSettingsResolvedSource { /// Only session-local SDK-host injection contributed. #[serde(rename = "client")] Client, - /// More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + /// A policy helper registered by device or server policy contributed. Device registration takes priority when present. + #[serde(rename = "policyHelper")] + PolicyHelper, + /// More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. #[serde(rename = "mixed")] Mixed, /// No managed policy is in force (no channel contributed). @@ -8203,7 +8516,7 @@ pub enum FactoryRunSettledStatus { Unknown, } -/// Source location type (e.g., project, personal-copilot, plugin, builtin) +/// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SkillSource { /// Skill defined in the current project's skill directories. @@ -8227,6 +8540,24 @@ pub enum SkillSource { /// Skill bundled with the runtime. #[serde(rename = "builtin")] Builtin, + /// Pathless skill supplied lazily by an SDK skill provider. + #[serde(rename = "sdk")] + Sdk, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Whether configured models are advisory preferences or required constraints +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AgentModelPolicy { + /// Treat the authored models as advisory preferences that callers may override. + #[serde(rename = "preferred")] + Preferred, + /// Require subagent execution to use one of the authored models. + #[serde(rename = "required")] + Required, /// Unknown variant for forward compatibility. #[default] #[serde(other)] diff --git a/rust/src/handler.rs b/rust/src/handler.rs index e036b75a10..61d9b192cb 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -294,10 +294,11 @@ pub trait McpAuthHandler: Send + Sync + 'static { ) -> McpAuthResult; } -/// Handler for `user_input.requested` events from the `ask_user` tool. +/// Handler for `user_input.requested` events from the legacy question-and-answer +/// `ask_user` variant. /// -/// When unset, `requestUserInput: false` goes on the wire and the -/// `ask_user` tool is disabled for the session. +/// When unset, `requestUserInput: false` goes on the wire, so this client +/// cannot handle legacy user-input requests. #[async_trait] pub trait UserInputHandler: Send + Sync + 'static { /// Answer a question on behalf of the user. Return `None` to signal diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4a9f73ca4c..fd7f12cf14 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -10,6 +10,8 @@ mod canvas_dispatch; #[cfg(feature = "bundled-cli")] pub(crate) mod embeddedcli; mod errors; +/// Connection-level extension launch profile provider. +pub mod extension_launch_provider; /// In-process FFI transport hosting the runtime cdylib (`Transport::InProcess`). #[cfg(feature = "bundled-in-process")] pub(crate) mod ffi; @@ -31,6 +33,7 @@ pub mod hooks; mod jsonrpc; /// Permission-policy helpers that produce a [`handler::PermissionHandler`]. pub mod permission; +mod process_tree; /// BYOK bearer-token provider callbacks. pub mod provider_token; mod provider_token_dispatch; @@ -179,8 +182,10 @@ pub enum Transport { /// How the SDK locates the GitHub Copilot CLI binary. #[derive(Debug, Clone, Default)] pub enum CliProgram { - /// Auto-resolve: `COPILOT_CLI_PATH` → embedded CLI → dev cache. - /// This is the default. + /// Auto-resolve the transport's program. Managed child-process transports + /// select `COPILOT_CLI_PATH`, then the bundled runtime wrapper. In-process + /// transport loads the wrapper's adjacent runtime library directly unless + /// `COPILOT_CLI_PATH` explicitly selects a legacy embedded host. #[default] Resolve, /// Use an explicit binary path (skips resolution). @@ -204,12 +209,9 @@ pub const HAS_BUNDLED_CLI: bool = cfg!(has_bundled_cli); /// Returns the path to the bundled Copilot CLI, extracting it from the /// embedded archive on first call. /// -/// This is the same path [`Client::start`] resolves to when -/// [`ClientOptions::program`] is [`CliProgram::Resolve`], no -/// `COPILOT_CLI_PATH` override is set, and no -/// [`ClientOptions::bundled_cli_extract_dir`] is configured — exposing -/// it directly so callers (health checks, diagnostics, version probes) -/// can reach the bundled binary without spinning up a full [`Client`]. +/// This exposes the CLI artifact directly for callers such as health checks, +/// diagnostics, version probes, and in-process hosting. Managed child-process +/// transports resolve the bundled `copilot-runtime` wrapper instead. /// /// Subsequent calls return the cached result. Extraction is skipped when /// an already-published binary passes a cheap integrity re-check; a @@ -235,12 +237,35 @@ pub fn install_bundled_cli() -> Option { } } +/// Returns the path to the bundled `copilot-runtime` executable, extracting it +/// with adjacent `runtime.node` on first call. +/// +/// This is intended for health checks and intermediate launchers that need the +/// concrete managed runtime path before [`Client::start`]. Subsequent calls +/// return the cached result. +/// +/// Returns `None` when the `bundled-cli` feature is off, the target platform +/// isn't supported, or extraction failed. It does not fall back to the +/// build-time extraction cache. +pub fn install_bundled_runtime() -> Option { + #[cfg(feature = "bundled-cli")] + { + embeddedcli::runtime_path() + } + #[cfg(not(feature = "bundled-cli"))] + { + None + } +} + /// Options for starting a [`Client`]. /// /// When `program` is [`CliProgram::Resolve`] (the default), [`Client::start`] -/// uses `COPILOT_CLI_PATH` when set to a real file. Otherwise it uses the -/// bundled Copilot CLI when the default `bundled-cli` cargo feature is enabled, -/// or the build-time extracted dev-cache CLI when that feature is disabled. +/// uses `COPILOT_CLI_PATH` when set to a real file. Managed child-process +/// transports next use the bundled `copilot-runtime` wrapper. In-process +/// transport loads the wrapper's adjacent runtime library. With `bundled-cli` +/// disabled, the corresponding artifact is resolved from the build-time +/// extraction cache. /// /// Set `program` to [`CliProgram::Path`] to use an explicit binary instead. /// This skips auto-resolution entirely. @@ -311,6 +336,14 @@ pub struct ClientOptions { /// [`CopilotRequestHandler`] /// instead of issuing the calls itself. pub request_handler: Option>, + /// Connection-level extension launch profile provider. + /// + /// When set, the SDK registers itself with the runtime during + /// [`Client::start`] before any session can be created. Incoming + /// `extensionLaunchProvider.resolve` requests are dispatched independently + /// of sessions. + pub extension_launch_provider: + Option>, /// Connection-level GitHub telemetry forwarding callback (experimental). /// /// When set, every session created or resumed on this client opts into @@ -368,6 +401,102 @@ pub struct ClientOptions { /// (the default) or are stripped to a minimal/safe baseline. See /// [`ClientMode`] for the contract and trade-offs. pub mode: ClientMode, + /// Declares the integrating application's identity, forwarded to the runtime on + /// the `server.connect` handshake. Declaring it lets the telemetry the + /// runtime emits on this connection be attributed to a consistent surface + /// (the application and its Copilot integration) instead of the runtime's own + /// build. All fields are optional; leave it `None` to keep the runtime's + /// default attribution. + pub client_info: Option, +} + +/// Identity of the integrating application, declared on the `server.connect` +/// handshake. +/// +/// Declaring it lets the telemetry the runtime emits on the connection be +/// attributed to a single, consistent surface instead of the runtime's own +/// build. All fields are optional; an empty field is omitted from the +/// handshake. +/// +/// The struct is `#[non_exhaustive]`, so construct it with [`ClientInfo::new`] +/// and the `with_*` builder methods rather than a struct literal. This lets the +/// SDK add identity fields in future releases without a breaking change. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[non_exhaustive] +pub struct ClientInfo { + /// Name of the application using the SDK. + pub application_name: Option, + /// Version of the application using the SDK. + pub application_version: Option, + /// Optional name of a specific integration within the application, such as an + /// extension or plugin. + pub integration_name: Option, + /// Optional version of the integration identified by [`Self::integration_name`]. + pub integration_version: Option, +} + +impl ClientInfo { + /// Create an empty `ClientInfo`. Populate fields with the `with_*` builder + /// methods; every field is optional. + pub fn new() -> Self { + Self::default() + } + + /// Set the name of the application using the SDK. + pub fn with_application_name(mut self, application_name: impl Into) -> Self { + self.application_name = Some(application_name.into()); + self + } + + /// Set the version of the application using the SDK. + pub fn with_application_version(mut self, application_version: impl Into) -> Self { + self.application_version = Some(application_version.into()); + self + } + + /// Set the name of a specific integration within the application, such as an + /// extension or plugin. + pub fn with_integration_name(mut self, integration_name: impl Into) -> Self { + self.integration_name = Some(integration_name.into()); + self + } + + /// Set the version of the integration identified by + /// [`Self::with_integration_name`]. + pub fn with_integration_version(mut self, integration_version: impl Into) -> Self { + self.integration_version = Some(integration_version.into()); + self + } + + /// Returns `true` when no field carries a non-empty value, in which case the + /// SDK omits `clientInfo` from the handshake and the runtime keeps its + /// default attribution. + fn is_empty(&self) -> bool { + Self::non_empty(&self.application_name).is_none() + && Self::non_empty(&self.application_version).is_none() + && Self::non_empty(&self.integration_name).is_none() + && Self::non_empty(&self.integration_version).is_none() + } + + /// Clone the field only when it holds a non-empty string, so empty fields are + /// dropped from the handshake. + fn non_empty(value: &Option) -> Option { + value.as_ref().filter(|s| !s.is_empty()).cloned() + } + + /// Map onto the generated connect wire shape, dropping empty fields. Returns + /// `None` when no field carries a non-empty value. + fn to_wire(&self) -> Option { + if self.is_empty() { + return None; + } + Some(crate::generated::api_types::ConnectClientInfo { + editor_name: Self::non_empty(&self.application_name), + editor_version: Self::non_empty(&self.application_version), + extension_name: Self::non_empty(&self.integration_name), + extension_version: Self::non_empty(&self.integration_version), + }) + } } impl std::fmt::Debug for ClientOptions { @@ -403,6 +532,10 @@ impl std::fmt::Debug for ClientOptions { "request_handler", &self.request_handler.as_ref().map(|_| ""), ) + .field( + "extension_launch_provider", + &self.extension_launch_provider.as_ref().map(|_| ""), + ) .field( "on_github_telemetry", &self.on_github_telemetry.as_ref().map(|_| ""), @@ -415,6 +548,7 @@ impl std::fmt::Debug for ClientOptions { .field("base_directory", &self.base_directory) .field("enable_remote_sessions", &self.enable_remote_sessions) .field("bundled_cli_extract_dir", &self.bundled_cli_extract_dir) + .field("client_info", &self.client_info) .finish() } } @@ -656,6 +790,7 @@ impl Default for ClientOptions { on_list_models: None, session_fs: None, request_handler: None, + extension_launch_provider: None, on_github_telemetry: None, on_get_trace_context: None, telemetry: None, @@ -663,6 +798,7 @@ impl Default for ClientOptions { enable_remote_sessions: false, bundled_cli_extract_dir: None, mode: ClientMode::default(), + client_info: None, } } } @@ -814,6 +950,18 @@ impl ClientOptions { self } + /// Register a connection-level extension launch profile provider. + /// + /// The provider is wrapped in [`Arc`] internally and registered with the + /// runtime before [`Client::start`] returns. + pub fn with_extension_launch_provider

(mut self, provider: P) -> Self + where + P: crate::extension_launch_provider::ExtensionLaunchProvider, + { + self.extension_launch_provider = Some(Arc::new(provider)); + self + } + /// Register a connection-level GitHub telemetry forwarding callback /// (internal/experimental). Registering a callback auto-enables telemetry /// forwarding on every session created or resumed on this client; the @@ -859,8 +1007,8 @@ impl ClientOptions { self } - /// Override the directory where the bundled CLI binary is extracted on - /// first use. See [`Self::bundled_cli_extract_dir`]. + /// Override the directory where bundled CLI and runtime artifacts are + /// extracted on first use. See [`Self::bundled_cli_extract_dir`]. /// /// Only applies when the `bundled-cli` cargo feature is on. With /// `bundled-cli` disabled (`default-features = false`), set @@ -881,6 +1029,14 @@ impl ClientOptions { self.mode = mode; self } + + /// Declare the integrating application's identity, forwarded to the runtime on + /// the `server.connect` handshake so its telemetry is attributed to a + /// consistent surface. See [`Self::client_info`]. + pub fn with_client_info(mut self, client_info: ClientInfo) -> Self { + self.client_info = Some(client_info); + self + } } /// Validate a [`SessionFsConfig`] before sending `sessionFs.setProvider`. @@ -1017,6 +1173,7 @@ impl std::fmt::Debug for Client { struct ClientInner { child: parking_lot::Mutex>, + process_tree: parking_lot::Mutex>, #[cfg(feature = "bundled-in-process")] /// In-process FFI runtime host, set only for [`Transport::InProcess`]. /// Closing it tears down the native runtime connection. @@ -1037,6 +1194,7 @@ struct ClientInner { /// Inbound `llmInference.*` dispatcher, installed when /// [`ClientOptions::request_handler`] is set. llm_inference: OnceLock>, + extension_launch_provider: Arc, /// Connection-level GitHub telemetry forwarding callback, set from /// [`ClientOptions::on_github_telemetry`]. Drives the /// `enableGitHubTelemetryForwarding` wire flag and the @@ -1048,6 +1206,10 @@ struct ClientInner { /// `None` for stdio and for external-server transport without an /// explicit token. effective_connection_token: Option, + /// Application identity forwarded on the `connect` handshake, set from + /// [`ClientOptions::client_info`]. `None` keeps the runtime's default + /// telemetry attribution. + client_info: Option, /// SDK [`ClientMode`] captured at start time. Drives empty-mode safe /// defaults inside `create_session` / `resume_session`. pub(crate) mode: ClientMode, @@ -1182,6 +1344,7 @@ impl Client { }; let session_fs_config = options.session_fs.clone(); let request_handler = options.request_handler.clone(); + let extension_launch_provider = options.extension_launch_provider.clone(); let session_fs_sqlite_declared = session_fs_config .as_ref() .and_then(|c| c.capabilities.as_ref()) @@ -1195,6 +1358,7 @@ impl Client { let resolve_start = Instant::now(); let resolved = resolve::copilot_binary_with_extract_dir( options.bundled_cli_extract_dir.as_deref(), + true, )?; let resolve_elapsed = resolve_start.elapsed(); timings.program_resolve_ms = Some(StartupTimings::millis(resolve_elapsed)); @@ -1202,7 +1366,7 @@ impl Client { elapsed_ms = resolve_elapsed.as_millis(), "Client::start CLI program resolution complete" ); - info!(path = %resolved.display(), "resolved copilot CLI"); + info!(path = %resolved.display(), "resolved copilot runtime"); #[cfg(windows)] { if let Some(ext) = resolved.extension().and_then(|e| e.to_str()).filter(|ext| { @@ -1250,21 +1414,24 @@ impl Client { reader, writer, None, + None, working_directory, options.on_list_models, + extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::Tcp { port, connection_token: _, } => { - let (mut child, actual_port, spawn_elapsed, port_wait_elapsed) = + let (mut child, tree, actual_port, spawn_elapsed, port_wait_elapsed) = Self::spawn_tcp(&program, &options, &working_directory, port).await?; timings.process_spawn_ms = Some(StartupTimings::millis(spawn_elapsed)); timings.port_wait_ms = Some(StartupTimings::millis(port_wait_elapsed)); @@ -1281,18 +1448,21 @@ impl Client { reader, writer, Some(child), + tree, working_directory, options.on_list_models, + extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::Stdio => { - let (mut child, spawn_elapsed) = + let (mut child, tree, spawn_elapsed) = Self::spawn_stdio(&program, &options, &working_directory)?; timings.process_spawn_ms = Some(StartupTimings::millis(spawn_elapsed)); let stdin = child.stdin.take().expect("stdin is piped"); @@ -1302,14 +1472,17 @@ impl Client { stdout, stdin, Some(child), + tree, working_directory, options.on_list_models, + extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::InProcess => { @@ -1353,20 +1526,31 @@ impl Client { if !use_logged_in_user { args.push("--no-auto-login".to_string()); } - let host = crate::ffi::FfiHost::create(&program, environment, args)?; + let explicit_cli = std::env::var_os("COPILOT_CLI_PATH") + .map(PathBuf::from) + .filter(|path| path.is_file()); + let host = crate::ffi::FfiHost::create( + &program, + explicit_cli.as_deref(), + environment, + args, + )?; let (reader, writer, shared) = host.start().await?; let client = Self::from_transport( reader, writer, None, + None, working_directory, options.on_list_models, + extension_launch_provider.clone(), session_fs_config.is_some(), session_fs_sqlite_declared, options.on_get_trace_context, options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )?; *client.inner.ffi_host.lock() = Some(shared); client @@ -1387,6 +1571,25 @@ impl Client { elapsed_ms = start_time.elapsed().as_millis(), "Client::start protocol verification complete" ); + let request_dispatcher = request_handler.map(|handler| { + let dispatcher = Arc::new(copilot_request_handler::CopilotRequestDispatcher::new( + handler, + )); + dispatcher.set_client(Arc::downgrade(&client.inner)); + let _ = client.inner.llm_inference.set(dispatcher.clone()); + dispatcher + }); + if client.inner.extension_launch_provider.is_configured() { + client.inner.router.ensure_started( + &client.inner.notification_tx, + &client.inner.request_rx, + client.inner.extension_launch_provider.clone(), + request_dispatcher.clone(), + client.inner.on_github_telemetry.clone(), + client.inner.github_token_registry.clone(), + ); + client.rpc().register_extension_launch_provider().await?; + } if !builtin_plugin_directories.is_empty() { client .call( @@ -1416,18 +1619,14 @@ impl Client { "Client::start session filesystem setup complete" ); } - if let Some(handler) = request_handler { + if let Some(dispatcher) = request_dispatcher { let llm_inference_start = Instant::now(); - let dispatcher = Arc::new(copilot_request_handler::CopilotRequestDispatcher::new( - handler, - )); - dispatcher.set_client(Arc::downgrade(&client.inner)); - let _ = client.inner.llm_inference.set(dispatcher.clone()); // Start the router early (before any session is registered) so the // startup model catalog request is dispatched to the handler. client.inner.router.ensure_started( &client.inner.notification_tx, &client.inner.request_rx, + client.inner.extension_launch_provider.clone(), Some(dispatcher.clone()), client.inner.on_github_telemetry.clone(), client.inner.github_token_registry.clone(), @@ -1484,14 +1683,45 @@ impl Client { reader, writer, None, + None, + cwd, + None, + None, + false, + false, + None, + None, + None, + ClientMode::default(), + None, + ) + } + + /// Construct a [`Client`] from raw streams with a preset extension launch + /// provider, for integration testing connection-global reverse requests. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_extension_launch_provider( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + provider: Arc, + ) -> Result { + Self::from_transport( + reader, + writer, + None, + None, cwd, None, + Some(provider), false, false, None, None, None, ClientMode::default(), + None, ) } @@ -1513,14 +1743,17 @@ impl Client { reader, writer, None, + None, cwd, None, + None, false, false, Some(provider), None, None, ClientMode::default(), + None, ) } @@ -1538,14 +1771,17 @@ impl Client { reader, writer, None, + None, cwd, None, + None, false, false, None, None, token, ClientMode::default(), + None, ) } @@ -1563,14 +1799,17 @@ impl Client { reader, writer, None, + None, cwd, None, + None, false, false, None, Some(on_github_telemetry), None, ClientMode::default(), + None, ) } @@ -1584,19 +1823,53 @@ impl Client { generate_connection_token() } + /// Construct a [`Client`] from raw streams with a preset + /// [`ClientInfo`], for integration testing the `connect` handshake's + /// application-identity forwarding path. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_client_info( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + client_info: Option, + ) -> Result { + Self::from_transport( + reader, + writer, + None, + None, + cwd, + None, + None, + false, + false, + None, + None, + None, + ClientMode::default(), + client_info, + ) + } + #[allow(clippy::too_many_arguments)] fn from_transport( reader: impl AsyncRead + Unpin + Send + 'static, writer: impl AsyncWrite + Unpin + Send + 'static, child: Option, + process_tree: Option, cwd: PathBuf, on_list_models: Option>, + extension_launch_provider: Option< + Arc, + >, session_fs_configured: bool, session_fs_sqlite_declared: bool, on_get_trace_context: Option>, on_github_telemetry: Option, effective_connection_token: Option, mode: ClientMode, + client_info: Option, ) -> Result { let setup_start = Instant::now(); let (request_tx, request_rx) = mpsc::unbounded_channel::(); @@ -1612,9 +1885,15 @@ impl Client { info!(pid = ?pid, "copilot CLI client ready"); let github_token_registry = Arc::new(github_token::GitHubTokenRegistry::new()); + let extension_launch_provider = Arc::new( + extension_launch_provider::ExtensionLaunchProviderDispatcher::new( + extension_launch_provider, + ), + ); let client = Self { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(child), + process_tree: parking_lot::Mutex::new(process_tree), #[cfg(feature = "bundled-in-process")] ffi_host: parking_lot::Mutex::new(None), rpc, @@ -1631,14 +1910,17 @@ impl Client { session_fs_configured, session_fs_sqlite_declared, llm_inference: OnceLock::new(), + extension_launch_provider: extension_launch_provider.clone(), on_github_telemetry, on_get_trace_context, effective_connection_token, mode, + client_info, startup_timings: OnceLock::new(), }), }; github_token_registry.set_client(Arc::downgrade(&client.inner)); + extension_launch_provider.set_client(Arc::downgrade(&client.inner)); client.spawn_lifecycle_dispatcher(); debug!( elapsed_ms = setup_start.elapsed().as_millis(), @@ -1751,13 +2033,6 @@ impl Client { .stdout(Stdio::piped()) .stderr(Stdio::piped()); - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - command.as_std_mut().creation_flags(CREATE_NO_WINDOW); - } - command } @@ -1814,7 +2089,7 @@ impl Client { program: &Path, options: &ClientOptions, working_directory: &Path, - ) -> Result<(Child, Duration)> { + ) -> Result<(Child, Option, Duration)> { info!(cwd = ?working_directory, program = %program.display(), "spawning copilot CLI (stdio)"); let mut command = Self::build_command(program, options, working_directory); command @@ -1826,13 +2101,13 @@ impl Client { .args(&options.extra_args) .stdin(Stdio::piped()); let spawn_start = Instant::now(); - let child = command.spawn()?; + let (child, tree) = process_tree::spawn(&mut command)?; let spawn_elapsed = spawn_start.elapsed(); debug!( elapsed_ms = spawn_elapsed.as_millis(), "Client::spawn_stdio subprocess spawned" ); - Ok((child, spawn_elapsed)) + Ok((child, tree, spawn_elapsed)) } async fn spawn_tcp( @@ -1840,7 +2115,13 @@ impl Client { options: &ClientOptions, working_directory: &Path, port: u16, - ) -> Result<(Child, u16, Duration, Duration)> { + ) -> Result<( + Child, + Option, + u16, + Duration, + Duration, + )> { info!(cwd = ?working_directory, program = %program.display(), port = %port, "spawning copilot CLI (tcp)"); let mut command = Self::build_command(program, options, working_directory); command @@ -1852,7 +2133,7 @@ impl Client { .args(&options.extra_args) .stdin(Stdio::null()); let spawn_start = Instant::now(); - let mut child = command.spawn()?; + let (mut child, tree) = process_tree::spawn(&mut command)?; let spawn_elapsed = spawn_start.elapsed(); debug!( elapsed_ms = spawn_elapsed.as_millis(), @@ -1899,7 +2180,7 @@ impl Client { "Client::spawn_tcp TCP port wait complete" ); info!(port = %actual_port, "CLI server listening"); - Ok((child, actual_port, spawn_elapsed, port_wait_elapsed)) + Ok((child, tree, actual_port, spawn_elapsed, port_wait_elapsed)) } fn drain_stderr(child: &mut Child) { @@ -2055,6 +2336,7 @@ impl Client { self.inner.router.ensure_started( &self.inner.notification_tx, &self.inner.request_rx, + self.inner.extension_launch_provider.clone(), self.inner.llm_inference.get().cloned(), self.inner.on_github_telemetry.clone(), self.inner.github_token_registry.clone(), @@ -2074,6 +2356,7 @@ impl Client { self.inner.router.ensure_started( &self.inner.notification_tx, &self.inner.request_rx, + self.inner.extension_launch_provider.clone(), self.inner.llm_inference.get().cloned(), self.inner.on_github_telemetry.clone(), self.inner.github_token_registry.clone(), @@ -2194,7 +2477,15 @@ impl Client { .on_github_telemetry .is_some() .then_some(true), - ..Default::default() + // Declare the integrating application's identity so the runtime attributes + // the telemetry it emits on this connection to a consistent surface + // instead of its own build. `None` when the app didn't supply it, and + // empty fields are dropped. + client_info: self + .inner + .client_info + .as_ref() + .and_then(ClientInfo::to_wire), }; let value = self .call( @@ -2293,6 +2584,7 @@ impl Client { self.inner.router.ensure_started( &self.inner.notification_tx, &self.inner.request_rx, + self.inner.extension_launch_provider.clone(), self.inner.llm_inference.get().cloned(), self.inner.on_github_telemetry.clone(), self.inner.github_token_registry.clone(), @@ -2439,11 +2731,12 @@ impl Client { /// Cooperatively shut down the client and the CLI child process. /// /// Walks every still-registered session and sends `session.destroy` - /// for each one, asks SDK-owned runtimes to shut down, then kills the - /// CLI child. Errors from per-session destroys, runtime shutdown, and - /// the final child-kill are collected into - /// [`StopErrors`] rather than short-circuiting on the first failure - /// — so callers see the full picture of teardown. + /// for each one, asks SDK-owned runtimes to shut down, terminates the + /// Windows-owned CLI Job Object when present, and reaps the root process. + /// Errors from per-session destroys, runtime shutdown, and final process + /// termination are collected into [`StopErrors`] rather than + /// short-circuiting on the first failure — so callers see the full picture + /// of teardown. /// /// If you have already called [`Session::disconnect`] on every /// session this client created, the per-session destroy step is a @@ -2466,6 +2759,7 @@ impl Client { let pid = self.pid(); info!(pid = ?pid, "stopping CLI process"); let mut errors: Vec = Vec::new(); + self.inner.extension_launch_provider.clear(); // Snapshot the registered session IDs without holding the router // lock across the destroy RPCs. @@ -2531,8 +2825,14 @@ impl Client { } let child = self.inner.child.lock().take(); + let process_tree = self.inner.process_tree.lock().take(); *self.inner.state.lock() = ConnectionState::Disconnected; *self.inner.models_cache.lock() = Arc::new(tokio::sync::OnceCell::new()); + if let Some(process_tree) = process_tree + && let Err(error) = process_tree.terminate() + { + errors.push(error.into()); + } if let Some(mut child) = child { match child.try_wait() { Ok(Some(_status)) => {} @@ -2551,12 +2851,12 @@ impl Client { } } - // The runtime.shutdown RPC above already asked the runtime to clean up; - // closing here tears down the transport. + // Provider registration is scoped to the connection. Closing the + // transport unregisters it and prevents stale callbacks after stop. + self.inner.rpc.force_close(); #[cfg(feature = "bundled-in-process")] { if let Some(host) = self.inner.ffi_host.lock().take() { - self.inner.rpc.force_close(); host.close(); } } @@ -2573,10 +2873,9 @@ impl Client { /// /// Synchronous fallback when [`stop`](Self::stop) is unsuitable — for /// example when the awaiting tokio runtime is shutting down or the - /// process is wedged on I/O. Sends a kill signal without awaiting - /// reaper completion and immediately drops all per-session router - /// state so dependent tasks observe a closed channel rather than a - /// hang. + /// process is wedged on I/O. Terminates the Windows-owned CLI Job Object + /// when present and immediately drops all per-session router state so + /// dependent tasks observe a closed channel rather than a hang. /// /// # Cancel safety /// @@ -2601,6 +2900,12 @@ impl Client { pub fn force_stop(&self) { let pid = self.pid(); info!(pid = ?pid, "force-stopping CLI process"); + self.inner.extension_launch_provider.clear(); + if let Some(process_tree) = self.inner.process_tree.lock().take() + && let Err(error) = process_tree.terminate() + { + error!(pid = ?pid, %error, "failed to terminate CLI process tree"); + } if let Some(mut child) = self.inner.child.lock().take() && let Err(e) = child.start_kill() { @@ -2662,8 +2967,13 @@ impl Client { impl Drop for ClientInner { fn drop(&mut self) { + let pid = self.child.lock().as_ref().and_then(Child::id); + if let Some(process_tree) = self.process_tree.lock().take() + && let Err(error) = process_tree.terminate() + { + error!(pid = ?pid, %error, "failed to terminate CLI process tree on drop"); + } if let Some(ref mut child) = *self.child.lock() { - let pid = child.id(); if let Err(e) = child.start_kill() { error!(pid = ?pid, error = %e, "failed to kill CLI process on drop"); } else { @@ -3323,14 +3633,17 @@ mod tests { client_read, client_write, Some(child), + None, temp.path().to_path_buf(), None, + None, false, false, None, None, None, ClientMode::default(), + None, ) .unwrap(); @@ -3411,6 +3724,7 @@ mod tests { Client { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(None), + process_tree: parking_lot::Mutex::new(None), #[cfg(feature = "bundled-in-process")] ffi_host: parking_lot::Mutex::new(None), rpc: { @@ -3433,10 +3747,14 @@ mod tests { session_fs_configured: false, session_fs_sqlite_declared: false, llm_inference: OnceLock::new(), + extension_launch_provider: Arc::new( + extension_launch_provider::ExtensionLaunchProviderDispatcher::new(None), + ), on_github_telemetry: None, on_get_trace_context: None, effective_connection_token: None, mode: ClientMode::default(), + client_info: None, startup_timings: OnceLock::new(), }), } diff --git a/rust/src/process_tree.rs b/rust/src/process_tree.rs new file mode 100644 index 0000000000..8dc7a451c5 --- /dev/null +++ b/rust/src/process_tree.rs @@ -0,0 +1,195 @@ +//! Windows crash-safe ownership of an SDK-spawned CLI process. +//! +//! A Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` lets Windows +//! terminate the CLI when the SDK-hosting process exits abruptly, even when +//! Rust cleanup code never runs. Other platforms retain Tokio's direct-child +//! ownership because no equivalent product failure has been demonstrated. + +use std::io; + +use tokio::process::{Child, Command}; + +pub(crate) fn spawn(command: &mut Command) -> io::Result<(Child, Option)> { + #[cfg(windows)] + { + platform::spawn(command).map(|(child, tree)| (child, Some(ProcessTree(Some(tree))))) + } + #[cfg(not(windows))] + { + command.spawn().map(|child| (child, None)) + } +} + +pub(crate) struct ProcessTree(Option); + +impl ProcessTree { + pub(crate) fn terminate(mut self) -> io::Result<()> { + self.0.take().expect("process tree is armed").terminate() + } +} + +impl Drop for ProcessTree { + fn drop(&mut self) { + if let Some(tree) = self.0.take() { + let _ = tree.terminate(); + } + } +} + +#[cfg(not(windows))] +mod platform { + pub(super) struct Tree; + + impl Tree { + pub(super) fn terminate(&self) -> std::io::Result<()> { + unreachable!("process-tree ownership is Windows-only") + } + } +} + +#[cfg(windows)] +mod platform { + use std::mem::size_of; + use std::os::windows::process::CommandExt; + use std::{io, ptr}; + + use tokio::process::{Child, Command}; + use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; + use windows_sys::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next, + }; + use windows_sys::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, + SetInformationJobObject, TerminateJobObject, + }; + use windows_sys::Win32::System::Threading::{ + CREATE_NO_WINDOW, CREATE_SUSPENDED, OpenThread, ResumeThread, THREAD_SUSPEND_RESUME, + }; + + struct OwnedHandle(HANDLE); + + // SAFETY: Win32 handles may be used and closed from any thread. + unsafe impl Send for OwnedHandle {} + unsafe impl Sync for OwnedHandle {} + + impl Drop for OwnedHandle { + fn drop(&mut self) { + // SAFETY: this value uniquely owns a valid handle. + unsafe { + CloseHandle(self.0); + } + } + } + + pub(super) struct Tree { + job: OwnedHandle, + } + + pub(super) fn spawn(command: &mut Command) -> io::Result<(Child, Tree)> { + // The root cannot run or create descendants before Job assignment. + command + .as_std_mut() + .creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED); + let mut child = command.spawn()?; + match attach_and_resume(&child) { + Ok(tree) => Ok((child, tree)), + Err(error) => { + let _ = child.start_kill(); + Err(error) + } + } + } + + fn attach_and_resume(child: &Child) -> io::Result { + // SAFETY: null security attributes and name create a private, + // non-inheritable Job Object. + let raw_job = unsafe { CreateJobObjectW(ptr::null(), ptr::null()) }; + if raw_job.is_null() { + return Err(io::Error::last_os_error()); + } + let job = OwnedHandle(raw_job); + + let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default(); + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + // SAFETY: `limits` has the layout required by the selected info class. + if unsafe { + SetInformationJobObject( + job.0, + JobObjectExtendedLimitInformation, + ptr::from_ref(&limits).cast(), + size_of::() as u32, + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + + let process = child.raw_handle().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "CLI exited before Job Object assignment", + ) + })?; + // SAFETY: both handles are valid and the child is still suspended. + if unsafe { AssignProcessToJobObject(job.0, process.cast()) } == 0 { + return Err(io::Error::last_os_error()); + } + + resume_initial_thread(child.id().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "CLI exited before thread resume") + })?)?; + Ok(Tree { job }) + } + + fn resume_initial_thread(pid: u32) -> io::Result<()> { + // SAFETY: the returned snapshot handle is owned and closed below. + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) }; + if snapshot == INVALID_HANDLE_VALUE { + return Err(io::Error::last_os_error()); + } + let snapshot = OwnedHandle(snapshot); + let mut entry = THREADENTRY32 { + dwSize: size_of::() as u32, + ..Default::default() + }; + + // SAFETY: `entry` has the documented size and remains live throughout + // enumeration. + let mut found = unsafe { Thread32First(snapshot.0, &mut entry) } != 0; + while found { + if entry.th32OwnerProcessID == pid { + // SAFETY: the thread id came from the live system snapshot. + let raw_thread = + unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) }; + if raw_thread.is_null() { + return Err(io::Error::last_os_error()); + } + let thread = OwnedHandle(raw_thread); + // SAFETY: this is the root's suspended initial thread. + if unsafe { ResumeThread(thread.0) } == u32::MAX { + return Err(io::Error::last_os_error()); + } + return Ok(()); + } + // SAFETY: same valid snapshot and initialized entry as above. + found = unsafe { Thread32Next(snapshot.0, &mut entry) } != 0; + } + + Err(io::Error::new( + io::ErrorKind::NotFound, + "CLI initial thread was not found", + )) + } + + impl Tree { + pub(super) fn terminate(&self) -> io::Result<()> { + // SAFETY: the handle is a live Job Object owned by this value. + if unsafe { TerminateJobObject(self.job.0, 1) } != 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + } + } +} diff --git a/rust/src/resolve.rs b/rust/src/resolve.rs index 1c88283a27..d8b996a11a 100644 --- a/rust/src/resolve.rs +++ b/rust/src/resolve.rs @@ -5,9 +5,9 @@ //! 1. An explicit path supplied by the application via //! [`CliProgram::Path`](crate::CliProgram::Path). //! 2. The `COPILOT_CLI_PATH` environment variable. -//! 3. The bundled CLI embedded in this crate at build time (when the +//! 3. The bundled program embedded in this crate at build time (when the //! `bundled-cli` cargo feature is on, the default). -//! 4. The build-time-extracted CLI in the per-user cache (when +//! 4. The build-time-extracted program in the per-user cache (when //! `bundled-cli` is off). //! //! There is no PATH scanning and no walking of standard install locations. @@ -35,6 +35,7 @@ use crate::{Error, ErrorKind}; /// under it. pub(crate) fn copilot_binary_with_extract_dir( extract_dir: Option<&Path>, + use_runtime_wrapper: bool, ) -> Result { if let Ok(value) = env::var("COPILOT_CLI_PATH") { let candidate = PathBuf::from(&value); @@ -49,11 +50,21 @@ pub(crate) fn copilot_binary_with_extract_dir( #[cfg(feature = "bundled-cli")] { - let bundled = match extract_dir { - Some(dir) => crate::embeddedcli::install_at(dir), - None => crate::embeddedcli::path(), + let bundled = if use_runtime_wrapper { + match extract_dir { + Some(dir) => crate::embeddedcli::install_runtime_at(dir), + None => crate::embeddedcli::runtime_path(), + } + } else { + match extract_dir { + Some(dir) => crate::embeddedcli::install_at(dir), + None => crate::embeddedcli::path(), + } }; if let Some(path) = bundled { + if use_runtime_wrapper { + validate_runtime_pair(&path)?; + } return Ok(path); } } @@ -61,16 +72,21 @@ pub(crate) fn copilot_binary_with_extract_dir( #[cfg(not(feature = "bundled-cli"))] { let _ = extract_dir; - if let Some(path) = extracted_cli_path() { - return Ok(path); + if let Some(program) = extracted_program(use_runtime_wrapper) { + return Ok(program); } } + let binary_name = if use_runtime_wrapper { + runtime_binary_name() + } else { + cli_binary_name() + }; Err(ErrorKind::BinaryNotFound { - name: "copilot".into(), + name: binary_name.into(), hint: Some( "the Copilot CLI is not bundled in this build of github-copilot-sdk and \ - COPILOT_CLI_PATH is not set. Either keep the default `bundled-cli` cargo \ + no applicable path override is set. Either keep the default `bundled-cli` cargo \ feature enabled, set COPILOT_CLI_PATH, or supply an explicit path via \ `CliProgram::Path(...)` on `ClientOptions::program`." .into(), @@ -79,7 +95,7 @@ pub(crate) fn copilot_binary_with_extract_dir( .into()) } -/// Path to the CLI extracted into the per-user cache by `build.rs` when +/// Path to the program extracted into the per-user cache by `build.rs` when /// `bundled-cli` is disabled. Returns `None` if the cached file is missing /// (e.g. the user deleted the cache after building, or built with /// `COPILOT_SKIP_CLI_DOWNLOAD`). @@ -93,14 +109,8 @@ pub(crate) fn copilot_binary_with_extract_dir( /// `$HOME` / `$LOCALAPPDATA` into the artifact, breaks sccache across /// machines, and prevents copying `target/` between hosts. #[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))] -fn extracted_cli_path() -> Option { +fn extracted_program(use_runtime_wrapper: bool) -> Option { let version = env!("COPILOT_SDK_CLI_VERSION"); - let binary = if cfg!(windows) { - "copilot.exe" - } else { - "copilot" - }; - let dir = match env::var_os("COPILOT_CLI_EXTRACT_DIR") { Some(custom) => PathBuf::from(custom), None => dirs::cache_dir() @@ -110,8 +120,16 @@ fn extracted_cli_path() -> Option { .join(sanitize_version(version)), }; - let path = dir.join(binary); - if path.is_file() { + let path = dir.join(if use_runtime_wrapper { + runtime_binary_name() + } else { + cli_binary_name() + }); + if use_runtime_wrapper { + if validate_runtime_pair(&path).is_ok() { + return Some(path); + } + } else if path.is_file() { return Some(path); } warn!( @@ -125,10 +143,83 @@ fn extracted_cli_path() -> Option { /// build opted out via `COPILOT_SKIP_CLI_DOWNLOAD`. In both cases there's /// no binary to look up, so the resolver returns `None` immediately. #[cfg(all(not(feature = "bundled-cli"), not(has_extracted_cli)))] -fn extracted_cli_path() -> Option { +fn extracted_program(_use_runtime_wrapper: bool) -> Option { None } +fn validate_runtime_pair(wrapper: &Path) -> Result<(), Error> { + let wrapper_valid = wrapper + .metadata() + .map(|metadata| metadata.is_file() && metadata.len() > 0) + .unwrap_or(false); + let runtime_node = wrapper + .parent() + .map(|parent| parent.join("runtime.node")) + .unwrap_or_else(|| PathBuf::from("runtime.node")); + let runtime_valid = runtime_node + .metadata() + .map(|metadata| metadata.is_file() && metadata.len() > 0) + .unwrap_or(false); + if wrapper_valid && runtime_valid { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let metadata = wrapper.metadata().map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "failed to inspect Copilot runtime wrapper permissions at '{}': {e}", + wrapper.display() + ), + ) + })?; + if metadata.permissions().mode() & 0o111 == 0 { + let mut permissions = metadata.permissions(); + permissions.set_mode(permissions.mode() | 0o111); + std::fs::set_permissions(wrapper, permissions).map_err(|e| { + Error::with_message( + ErrorKind::InvalidConfig, + format!( + "failed to make Copilot runtime wrapper executable at '{}': {e}", + wrapper.display() + ), + ) + })?; + } + } + return Ok(()); + } + let detail = format!( + "The runtime wrapper and its adjacent runtime.node must both be non-empty files; checked '{}' and '{}'", + wrapper.display(), + runtime_node.display() + ); + Err(Error::with_message( + ErrorKind::BinaryNotFound { + name: runtime_binary_name().into(), + hint: Some(detail.clone()), + }, + detail, + )) +} + +fn cli_binary_name() -> &'static str { + if cfg!(windows) { + "copilot.exe" + } else { + "copilot" + } +} + +fn runtime_binary_name() -> &'static str { + if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + } +} + /// Replace characters outside `[a-zA-Z0-9._-]` with `_`. Kept in sync /// with `build.rs::sanitize_version` and `embeddedcli::sanitize_version` /// so all three resolve to the same cache directory for any given @@ -143,3 +234,29 @@ fn sanitize_version(version: &str) -> String { }) .collect() } + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::validate_runtime_pair; + + #[test] + fn runtime_pair_requires_adjacent_nonempty_runtime_node() { + let dir = tempdir().expect("temp dir"); + let wrapper = dir.path().join(if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + }); + fs::write(&wrapper, b"wrapper").expect("write wrapper"); + + let error = validate_runtime_pair(&wrapper).expect_err("runtime.node is required"); + assert!(error.to_string().contains("runtime.node")); + + fs::write(dir.path().join("runtime.node"), b"runtime").expect("write runtime.node"); + validate_runtime_pair(&wrapper).expect("complete pair is valid"); + } +} diff --git a/rust/src/router.rs b/rust/src/router.rs index 1dec9d16f3..4815d0e1c5 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -85,6 +85,9 @@ impl SessionRouter { &self, notification_tx: &broadcast::Sender, request_rx: &Mutex>>, + extension_launch_provider: Arc< + crate::extension_launch_provider::ExtensionLaunchProviderDispatcher, + >, llm_inference: Option>, github_telemetry: Option, github_token_registry: Arc, @@ -182,6 +185,10 @@ impl SessionRouter { let sessions = self.sessions.clone(); tokio::spawn(async move { while let Some(request) = rx.recv().await { + if request.method == crate::extension_launch_provider::RESOLVE_METHOD { + extension_launch_provider.dispatch(request).await; + continue; + } if request.method == "gitHubToken.getToken" { github_token_registry.dispatch(request).await; continue; diff --git a/rust/src/session.rs b/rust/src/session.rs index b9d2173055..65c2aff7c4 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1567,7 +1567,7 @@ fn spawn_event_loop( _ = shutdown.cancelled() => break, Some(notification) = notifications.recv() => { handle_notification( - &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, + &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, &shutdown, ).await; } Some(request) = requests.recv() => { @@ -1719,6 +1719,7 @@ async fn handle_notification( capabilities: &Arc>, open_canvases: &Arc>>, event_tx: &tokio::sync::broadcast::Sender, + shutdown: &CancellationToken, ) { let dispatch_start = Instant::now(); let event = notification.event.clone(); @@ -1856,6 +1857,7 @@ async fn handle_notification( }; let client = client.clone(); let sid = session_id.clone(); + let shutdown = shutdown.clone(); let data = permission_request_data( ¬ification.event.data, handlers.managed_settings_enabled, @@ -1885,18 +1887,39 @@ async fn handle_notification( return; }; let rpc_start = Instant::now(); - let _ = client - .call( - rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST, - Some(params), - ) - .await; - tracing::debug!( - elapsed_ms = rpc_start.elapsed().as_millis(), - session_id = %sid, - request_id = %request_id, - "Session::handle_notification response sent successfully" - ); + let method = + rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST; + tokio::select! { + biased; + response = client.call(method, Some(params)) => { + match response { + Ok(_) => tracing::debug!( + elapsed_ms = rpc_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + method, + "Session::handle_notification response sent successfully" + ), + Err(error) => warn!( + error = %error, + session_id = %sid, + request_id = %request_id, + method, + "failed to deliver permission decision back to the runtime" + ), + } + } + _ = shutdown.cancelled() => { + warn!( + elapsed_ms = rpc_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + method, + delivery_outcome = "unknown", + "permission confirmation acknowledgement wait cancelled during session shutdown" + ); + } + } } .instrument(span), ); diff --git a/rust/src/startup_timings.rs b/rust/src/startup_timings.rs index 7938a462b7..7784a6ab3b 100644 --- a/rust/src/startup_timings.rs +++ b/rust/src/startup_timings.rs @@ -38,7 +38,7 @@ use std::time::Duration; #[non_exhaustive] pub struct StartupTimings { /// Time spent in `resolve::copilot_binary_with_extract_dir` locating (and, - /// for a bundled CLI, extracting) the copilot binary. `None` when the + /// for bundled artifacts, extracting) the Copilot program. `None` when the /// caller passes an explicit [`CliProgram::Path`](crate::CliProgram::Path). pub program_resolve_ms: Option, /// Time spent spawning the CLI subprocess (`command.spawn()`). `None` for diff --git a/rust/src/types.rs b/rust/src/types.rs index 6e451eb452..ee3ac3df26 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -21,6 +21,8 @@ pub use crate::copilot_request_handler::{ CopilotWebSocketResponse, WebSocketTransform, forward_http, }; use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance}; +/// Routing tier for the `auto` model with Auto mode V2. +pub use crate::generated::session_events::AutoTier; use crate::generated::session_events::ReasoningSummary; /// Context window tier for models that support tiered context windows. pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig}; @@ -1417,6 +1419,16 @@ impl ProviderConfig { #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct CapiSessionOptions { + /// Routing tier, meaningful only with model `auto` (Auto mode V2). + /// Requires a runtime version that supports `capi.autoTier`. + /// + /// When omitted, the runtime chooses its default on create and preserves + /// the persisted or current tier on resume. An explicit tier overrides the + /// persisted tier on cold resume; the runtime rejects a conflicting tier + /// when resuming a session already resident in memory. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, + /// Whether to use WebSocket transport for CAPI Responses API calls. /// /// When `Some(false)`, the runtime uses HTTP Responses transport even if @@ -1432,6 +1444,12 @@ impl CapiSessionOptions { Self::default() } + /// Set the routing tier for the `auto` model (Auto mode V2). + pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self { + self.auto_tier = Some(auto_tier); + self + } + /// Set whether to use WebSocket transport for CAPI Responses API calls. pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self { self.enable_web_socket_responses = Some(enable); @@ -1851,6 +1869,18 @@ impl ManagedSettings { } } +/// Selects the model-facing shape of the built-in `ask_user` tool. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +#[non_exhaustive] +pub enum AskUserVariant { + /// Use the legacy user-input request flow. + #[default] + Legacy, + /// Use the elicitation request flow. + Elicitation, +} + /// Configuration for creating a new session via the `session.create` RPC. /// /// All fields are optional — the CLI applies sensible defaults. @@ -1924,6 +1954,11 @@ pub struct SessionConfig { pub streaming: Option, /// Custom system message configuration. pub system_message: Option, + /// Selects the model-facing shape of the built-in `ask_user` tool. + /// + /// When omitted, the runtime uses [`AskUserVariant::Legacy`]. To use + /// [`AskUserVariant::Elicitation`], also install an [`ElicitationHandler`]. + pub ask_user_variant: Option, /// Client-defined tool declarations to expose to the agent. pub tools: Option>, /// Canvas declarations this connection provides to the runtime. @@ -2141,6 +2176,12 @@ pub struct SessionConfig { /// each command appears as `/name` for the user to invoke and the /// associated [`CommandHandler`] is called when executed. pub commands: Option>, + /// Feature-flag values resolved by the host for this session. + /// + /// Re-supply these values through [`ResumeSessionConfig::feature_flags`] + /// when resuming after a CLI process restart. Set via + /// [`with_feature_flags`](Self::with_feature_flags). + pub feature_flags: Option>, /// ExP assignment ("flight") data injected by a trusted integrator, in /// the same JSON shape the Copilot CLI fetches from the experimentation /// service (`CopilotExpAssignmentResponse`). When supplied, the runtime @@ -2181,9 +2222,9 @@ pub struct SessionConfig { /// Optional MCP OAuth request handler. When set, the SDK can satisfy MCP /// server OAuth requests with host-acquired token data or cancellation. pub mcp_auth_handler: Option>, - /// Optional user-input handler. When `None`, - /// `requestUserInput: false` goes on the wire and the `ask_user` - /// tool is disabled. + /// Optional handler for the legacy question-and-answer `ask_user` variant. + /// When `None`, `requestUserInput: false` goes on the wire, so this client + /// cannot handle legacy user-input requests. pub user_input_handler: Option>, /// Optional exit-plan-mode handler. When `None`, /// `requestExitPlanMode: false` goes on the wire. @@ -2238,6 +2279,7 @@ impl std::fmt::Debug for SessionConfig { .field("context_tier", &self.context_tier) .field("streaming", &self.streaming) .field("system_message", &self.system_message) + .field("ask_user_variant", &self.ask_user_variant) .field("tools", &self.tools) .field("canvases", &self.canvases) .field( @@ -2318,6 +2360,7 @@ impl std::fmt::Debug for SessionConfig { &self.include_sub_agent_streaming_events, ) .field("commands", &self.commands) + .field("feature_flags", &self.feature_flags) .field("exp_assignments", &self.exp_assignments) .field("enable_managed_settings", &self.enable_managed_settings) .field("enable_experimental_mode", &self.enable_experimental_mode) @@ -2378,6 +2421,7 @@ impl Default for SessionConfig { context_tier: None, streaming: None, system_message: None, + ask_user_variant: None, tools: None, canvases: None, canvas_handler: None, @@ -2434,6 +2478,7 @@ impl Default for SessionConfig { cloud: None, include_sub_agent_streaming_events: None, commands: None, + feature_flags: None, exp_assignments: None, enable_managed_settings: None, managed_settings: None, @@ -2545,6 +2590,7 @@ impl SessionConfig { context_tier: self.context_tier, streaming: self.streaming, system_message: self.system_message, + ask_user_variant: self.ask_user_variant, tools: self.tools, canvases: wire_canvases, request_canvas_renderer: self.request_canvas_renderer, @@ -2608,6 +2654,7 @@ impl SessionConfig { include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, enable_github_telemetry_forwarding: None, commands: wire_commands, + feature_flags: self.feature_flags, exp_assignments: self.exp_assignments, enable_managed_settings: self.enable_managed_settings, is_experimental_mode: self.enable_experimental_mode, @@ -2656,13 +2703,19 @@ impl SessionConfig { self } - /// Install a [`UserInputHandler`]. Required for the `ask_user` tool - /// to be enabled. + /// Install a [`UserInputHandler`] for the legacy question-and-answer + /// `ask_user` variant. pub fn with_user_input_handler(mut self, handler: Arc) -> Self { self.user_input_handler = Some(handler); self } + /// Select the model-facing shape of the built-in `ask_user` tool. + pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self { + self.ask_user_variant = Some(variant); + self + } + /// Install an [`ExitPlanModeHandler`]. pub fn with_exit_plan_mode_handler(mut self, handler: Arc) -> Self { self.exit_plan_mode_handler = Some(handler); @@ -3241,6 +3294,12 @@ impl SessionConfig { self } + /// Set feature-flag values resolved by the host for this session. + pub fn with_feature_flags(mut self, feature_flags: HashMap) -> Self { + self.feature_flags = Some(feature_flags); + self + } + /// Inject ExP assignment ("flight") data for this session, in the same /// JSON shape the Copilot CLI fetches from the experimentation service /// (`CopilotExpAssignmentResponse`). The runtime feeds it into the same @@ -3304,6 +3363,11 @@ pub struct ResumeSessionConfig { /// Re-supply the system message so the agent retains workspace context /// across CLI process restarts. pub system_message: Option, + /// Selects the model-facing shape of the built-in `ask_user` tool on a cold resume. + /// + /// When omitted, the runtime uses [`AskUserVariant::Legacy`]. To use + /// [`AskUserVariant::Elicitation`], also install an [`ElicitationHandler`]. + pub ask_user_variant: Option, /// Client-defined tool declarations to re-supply on resume. pub tools: Option>, /// Canvas declarations this connection provides to the runtime. @@ -3464,6 +3528,10 @@ pub struct ResumeSessionConfig { /// [`SessionConfig::commands`] — commands are not persisted server-side, /// so the resume payload re-supplies the registration. pub commands: Option>, + /// Feature-flag values resolved by the host to apply on resume. + /// + /// See [`SessionConfig::feature_flags`]. + pub feature_flags: Option>, /// ExP assignment ("flight") data injected on resume. See /// [`SessionConfig::exp_assignments`]. Re-supply on resume so the runtime /// re-applies the assignments after a CLI process restart. Set via @@ -3547,6 +3615,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("context_tier", &self.context_tier) .field("streaming", &self.streaming) .field("system_message", &self.system_message) + .field("ask_user_variant", &self.ask_user_variant) .field("tools", &self.tools) .field("canvases", &self.canvases) .field( @@ -3627,6 +3696,7 @@ impl std::fmt::Debug for ResumeSessionConfig { &self.include_sub_agent_streaming_events, ) .field("commands", &self.commands) + .field("feature_flags", &self.feature_flags) .field("exp_assignments", &self.exp_assignments) .field("enable_managed_settings", &self.enable_managed_settings) .field("enable_experimental_mode", &self.enable_experimental_mode) @@ -3730,6 +3800,7 @@ impl ResumeSessionConfig { context_tier: self.context_tier, streaming: self.streaming, system_message: self.system_message, + ask_user_variant: self.ask_user_variant, tools: self.tools, canvases: wire_canvases, open_canvases: self.open_canvases, @@ -3793,6 +3864,7 @@ impl ResumeSessionConfig { include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, enable_github_telemetry_forwarding: None, commands: wire_commands, + feature_flags: self.feature_flags, exp_assignments: self.exp_assignments, enable_managed_settings: self.enable_managed_settings, is_experimental_mode: self.enable_experimental_mode, @@ -3836,6 +3908,7 @@ impl ResumeSessionConfig { context_tier: None, streaming: None, system_message: None, + ask_user_variant: None, tools: None, canvases: None, canvas_handler: None, @@ -3892,6 +3965,7 @@ impl ResumeSessionConfig { remote_session: None, include_sub_agent_streaming_events: None, commands: None, + feature_flags: None, exp_assignments: None, enable_managed_settings: None, managed_settings: None, @@ -3939,6 +4013,12 @@ impl ResumeSessionConfig { self } + /// Select the model-facing shape of the built-in `ask_user` tool on resume. + pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self { + self.ask_user_variant = Some(variant); + self + } + /// Install an [`ExitPlanModeHandler`] for the resumed session. pub fn with_exit_plan_mode_handler(mut self, handler: Arc) -> Self { self.exit_plan_mode_handler = Some(handler); @@ -4511,6 +4591,12 @@ impl ResumeSessionConfig { self } + /// Re-supply feature-flag values resolved by the host on resume. + pub fn with_feature_flags(mut self, feature_flags: HashMap) -> Self { + self.feature_flags = Some(feature_flags); + self + } + /// Inject ExP assignment ("flight") data on resume. See /// [`SessionConfig::with_exp_assignments`]. Re-supply the assignments on /// resume so the runtime re-applies them after a CLI process restart. @@ -5966,9 +6052,9 @@ mod tests { use super::{ AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, - AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState, - CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry, - ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, + AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, + ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, + ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, @@ -6239,6 +6325,8 @@ mod tests { assert!(!wire.request_auto_mode_switch); assert!(!wire.hooks); assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); } #[test] @@ -6255,6 +6343,8 @@ mod tests { assert!(!wire.request_auto_mode_switch); assert!(!wire.hooks); assert!(!wire.request_mcp_apps); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("askUserVariant").is_none()); } #[test] @@ -6407,6 +6497,46 @@ mod tests { assert!(empty_json.get("memory").is_none()); } + #[test] + fn feature_flags_serialize_on_create_and_resume() { + let feature_flags = HashMap::from([ + ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), + ("DISABLED_TEST_FLAG".to_string(), false), + ]); + let expected = serde_json::json!({ + "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, + "DISABLED_TEST_FLAG": false, + }); + + let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); + assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); + let (create_wire, _) = create_config + .into_wire(Some(SessionId::from("feature-flags-create"))) + .expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["featureFlags"], expected); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) + .with_feature_flags(feature_flags) + .into_wire() + .expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["featureFlags"], expected); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("feature-flags-create-unset"))) + .expect("no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("featureFlags").is_none()); + + let (unset_resume_wire, _) = + ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("featureFlags").is_none()); + } + fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { CopilotExpAssignmentResponse { features: vec!["copilot_exp_flag".to_string()], @@ -7210,6 +7340,56 @@ mod tests { let unset = CapiSessionOptions::new(); let wire_unset = serde_json::to_value(&unset).unwrap(); assert!(wire_unset.get("enableWebSocketResponses").is_none()); + assert!(wire_unset.get("autoTier").is_none()); + assert_eq!(wire_unset, json!({})); + } + + #[test] + fn capi_auto_tier_canonical_values_round_trip_and_forward() { + for (tier, value) in [ + (AutoTier::Efficiency, "efficiency"), + (AutoTier::Balance, "balance"), + (AutoTier::Intelligence, "intelligence"), + ] { + let exported: crate::AutoTier = tier.clone(); + let capi = CapiSessionOptions::new().with_auto_tier(exported); + assert_eq!(capi.auto_tier, Some(tier)); + assert_eq!( + serde_json::to_value(&capi).unwrap(), + json!({"autoTier": value}) + ); + assert_eq!( + serde_json::from_value::(json!({"autoTier": value})).unwrap(), + capi + ); + + let capi = capi.with_enable_web_socket_responses(false); + let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); + let (create, _) = SessionConfig::default() + .with_model("auto") + .with_capi(capi.clone()) + .into_wire(Some(SessionId::from("capi-create"))) + .unwrap(); + assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); + + let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(capi) + .into_wire() + .unwrap(); + assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); + } + } + + #[test] + 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 + ); + } + let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); + assert_eq!(capi.auto_tier, None); } #[test] diff --git a/rust/src/wire.rs b/rust/src/wire.rs index f7de33839c..75e17f4e9c 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -13,6 +13,7 @@ //! configs hold trait-object handlers, the wire structs hold only the //! plain data the runtime needs. +use std::collections::HashMap; use std::path::PathBuf; use indexmap::IndexMap; @@ -24,11 +25,11 @@ use crate::generated::api_types::{ }; use crate::generated::session_events::ReasoningSummary; use crate::types::{ - CanvasProviderIdentity, CapiSessionOptions, CloudSessionOptions, CustomAgentConfig, - DefaultAgentConfig, ExtensionInfo, GitHubMcpToolConfig, InfiniteSessionConfig, - LargeToolOutputConfig, McpServerConfig, MemoryConfiguration, NamedProviderConfig, - ProviderConfig, ProviderModelConfig, SessionId, SessionLimitsConfig, SystemMessageConfig, Tool, - ToolSearchConfig, + AskUserVariant, CanvasProviderIdentity, CapiSessionOptions, CloudSessionOptions, + CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, GitHubMcpToolConfig, + InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, MemoryConfiguration, + NamedProviderConfig, ProviderConfig, ProviderModelConfig, SessionId, SessionLimitsConfig, + SystemMessageConfig, Tool, ToolSearchConfig, }; /// Wire representation of a slash command (name + description only). The @@ -64,6 +65,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub system_message: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub ask_user_variant: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub canvases: Option>, @@ -189,6 +192,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub commands: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_managed_settings: Option, @@ -218,6 +223,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub system_message: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub ask_user_variant: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub canvases: Option>, @@ -347,6 +354,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_managed_settings: Option, diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 9b86b1367a..8ed40e7c76 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -3,11 +3,53 @@ #![allow(clippy::unwrap_used)] +use github_copilot_sdk::AutoTier; use github_copilot_sdk::rpc::{ Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest, }; -use github_copilot_sdk::session_events::{PermissionRequest, PermissionRequestedData}; +use github_copilot_sdk::session_events::{ + PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent, +}; + +#[test] +fn session_events_deserialize_auto_tier() { + for event_type in ["session.start", "session.resume"] { + for (tier, wire_tier) in [ + (Some(AutoTier::Efficiency), Some("efficiency")), + (Some(AutoTier::Balance), Some("balance")), + (Some(AutoTier::Intelligence), Some("intelligence")), + (None, None), + ] { + let mut wire = serde_json::json!({ + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": event_type, + "data": { + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1 + } + }); + if let Some(wire_tier) = wire_tier { + wire["data"]["autoTier"] = serde_json::json!(wire_tier); + } + let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); + let actual: Option = match event.payload { + SessionEventData::SessionStart(data) if event_type == "session.start" => { + data.auto_tier + } + SessionEventData::SessionResume(data) if event_type == "session.resume" => { + data.auto_tier + } + _ => panic!("expected {event_type}"), + }; + assert_eq!(actual, tier); + } + } +} #[test] fn extension_running_has_expected_status_and_source() { diff --git a/rust/tests/cli_resolution_test.rs b/rust/tests/cli_resolution_test.rs index 9e4927e676..75773a0c0d 100644 --- a/rust/tests/cli_resolution_test.rs +++ b/rust/tests/cli_resolution_test.rs @@ -10,7 +10,10 @@ use std::path::PathBuf; use github_copilot_sdk::{ CliProgram, Client, ClientOptions, ErrorKind, HAS_BUNDLED_CLI, install_bundled_cli, + install_bundled_runtime, }; +#[cfg(all(feature = "bundled-cli", has_bundled_cli))] +use github_copilot_sdk::{SessionConfig, Transport}; use serial_test::serial; fn unset_env(key: &str) { @@ -95,7 +98,7 @@ async fn stale_env_override_falls_through() { } } -/// With `bundled-cli` off, `build.rs` extracts the binary into the +/// With `bundled-cli` off, `build.rs` extracts the runtime wrapper into the /// per-user cache and the runtime resolver recomputes its location from /// `COPILOT_SDK_CLI_VERSION` + the OS-derived binary name. This test /// mirrors that convention and asserts the file is on disk where the @@ -105,9 +108,9 @@ async fn stale_env_override_falls_through() { fn extracted_binary_present_at_conventional_path() { let version = env!("COPILOT_SDK_CLI_VERSION"); let binary = if cfg!(windows) { - "copilot.exe" + "copilot-runtime.exe" } else { - "copilot" + "copilot-runtime" }; let sanitized = sanitize_version_for_test(version); let path = dirs::cache_dir() @@ -158,21 +161,19 @@ async fn unbundled_resolver_finds_extracted_binary() { /// With `bundled-cli` off, `COPILOT_CLI_EXTRACT_DIR` set at runtime /// redirects the resolver to look directly under the named directory /// (no per-version subdir, matching the build-time write semantics). -/// We place a fake `copilot[.exe]` there and assert the resolver picks -/// it up — failing here means the build-time / runtime convention has -/// drifted. #[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))] #[tokio::test(flavor = "current_thread")] #[serial(copilot_cli_path)] async fn extract_dir_runtime_override_is_honored() { let tmp = tempfile::tempdir().expect("create tempdir"); let binary = if cfg!(windows) { - "copilot.exe" + "copilot-runtime.exe" } else { - "copilot" + "copilot-runtime" }; let fake = tmp.path().join(binary); - std::fs::write(&fake, b"").expect("write fake binary"); + std::fs::write(&fake, b"runtime").expect("write fake binary"); + std::fs::write(tmp.path().join("runtime.node"), b"runtime").expect("write runtime.node"); unset_env("COPILOT_CLI_PATH"); set_env( @@ -265,6 +266,14 @@ fn install_bundled_cli_returns_extracted_path() { "install_bundled_cli returned a path that is not a file: {}", first.display() ); + assert_eq!( + first.file_name().and_then(|name| name.to_str()), + Some(if cfg!(windows) { + "copilot.exe" + } else { + "copilot" + }) + ); let second = install_bundled_cli().expect("second call should also succeed"); assert_eq!( @@ -293,30 +302,6 @@ fn install_bundled_cli_returns_extracted_path() { } } -/// `install_bundled_cli` returns the same path the runtime resolver -/// hands to `Client::start` for `CliProgram::Resolve` with no -/// `COPILOT_CLI_PATH` override. Observed indirectly: the binary the -/// public API points at must exist, and `Client::start` must not -/// report `BinaryNotFound` under the same env conditions. -#[cfg(all(feature = "bundled-cli", has_bundled_cli))] -#[tokio::test(flavor = "current_thread")] -#[serial(copilot_cli_path)] -async fn install_bundled_cli_matches_resolver() { - unset_env("COPILOT_CLI_PATH"); - unset_env("COPILOT_CLI_EXTRACT_DIR"); - - let direct = install_bundled_cli().expect("bundled CLI should install"); - assert!(direct.is_file()); - - let opts = ClientOptions::default().with_program(CliProgram::Resolve); - if let Err(e) = Client::start(opts).await { - assert!( - !matches!(e.kind(), ErrorKind::BinaryNotFound { .. }), - "resolver returned BinaryNotFound while install_bundled_cli succeeded: {e}" - ); - } -} - /// With `bundled-cli` off (or the target unsupported), the public API /// reports no bundled CLI and does not fall back to the /// build-time-extracted dev-cache path that `CliProgram::Resolve` uses. @@ -329,3 +314,97 @@ fn install_bundled_cli_is_none_without_embed() { "install_bundled_cli must not fall back to the dev-cache path" ); } + +#[cfg(all(feature = "bundled-cli", has_bundled_cli))] +#[test] +fn install_bundled_runtime_returns_wrapper_bundle() { + let first = install_bundled_runtime().expect("bundled runtime should install"); + assert_eq!( + first.file_name().and_then(|name| name.to_str()), + Some(if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + }) + ); + let runtime_node = first + .parent() + .expect("install directory") + .join("runtime.node"); + assert!( + runtime_node.is_file(), + "runtime.node was not installed: {}", + runtime_node.display() + ); + let second = install_bundled_runtime().expect("second call should also succeed"); + assert_eq!(first, second); +} + +#[cfg(all(feature = "bundled-cli", has_bundled_cli))] +#[tokio::test(flavor = "current_thread")] +#[serial(copilot_cli_path)] +async fn bundled_runtime_clean_extract_starts_without_cli_host() { + let temp = tempfile::tempdir().expect("create tempdir"); + let extract_dir = temp.path().join("runtime"); + let empty_path = temp.path().join("empty-path"); + let working_dir = temp.path().join("work"); + std::fs::create_dir(&empty_path).expect("create empty PATH directory"); + std::fs::create_dir(&working_dir).expect("create working directory"); + assert!(!extract_dir.exists()); + + let options = ClientOptions::new() + .with_bundled_cli_extract_dir(&extract_dir) + .with_cwd(&working_dir) + .with_env([("PATH", empty_path.as_os_str())]) + .with_env_remove([ + "COPILOT_RUNTIME_HOST_COMMAND", + "COPILOT_CLI_PATH", + "COPILOT_RUNTIME_PROVIDER_LIB", + ]) + .with_transport(Transport::Stdio) + .with_use_logged_in_user(false); + let client = Client::start(options) + .await + .expect("start bundled runtime from clean extraction"); + let response = client + .ping(Some("hostless runtime")) + .await + .expect("ping bundled runtime"); + assert_eq!(response.message, "pong: hostless runtime"); + + let session = client + .create_session(SessionConfig::default()) + .await + .expect("create session"); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop bundled runtime"); + + assert!(extract_dir.join("runtime.node").is_file()); + assert!( + extract_dir + .join(if cfg!(windows) { + "copilot-runtime.exe" + } else { + "copilot-runtime" + }) + .is_file() + ); + assert!( + !extract_dir + .join(if cfg!(windows) { + "copilot.exe" + } else { + "copilot" + }) + .exists() + ); +} + +#[cfg(not(all(feature = "bundled-cli", has_bundled_cli)))] +#[test] +fn install_bundled_runtime_is_none_without_embed() { + assert!( + install_bundled_runtime().is_none(), + "install_bundled_runtime must not fall back to the dev-cache path" + ); +} diff --git a/rust/tests/e2e/canvas.rs b/rust/tests/e2e/canvas.rs index 2418e9e5a6..a2873eed23 100644 --- a/rust/tests/e2e/canvas.rs +++ b/rust/tests/e2e/canvas.rs @@ -64,7 +64,6 @@ fn canvas_session_config( ctx.approve_all_session_config() .with_request_canvas_renderer(true) - .with_request_extensions(true) .with_extension_info(ExtensionInfo::new("rust-sdk-tests", "canvas-provider")) .with_canvases([decl]) .with_canvas_handler(handler) diff --git a/rust/tests/e2e/client.rs b/rust/tests/e2e/client.rs index 880c1a28c4..3abedce1a8 100644 --- a/rust/tests/e2e/client.rs +++ b/rust/tests/e2e/client.rs @@ -120,7 +120,7 @@ async fn should_list_models_when_authenticated() { let models = client.list_models().await.expect("list models"); assert!( - models.iter().any(|model| model.id == "claude-sonnet-4.5"), + models.iter().any(|model| model.id == "claude-sonnet-5"), "expected default replay model in {models:?}" ); diff --git a/rust/tests/e2e/client_lifecycle.rs b/rust/tests/e2e/client_lifecycle.rs index 75646b4860..92bfa6ff6d 100644 --- a/rust/tests/e2e/client_lifecycle.rs +++ b/rust/tests/e2e/client_lifecycle.rs @@ -1,3 +1,5 @@ +#[cfg(windows)] +use github_copilot_sdk::CliProgram; use github_copilot_sdk::SessionLifecycleEventType; use serde_json::json; @@ -135,6 +137,151 @@ async fn dispose_disconnects_client_and_disposes_rpc_surface_drop() { .await; } +// This test represents github/app#2303: the SDK-hosting GitHub Copilot app +// process exits abruptly, so Client cleanup never runs. The helper starts a +// real CLI client, is terminated through `TerminateProcess`, and relies only +// on Job Object kill-on-close behavior to terminate the CLI. +#[cfg(windows)] +#[tokio::test] +async fn abrupt_host_termination_still_kills_cli_via_job_object() { + with_e2e_context( + "client_lifecycle", + "abrupt_host_termination_still_kills_cli_via_job_object", + |ctx| { + Box::pin(async move { + let options = ctx.client_options(); + let program = match &options.program { + CliProgram::Path(path) => path + .to_str() + .expect("CLI program path is valid UTF-8") + .to_owned(), + CliProgram::Resolve => { + panic!("E2E client options should resolve to an explicit CLI path") + } + }; + let prefix_args: Vec = options + .prefix_args + .iter() + .map(|arg| arg.to_str().expect("prefix arg is valid UTF-8").to_owned()) + .collect(); + let env_pairs: Vec<(String, String)> = options + .env + .iter() + .map(|(k, v)| { + ( + k.to_str().expect("env key is valid UTF-8").to_owned(), + v.to_str().expect("env value is valid UTF-8").to_owned(), + ) + }) + .collect(); + let cwd = options + .working_directory + .to_str() + .expect("cwd is valid UTF-8") + .to_owned(); + let pid_file = ctx.work_dir().join("host-crash-fixture-cli.pid"); + + let mut host = + std::process::Command::new(env!("CARGO_BIN_EXE_copilot-host-crash-fixture")) + .env("HOST_CRASH_FIXTURE_PROGRAM", &program) + .env( + "HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON", + serde_json::to_string(&prefix_args).expect("serialize prefix args"), + ) + .env("HOST_CRASH_FIXTURE_CWD", &cwd) + .env( + "HOST_CRASH_FIXTURE_ENV_JSON", + serde_json::to_string(&env_pairs).expect("serialize env pairs"), + ) + .env("HOST_CRASH_FIXTURE_PID_FILE", &pid_file) + .spawn() + .expect("spawn host-crash fixture process"); + + let cli_pid = wait_for_pid_file_windows(&pid_file).await; + assert!( + process_alive_windows(cli_pid), + "CLI should be alive before its host process is terminated" + ); + + // `Child::kill` maps to `TerminateProcess`, which runs none + // of the target process's cleanup code. + host.kill().expect("terminate host-crash fixture process"); + host.wait().expect("reap host-crash fixture process"); + + let cli_exited = wait_for_process_exit_windows(cli_pid).await; + if !cli_exited { + kill_process_windows(cli_pid); + } + assert!( + cli_exited, + "CLI survived its abruptly terminated host process; Job Object \ + kill-on-close did not terminate it" + ); + }) + }, + ) + .await; +} + +#[cfg(windows)] +async fn wait_for_pid_file_windows(path: &std::path::Path) -> u32 { + super::support::wait_for_condition("host-crash fixture CLI pid file", || async { + path.exists() + }) + .await; + std::fs::read_to_string(path) + .expect("read host-crash fixture CLI pid") + .trim() + .parse() + .expect("parse host-crash fixture CLI pid") +} + +#[cfg(windows)] +async fn wait_for_process_exit_windows(pid: u32) -> bool { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15); + while process_alive_windows(pid) { + if std::time::Instant::now() >= deadline { + return false; + } + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + } + true +} + +#[cfg(windows)] +fn process_alive_windows(pid: u32) -> bool { + use windows_sys::Win32::Foundation::{CloseHandle, WAIT_TIMEOUT}; + use windows_sys::Win32::System::Threading::{ + OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject, + }; + + // SAFETY: the process handle is closed before returning. + unsafe { + let process = OpenProcess(PROCESS_SYNCHRONIZE, 0, pid); + if process.is_null() { + return false; + } + let alive = WaitForSingleObject(process, 0) == WAIT_TIMEOUT; + CloseHandle(process); + alive + } +} + +#[cfg(windows)] +fn kill_process_windows(pid: u32) { + use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess}; + + // SAFETY: the pid came from this test's controlled fixture-spawned CLI. + unsafe { + let process = OpenProcess(PROCESS_TERMINATE, 0, pid); + if !process.is_null() { + TerminateProcess(process, 1); + CloseHandle(process); + } + } +} + #[tokio::test] async fn should_receive_session_updated_lifecycle_event_for_non_ephemeral_activity() { with_e2e_context( diff --git a/rust/tests/e2e/client_options.rs b/rust/tests/e2e/client_options.rs index fc1ceebb83..fa8e6f68f7 100644 --- a/rust/tests/e2e/client_options.rs +++ b/rust/tests/e2e/client_options.rs @@ -27,7 +27,7 @@ async fn should_forward_advanced_session_creation_options_to_the_cli() { SessionConfig::default() .with_session_id("advanced-session-id") .with_client_name("rust-sdk-e2e-client") - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_reasoning_effort("low") .with_reasoning_summary(ReasoningSummary::None) .with_context_tier("long_context") @@ -90,7 +90,7 @@ async fn should_forward_advanced_session_creation_options_to_the_cli() { [ ("sessionId", json!("advanced-session-id")), ("clientName", json!("rust-sdk-e2e-client")), - ("model", json!("claude-sonnet-4.5")), + ("model", json!("claude-sonnet-5")), ("reasoningEffort", json!("low")), ("reasoningSummary", json!("none")), ("contextTier", json!("long_context")), diff --git a/rust/tests/e2e/commands.rs b/rust/tests/e2e/commands.rs index 2e15c24a0f..78c8e07616 100644 --- a/rust/tests/e2e/commands.rs +++ b/rust/tests/e2e/commands.rs @@ -210,6 +210,7 @@ async fn session_commands_enqueue_and_respond_to_queued_command() { .commands() .enqueue(EnqueueCommandParams { command: "/help".to_string(), + display_text: None, }) .await .expect("enqueue command"); diff --git a/rust/tests/e2e/copilot_request_handler.rs b/rust/tests/e2e/copilot_request_handler.rs index 46b4e510cd..478845f48e 100644 --- a/rust/tests/e2e/copilot_request_handler.rs +++ b/rust/tests/e2e/copilot_request_handler.rs @@ -101,8 +101,8 @@ fn sse(event_type: &str, data: &Value) -> String { fn model_catalog(supported_endpoints: Option<&[&str]>) -> String { let mut model = json!({ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -110,7 +110,7 @@ fn model_catalog(supported_endpoints: Option<&[&str]>) -> String { "model_picker_enabled": true, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": { "max_context_window_tokens": 200000, @@ -232,7 +232,7 @@ fn synth_inference_response(url: &str, body: &[u8], text: &str) -> CopilotHttpRe "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", }) }; let mut c1 = base(); @@ -257,7 +257,7 @@ fn synth_inference_response(url: &str, body: &[u8], text: &str) -> CopilotHttpRe "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [{ "index": 0, "message": { "role": "assistant", "content": text }, @@ -668,14 +668,14 @@ async fn threads_session_id_into_inference() { let before = handler.inference_records().len(); let byok_config = SessionConfig::default() .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_provider( ProviderConfig::new("https://byok.invalid/v1") .with_provider_type("openai") .with_wire_api("responses") .with_api_key("byok-secret") - .with_model_id("claude-sonnet-4.5") - .with_wire_model("claude-sonnet-4.5"), + .with_model_id("claude-sonnet-5") + .with_wire_model("claude-sonnet-5"), ); let byok_session = client .create_session(byok_config) diff --git a/rust/tests/e2e/rewind.rs b/rust/tests/e2e/rewind.rs index c998f96df6..485c389ece 100644 --- a/rust/tests/e2e/rewind.rs +++ b/rust/tests/e2e/rewind.rs @@ -9,15 +9,12 @@ use github_copilot_sdk::rpc::{ use super::support::assistant_message_content; const FILE_NAME: &str = "rewind-sdk.txt"; +const ORIGINAL_FILE_CONTENT: &str = "Original rewind content"; +const PREPARED_FILE_CONTENT: &str = "Prepared rewind content"; const FILE_CONTENT: &str = "SDK rewind content"; #[tokio::test] async fn should_restore_tracked_file_and_conversation() { - // TODO(cli-1.0.81): Re-enable when Windows file-change tracking records built-in create tool writes. - if cfg!(windows) { - return; - } - super::support::with_shared_e2e_context( &E2E, "rewind", @@ -26,21 +23,37 @@ async fn should_restore_tracked_file_and_conversation() { Box::pin(async move { ctx.set_default_copilot_user(); let file_path = ctx.work_dir().join(FILE_NAME); + std::fs::write(&file_path, ORIGINAL_FILE_CONTENT).expect("write original file"); let client = ctx.start_client().await; let session = client .create_session( ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_file_change_tracking(true), ) .await .expect("create session"); + let ready = session + .send_and_wait(format!( + "Use the edit tool to replace the exact contents of {FILE_NAME} from \ + {ORIGINAL_FILE_CONTENT} to {PREPARED_FILE_CONTENT}. After the tool \ + succeeds, reply with exactly SDK_REWIND_READY." + )) + .await + .expect("send readiness turn") + .expect("readiness response"); + assert_eq!(assistant_message_content(&ready), "SDK_REWIND_READY"); + assert_eq!( + std::fs::read_to_string(&file_path).expect("read prepared file"), + PREPARED_FILE_CONTENT + ); + let response = session .send_and_wait(format!( - "Use the create tool to create {FILE_NAME} containing exactly \ - {FILE_CONTENT}. After the tool succeeds, reply with exactly \ - SDK_REWIND_DONE." + "Use the edit tool to replace the exact contents of {FILE_NAME} from \ + {PREPARED_FILE_CONTENT} to {FILE_CONTENT}. After the tool succeeds, \ + reply with exactly SDK_REWIND_DONE." )) .await .expect("send rewind setup prompt") @@ -53,8 +66,9 @@ async fn should_restore_tracked_file_and_conversation() { let rewind_points = wait_for_rewind_points(&session).await; assert!(rewind_points.file_change_tracking_enabled); - assert_eq!(rewind_points.points.len(), 1); - let rewind_point = &rewind_points.points[0]; + assert_eq!(rewind_points.points.len(), 2); + let rewind_point = &rewind_points.points[1]; + assert!(rewind_point.turn_changed_files); assert!(rewind_point.can_restore_files); assert_eq!(rewind_point.file_count, 1); @@ -83,7 +97,10 @@ async fn should_restore_tracked_file_and_conversation() { assert!(rewind.events_removed.is_some_and(|count| count > 0)); assert_eq!(rewind.restored_files.len(), 1); assert_same_path(&file_path, Path::new(&rewind.restored_files[0])); - assert!(!file_path.exists()); + assert_eq!( + std::fs::read_to_string(&file_path).expect("read restored file"), + PREPARED_FILE_CONTENT + ); let events = session.get_events().await.expect("get events after rewind"); assert!(events.iter().all(|event| event.id != rewind_point.event_id)); @@ -108,10 +125,10 @@ async fn wait_for_rewind_points( .await .expect("list rewind points"); if result.unavailable_reason.is_none() - && result - .points - .first() - .is_some_and(|point| point.can_restore_files && point.file_count == 1) + && result.points.len() == 2 + && result.points[1].turn_changed_files + && result.points[1].can_restore_files + && result.points[1].file_count == 1 { return result; } diff --git a/rust/tests/e2e/rpc_queue.rs b/rust/tests/e2e/rpc_queue.rs index 6f4f881659..d5f1228302 100644 --- a/rust/tests/e2e/rpc_queue.rs +++ b/rust/tests/e2e/rpc_queue.rs @@ -153,6 +153,7 @@ async fn pendingitems_reports_queued_command_and_remove_and_clear_update_queue() .commands() .enqueue(EnqueueCommandParams { command: first_command, + display_text: None, }) .await .expect("enqueue command"); @@ -167,6 +168,7 @@ async fn pendingitems_reports_queued_command_and_remove_and_clear_update_queue() .commands() .enqueue(EnqueueCommandParams { command: second_command.clone(), + display_text: None, }) .await .expect("enqueue second command"); @@ -187,6 +189,7 @@ async fn pendingitems_reports_queued_command_and_remove_and_clear_update_queue() .commands() .enqueue(EnqueueCommandParams { command: third_command.clone(), + display_text: None, }) .await .expect("enqueue third command"); diff --git a/rust/tests/e2e/rpc_server.rs b/rust/tests/e2e/rpc_server.rs index 8df662ea8e..2e80ae1d70 100644 --- a/rust/tests/e2e/rpc_server.rs +++ b/rust/tests/e2e/rpc_server.rs @@ -70,7 +70,7 @@ async fn should_call_rpc_models_list_with_typed_result() { result .models .iter() - .any(|model| model.id == "claude-sonnet-4.5") + .any(|model| model.id == "claude-sonnet-5") ); assert!(result.models.iter().all(|model| !model.name.is_empty())); client.stop().await.expect("stop client"); diff --git a/rust/tests/e2e/rpc_session_state.rs b/rust/tests/e2e/rpc_session_state.rs index aa67312473..e6d6a2b546 100644 --- a/rust/tests/e2e/rpc_session_state.rs +++ b/rust/tests/e2e/rpc_session_state.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use std::sync::Arc; +use async_trait::async_trait; use github_copilot_sdk::rpc::{ AuthInfoType, HistoryTruncateRequest, LspInitializeRequest, MetadataContextInfoRequest, MetadataRecomputeContextTokensRequest, MetadataRecordContextChangeRequest, @@ -16,10 +18,16 @@ use github_copilot_sdk::session_events::{ SessionTitleChangedData, SessionWorkspaceFileChangedData, ShutdownType, WorkspaceFileChangedOperation, }; +use github_copilot_sdk::tool::ToolHandler; +use github_copilot_sdk::{Error, Tool, ToolInvocation, ToolResult}; +use serde_json::json; +use tokio::sync::{Mutex, mpsc, oneshot}; -use super::support::{assistant_message_content, wait_for_condition, wait_for_event}; +use super::support::{ + assistant_message_content, recv_with_timeout, wait_for_condition, wait_for_event, +}; -const MODEL_ID: &str = "claude-sonnet-4.5"; +const MODEL_ID: &str = "claude-sonnet-5"; #[tokio::test] async fn should_call_session_rpc_model_getcurrent() { @@ -1111,8 +1119,28 @@ async fn should_report_processing_and_context_metadata() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; + let (entered_tx, mut entered_rx) = mpsc::unbounded_channel(); + let (release_tx, release_rx) = oneshot::channel(); let session = client - .create_session(ctx.approve_all_session_config().with_model(MODEL_ID)) + .create_session( + ctx.approve_all_session_config() + .with_model(MODEL_ID) + .with_tools(vec![ + Tool::new("processing_barrier") + .with_description( + "Blocks until the processing state has been observed", + ) + .with_parameters(json!({ + "type": "object", + "properties": {}, + "additionalProperties": false + })) + .with_handler(Arc::new(ProcessingBarrierTool { + entered_tx, + release_rx: Mutex::new(Some(release_rx)), + })), + ]), + ) .await .expect("create session"); @@ -1126,19 +1154,20 @@ async fn should_report_processing_and_context_metadata() { .processing ); session - .send("Reply with exactly: RUST_CONTEXT_INFO") + .send("Use processing_barrier, then reply with exactly: RUST_CONTEXT_INFO") .await .expect("send"); - wait_for_condition("session processing started", || async { + recv_with_timeout(&mut entered_rx, "processing barrier entry").await; + assert!( session .rpc() .metadata() .is_processing() .await - .expect("processing poll") + .expect("processing during tool call") .processing - }) - .await; + ); + release_tx.send(()).expect("release processing barrier"); wait_for_condition("session processing completed", || async { !session .rpc() @@ -1180,6 +1209,26 @@ async fn should_report_processing_and_context_metadata() { .await; } +struct ProcessingBarrierTool { + entered_tx: mpsc::UnboundedSender<()>, + release_rx: Mutex>>, +} + +#[async_trait] +impl ToolHandler for ProcessingBarrierTool { + async fn call(&self, _invocation: ToolInvocation) -> Result { + let _ = self.entered_tx.send(()); + self.release_rx + .lock() + .await + .take() + .expect("processing barrier called once") + .await + .expect("processing barrier released"); + Ok(ToolResult::Text("PROCESSING_OBSERVED".to_string())) + } +} + fn expect_err_contains(result: Result, expected: &str) { let err = match result { Ok(_) => panic!("expected error containing {expected:?}"), diff --git a/rust/tests/e2e/rpc_session_state_extras.rs b/rust/tests/e2e/rpc_session_state_extras.rs index 81901d06ae..e764c53933 100644 --- a/rust/tests/e2e/rpc_session_state_extras.rs +++ b/rust/tests/e2e/rpc_session_state_extras.rs @@ -12,7 +12,7 @@ use github_copilot_sdk::session_events::PermissionMode; use super::support::{assistant_message_content, with_e2e_context}; -const MODEL_ID: &str = "claude-sonnet-4.5"; +const MODEL_ID: &str = "claude-sonnet-5"; #[tokio::test] async fn should_list_models_for_session() { @@ -495,6 +495,7 @@ async fn should_update_and_clear_live_subagent_settings() { ), effort_level: Some("low".to_string()), model: Some("gpt-5-mini".to_string()), + model_policy: None, }, )])), disabled_subagents: Some(vec!["legacy-agent".to_string()]), diff --git a/rust/tests/e2e/session.rs b/rust/tests/e2e/session.rs index e2ca76c478..04c99422ea 100644 --- a/rust/tests/e2e/session.rs +++ b/rust/tests/e2e/session.rs @@ -37,7 +37,7 @@ async fn shouldcreateanddisconnectsessions() { let session = client .create_session( ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5"), + .with_model("claude-sonnet-5"), ) .await .expect("create session"); @@ -501,8 +501,7 @@ async fn should_abort_a_session() { #[tokio::test] async fn should_resume_a_session_using_the_same_client() { - super::support::with_shared_e2e_context( - &E2E, + super::support::with_dedicated_e2e_context( "session", "should_resume_a_session_using_the_same_client", |ctx| { @@ -1752,4 +1751,4 @@ fn secret_number_tool() -> Tool { .with_handler(Arc::new(SecretNumberTool)) } static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("session", 30); + super::support::SharedE2eGroup::standard("session", 29); diff --git a/rust/tests/e2e/session_config.rs b/rust/tests/e2e/session_config.rs index c3f6b57aea..2c844a8415 100644 --- a/rust/tests/e2e/session_config.rs +++ b/rust/tests/e2e/session_config.rs @@ -362,7 +362,7 @@ fn anthropic_message_stream_body(text: &str) -> String { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [], "stop_reason": null, "stop_sequence": null, @@ -414,8 +414,8 @@ fn synth_non_inference_response(url: &str) -> CopilotHttpResponse { json_headers(), json!({ "data": [{ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -423,7 +423,7 @@ fn synth_non_inference_response(url: &str) -> CopilotHttpResponse { "model_picker_enabled": true, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": { "max_context_window_tokens": 200000, @@ -459,7 +459,7 @@ fn synth_inference_response(url: &str, body: &[u8]) -> CopilotHttpResponse { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [{ "type": "text", "text": SYNTHETIC_TEXT }], "stop_reason": "end_turn", "stop_sequence": null, @@ -474,7 +474,7 @@ fn synth_inference_response(url: &str, body: &[u8]) -> CopilotHttpResponse { "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [{ "index": 0, "message": { "role": "assistant", "content": SYNTHETIC_TEXT }, @@ -489,8 +489,8 @@ fn anthropic_provider() -> ProviderConfig { ProviderConfig::new("https://anthropic-citations.invalid/v1") .with_provider_type("anthropic") .with_api_key("test-provider-key") - .with_model_id("claude-sonnet-4.5") - .with_wire_model("claude-sonnet-4.5") + .with_model_id("claude-sonnet-5") + .with_wire_model("claude-sonnet-5") } fn pdf_attachment() -> Attachment { @@ -532,7 +532,7 @@ async fn should_enable_citations_for_anthropic_file_attachments_on_create() { .create_session( SessionConfig::default() .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_citations(true) .with_provider(anthropic_provider()), ) @@ -592,7 +592,7 @@ async fn should_enable_citations_for_anthropic_file_attachments_on_resume() { .resume_session( ResumeSessionConfig::new(session1.id().clone()) .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_citations(true) .with_provider(anthropic_provider()), ) diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index 76f9a21c8f..42accf2ec7 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -31,6 +31,7 @@ static SHARED_E2E_RUNTIME: LazyLock = LazyLock::new(|| .expect("create shared E2E runtime") }); const SHARED_E2E_CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +const PROXY_STARTUP_TIMEOUT: Duration = Duration::from_secs(30); pub const DEFAULT_TEST_TOKEN: &str = "rust-e2e-token"; @@ -510,12 +511,8 @@ impl E2eContext { .expect("start E2E client") } - /// Start a client that hosts the runtime in-process over FFI - /// ([`Transport::InProcess`]). Unlike the stdio harness, the CLI - /// entrypoint is passed as the program directly (the FFI host builds the - /// `node --embedded-host` argv itself and loads the sibling - /// runtime cdylib), so a `.js` entrypoint is not split into node + - /// prefix_args here. + /// Start a client that hosts the bundled runtime directly in-process over + /// FFI ([`Transport::InProcess`]). #[cfg_attr(not(feature = "bundled-in-process"), allow(dead_code))] pub async fn start_inprocess_client(&self) -> Client { let options = ClientOptions::new().with_transport(Transport::InProcess); @@ -1070,7 +1067,8 @@ impl InProcessEnvGuard { pairs.push(("COPILOT_SDK_AUTH_TOKEN".into(), "".into())); pairs.push(( "COPILOT_CLI_PATH".into(), - ctx.cli_path.clone().into_os_string(), + std::env::var_os("COPILOT_CLI_PATH") + .unwrap_or_else(|| ctx.cli_path.clone().into_os_string()), )); // Some tests opt into gated runtime APIs via per-client `options.env`, which the // in-process transport does not pass to the shared native runtime (see issue #1934). @@ -1277,7 +1275,7 @@ impl CapiProxy { } }); let re = regex::Regex::new(r"Listening: (http://[^\s]+)\s+(\{.*\})$").unwrap(); - let deadline = Instant::now() + SHARED_E2E_CLEANUP_TIMEOUT; + let deadline = Instant::now() + PROXY_STARTUP_TIMEOUT; while let Some(remaining) = deadline.checked_duration_since(Instant::now()) { let line = match line_rx.recv_timeout(remaining) { Ok(Ok(line)) => line, @@ -1344,7 +1342,7 @@ impl CapiProxy { kill_and_wait_child(&mut child); Err(std::io::Error::other(format!( - "timed out after {SHARED_E2E_CLEANUP_TIMEOUT:?} waiting for proxy startup" + "timed out after {PROXY_STARTUP_TIMEOUT:?} waiting for proxy startup" ))) } diff --git a/rust/tests/extension_launch_provider_runtime_test.rs b/rust/tests/extension_launch_provider_runtime_test.rs new file mode 100644 index 0000000000..f08d6af7f0 --- /dev/null +++ b/rust/tests/extension_launch_provider_runtime_test.rs @@ -0,0 +1,321 @@ +//! Real-boundary coverage for hostless extension discovery and lifecycle. +//! +//! Router tests can prove provider dispatch without exercising runtime session +//! setup, while an empty extension-list smoke test never requires the runtime +//! to install extension services. This test requires both boundaries and is +//! ignored until a real wrapper/runtime.node pair is supplied explicitly. + +#![cfg(unix)] +#![allow(clippy::unwrap_used)] + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use async_trait::async_trait; +use github_copilot_sdk::extension_launch_provider::{ + ExtensionLaunchProfile, ExtensionLaunchProvider, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::rpc::{ + ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, + ToolResult, ToolResultType, ToolsExecuteRequest, +}; +use github_copilot_sdk::{CliProgram, Client, ClientOptions, SessionConfig, Transport}; +use serde_json::{Value, json}; +use tokio::sync::mpsc; +use tokio::time::{sleep, timeout}; + +const TEST_TIMEOUT: Duration = Duration::from_secs(15); +const RUNTIME_PATH_ENV: &str = "COPILOT_RUNTIME_E2E_PATH"; + +struct RecordingProvider { + executable: PathBuf, + state_path: PathBuf, + requests: mpsc::UnboundedSender, +} + +#[async_trait] +impl ExtensionLaunchProvider for RecordingProvider { + async fn resolve( + &self, + request: ExtensionLaunchProviderResolveRequest, + ) -> github_copilot_sdk::Result { + self.requests.send(request.clone()).unwrap(); + Ok(ExtensionLaunchProviderResolveResult { + launch: Some(ExtensionLaunchProfile { + executable: self.executable.to_string_lossy().into_owned(), + args: Vec::new(), + env: HashMap::from([ + ("EXTENSION_PATH".to_string(), request.module_path.clone()), + ( + "FIXTURE_STATE_PATH".to_string(), + self.state_path.to_string_lossy().into_owned(), + ), + ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()), + ( + "COPILOT_SDK_PATH".to_string(), + "provider-must-not-win".to_string(), + ), + ( + "SESSION_ID".to_string(), + "provider-must-not-win".to_string(), + ), + ( + "COPILOT_EXTENSION_PARENT_PID".to_string(), + "provider-must-not-win".to_string(), + ), + ]), + }), + }) + } +} + +#[tokio::test(flavor = "multi_thread")] +#[ignore = "requires COPILOT_RUNTIME_E2E_PATH to name a real wrapper/runtime.node pair"] +async fn real_wrapper_installs_and_runs_hostless_extensions() { + let runtime_path = std::env::var_os(RUNTIME_PATH_ENV) + .map(PathBuf::from) + .expect("COPILOT_RUNTIME_E2E_PATH must name a copilot-runtime executable"); + assert_runtime_pair(&runtime_path); + + let home = tempfile::tempdir().unwrap(); + let workspace = tempfile::tempdir().unwrap(); + let extension_dir = workspace + .path() + .join(".github") + .join("extensions") + .join("lifecycle"); + std::fs::create_dir_all(&extension_dir).unwrap(); + let module_path = extension_dir.join("extension.mjs"); + std::fs::write(&module_path, "export default {};\n").unwrap(); + let module_path = std::fs::canonicalize(module_path).unwrap(); + let sdk_path = home.path().join("extension-sdk"); + std::fs::create_dir_all(&sdk_path).unwrap(); + let sdk_path = std::fs::canonicalize(sdk_path).unwrap(); + let state_path = home.path().join("extension-state.jsonl"); + let (request_tx, mut request_rx) = mpsc::unbounded_channel(); + + let options = ClientOptions::new() + .with_program(CliProgram::Path(runtime_path)) + .with_transport(Transport::Stdio) + .with_cwd(workspace.path()) + .with_base_directory(home.path()) + .with_use_logged_in_user(false) + .with_env_remove(["COPILOT_CLI_DIST_DIR"]) + .with_extension_launch_provider(RecordingProvider { + executable: PathBuf::from(env!("CARGO_BIN_EXE_copilot-extension-test-fixture")), + state_path: state_path.clone(), + requests: request_tx, + }); + let client = Client::start(options).await.unwrap(); + let session = client + .create_session( + SessionConfig::default() + .with_request_extensions(true) + .with_extension_sdk_path(sdk_path.to_string_lossy()) + .with_permission_handler(Arc::new(ApproveAllHandler)), + ) + .await + .unwrap(); + + let first_request = recv_request(&mut request_rx).await; + assert_request(&first_request, &module_path); + let first_start = wait_for_starts(&state_path, 1).await.remove(0); + assert_runtime_owned_environment(&first_start, session.id(), &sdk_path, &module_path); + let first_pid = json_pid(&first_start); + let wrapper_pid = json_parent_pid(&first_start); + assert_eq!(direct_child_pids(wrapper_pid), vec![first_pid]); + + let listed = session.rpc().extensions().list().await.unwrap(); + assert_eq!(listed.extensions.len(), 1); + assert_eq!(listed.extensions[0].id, "project:lifecycle"); + assert_eq!(listed.extensions[0].status, ExtensionStatus::Running); + + let result = session + .rpc() + .tools() + .execute(ToolsExecuteRequest { + arguments: json!({ "text": "sdk-boundary" }), + name: "fixture_echo".to_string(), + tool_call_id: Some("fixture-call".to_string()), + }) + .await + .unwrap(); + match result { + ToolResult::String(value) => assert_eq!(value, "echoed"), + ToolResult::ToolResultExpanded(result) => { + assert_eq!(result.text_result_for_llm, "echoed"); + assert_eq!(result.result_type, ToolResultType::Success); + } + } + wait_for_invocations(&state_path, 1).await; + + session + .rpc() + .extensions() + .disable(ExtensionsDisableRequest { + id: "project:lifecycle".to_string(), + }) + .await + .unwrap(); + wait_for_process_exit(first_pid).await; + let disabled = session.rpc().extensions().list().await.unwrap(); + assert_eq!(disabled.extensions[0].status, ExtensionStatus::Disabled); + + session + .rpc() + .extensions() + .enable(ExtensionsEnableRequest { + id: "project:lifecycle".to_string(), + }) + .await + .unwrap(); + let second_request = recv_request(&mut request_rx).await; + assert_request(&second_request, &module_path); + let starts = wait_for_starts(&state_path, 2).await; + let second_pid = json_pid(&starts[1]); + assert_ne!(second_pid, first_pid); + assert!(process_exists(second_pid)); + assert_eq!(json_parent_pid(&starts[1]), wrapper_pid); + assert_eq!(direct_child_pids(wrapper_pid), vec![second_pid]); + let enabled = session.rpc().extensions().list().await.unwrap(); + assert_eq!(enabled.extensions[0].status, ExtensionStatus::Running); + + client.stop().await.unwrap(); + wait_for_process_exit(second_pid).await; + wait_for_process_exit(wrapper_pid).await; + assert!(request_rx.try_recv().is_err()); +} + +fn assert_runtime_pair(runtime_path: &Path) { + assert!(runtime_path.is_file(), "missing {}", runtime_path.display()); + let runtime_node = runtime_path.parent().unwrap().join("runtime.node"); + assert!(runtime_node.is_file(), "missing {}", runtime_node.display()); +} + +async fn recv_request( + requests: &mut mpsc::UnboundedReceiver, +) -> ExtensionLaunchProviderResolveRequest { + timeout(TEST_TIMEOUT, requests.recv()) + .await + .expect( + "runtime never invoked extensionLaunchProvider.resolve; hostless extension services \ + may not be installed", + ) + .expect("provider request channel closed") +} + +fn assert_request(request: &ExtensionLaunchProviderResolveRequest, module_path: &Path) { + assert_eq!(request.id, "project:lifecycle"); + assert_eq!(request.name, "lifecycle"); + assert_eq!(request.source, ExtensionSource::Project); + assert_eq!(Path::new(&request.module_path), module_path); +} + +fn read_state(path: &Path) -> Vec { + let Ok(contents) = std::fs::read_to_string(path) else { + return Vec::new(); + }; + contents + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect() +} + +async fn wait_for_starts(path: &Path, count: usize) -> Vec { + wait_for_state(path, "start", count).await +} + +async fn wait_for_invocations(path: &Path, count: usize) -> Vec { + wait_for_state(path, "invoke", count).await +} + +async fn wait_for_state(path: &Path, kind: &str, count: usize) -> Vec { + let deadline = Instant::now() + TEST_TIMEOUT; + loop { + let matching: Vec<_> = read_state(path) + .into_iter() + .filter(|entry| entry["kind"] == kind) + .collect(); + if matching.len() >= count { + return matching; + } + assert!( + Instant::now() < deadline, + "timed out waiting for {count} {kind} entries; state: {:?}", + read_state(path) + ); + sleep(Duration::from_millis(25)).await; + } +} + +fn assert_runtime_owned_environment( + start: &Value, + session_id: &str, + sdk_path: &Path, + module_path: &Path, +) { + assert_eq!(start["sessionId"], session_id); + assert_eq!( + start["sdkPath"].as_str(), + Some(sdk_path.to_string_lossy().as_ref()) + ); + assert_eq!( + start["extensionPath"].as_str(), + Some(module_path.to_string_lossy().as_ref()) + ); + assert_eq!(start["autoUpdate"], "false"); + assert!(start["cliDistDir"].is_null()); + let parent_pid = start["parentPid"].as_str().unwrap(); + assert_ne!(parent_pid, "provider-must-not-win"); + assert!(parent_pid.parse::().is_ok()); +} + +fn json_pid(value: &Value) -> u32 { + value["pid"].as_u64().unwrap() as u32 +} + +fn json_parent_pid(value: &Value) -> u32 { + value["parentPid"].as_str().unwrap().parse().unwrap() +} + +fn direct_child_pids(parent_pid: u32) -> Vec { + let output = Command::new("ps") + .args(["-axo", "pid=,ppid="]) + .output() + .expect("list processes"); + assert!(output.status.success(), "ps failed: {output:?}"); + let mut children: Vec<_> = String::from_utf8(output.stdout) + .unwrap() + .lines() + .filter_map(|line| { + let mut columns = line.split_whitespace(); + let pid = columns.next()?.parse::().ok()?; + let ppid = columns.next()?.parse::().ok()?; + (ppid == parent_pid).then_some(pid) + }) + .collect(); + children.sort_unstable(); + children +} + +fn process_exists(pid: u32) -> bool { + Command::new("kill") + .args(["-0", &pid.to_string()]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()) +} + +async fn wait_for_process_exit(pid: u32) { + let deadline = Instant::now() + TEST_TIMEOUT; + while process_exists(pid) { + assert!(Instant::now() < deadline, "process {pid} did not exit"); + sleep(Duration::from_millis(25)).await; + } +} diff --git a/rust/tests/extension_launch_provider_test.rs b/rust/tests/extension_launch_provider_test.rs new file mode 100644 index 0000000000..b1a9592133 --- /dev/null +++ b/rust/tests/extension_launch_provider_test.rs @@ -0,0 +1,493 @@ +#![allow(clippy::unwrap_used)] + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::extension_launch_provider::{ + ExtensionLaunchProfile, ExtensionLaunchProvider, ExtensionLaunchProviderResolveRequest, + ExtensionLaunchProviderResolveResult, +}; +use github_copilot_sdk::rpc::ExtensionSource; +use github_copilot_sdk::{CliProgram, Client, ClientOptions, Error, ErrorKind, Transport}; +use serde_json::{Value, json}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, duplex}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::{Notify, mpsc}; +use tokio::time::timeout; + +const TEST_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)) -> Option { + let mut header = String::new(); + loop { + let mut byte = [0u8; 1]; + match reader.read_exact(&mut byte).await { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => return None, + Err(error) => panic!("failed to read frame header: {error}"), + } + header.push(byte[0] as char); + if header.ends_with("\r\n\r\n") { + break; + } + } + + let length = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut body = vec![0; length]; + reader.read_exact(&mut body).await.unwrap(); + Some(serde_json::from_slice(&body).unwrap()) +} + +fn resolve_params() -> Value { + json!({ + "id": "project:legacy-extension", + "modulePath": "/extensions/legacy/index.js", + "name": "Legacy extension", + "source": "project" + }) +} + +fn app_launch_result(executable: &str) -> ExtensionLaunchProviderResolveResult { + ExtensionLaunchProviderResolveResult { + launch: Some(ExtensionLaunchProfile { + executable: executable.to_string(), + args: vec!["/app/preloads/extension_bootstrap.mjs".to_string()], + env: HashMap::from([ + ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()), + ( + "EXTENSION_PATH".to_string(), + "/extensions/legacy/index.js".to_string(), + ), + ]), + }), + } +} + +struct AppProvider { + executable: String, + observed: Option>, + release: Option>, + calls: Arc, +} + +#[async_trait] +impl ExtensionLaunchProvider for AppProvider { + async fn resolve( + &self, + request: ExtensionLaunchProviderResolveRequest, + ) -> github_copilot_sdk::Result { + self.calls.fetch_add(1, Ordering::SeqCst); + if let Some(observed) = &self.observed { + observed.send(request).unwrap(); + } + if let Some(release) = &self.release { + release.notified().await; + } + Ok(app_launch_result(&self.executable)) + } +} + +struct FailingProvider; + +#[async_trait] +impl ExtensionLaunchProvider for FailingProvider { + async fn resolve( + &self, + _request: ExtensionLaunchProviderResolveRequest, + ) -> github_copilot_sdk::Result { + Err(Error::with_message( + ErrorKind::InvalidConfig, + "extension profile lookup failed", + )) + } +} + +#[test] +fn launch_profile_request_and_result_serialize_exactly() { + let request: ExtensionLaunchProviderResolveRequest = + serde_json::from_value(resolve_params()).unwrap(); + assert_eq!(request.id, "project:legacy-extension"); + assert_eq!(request.module_path, "/extensions/legacy/index.js"); + assert_eq!(request.name, "Legacy extension"); + assert_eq!(request.source, ExtensionSource::Project); + assert_eq!(serde_json::to_value(request).unwrap(), resolve_params()); + + let result = app_launch_result("/app/copilot"); + assert_eq!( + serde_json::to_value(&result).unwrap(), + json!({ + "launch": { + "executable": "/app/copilot", + "args": ["/app/preloads/extension_bootstrap.mjs"], + "env": { + "COPILOT_AUTO_UPDATE": "false", + "EXTENSION_PATH": "/extensions/legacy/index.js" + } + } + }) + ); + + let round_trip: ExtensionLaunchProviderResolveResult = + serde_json::from_value(serde_json::to_value(result).unwrap()).unwrap(); + let launch = round_trip.launch.unwrap(); + assert_eq!(launch.executable, "/app/copilot"); + assert_eq!(launch.args, vec!["/app/preloads/extension_bootstrap.mjs"]); + assert_eq!( + launch.env, + HashMap::from([ + ("COPILOT_AUTO_UPDATE".to_string(), "false".to_string()), + ( + "EXTENSION_PATH".to_string(), + "/extensions/legacy/index.js".to_string() + ) + ]) + ); +} + +#[tokio::test] +async fn configured_async_provider_dispatches_without_a_session() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let temp = tempfile::tempdir().unwrap(); + let (observed_tx, mut observed_rx) = mpsc::unbounded_channel(); + let release = Arc::new(Notify::new()); + let calls = Arc::new(AtomicUsize::new(0)); + let client = Client::from_streams_with_extension_launch_provider( + client_read, + client_write, + temp.path().to_path_buf(), + Arc::new(AppProvider { + executable: "/app/copilot".to_string(), + observed: Some(observed_tx), + release: Some(release.clone()), + calls: calls.clone(), + }), + ) + .unwrap(); + client.start_router_for_test(); + + write_framed( + &mut server_write, + &json!({ + "jsonrpc": "2.0", + "id": 41, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + + let observed = timeout(TEST_TIMEOUT, observed_rx.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(observed.id, "project:legacy-extension"); + assert!( + timeout(Duration::from_millis(25), read_framed(&mut server_read)) + .await + .is_err() + ); + + release.notify_one(); + let response = timeout(TEST_TIMEOUT, read_framed(&mut server_read)) + .await + .unwrap() + .unwrap(); + assert_eq!(response["id"], 41); + assert_eq!( + response["result"], + serde_json::to_value(app_launch_result("/app/copilot")).unwrap() + ); + assert_eq!(calls.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn missing_provider_returns_client_global_handler_error() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let temp = tempfile::tempdir().unwrap(); + let client = + Client::from_streams(client_read, client_write, temp.path().to_path_buf()).unwrap(); + client.start_router_for_test(); + + write_framed( + &mut server_write, + &json!({ + "jsonrpc": "2.0", + "id": 42, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + + let response = timeout(TEST_TIMEOUT, read_framed(&mut server_read)) + .await + .unwrap() + .unwrap(); + assert_eq!(response["id"], 42); + assert_eq!(response["error"]["code"], -32603); + assert_eq!( + response["error"]["message"], + "No extensionLaunchProvider client-global handler registered" + ); +} + +#[tokio::test] +async fn provider_error_is_returned_as_json_rpc_error() { + let (client_write, mut server_read) = duplex(8192); + let (mut server_write, client_read) = duplex(8192); + let temp = tempfile::tempdir().unwrap(); + let client = Client::from_streams_with_extension_launch_provider( + client_read, + client_write, + temp.path().to_path_buf(), + Arc::new(FailingProvider), + ) + .unwrap(); + client.start_router_for_test(); + + write_framed( + &mut server_write, + &json!({ + "jsonrpc": "2.0", + "id": 43, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + + let response = timeout(TEST_TIMEOUT, read_framed(&mut server_read)) + .await + .unwrap() + .unwrap(); + assert_eq!(response["id"], 43); + assert_eq!(response["error"]["code"], -32603); + assert!( + response["error"]["message"] + .as_str() + .unwrap() + .contains("extension profile lookup failed") + ); +} + +async fn respond_to_connect( + reader: &mut (impl AsyncRead + Unpin), + writer: &mut (impl AsyncWrite + Unpin), +) { + let request = read_framed(reader).await.unwrap(); + assert_eq!(request["method"], "connect"); + write_framed( + writer, + &json!({ + "jsonrpc": "2.0", + "id": request["id"], + "result": { + "ok": true, + "protocolVersion": 3, + "version": "test" + } + }), + ) + .await; +} + +async fn read_registration(reader: &mut (impl AsyncRead + Unpin)) -> Value { + let request = read_framed(reader).await.unwrap(); + assert_eq!(request["method"], "registerExtensionLaunchProvider"); + assert_eq!(request["params"], json!({})); + request +} + +fn external_options(port: u16, provider: AppProvider) -> ClientOptions { + ClientOptions::new() + .with_program(CliProgram::Path("unused-for-external-transport".into())) + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port, + connection_token: None, + }) + .with_extension_launch_provider(provider) +} + +#[tokio::test] +async fn registration_precedes_session_work_and_routes_during_start() { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let calls = Arc::new(AtomicUsize::new(0)); + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let (mut reader, mut writer) = stream.into_split(); + respond_to_connect(&mut reader, &mut writer).await; + let registration = read_registration(&mut reader).await; + + write_framed( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": 501, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + let callback_response = read_framed(&mut reader).await.unwrap(); + assert_eq!(callback_response["id"], 501); + assert_eq!( + callback_response["result"], + serde_json::to_value(app_launch_result("/app/copilot")).unwrap() + ); + + write_framed( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": registration["id"], + "result": null + }), + ) + .await; + assert!( + timeout(Duration::from_millis(50), read_framed(&mut reader)) + .await + .is_err() + ); + }); + + let client = Client::start(external_options( + port, + AppProvider { + executable: "/app/copilot".to_string(), + observed: None, + release: None, + calls: calls.clone(), + }, + )) + .await + .unwrap(); + + timeout(TEST_TIMEOUT, server).await.unwrap().unwrap(); + assert_eq!(calls.load(Ordering::SeqCst), 1); + client.stop().await.unwrap(); +} + +async fn serve_registered_client(stream: TcpStream, request_id: u64, expected_executable: &str) { + let (mut reader, mut writer) = stream.into_split(); + respond_to_connect(&mut reader, &mut writer).await; + let registration = read_registration(&mut reader).await; + write_framed( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": registration["id"], + "result": null + }), + ) + .await; + + write_framed( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": request_id, + "method": "extensionLaunchProvider.resolve", + "params": resolve_params() + }), + ) + .await; + let response = read_framed(&mut reader).await.unwrap(); + assert_eq!(response["id"], request_id); + assert_eq!( + response["result"]["launch"]["executable"], + expected_executable + ); + assert_eq!(read_framed(&mut reader).await, None); +} + +#[tokio::test] +async fn shutdown_and_restart_do_not_reuse_or_duplicate_providers() { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let first_calls = Arc::new(AtomicUsize::new(0)); + let second_calls = Arc::new(AtomicUsize::new(0)); + let (served_tx, mut served_rx) = mpsc::unbounded_channel(); + + let server = tokio::spawn(async move { + let (first, _) = listener.accept().await.unwrap(); + serve_registered_client(first, 601, "/app/first-copilot").await; + served_tx.send(()).unwrap(); + + let (second, _) = listener.accept().await.unwrap(); + serve_registered_client(second, 602, "/app/second-copilot").await; + served_tx.send(()).unwrap(); + }); + + let first = Client::start(external_options( + port, + AppProvider { + executable: "/app/first-copilot".to_string(), + observed: None, + release: None, + calls: first_calls.clone(), + }, + )) + .await + .unwrap(); + timeout(TEST_TIMEOUT, async { + while first_calls.load(Ordering::SeqCst) == 0 { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + first.stop().await.unwrap(); + timeout(TEST_TIMEOUT, served_rx.recv()) + .await + .unwrap() + .unwrap(); + + let second = Client::start(external_options( + port, + AppProvider { + executable: "/app/second-copilot".to_string(), + observed: None, + release: None, + calls: second_calls.clone(), + }, + )) + .await + .unwrap(); + timeout(TEST_TIMEOUT, async { + while second_calls.load(Ordering::SeqCst) == 0 { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + second.stop().await.unwrap(); + timeout(TEST_TIMEOUT, served_rx.recv()) + .await + .unwrap() + .unwrap(); + timeout(TEST_TIMEOUT, server).await.unwrap().unwrap(); + + assert_eq!(first_calls.load(Ordering::SeqCst), 1); + assert_eq!(second_calls.load(Ordering::SeqCst), 1); +} diff --git a/rust/tests/fixtures/extension_fixture.rs b/rust/tests/fixtures/extension_fixture.rs new file mode 100644 index 0000000000..787c82503a --- /dev/null +++ b/rust/tests/fixtures/extension_fixture.rs @@ -0,0 +1,127 @@ +use std::fs::OpenOptions; +use std::io::{BufRead, BufReader, Write}; + +use serde_json::{Value, json}; + +fn main() { + let session_id = std::env::var("SESSION_ID").expect("SESSION_ID"); + record_state(json!({ + "kind": "start", + "pid": std::process::id(), + "sessionId": session_id, + "sdkPath": std::env::var("COPILOT_SDK_PATH").ok(), + "parentPid": std::env::var("COPILOT_EXTENSION_PARENT_PID").ok(), + "extensionPath": std::env::var("EXTENSION_PATH").ok(), + "autoUpdate": std::env::var("COPILOT_AUTO_UPDATE").ok(), + "cliDistDir": std::env::var("COPILOT_CLI_DIST_DIR").ok(), + })); + + let stdin = std::io::stdin(); + let mut reader = BufReader::new(stdin.lock()); + let stdout = std::io::stdout(); + let mut writer = stdout.lock(); + write_message( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "session.resume", + "params": { + "sessionId": session_id, + "tools": [{ + "name": "fixture_echo", + "description": "Returns a deterministic fixture result", + "parameters": { + "type": "object", + "properties": { "text": { "type": "string" } }, + "required": ["text"] + } + }], + "requestPermission": false, + "enableConfigDiscovery": false, + "disableResume": true, + "streaming": true + } + }), + ); + + let mut next_id = 2_u64; + while let Some(message) = read_message(&mut reader) { + if message.get("id") == Some(&json!(1)) { + assert!( + message.get("error").is_none(), + "extension resume failed: {message}" + ); + continue; + } + if message.pointer("/params/event/type") != Some(&json!("external_tool.requested")) { + continue; + } + let Some(request_id) = message + .pointer("/params/event/data/requestId") + .and_then(Value::as_str) + else { + continue; + }; + record_state(json!({ + "kind": "invoke", + "pid": std::process::id(), + "requestId": request_id, + })); + write_message( + &mut writer, + &json!({ + "jsonrpc": "2.0", + "id": next_id, + "method": "session.tools.handlePendingToolCall", + "params": { + "sessionId": session_id, + "requestId": request_id, + "result": "echoed" + } + }), + ); + next_id += 1; + } +} + +fn record_state(value: Value) { + let path = std::env::var("FIXTURE_STATE_PATH").expect("FIXTURE_STATE_PATH"); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(path) + .expect("open fixture state"); + writeln!(file, "{value}").expect("write fixture state"); +} + +fn write_message(writer: &mut impl Write, value: &Value) { + let body = serde_json::to_vec(value).expect("serialize fixture message"); + write!(writer, "Content-Length: {}\r\n\r\n", body.len()).expect("write fixture header"); + writer.write_all(&body).expect("write fixture body"); + writer.flush().expect("flush fixture message"); +} + +fn read_message(reader: &mut impl BufRead) -> Option { + let mut content_length = None; + loop { + let mut line = String::new(); + if reader.read_line(&mut line).expect("read fixture header") == 0 { + return None; + } + if line == "\r\n" { + break; + } + if let Some(value) = line.strip_prefix("Content-Length:") { + content_length = Some( + value + .trim() + .parse::() + .expect("parse fixture content length"), + ); + } + } + let mut body = vec![0; content_length.expect("fixture content length")]; + reader.read_exact(&mut body).expect("read fixture body"); + Some(serde_json::from_slice(&body).expect("parse fixture message")) +} diff --git a/rust/tests/fixtures/host_crash_fixture.rs b/rust/tests/fixtures/host_crash_fixture.rs new file mode 100644 index 0000000000..c688cf3e87 --- /dev/null +++ b/rust/tests/fixtures/host_crash_fixture.rs @@ -0,0 +1,60 @@ +//! Test-only binary that hosts a single [`github_copilot_sdk::Client`] and then +//! blocks forever, so an external test can terminate *this* process abruptly +//! (simulating an SDK-embedding app process crashing) without ever running any +//! of this process's own cleanup code (`Client::stop`, `force_stop`, or +//! `Drop`). +//! +//! Configuration is passed entirely through environment variables so the +//! caller doesn't need this crate's non-`pub` types: +//! - `HOST_CRASH_FIXTURE_PROGRAM`: CLI program path. +//! - `HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON`: JSON array of prefix args. +//! - `HOST_CRASH_FIXTURE_CWD`: working directory for the spawned CLI. +//! - `HOST_CRASH_FIXTURE_ENV_JSON`: JSON array of `[key, value]` pairs to set +//! on the spawned CLI's environment. +//! - `HOST_CRASH_FIXTURE_PID_FILE`: path this process writes the CLI child's +//! OS process id to, once the client finishes starting. + +use std::path::PathBuf; + +use github_copilot_sdk::{CliProgram, Client, ClientOptions, Transport}; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let program = std::env::var("HOST_CRASH_FIXTURE_PROGRAM").expect("HOST_CRASH_FIXTURE_PROGRAM"); + let prefix_args: Vec = serde_json::from_str( + &std::env::var("HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON") + .expect("HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON"), + ) + .expect("parse HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON"); + let cwd = std::env::var("HOST_CRASH_FIXTURE_CWD").expect("HOST_CRASH_FIXTURE_CWD"); + let env_pairs: Vec<(String, String)> = serde_json::from_str( + &std::env::var("HOST_CRASH_FIXTURE_ENV_JSON").expect("HOST_CRASH_FIXTURE_ENV_JSON"), + ) + .expect("parse HOST_CRASH_FIXTURE_ENV_JSON"); + let pid_file = PathBuf::from( + std::env::var("HOST_CRASH_FIXTURE_PID_FILE").expect("HOST_CRASH_FIXTURE_PID_FILE"), + ); + + let options = ClientOptions::new() + .with_program(CliProgram::Path(PathBuf::from(program))) + .with_prefix_args(prefix_args) + .with_cwd(PathBuf::from(cwd)) + .with_env(env_pairs) + .with_use_logged_in_user(false) + .with_transport(Transport::Stdio); + + let client = Client::start(options).await.expect("start CLI client"); + let pid = client.pid().expect("client reports spawned CLI pid"); + std::fs::write(&pid_file, pid.to_string()).expect("write pid file"); + + // Deliberately leak the client so nothing in this process — including its + // `Drop` impls — ever runs cleanup code. The external test process + // terminates this process abruptly (e.g. `TerminateProcess` on Windows) + // to simulate an SDK-embedding host crashing, and asserts that the CLI + // still dies via the OS containment primitive alone. + std::mem::forget(client); + + loop { + std::thread::sleep(std::time::Duration::from_secs(3600)); + } +} diff --git a/rust/tests/protocol_version_test.rs b/rust/tests/protocol_version_test.rs index 0d1268c59e..cd8563f87b 100644 --- a/rust/tests/protocol_version_test.rs +++ b/rust/tests/protocol_version_test.rs @@ -239,3 +239,153 @@ async fn connect_handshake_forwards_auto_generated_token() { .unwrap() .unwrap(); } + +/// Positive coverage for application-identity forwarding on the `connect` +/// handshake. A client constructed with a [`ClientInfo`] MUST serialize it +/// (camelCase) into the outbound `connect` request's `clientInfo` param so +/// the runtime attributes this connection's telemetry to the application. +#[tokio::test] +async fn connect_handshake_forwards_client_info() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_application_name("acme-developer-portal") + .with_application_version("2.4.0") + .with_integration_name("copilot-assistant") + .with_integration_version("1.5.0"), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + let client_info = &req["params"]["clientInfo"]; + assert_eq!(client_info["editorName"], "acme-developer-portal"); + assert_eq!(client_info["editorVersion"], "2.4.0"); + assert_eq!(client_info["extensionName"], "copilot-assistant"); + assert_eq!(client_info["extensionVersion"], "1.5.0"); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} + +/// A [`ClientInfo`] with only some fields set must omit the empty ones from +/// the wire, and a fully-empty one must drop `clientInfo` entirely so the +/// runtime keeps its default attribution. +#[tokio::test] +async fn connect_handshake_omits_empty_client_info_fields() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_application_name("example-app") + .with_application_version(""), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + let client_info = &req["params"]["clientInfo"]; + assert_eq!(client_info["editorName"], "example-app"); + assert!(client_info.get("editorVersion").is_none()); + assert!(client_info.get("extensionName").is_none()); + assert!(client_info.get("extensionVersion").is_none()); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} + +/// A [`ClientInfo`] whose every field is empty must drop `clientInfo` from the +/// handshake entirely so the runtime keeps its default attribution. +#[tokio::test] +async fn connect_handshake_omits_all_empty_client_info() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_application_name("") + .with_application_version("") + .with_integration_name("") + .with_integration_version(""), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + assert!( + req["params"].get("clientInfo").is_none(), + "an all-empty clientInfo must be omitted from the handshake" + ); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index a51d61910d..9f86777ffc 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -1,8 +1,10 @@ #![allow(clippy::unwrap_used)] +use std::collections::HashMap; +use std::fmt; use std::path::Path; use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::time::Duration; use async_trait::async_trait; @@ -24,8 +26,8 @@ use github_copilot_sdk::session_events::{ SessionManagedSettingsResolvedData, }; use github_copilot_sdk::types::{ - CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, CommandContext, - CommandDefinition, CommandHandler, DeliveryMode, DisableBypassPermissionsModes, + AskUserVariant, CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, + CommandContext, CommandDefinition, CommandHandler, DeliveryMode, DisableBypassPermissionsModes, ElicitationRequest, ElicitationResult, ExitPlanModeData, ExtensionInfo, ManagedSettings, ManagedSettingsPermissions, MessageOptions, PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, PermissionDecisionSurface, RequestId, @@ -34,9 +36,151 @@ use github_copilot_sdk::types::{ use github_copilot_sdk::{Client, ContextTier, ErrorKind, ProtocolErrorKind, tool}; use serde_json::Value; use tokio::io::{AsyncWrite, AsyncWriteExt, duplex}; +use tokio::sync::Notify; use tokio::time::timeout; +use tracing::field::{Field, Visit}; +use tracing::span::{Attributes, Id, Record}; +use tracing::{Event, Metadata, Subscriber}; const TIMEOUT: Duration = Duration::from_secs(2); +const PERMISSION_CONFIRMATION_METHOD: &str = "session.permissions.handlePendingPermissionRequest"; + +#[derive(Clone, Debug)] +struct CapturedTraceEvent { + fields: HashMap, +} + +impl CapturedTraceEvent { + fn message_contains(&self, expected: &str) -> bool { + self.fields + .get("message") + .is_some_and(|message| message.contains(expected)) + } + + fn field_is(&self, name: &str, expected: &str) -> bool { + self.fields.get(name).is_some_and(|value| value == expected) + } +} + +#[derive(Clone, Default)] +struct TraceCapture { + events: Arc>>, +} + +impl TraceCapture { + fn permission_outcome(&self, request_id: &str) -> Option { + self.events + .lock() + .unwrap() + .iter() + .find(|event| { + event.field_is("request_id", request_id) + && (event.message_contains( + "Session::handle_notification response sent successfully", + ) || event.message_contains( + "failed to deliver permission decision back to the runtime", + ) || event + .message_contains("permission confirmation acknowledgement wait cancelled")) + }) + .cloned() + } + + async fn wait_for_permission_outcome(&self, request_id: &str) -> CapturedTraceEvent { + timeout(TIMEOUT, async { + loop { + if let Some(event) = self.permission_outcome(request_id) { + return event; + } + tokio::task::yield_now().await; + } + }) + .await + .expect("timed out waiting for permission confirmation diagnostic") + } +} + +#[derive(Default)] +struct TraceFieldVisitor { + fields: HashMap, +} + +impl Visit for TraceFieldVisitor { + fn record_bool(&mut self, field: &Field, value: bool) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_i64(&mut self, field: &Field, value: i64) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_u64(&mut self, field: &Field, value: u64) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_str(&mut self, field: &Field, value: &str) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { + self.fields + .insert(field.name().to_string(), format!("{value:?}")); + } +} + +struct CaptureSubscriber { + capture: TraceCapture, + next_span_id: AtomicU64, +} + +impl CaptureSubscriber { + fn new(capture: TraceCapture) -> Self { + Self { + capture, + next_span_id: AtomicU64::new(1), + } + } +} + +impl Subscriber for CaptureSubscriber { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } + + fn new_span(&self, _span: &Attributes<'_>) -> Id { + Id::from_u64(self.next_span_id.fetch_add(1, Ordering::Relaxed)) + } + + fn record(&self, _span: &Id, _values: &Record<'_>) {} + + fn record_follows_from(&self, _span: &Id, _follows: &Id) {} + + fn event(&self, event: &Event<'_>) { + let mut visitor = TraceFieldVisitor::default(); + event.record(&mut visitor); + self.capture + .events + .lock() + .unwrap() + .push(CapturedTraceEvent { + fields: visitor.fields, + }); + } + + fn enter(&self, _span: &Id) {} + + fn exit(&self, _span: &Id) {} +} + +fn capture_traces() -> (TraceCapture, tracing::dispatcher::DefaultGuard) { + let capture = TraceCapture::default(); + let dispatch = tracing::Dispatch::new(CaptureSubscriber::new(capture.clone())); + let guard = tracing::dispatcher::set_default(&dispatch); + (capture, guard) +} struct TestCanvasHandler; @@ -44,6 +188,25 @@ struct CancelMcpAuthHandler; struct ContextualApproveHandler; +struct GatedApproveHandler { + entered: Arc, + release: Arc, +} + +#[async_trait] +impl PermissionHandler for GatedApproveHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _data: github_copilot_sdk::PermissionRequestData, + ) -> PermissionResult { + self.entered.notify_one(); + self.release.notified().await; + PermissionResult::approve_once() + } +} + #[async_trait] impl PermissionHandler for ContextualApproveHandler { async fn handle( @@ -158,6 +321,16 @@ impl FakeServer { write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; } + async fn respond_error(&mut self, request: &Value, code: i64, message: &str) { + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + async fn send_notification(&mut self, method: &str, params: Value) { let notification = serde_json::json!({ "jsonrpc": "2.0", @@ -918,6 +1091,60 @@ async fn create_session_sends_new_session_options() { timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); } +#[tokio::test] +async fn create_session_forwards_ask_user_variant() { + let (client, mut server_read, mut server_write) = make_client(); + + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default().with_ask_user_variant(AskUserVariant::Elicitation), + ) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.create"); + assert_eq!(request["params"]["askUserVariant"], "elicitation"); + + let session_id = requested_session_id(&request).to_string(); + server_respond_create(&mut server_write, &request, &session_id).await; + timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn cold_resume_session_forwards_ask_user_variant() { + use github_copilot_sdk::types::ResumeSessionConfig; + + let (client, mut server_read, mut server_write) = make_client(); + + let resume_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(SessionId::from("ask-user-variant")) + .with_ask_user_variant(AskUserVariant::Legacy), + ) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.resume"); + assert_eq!(request["params"]["sessionId"], "ask-user-variant"); + assert_eq!(request["params"]["askUserVariant"], "legacy"); + + server_respond_create(&mut server_write, &request, "ask-user-variant").await; + respond_to_reload(&mut server_read, &mut server_write).await; + timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); +} + #[tokio::test] async fn resume_session_sends_new_session_options() { use github_copilot_sdk::types::ResumeSessionConfig; @@ -2738,7 +2965,8 @@ async fn user_input_requested_notification_does_not_double_dispatch() { } #[tokio::test] -async fn approve_all_handler_approves_permission() { +async fn permission_confirmation_success_behavior_is_unchanged() { + let (capture, _guard) = capture_traces(); let (_session, mut server) = create_session_pair_with_config(|cfg| { cfg.with_permission_handler(Arc::new(ApproveAllHandler)) }) @@ -2762,6 +2990,207 @@ async fn approve_all_handler_approves_permission() { ); assert_eq!(request["params"]["requestId"], "perm-auto"); assert_eq!(request["params"]["result"]["kind"], "approve-once"); + server.respond(&request, serde_json::json!({})).await; + + let outcome = capture.wait_for_permission_outcome("perm-auto").await; + assert!(outcome.message_contains("Session::handle_notification response sent successfully")); + assert!(outcome.field_is("session_id", &server.session_id)); + assert!(outcome.field_is("request_id", "perm-auto")); +} + +#[tokio::test] +async fn permission_confirmation_json_rpc_error_is_observable_and_connection_stays_responsive() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session = Arc::new(session); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-rpc-error", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + server + .respond_error(&confirmation, -32603, "permission response rejected") + .await; + + let get_events = tokio::spawn({ + let session = session.clone(); + async move { session.get_events().await } + }); + let follow_up = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(follow_up["method"], "session.getMessages"); + server + .respond(&follow_up, serde_json::json!({ "events": [] })) + .await; + assert!(timeout(TIMEOUT, get_events).await.unwrap().unwrap().is_ok()); + + let outcome = capture.wait_for_permission_outcome("perm-rpc-error").await; + assert!(outcome.message_contains("failed to deliver permission decision back to the runtime")); + assert!(outcome.field_is("session_id", &server.session_id)); + assert!(outcome.field_is("request_id", "perm-rpc-error")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); +} + +#[tokio::test] +async fn permission_confirmation_write_failure_is_observable_and_events_stay_responsive() { + let (capture, _guard) = capture_traces(); + let entered = Arc::new(Notify::new()); + let release = Arc::new(Notify::new()); + let handler = Arc::new(GatedApproveHandler { + entered: entered.clone(), + release: release.clone(), + }); + let (session, mut server) = + create_session_pair_with_config(move |cfg| cfg.with_permission_handler(handler)).await; + let mut subscription = session.subscribe(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-write-error", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let permission_event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(permission_event.event_type, "permission.requested"); + timeout(TIMEOUT, entered.notified()).await.unwrap(); + + let FakeServer { + read, + mut write, + session_id, + } = server; + drop(read); + release.notify_one(); + + let idle_event = serde_json::json!({ + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": session_id, + "event": { + "id": "evt-after-write-error", + "timestamp": "2025-01-01T00:00:00Z", + "type": "session.idle", + "data": {}, + }, + }, + }); + write_framed(&mut write, &serde_json::to_vec(&idle_event).unwrap()).await; + let event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(event.event_type, "session.idle"); + + let outcome = capture + .wait_for_permission_outcome("perm-write-error") + .await; + assert!(outcome.message_contains("failed to deliver permission decision back to the runtime")); + assert!(outcome.field_is("session_id", &session_id)); + assert!(outcome.field_is("request_id", "perm-write-error")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); +} + +#[tokio::test] +async fn permission_confirmation_without_response_does_not_block_events_or_other_rpcs() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session = Arc::new(session); + let mut subscription = session.subscribe(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-no-response", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let permission_event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(permission_event.event_type, "permission.requested"); + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + + server + .send_event("session.idle", serde_json::json!({})) + .await; + let event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(event.event_type, "session.idle"); + + let get_events = tokio::spawn({ + let session = session.clone(); + async move { session.get_events().await } + }); + let follow_up = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(follow_up["method"], "session.getMessages"); + server + .respond(&follow_up, serde_json::json!({ "events": [] })) + .await; + assert!(timeout(TIMEOUT, get_events).await.unwrap().unwrap().is_ok()); + assert!( + capture.permission_outcome("perm-no-response").is_none(), + "the confirmation task should still be waiting silently for its response" + ); +} + +#[tokio::test] +async fn permission_confirmation_wait_is_cancelled_on_session_teardown() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session_id = server.session_id.clone(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-teardown", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + + drop(session); + + let outcome = capture.wait_for_permission_outcome("perm-teardown").await; + assert!(outcome.message_contains("permission confirmation acknowledgement wait cancelled")); + assert!(outcome.field_is("session_id", &session_id)); + assert!(outcome.field_is("request_id", "perm-teardown")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); } #[tokio::test] diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index acdea09727..b4297a239c 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -3967,7 +3967,10 @@ async function generateRpc(schemaPath?: string): Promise { if (generatedTypeCode.includes("time.Time")) { imports.push(`"time"`); } - if (schema.clientSession || schema.clientGlobal) { + const publicClientSession = schema.clientSession + ? filterNodeByVisibility(schema.clientSession, "public") + : null; + if (publicClientSession || schema.clientGlobal) { imports.push(`"errors"`, `"fmt"`); } imports.push(`"github.com/github/copilot-sdk/go/internal/jsonrpc2"`); @@ -4268,8 +4271,9 @@ function clientHandlerMethodName(rpcMethod: string): string { return toPascalCase(rpcMethod.split(".").at(-1)!); } -function emitClientSessionApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { - const groups = collectClientGroups(clientSchema); +export function emitClientSessionApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { + const publicClientSchema = filterNodeByVisibility(clientSchema, "public") ?? {}; + const groups = collectClientGroups(publicClientSchema); for (const { groupName, groupNode, methods } of groups) { const interfaceName = clientHandlerInterfaceName(groupName); @@ -4324,17 +4328,19 @@ function emitClientSessionApiRegistration(lines: string[], clientSchema: Record< lines.push(`}`); lines.push(``); - lines.push(`func clientSessionHandlerError(err error) *jsonrpc2.Error {`); - lines.push(`\tif err == nil {`); - lines.push(`\t\treturn nil`); - lines.push(`\t}`); - lines.push(`\tvar rpcErr *jsonrpc2.Error`); - lines.push(`\tif errors.As(err, &rpcErr) {`); - lines.push(`\t\treturn rpcErr`); - lines.push(`\t}`); - lines.push(`\treturn &jsonrpc2.Error{Code: -32603, Message: err.Error()}`); - lines.push(`}`); - lines.push(``); + if (groups.length > 0) { + lines.push(`func clientSessionHandlerError(err error) *jsonrpc2.Error {`); + lines.push(`\tif err == nil {`); + lines.push(`\t\treturn nil`); + lines.push(`\t}`); + lines.push(`\tvar rpcErr *jsonrpc2.Error`); + lines.push(`\tif errors.As(err, &rpcErr) {`); + lines.push(`\t\treturn rpcErr`); + lines.push(`\t}`); + lines.push(`\treturn &jsonrpc2.Error{Code: -32603, Message: err.Error()}`); + lines.push(`}`); + lines.push(``); + } lines.push(`// RegisterClientSessionAPIHandlers registers handlers for server-to-client session API calls.`); lines.push(`func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func(sessionID string) *ClientSessionAPIHandlers) {`); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index bdb4d095b7..b3bfcf8bc9 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -143,12 +143,22 @@ function placeholderToQuicktypeIdentifiers(placeholder: string): string[] { return [...new Set([basic, basic.replace(/Mcp/g, "MCP")])]; } -function postProcessExternalRefsForPython( +export function postProcessExternalRefsForPython( code: string, placeholderToReal: Map, externalEnumNames: Set = new Set() ): string { for (const [placeholder, realName] of placeholderToReal) { + const markerProperty = `__externalRefMarker_${placeholder}`; + const markerClass = [ + ...code.matchAll( + /(?:^|\n)(@dataclass\r?\nclass (\w+)\b[\s\S]*?)(?=\n@dataclass\b|\nclass\s+\w|\ndef\s+\w|$)/g + ), + ].find((match) => match[1].includes(`"${markerProperty}"`)); + if (markerClass) { + code = code.replace(markerClass[0], "\n"); + code = code.replace(new RegExp(`\\b${escapeRegExp(markerClass[2])}\\b`, "g"), realName); + } for (const quicktypeName of placeholderToQuicktypeIdentifiers(placeholder)) { code = code.replace( new RegExp( @@ -3763,12 +3773,13 @@ function clientSessionHandlerMethodName(rpcMethod: string): string { return toSnakeCase(parts[parts.length - 1]); } -function emitClientSessionApiRegistration( +export function emitClientSessionApiRegistration( lines: string[], node: Record, resolveType: (name: string) => string ): void { - const groups = Object.entries(node).filter(([, value]) => typeof value === "object" && value !== null && !isRpcMethod(value)); + const publicNode = filterNodeByVisibility(node, "public") ?? {}; + const groups = Object.entries(publicNode).filter(([, value]) => typeof value === "object" && value !== null && !isRpcMethod(value)); for (const [groupName, groupNode] of groups) { const handlerName = `${toPascalCase(groupName)}Handler`; diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 0feec5e98a..b3cc5d5753 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -378,9 +378,7 @@ function tryEmitRustUnion( const lines: string[] = []; if (schema.description) { - for (const line of schema.description.split(/\r?\n/)) { - lines.push(`/// ${line}`); - } + pushRustDoc(lines, schema.description); } pushRustExperimentalDocs(lines, isSchemaExperimental(schema) || ctx.experimentalTypeNames.has(enumName)); lines.push("#[derive(Debug, Clone, Serialize, Deserialize)]"); @@ -468,7 +466,8 @@ function pushRustExperimentalDocs( function pushRustDoc(lines: string[], text: string | undefined, indent = ""): void { if (!text) return; - for (const paragraph of text.trim().split(/\r?\n/)) { + const sanitized = text.replace(/\[::\]/g, "`[::]`"); + for (const paragraph of sanitized.trim().split(/\r?\n/)) { if (paragraph.trim().length === 0) { lines.push(`${indent}///`); } else { @@ -971,9 +970,7 @@ function emitRustStruct( for (const { propName, prop, isReq, rustField, rustType } of fields) { if (prop.description) { - for (const line of prop.description.split(/\r?\n/)) { - lines.push(` /// ${line}`); - } + pushRustDoc(lines, prop.description, " "); } pushRustExperimentalDocs(lines, isSchemaExperimental(prop), " "); const propIsInternal = isSchemaInternal(prop); diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 4984816d8d..f5e8acb146 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -25,6 +25,7 @@ import { collectExperimentalOnlyRpcReferencedDefinitionNames, collectReachableDefinitionNames, collectRpcMethodReferencedDefinitionNames, + filterNodeByVisibility, findSharedSchemaDefinitions, hasSchemaPayload, parseExternalSchemaRef, @@ -1076,15 +1077,16 @@ function handlerMethodName(rpcMethod: string): string { * `getHandler` callback that resolves a sessionId to a handler object. * Param types include sessionId — handler code can simply ignore it. */ -function emitClientSessionApiRegistration(clientSchema: Record): string[] { +export function emitClientSessionApiRegistration(clientSchema: Record): string[] { const lines: string[] = []; - const groups = collectClientGroups(clientSchema); + const publicClientSchema = filterNodeByVisibility(clientSchema, "public") ?? {}; + const groups = collectClientGroups(publicClientSchema); // Emit a handler interface per group for (const [groupName, methods] of groups) { const interfaceName = toPascalCase(groupName) + "Handler"; - const groupDeprecated = isNodeFullyDeprecated(clientSchema[groupName] as Record); - const groupExperimental = isNodeFullyExperimental(clientSchema[groupName] as Record); + const groupDeprecated = isNodeFullyDeprecated(publicClientSchema[groupName] as Record); + const groupExperimental = isNodeFullyExperimental(publicClientSchema[groupName] as Record); if (groupDeprecated) { lines.push(`/** @deprecated Handler for \`${groupName}\` client session API methods. */`); } else if (groupExperimental) { diff --git a/test/harness/capturingHttpProxy.test.ts b/test/harness/capturingHttpProxy.test.ts index f434d3e67c..cba4d45e53 100644 --- a/test/harness/capturingHttpProxy.test.ts +++ b/test/harness/capturingHttpProxy.test.ts @@ -10,9 +10,21 @@ describe("Capturing HTTP Proxy", () => { let proxy: CapturingHttpProxy; let testServer: http.Server; let testServerAddress: string; + let onHangingRequest: (() => void) | undefined; + let onStreamingResponse: (() => void) | undefined; beforeEach(async () => { testServer = http.createServer((req, res) => { + if (req.url === "/hang") { + onHangingRequest?.(); + return; + } + if (req.url === "/stream") { + res.writeHead(200, { "content-type": "text/plain" }); + res.write("started"); + onStreamingResponse?.(); + return; + } res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ message: "Hello", path: req.url })); }); @@ -71,4 +83,34 @@ describe("Capturing HTTP Proxy", () => { } as CapturedExchange, ]); }); + + test("stops while a proxied request is still active", async () => { + proxy = new CapturingHttpProxy(testServerAddress); + const proxyUrl = await proxy.start(); + const requestStarted = new Promise((resolve) => { + onHangingRequest = resolve; + }); + const request = fetch(`${proxyUrl}/hang`).catch(() => undefined); + await requestStarted; + + await proxy.stop(); + + await request; + }); + + test("stops while a proxied response is still streaming", async () => { + proxy = new CapturingHttpProxy(testServerAddress); + const proxyUrl = await proxy.start(); + const responseStarted = new Promise((resolve) => { + onStreamingResponse = resolve; + }); + const responsePromise = fetch(`${proxyUrl}/stream`); + await responseStarted; + const response = await responsePromise; + const body = response.text().catch(() => undefined); + + await proxy.stop(); + + await body; + }); }); diff --git a/test/harness/capturingHttpProxy.ts b/test/harness/capturingHttpProxy.ts index edccca4ead..fdc1fc46c1 100644 --- a/test/harness/capturingHttpProxy.ts +++ b/test/harness/capturingHttpProxy.ts @@ -10,7 +10,10 @@ import https from "https"; */ export class CapturingHttpProxy { private readonly capturedExchanges: CapturedExchange[] = []; + private readonly activeRequests = new Set(); + private readonly activeResponses = new Set(); private server?: http.Server; + private stopPromise?: Promise; constructor(private targetUrl: string) {} @@ -90,6 +93,10 @@ export class CapturingHttpProxy { res.end(); }, onError: (err) => { + if (!this.server) { + res.destroy(); + return; + } console.error("Error in proxying request:", err); const endTime = Date.now(); const formattedError = @@ -130,9 +137,19 @@ export class CapturingHttpProxy { } async stop(): Promise { - if (this.server) { - return new Promise((resolve, reject) => { - this.server!.close((err) => { + if (this.stopPromise) { + return this.stopPromise; + } + + const server = this.server; + if (!server) { + return; + } + + this.server = undefined; + this.stopPromise = (async () => { + const closed = new Promise((resolve, reject) => { + server.close((err) => { if (err) { reject(err); } else { @@ -140,14 +157,38 @@ export class CapturingHttpProxy { } }); }); - } + + // server.close() waits for active connections. A replayed streaming request + // can otherwise wedge fixture teardown after its test has already passed. + server.closeAllConnections(); + for (const response of this.activeResponses) { + response.destroy(); + } + this.activeResponses.clear(); + for (const request of this.activeRequests) { + request.destroy(); + } + this.activeRequests.clear(); + + await closed; + })(); + return this.stopPromise; } performRequest(options: PerformRequestOptions): void { + if (this.stopPromise) { + options.onError(new Error("Proxy is stopping")); + return; + } + const protocol = options.isHttps ? https : http; const upstreamRequest = protocol.request( options.requestOptions, (upstreamResponse) => { + this.activeResponses.add(upstreamResponse); + upstreamResponse.once("close", () => { + this.activeResponses.delete(upstreamResponse); + }); options.onResponseStart( upstreamResponse.statusCode || 500, upstreamResponse.headers, @@ -157,6 +198,10 @@ export class CapturingHttpProxy { }, ); + this.activeRequests.add(upstreamRequest); + upstreamRequest.once("close", () => { + this.activeRequests.delete(upstreamRequest); + }); upstreamRequest.on("error", options.onError); if (options.body) { diff --git a/test/harness/modelProtocolAdapters.test.ts b/test/harness/modelProtocolAdapters.test.ts index c0ed084150..add59ae77f 100644 --- a/test/harness/modelProtocolAdapters.test.ts +++ b/test/harness/modelProtocolAdapters.test.ts @@ -42,7 +42,7 @@ const endpoints: Record = { const models: Record = { capi: "gpt-4.1", - "anthropic-messages": "claude-sonnet-4.5", + "anthropic-messages": "claude-sonnet-5", "openai-responses": "gpt-4.1", "openai-completions": "gpt-4.1", }; diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 710d725318..288ec1db3c 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.83-3", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -472,8 +472,8 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.82-0", - "integrity": "sha512-fSZVNAzFFYaS6btYD0+cKF7SrrtOklhpkPs/cIMZY7Fgxoa6rfZrlTWXlQNgNIdwLp51XxwTfxnZO+D9+MQ5yg==", + "version": "1.0.83-3", + "integrity": "sha512-4+5wVGC2IvLYog3kdfmY6rg+NIGJesjENVrTONZr6uic6zR+8Ksgy+sCWO86n6AARs09MXktAZNHbbrXz+hl7A==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -483,19 +483,19 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.82-0", - "@github/copilot-darwin-x64": "1.0.82-0", - "@github/copilot-linux-arm64": "1.0.82-0", - "@github/copilot-linux-x64": "1.0.82-0", - "@github/copilot-linuxmusl-arm64": "1.0.82-0", - "@github/copilot-linuxmusl-x64": "1.0.82-0", - "@github/copilot-win32-arm64": "1.0.82-0", - "@github/copilot-win32-x64": "1.0.82-0" + "@github/copilot-darwin-arm64": "1.0.83-3", + "@github/copilot-darwin-x64": "1.0.83-3", + "@github/copilot-linux-arm64": "1.0.83-3", + "@github/copilot-linux-x64": "1.0.83-3", + "@github/copilot-linuxmusl-arm64": "1.0.83-3", + "@github/copilot-linuxmusl-x64": "1.0.83-3", + "@github/copilot-win32-arm64": "1.0.83-3", + "@github/copilot-win32-x64": "1.0.83-3" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-TzBYfyvxcw3z9Mu7U8TsFo/Nq7m5XS6ahT71aPL+gx/YId0kmenI27b9daXsK6LA1D0gsFGwNBDKINddqntt1g==", + "version": "1.0.83-3", + "integrity": "sha512-pNI71CRL2WR6Wp+Nm+HOsSBcUIOoybcSZtMHqm2zwJGdzAjzv6MU2lLOFFeqhBh8UNQGltD4KtPU/pr+t6t4Uw==", "cpu": [ "arm64" ], @@ -510,8 +510,8 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.82-0", - "integrity": "sha512-Lm/U5Q8kN8yEeBTWKTfIxXAgXaT6zqdBzAAO7lA4DWHZ92AEeZZiVTs6jEWsQ2aWB2uMs6zbn0vi6t+/jIqCGw==", + "version": "1.0.83-3", + "integrity": "sha512-9LKUwR7em12mz76s2ytWl/xkHyF13t0TLScAUcnNNj171/Kvg0lWNemwsmPK4m0QbbcmRUs7FyFFF79TmKBAmA==", "cpu": [ "x64" ], @@ -526,8 +526,8 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-YERVMC1Q4p6l6KQHL5rVOI52rWbvgp9IwyzUBaVSGrfFuqu5BEvZ9bgHPsxTYi3Npkt5KVOXEyPMU5rAY09qQQ==", + "version": "1.0.83-3", + "integrity": "sha512-ouGA46t6flyUqUdutQL+94bnD+IwcCurR+5KS2JPHozbkeiR2BW4ed0ZZ5KT/6I13mTsjO9uu9LvWwfO5+PjiQ==", "cpu": [ "arm64" ], @@ -542,8 +542,8 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.82-0", - "integrity": "sha512-z2hxMVjqt4+xDRFTZv3/0K3X+aqcJhd6zPO2JxCpOVTh5CNZFaWk+XIa2iXAPWxFqdKJsQ4muXMl3zInaAOkRw==", + "version": "1.0.83-3", + "integrity": "sha512-AiAf2yVrnP+Dw0M8RpacpOoK89sMFizPMuQfFPxAJUWS9hIw5mq4o4invKtUfiz0F7cjxaDJZz1JLUSuGEAQhw==", "cpu": [ "x64" ], @@ -558,8 +558,8 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-EcUCv2PKhBzCCvpTaS511VYTDWyhudyIRPvBpc9gFNO3hjlgiNDusf4k9vP6+E3/lHenwGYzMsyRHcYIOy4vcQ==", + "version": "1.0.83-3", + "integrity": "sha512-TmXPXi65OX/Wfd7JnU8RZjZxzc5kFZU/3Gvr/N1Y+G+cJJyB0NBmWk2PP+yD381ASYOOgeNgWitlYMw8tU7Ddg==", "cpu": [ "arm64" ], @@ -574,8 +574,8 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.82-0", - "integrity": "sha512-1fKVjUiZ1tdb0/d/re90EpFGXhlIPfjENp2Wo/2Kj592dWO3+IwM2qV/AOdMQ4pacW5iYHII7nibx1/EYq3LGQ==", + "version": "1.0.83-3", + "integrity": "sha512-Zlbya4anjkbI8LcbenwuBhxUUeVIrGJqeYh/6JUWwnisOiuuimqQ4zb2UU2pX3vxE03f2PbTcueOo/GkF6AS8A==", "cpu": [ "x64" ], @@ -590,8 +590,8 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.82-0", - "integrity": "sha512-H341wuxQHhwe/yLmORHzwC3DGzFZgGzh+TpwfyK2zjeYVbwDZ3Bax8M+9CzIED9jIBdEfAmfGzxKUHFugpXTtQ==", + "version": "1.0.83-3", + "integrity": "sha512-zNmVj3ZDmI3dFmBigfEMzEvMxyjBjL5+nTVxrt9fvTA+29jI0C6A+cdCqrad3fJ1RKgn2RbsZyhnpyViPNhNDw==", "cpu": [ "arm64" ], @@ -606,8 +606,8 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.82-0", - "integrity": "sha512-f1ba3gG8NaoYWFHtHaHcLN4It7mclkWdCOXvwFPqPEwqCEIx/+Zh6VHiOeIcNWRS0elRP6QYDCKaTDy1TW27uQ==", + "version": "1.0.83-3", + "integrity": "sha512-pbw739Jdwjr4ovsjwpMI1hguZyOPwTy/fdVnrgBv1nazXxIFrwE3tq0FgzF0NnNcs4r5LXdbIBjKQP+HKFZagA==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index cceca0b959..7b04900614 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.82-0", + "@github/copilot": "^1.0.83-3", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 245b035c5c..2aa535e798 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -24,13 +24,21 @@ import { ShellConfig } from "./util"; describe("ReplayingCapiProxy", () => { let tempDir: string; let workDir: string; + let githubActions: string | undefined; beforeEach(async () => { + githubActions = process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_ACTIONS; tempDir = await mkdtemp(path.join(os.tmpdir(), "capi-proxy-test-")); workDir = path.join(tempDir, "work"); }); afterEach(async () => { + if (githubActions === undefined) { + delete process.env.GITHUB_ACTIONS; + } else { + process.env.GITHUB_ACTIONS = githubActions; + } await rm(tempDir, { recursive: true, force: true }); }); @@ -1591,6 +1599,42 @@ Always include PINEAPPLE_COCONUT_42. } }); + test.each([false, true])( + "defaults to Sonnet 5 without stored models (capture exists: %s)", + async (captureExists) => { + const cachePath = path.join(tempDir, "cache.yaml"); + if (captureExists) { + await writeFile( + cachePath, + yaml.stringify({ + models: [], + conversations: [], + } satisfies NormalizedData), + ); + } + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/models", { + method: "GET", + }); + expect(response.status).toBe(200); + const parsed = JSON.parse(response.body) as { + data: Array<{ id: string }>; + }; + expect(parsed.data.map((model) => model.id)).toEqual(["claude-sonnet-5"]); + } finally { + await proxy.stop(); + } + }, + ); + test("returns cached models for /models endpoint", async () => { const cachePath = path.join(tempDir, "cache.yaml"); const cacheContent = yaml.stringify({ diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index f30a9fbd77..7da04cabc4 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -107,7 +107,7 @@ const normalizedToolNames: Record = { * Default model to use when no stored data is available for a given test. * This enables responding to /models without needing to have a capture file. */ -const defaultModel = "claude-sonnet-4.5"; +const defaultModel = "claude-sonnet-5"; /** * An HTTP proxy that not only captures HTTP exchanges, but also stores them in a file on disk and diff --git a/test/snapshots/abort/should_abort_during_active_streaming.yaml b/test/snapshots/abort/should_abort_during_active_streaming.yaml index 70981ee597..8556fec349 100644 --- a/test/snapshots/abort/should_abort_during_active_streaming.yaml +++ b/test/snapshots/abort/should_abort_during_active_streaming.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/abort/should_abort_during_active_tool_execution.yaml b/test/snapshots/abort/should_abort_during_active_tool_execution.yaml index 99ea89f7b0..a975cae284 100644 --- a/test/snapshots/abort/should_abort_during_active_tool_execution.yaml +++ b/test/snapshots/abort/should_abort_during_active_tool_execution.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml b/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml index ac5cc94336..498bfebad0 100644 --- a/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml +++ b/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml b/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml index 4ba16d4d81..1624cb5317 100644 --- a/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml +++ b/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml b/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml index 49944c9732..c33ce7e8f2 100644 --- a/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml +++ b/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml b/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml index 4549b99dc1..417f0b3446 100644 --- a/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml +++ b/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml b/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml index 705378061f..97e08e5852 100644 --- a/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml +++ b/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml b/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml index 01cf1298d3..5c3c638d50 100644 --- a/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml +++ b/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_capture_stderr_output.yaml b/test/snapshots/builtin_tools/should_capture_stderr_output.yaml index 0ba318148d..ba0bd164ea 100644 --- a/test/snapshots/builtin_tools/should_capture_stderr_output.yaml +++ b/test/snapshots/builtin_tools/should_capture_stderr_output.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_create_a_new_file.yaml b/test/snapshots/builtin_tools/should_create_a_new_file.yaml index 8afe8b38b6..869777e9a4 100644 --- a/test/snapshots/builtin_tools/should_create_a_new_file.yaml +++ b/test/snapshots/builtin_tools/should_create_a_new_file.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml b/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml index 3f4e986906..922d7751dd 100644 --- a/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml +++ b/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml b/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml index 6cf85ea51d..338dd03ef2 100644 --- a/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml +++ b/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml b/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml index c5c00fb65c..410da4e089 100644 --- a/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml +++ b/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml b/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml index 601ae0f04c..23a7fec7a1 100644 --- a/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml +++ b/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml b/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml index f0af500b6e..615b9ae39e 100644 --- a/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml +++ b/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml b/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml +++ b/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml b/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml +++ b/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_force_stop_client.yaml b/test/snapshots/client/should_force_stop_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_force_stop_client.yaml +++ b/test/snapshots/client/should_force_stop_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_get_authenticated_status.yaml b/test/snapshots/client/should_get_authenticated_status.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_get_authenticated_status.yaml +++ b/test/snapshots/client/should_get_authenticated_status.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_get_status.yaml b/test/snapshots/client/should_get_status.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_get_status.yaml +++ b/test/snapshots/client/should_get_status.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_list_models_when_authenticated.yaml b/test/snapshots/client/should_list_models_when_authenticated.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_list_models_when_authenticated.yaml +++ b/test/snapshots/client/should_list_models_when_authenticated.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml b/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml +++ b/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml b/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml +++ b/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_stop_client_with_active_session.yaml b/test/snapshots/client/should_stop_client_with_active_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_stop_client_with_active_session.yaml +++ b/test/snapshots/client/should_stop_client_with_active_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client_api/should_delete_session_by_id.yaml b/test/snapshots/client_api/should_delete_session_by_id.yaml index 0981462bf6..bfeaca5f6d 100644 --- a/test/snapshots/client_api/should_delete_session_by_id.yaml +++ b/test/snapshots/client_api/should_delete_session_by_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml b/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml index 8486832a46..8e3aa9d94c 100644 --- a/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml +++ b/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml b/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml index beb8b443d2..3569a8ca8e 100644 --- a/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml +++ b/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml b/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml index 4419c5854e..bb4a148072 100644 --- a/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml +++ b/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml b/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml index 3b9da534c2..62da2b03a2 100644 --- a/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml +++ b/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml b/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml +++ b/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml b/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml index 469d091288..c87d0cb124 100644 --- a/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml +++ b/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml b/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/commands/session_with_commands_creates_successfully.yaml b/test/snapshots/commands/session_with_commands_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/commands/session_with_commands_creates_successfully.yaml +++ b/test/snapshots/commands/session_with_commands_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/commands/session_with_commands_resumes_successfully.yaml b/test/snapshots/commands/session_with_commands_resumes_successfully.yaml index 0981462bf6..bfeaca5f6d 100644 --- a/test/snapshots/commands/session_with_commands_resumes_successfully.yaml +++ b/test/snapshots/commands/session_with_commands_resumes_successfully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml b/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml +++ b/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml b/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml index 9773a132f5..6d966efe2f 100644 --- a/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml +++ b/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml index 9deca12228..7d476ec66c 100644 --- a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml +++ b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml b/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml +++ b/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml b/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml index 16db486e88..e454b1e96c 100644 --- a/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml +++ b/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml b/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml +++ b/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml b/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml +++ b/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml b/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml +++ b/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml b/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml +++ b/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml b/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml +++ b/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/input_returns_freeform_value.yaml b/test/snapshots/elicitation/input_returns_freeform_value.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/input_returns_freeform_value.yaml +++ b/test/snapshots/elicitation/input_returns_freeform_value.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/select_returns_selected_option.yaml b/test/snapshots/elicitation/select_returns_selected_option.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/select_returns_selected_option.yaml +++ b/test/snapshots/elicitation/select_returns_selected_option.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml b/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml +++ b/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml b/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml +++ b/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml b/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml +++ b/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml b/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml index caac261e2a..1499c083ca 100644 --- a/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml +++ b/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml b/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml +++ b/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml b/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml index c8f272e6b9..726cbf9322 100644 --- a/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml +++ b/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml b/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml index ecc10bdbd6..2b24bf8a50 100644 --- a/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml +++ b/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml b/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml +++ b/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml b/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml index 46fd7715ab..717f09ea24 100644 --- a/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml +++ b/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml b/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml +++ b/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml b/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml index 1797cc16b1..6fcaebc6a0 100644 --- a/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml +++ b/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml b/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml index 10bca8e4b0..296ee0db9a 100644 --- a/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml +++ b/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml b/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml +++ b/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml b/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml index 2a5f1ae446..3bf5ee1f4d 100644 --- a/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml b/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml index f695c60f3d..86c2865756 100644 --- a/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml b/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml index 2860f52c15..a1ecaef30e 100644 --- a/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml +++ b/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml +++ b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml +++ b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml b/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml index 2a5f1ae446..3bf5ee1f4d 100644 --- a/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml b/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml index f695c60f3d..86c2865756 100644 --- a/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml b/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml index a340e9326c..f5cbaff9c6 100644 --- a/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml +++ b/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml b/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml index 8415fe771b..89c3ee9234 100644 --- a/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml +++ b/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml b/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml index 6485670a1c..6c1a055c04 100644 --- a/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml +++ b/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml b/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml index dcd2f32be2..2a17b8a0a3 100644 --- a/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml b/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml index bde2373cc2..bd61da81cb 100644 --- a/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml b/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml index 0d2da93e50..e200c7f68f 100644 --- a/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml b/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml index beb8b443d2..3569a8ca8e 100644 --- a/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml b/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml index bbe815735c..a1c3f4fb2d 100644 --- a/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml +++ b/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml b/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml index 75fccc4e1f..017000cfba 100644 --- a/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml +++ b/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml b/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml index c1d643b4c3..725222bfd3 100644 --- a/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml +++ b/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -13,7 +13,7 @@ conversations: Hi! 👋 - I'm GitHub Copilot CLI, powered by claude-sonnet-4.5. I'm here to help you with software engineering tasks + I'm GitHub Copilot CLI, powered by claude-sonnet-5. I'm here to help you with software engineering tasks like exploring codebases, running commands, making code changes, and more. diff --git a/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml b/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml index ceb291c9dd..688dc01e1e 100644 --- a/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml +++ b/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml b/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml index db2b029680..1fdf62b876 100644 --- a/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml +++ b/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml b/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml index 2f02a0570d..ef34d886b1 100644 --- a/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml +++ b/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml index 9703495c66..dbc06f70b1 100644 --- a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml index 82c9917c34..96321ffe3a 100644 --- a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml +++ b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml index 16db486e88..e454b1e96c 100644 --- a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml +++ b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml index 9703495c66..dbc06f70b1 100644 --- a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml index 65fe6664e6..5fab1b6334 100644 --- a/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml b/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml b/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml b/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml index f5506bb184..bf8c7b4d33 100644 --- a/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml +++ b/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml b/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml index 29ba0fc68b..b3eab15f66 100644 --- a/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml +++ b/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml b/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml index c1df8e8023..9aa12434df 100644 --- a/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml +++ b/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml b/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml +++ b/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml b/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml index 82c9917c34..96321ffe3a 100644 --- a/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml +++ b/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml b/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml index fac88270d5..6193c536b4 100644 --- a/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml +++ b/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml b/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml +++ b/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml b/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml +++ b/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml b/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml +++ b/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml b/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml index 6f23714d94..58ec8e370a 100644 --- a/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml +++ b/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml b/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml index 5d63a94018..cfa604adbe 100644 --- a/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml +++ b/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml b/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml index 19c271b4f1..30fee89306 100644 --- a/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml +++ b/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml @@ -1,8 +1,8 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - auto errors: - - model: claude-sonnet-4.5 + - model: claude-sonnet-5 status: 429 code: user_weekly_rate_limited message: You've reached your weekly rate limit. diff --git a/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml b/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml index 078ba05483..9ee28ad083 100644 --- a/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml +++ b/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml b/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml index 62f0d004a0..89f28fcbe1 100644 --- a/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml +++ b/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml b/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml index b6410e0d1b..925e8f3076 100644 --- a/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml +++ b/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml b/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml index 2397bfdc01..fce177c1c3 100644 --- a/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml +++ b/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml b/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml index ba9db87d08..105a472f9c 100644 --- a/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml +++ b/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml b/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml index c04864d827..39d56792d5 100644 --- a/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml +++ b/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml b/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml +++ b/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml b/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml index 583366363a..7d991ce94e 100644 --- a/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml +++ b/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml b/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml index 96dc365c65..3ea91fec16 100644 --- a/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml +++ b/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml index acae9a8b24..29574ef20b 100644 --- a/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml index f9fcc188a7..c39ad4f6e4 100644 --- a/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml index 4856cdc4c6..4e001d5cf2 100644 --- a/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml index 8a32e431a7..d8ffe5df61 100644 --- a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml +++ b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml index 4ae08f8a80..85098c636c 100644 --- a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml +++ b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml b/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml index 3b5c7dfe41..bef3a92876 100644 --- a/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml +++ b/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml b/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml index d7117cee65..de0c2634b1 100644 --- a/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml +++ b/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml b/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml +++ b/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml b/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml +++ b/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml b/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml +++ b/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml b/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml +++ b/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/permissions/async_permission_handler.yaml b/test/snapshots/permissions/async_permission_handler.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/async_permission_handler.yaml +++ b/test/snapshots/permissions/async_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/deny_permission.yaml b/test/snapshots/permissions/deny_permission.yaml index 480b640fb3..63ed54f671 100644 --- a/test/snapshots/permissions/deny_permission.yaml +++ b/test/snapshots/permissions/deny_permission.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_errors.yaml b/test/snapshots/permissions/permission_handler_errors.yaml index cee78a0929..f8f274d66e 100644 --- a/test/snapshots/permissions/permission_handler_errors.yaml +++ b/test/snapshots/permissions/permission_handler_errors.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_for_shell_commands.yaml b/test/snapshots/permissions/permission_handler_for_shell_commands.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/permission_handler_for_shell_commands.yaml +++ b/test/snapshots/permissions/permission_handler_for_shell_commands.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_for_write_operations.yaml b/test/snapshots/permissions/permission_handler_for_write_operations.yaml index 3f05a8c6de..9811c4a6ab 100644 --- a/test/snapshots/permissions/permission_handler_for_write_operations.yaml +++ b/test/snapshots/permissions/permission_handler_for_write_operations.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/resume_session_with_permission_handler.yaml b/test/snapshots/permissions/resume_session_with_permission_handler.yaml index 6296a0d73e..ade442a927 100644 --- a/test/snapshots/permissions/resume_session_with_permission_handler.yaml +++ b/test/snapshots/permissions/resume_session_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml b/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml index ef6f60dbed..8e28d8156d 100644 --- a/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml +++ b/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml b/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml index 0dfbd9e6b2..5f9a98ac80 100644 --- a/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml +++ b/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml index 0d25979c7c..0f7c4782cc 100644 --- a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml +++ b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml index 9a23c55f0a..fc802f16da 100644 --- a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml +++ b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_async_permission_handler.yaml b/test/snapshots/permissions/should_handle_async_permission_handler.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/should_handle_async_permission_handler.yaml +++ b/test/snapshots/permissions/should_handle_async_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml b/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml index 924123536c..7432c4caf9 100644 --- a/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml +++ b/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml b/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml index 2a2db62101..17b75a4925 100644 --- a/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml +++ b/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml b/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml index ef6f60dbed..8e28d8156d 100644 --- a/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml +++ b/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml b/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml index 3f05a8c6de..9811c4a6ab 100644 --- a/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml +++ b/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml b/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml index 90407df6fc..cd73f7e165 100644 --- a/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml +++ b/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml b/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml index 6296a0d73e..ade442a927 100644 --- a/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml +++ b/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml b/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml index 3a6d66dc8d..1c33d19a98 100644 --- a/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml +++ b/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml b/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml index 19398ce5d6..9ecbdb6a51 100644 --- a/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml +++ b/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml b/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml index 9199977dba..554b4b1f70 100644 --- a/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml +++ b/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml b/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml index 90407df6fc..cd73f7e165 100644 --- a/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml +++ b/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml index c771647842..5e1970247a 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml index d7ff876a6c..b8d0e30d19 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml index 1d92fe8eed..cf77c42735 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml b/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml +++ b/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml b/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml index 2ef3733e00..31753aed11 100644 --- a/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml +++ b/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml @@ -1,21 +1,37 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system content: ${system} - role: user - content: Use the create tool to create rewind-sdk.txt containing exactly SDK rewind content. After the tool succeeds, - reply with exactly SDK_REWIND_DONE. + content: Use the edit tool to replace the exact contents of rewind-sdk.txt from Original rewind content to Prepared + rewind content. After the tool succeeds, reply with exactly SDK_REWIND_READY. - role: assistant tool_calls: - id: toolcall_0 type: function function: - name: create - arguments: '{"path":"${workdir}/rewind-sdk.txt","file_text":"SDK rewind content"}' + name: edit + arguments: '{"path":"${workdir}/rewind-sdk.txt","old_str":"Original rewind content","new_str":"Prepared rewind + content"}' - role: tool tool_call_id: toolcall_0 - content: Created file ${workdir}/rewind-sdk.txt with 18 characters + content: File ${workdir}/rewind-sdk.txt updated with changes. + - role: assistant + content: SDK_REWIND_READY + - role: user + content: Use the edit tool to replace the exact contents of rewind-sdk.txt from Prepared rewind content to SDK rewind + content. After the tool succeeds, reply with exactly SDK_REWIND_DONE. + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: edit + arguments: '{"path":"${workdir}/rewind-sdk.txt","old_str":"Prepared rewind content","new_str":"SDK rewind content"}' + - role: tool + tool_call_id: toolcall_1 + content: File ${workdir}/rewind-sdk.txt updated with changes. - role: assistant content: SDK_REWIND_DONE diff --git a/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml b/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml +++ b/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml b/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml +++ b/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml b/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml +++ b/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml b/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml +++ b/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml b/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml +++ b/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml b/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml +++ b/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_call_agent_reload.yaml b/test/snapshots/rpc_agents/should_call_agent_reload.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_call_agent_reload.yaml +++ b/test/snapshots/rpc_agents/should_call_agent_reload.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_deselect_current_agent.yaml b/test/snapshots/rpc_agents/should_deselect_current_agent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_deselect_current_agent.yaml +++ b/test/snapshots/rpc_agents/should_deselect_current_agent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml b/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml +++ b/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml b/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml +++ b/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml b/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml +++ b/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml b/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml +++ b/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml b/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml +++ b/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml b/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml index 7c58a8da96..200cc28dba 100644 --- a/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml +++ b/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml b/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml b/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml index 7c58a8da96..200cc28dba 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml b/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml b/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml b/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml b/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml +++ b/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml b/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml +++ b/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml b/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml b/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml b/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml b/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml b/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml b/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml +++ b/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml b/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml +++ b/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml b/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml +++ b/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml b/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml +++ b/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml b/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml +++ b/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml b/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml +++ b/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml b/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml +++ b/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml b/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml +++ b/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml b/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml +++ b/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml b/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml +++ b/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml b/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml +++ b/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml b/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml +++ b/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml b/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml +++ b/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml b/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml +++ b/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml b/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml +++ b/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml b/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml +++ b/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml b/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml +++ b/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml b/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml b/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml b/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml b/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml b/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml index b276b6a398..9011c931e7 100644 --- a/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml @@ -1,4 +1,4 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - gpt-5.4 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml b/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml b/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml +++ b/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml b/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml index 001e828461..ad474d2472 100644 --- a/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml +++ b/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml b/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml +++ b/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml b/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml +++ b/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml b/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml index 76ba212c5e..c1223c8a20 100644 --- a/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml +++ b/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml b/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml index 2313bd1483..f6fc12e544 100644 --- a/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml +++ b/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml b/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml +++ b/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml b/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml +++ b/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml b/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml +++ b/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml b/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml +++ b/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml b/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml +++ b/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml b/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml +++ b/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml b/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml +++ b/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml b/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml index 788c5b75f2..e59f571b4a 100644 --- a/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml +++ b/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml b/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml +++ b/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml b/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml index 6760888d7b..6ea625da4d 100644 --- a/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml +++ b/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml @@ -1,10 +1,20 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system content: ${system} - role: user - content: "Reply with exactly: RUST_CONTEXT_INFO" + content: "Use processing_barrier, then reply with exactly: RUST_CONTEXT_INFO" + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: processing_barrier + arguments: "{}" + - role: tool + tool_call_id: toolcall_0 + content: PROCESSING_OBSERVED - role: assistant content: RUST_CONTEXT_INFO diff --git a/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml b/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml +++ b/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml b/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml +++ b/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml b/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml +++ b/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml b/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml b/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml index c4798dc83d..24c4d6bbf2 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml b/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml index 73f0499002..33bb479064 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml b/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml b/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml b/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml b/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml +++ b/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml b/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml +++ b/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml b/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml b/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml +++ b/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml b/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml +++ b/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml b/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml b/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml b/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml index 65ced1e366..d2d8a272c1 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml b/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml b/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml b/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml +++ b/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml b/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml +++ b/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml b/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml b/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml b/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml index 41bbe583d2..a9dbdd3751 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml b/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml index fec44be1fe..c5c1ce1c11 100644 --- a/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml +++ b/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml b/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml index 20eefc57a9..8bfc5f5053 100644 --- a/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml +++ b/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml b/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml index 192105ac7a..476d5aeb1c 100644 --- a/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml +++ b/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml b/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml index c97e969df6..57bc48df32 100644 --- a/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml +++ b/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml b/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml +++ b/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml b/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml +++ b/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml b/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml +++ b/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml b/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml index c2e705ed2d..4ef08ab255 100644 --- a/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml +++ b/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml b/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml +++ b/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_throws_on_timeout.yaml b/test/snapshots/session/sendandwait_throws_on_timeout.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/sendandwait_throws_on_timeout.yaml +++ b/test/snapshots/session/sendandwait_throws_on_timeout.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml b/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml index a03140fa17..7e1a256daa 100644 --- a/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml +++ b/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_abort_a_session.yaml b/test/snapshots/session/should_abort_a_session.yaml index dbbbd32aa7..4ec09b51f3 100644 --- a/test/snapshots/session/should_abort_a_session.yaml +++ b/test/snapshots/session/should_abort_a_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_accept_blob_attachments.yaml b/test/snapshots/session/should_accept_blob_attachments.yaml index 1cca7142db..4caf7c8707 100644 --- a/test/snapshots/session/should_accept_blob_attachments.yaml +++ b/test/snapshots/session/should_accept_blob_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml index 3bf4a39f05..a0a9f1c391 100644 --- a/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_availabletools.yaml b/test/snapshots/session/should_create_a_session_with_availabletools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_availabletools.yaml +++ b/test/snapshots/session/should_create_a_session_with_availabletools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml index f3ce077a62..50d9cbecfa 100644 --- a/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -8,8 +8,8 @@ conversations: content: Who are you? - role: assistant content: >- - I'm **GitHub Copilot CLI**, a terminal assistant built by GitHub. I'm powered by claude-sonnet-4.5 (model ID: - claude-sonnet-4.5). + I'm **GitHub Copilot CLI**, a terminal assistant built by GitHub. I'm powered by claude-sonnet-5 (model ID: + claude-sonnet-5). I'm here to help you with software engineering tasks, including: diff --git a/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml b/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml +++ b/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_excludedtools.yaml b/test/snapshots/session/should_create_a_session_with_excludedtools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_excludedtools.yaml +++ b/test/snapshots/session/should_create_a_session_with_excludedtools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml index ebe0881b90..88d25dc817 100644 --- a/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_session_with_custom_config_dir.yaml b/test/snapshots/session/should_create_session_with_custom_config_dir.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_session_with_custom_config_dir.yaml +++ b/test/snapshots/session/should_create_session_with_custom_config_dir.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_session_with_custom_tool.yaml b/test/snapshots/session/should_create_session_with_custom_tool.yaml index 4ae6dab721..3a08cbd9fe 100644 --- a/test/snapshots/session/should_create_session_with_custom_tool.yaml +++ b/test/snapshots/session/should_create_session_with_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_delete_session.yaml b/test/snapshots/session/should_delete_session.yaml index fb8249d325..72176060ce 100644 --- a/test/snapshots/session/should_delete_session.yaml +++ b/test/snapshots/session/should_delete_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_last_session_id.yaml b/test/snapshots/session/should_get_last_session_id.yaml index 3b9da534c2..62da2b03a2 100644 --- a/test/snapshots/session/should_get_last_session_id.yaml +++ b/test/snapshots/session/should_get_last_session_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_session_metadata.yaml b/test/snapshots/session/should_get_session_metadata.yaml index b326528e1d..f9b44b833a 100644 --- a/test/snapshots/session/should_get_session_metadata.yaml +++ b/test/snapshots/session/should_get_session_metadata.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_session_metadata_by_id.yaml b/test/snapshots/session/should_get_session_metadata_by_id.yaml index b326528e1d..f9b44b833a 100644 --- a/test/snapshots/session/should_get_session_metadata_by_id.yaml +++ b/test/snapshots/session/should_get_session_metadata_by_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_have_stateful_conversation.yaml b/test/snapshots/session/should_have_stateful_conversation.yaml index 39d3c5acc5..1309304fc4 100644 --- a/test/snapshots/session/should_have_stateful_conversation.yaml +++ b/test/snapshots/session/should_have_stateful_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_list_sessions.yaml b/test/snapshots/session/should_list_sessions.yaml index 4683506570..d2c3ebdd4f 100644 --- a/test/snapshots/session/should_list_sessions.yaml +++ b/test/snapshots/session/should_list_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_list_sessions_with_context.yaml b/test/snapshots/session/should_list_sessions_with_context.yaml index 8486832a46..8e3aa9d94c 100644 --- a/test/snapshots/session/should_list_sessions_with_context.yaml +++ b/test/snapshots/session/should_list_sessions_with_context.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_log_messages_at_various_levels.yaml b/test/snapshots/session/should_log_messages_at_various_levels.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/should_log_messages_at_various_levels.yaml +++ b/test/snapshots/session/should_log_messages_at_various_levels.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_receive_session_events.yaml b/test/snapshots/session/should_receive_session_events.yaml index 229563a4cf..406c34f455 100644 --- a/test/snapshots/session/should_receive_session_events.yaml +++ b/test/snapshots/session/should_receive_session_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml b/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml index bd02858372..667d0c3528 100644 --- a/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml +++ b/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml b/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml index b012e26ea8..ff54ee73da 100644 --- a/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml +++ b/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_custom_requestheaders.yaml b/test/snapshots/session/should_send_with_custom_requestheaders.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session/should_send_with_custom_requestheaders.yaml +++ b/test/snapshots/session/should_send_with_custom_requestheaders.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_directory_attachment.yaml b/test/snapshots/session/should_send_with_directory_attachment.yaml index aa410c9295..f2e8835c5d 100644 --- a/test/snapshots/session/should_send_with_directory_attachment.yaml +++ b/test/snapshots/session/should_send_with_directory_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_file_attachment.yaml b/test/snapshots/session/should_send_with_file_attachment.yaml index 2e8e4d1d2d..f2acea13fb 100644 --- a/test/snapshots/session/should_send_with_file_attachment.yaml +++ b/test/snapshots/session/should_send_with_file_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_github_reference_attachment.yaml b/test/snapshots/session/should_send_with_github_reference_attachment.yaml index 6e298de554..851f34c50f 100644 --- a/test/snapshots/session/should_send_with_github_reference_attachment.yaml +++ b/test/snapshots/session/should_send_with_github_reference_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_mode_property.yaml b/test/snapshots/session/should_send_with_mode_property.yaml index 4fec86c7f4..e6639acbdb 100644 --- a/test/snapshots/session/should_send_with_mode_property.yaml +++ b/test/snapshots/session/should_send_with_mode_property.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_selection_attachment.yaml b/test/snapshots/session/should_send_with_selection_attachment.yaml index ad6a2a28e8..c46f3d679b 100644 --- a/test/snapshots/session/should_send_with_selection_attachment.yaml +++ b/test/snapshots/session/should_send_with_selection_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_set_model_on_existing_session.yaml b/test/snapshots/session/should_set_model_on_existing_session.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/should_set_model_on_existing_session.yaml +++ b/test/snapshots/session/should_set_model_on_existing_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_set_model_with_reasoningeffort.yaml b/test/snapshots/session/should_set_model_with_reasoningeffort.yaml index ccf204d2ae..db151e253a 100644 --- a/test/snapshots/session/should_set_model_with_reasoningeffort.yaml +++ b/test/snapshots/session/should_set_model_with_reasoningeffort.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - gpt-5.4 conversations: - messages: diff --git a/test/snapshots/session_config/should_accept_blob_attachments.yaml b/test/snapshots/session_config/should_accept_blob_attachments.yaml index 672ca74d4e..71c7900348 100644 --- a/test/snapshots/session_config/should_accept_blob_attachments.yaml +++ b/test/snapshots/session_config/should_accept_blob_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_accept_message_attachments.yaml b/test/snapshots/session_config/should_accept_message_attachments.yaml index 5525d1fb04..188905752b 100644 --- a/test/snapshots/session_config/should_accept_message_attachments.yaml +++ b/test/snapshots/session_config/should_accept_message_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml b/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml b/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml index 3cbf86e981..4ef99a4d36 100644 --- a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml +++ b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml b/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml +++ b/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml b/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml +++ b/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml b/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml b/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_session_limits_on_create.yaml b/test/snapshots/session_config/should_apply_session_limits_on_create.yaml index 904d69c872..142c4f6229 100644 --- a/test/snapshots/session_config/should_apply_session_limits_on_create.yaml +++ b/test/snapshots/session_config/should_apply_session_limits_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml b/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml index 904d69c872..142c4f6229 100644 --- a/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml b/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml index a455f6f6f3..a5111fd56a 100644 --- a/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml b/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml index 9d3dd78ff1..ec7d207e2e 100644 --- a/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml b/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml +++ b/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml b/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml +++ b/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml b/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml +++ b/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml b/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml +++ b/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml b/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml +++ b/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_use_custom_session_id.yaml b/test/snapshots/session_config/should_use_custom_session_id.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_use_custom_session_id.yaml +++ b/test/snapshots/session_config/should_use_custom_session_id.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml b/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml +++ b/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml b/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml index 40000d491b..dd2b4592b3 100644 --- a/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml +++ b/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml b/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml index 1eb0acd729..a4000a80e6 100644 --- a/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml +++ b/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml b/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml index b987a4e630..6afc03cb05 100644 --- a/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml +++ b/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml b/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml index 4744667cd7..2d2d1e38ae 100644 --- a/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml +++ b/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml b/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml index e80ce51e64..9b8f1eb0d9 100644 --- a/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml +++ b/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml b/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml index 5b0e81b22d..90ceee231a 100644 --- a/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml +++ b/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml b/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml index 269a80f11a..9aba0fefe1 100644 --- a/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml +++ b/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml b/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml index 455652bfd8..086c69225d 100644 --- a/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml +++ b/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml b/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml index 5e62634668..67f8e0121e 100644 --- a/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml +++ b/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml b/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml index 0a0325417c..59ed4624c7 100644 --- a/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml +++ b/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml b/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml index 32b8bc354f..b2b50348f5 100644 --- a/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml +++ b/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml b/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml index b26cfbfebe..e51bc5d247 100644 --- a/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml +++ b/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml b/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml index b302f117d4..6e38f93031 100644 --- a/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml +++ b/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml b/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml index f4add013a1..47bd9bc305 100644 --- a/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml +++ b/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml b/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml index 274ab7d2fa..f39bcb95cd 100644 --- a/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml +++ b/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml b/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml index fd621f2b0b..005604fdd6 100644 --- a/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml +++ b/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml b/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml index fdb7ebca03..180fb52406 100644 --- a/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml +++ b/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml b/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml index 425c772fce..0a4b41c9f2 100644 --- a/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml +++ b/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml b/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml index 007c5c1c53..fcecde4286 100644 --- a/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml +++ b/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml b/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml index 38b35946b0..ee9e6452d2 100644 --- a/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml +++ b/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml b/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml index 0c678deab8..1cc5776a34 100644 --- a/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml +++ b/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml b/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml index 0c678deab8..1cc5776a34 100644 --- a/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml +++ b/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml b/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml index 3b18558220..c6d74510f4 100644 --- a/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml +++ b/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml b/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml index 25e10c4b12..ca514e0135 100644 --- a/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml +++ b/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml b/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml index d210f22ea0..c9c9d1200f 100644 --- a/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml +++ b/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml b/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml index 3b18558220..c6d74510f4 100644 --- a/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml +++ b/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml b/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml index 25e10c4b12..ca514e0135 100644 --- a/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml +++ b/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml b/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml index f920f8705b..4c5419a0ab 100644 --- a/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml +++ b/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml b/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml index c033a6cba1..5230710868 100644 --- a/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml +++ b/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml b/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml index 97939357ca..54e64b3edb 100644 --- a/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml +++ b/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml b/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml index 32e07aa5d5..3bf332978d 100644 --- a/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml +++ b/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml b/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml index a3a35bf25b..d853722e5b 100644 --- a/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml +++ b/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml b/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml index 5721955d27..e53acdf159 100644 --- a/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml +++ b/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml b/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml index 9d2c688c18..f73112abb5 100644 --- a/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml +++ b/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -13,5 +13,5 @@ conversations: watering, pests, plant identification, or growing tips, I'm here to help! - I'm powered by claude-sonnet-4.5, but I focus specifically on gardening topics. What plant or gardening + I'm powered by claude-sonnet-5, but I focus specifically on gardening topics. What plant or gardening question can I help you with today? diff --git a/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml index 98e57919c6..4db03cac93 100644 --- a/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml +++ b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml index c54f25e2aa..93b7221aa3 100644 --- a/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml +++ b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml index 32d6367390..94c50ff0bd 100644 --- a/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml +++ b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml b/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml index f8342047b7..c919c8d0b1 100644 --- a/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml +++ b/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml b/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml index 621dfc4e8d..0992eae919 100644 --- a/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml +++ b/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml index 6a829fb23f..3658586343 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml index 3fddb1600b..b328558471 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml index 891f75cb55..d329be915e 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml b/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml index 2388d7d8fd..6c8ee43bb9 100644 --- a/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml +++ b/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml b/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml index 71021d3b8d..bc65d05d6b 100644 --- a/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml +++ b/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/can_receive_and_return_complex_types.yaml b/test/snapshots/tools/can_receive_and_return_complex_types.yaml index be869484ea..982f66e3ad 100644 --- a/test/snapshots/tools/can_receive_and_return_complex_types.yaml +++ b/test/snapshots/tools/can_receive_and_return_complex_types.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml b/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml index 36d5adce4a..39d2bed2f0 100644 --- a/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml +++ b/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml b/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml index 47f9286e0c..59acd092aa 100644 --- a/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml +++ b/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_arity0.yaml b/test/snapshots/tools/ergonomic_tool_arity0.yaml index a55f486816..16bbbf1cd6 100644 --- a/test/snapshots/tools/ergonomic_tool_arity0.yaml +++ b/test/snapshots/tools/ergonomic_tool_arity0.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_arity2.yaml b/test/snapshots/tools/ergonomic_tool_arity2.yaml index e34c695bd4..b6cdd07614 100644 --- a/test/snapshots/tools/ergonomic_tool_arity2.yaml +++ b/test/snapshots/tools/ergonomic_tool_arity2.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_definition.yaml b/test/snapshots/tools/ergonomic_tool_definition.yaml index ebb05ce1b9..e61e95f930 100644 --- a/test/snapshots/tools/ergonomic_tool_definition.yaml +++ b/test/snapshots/tools/ergonomic_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/handles_tool_calling_errors.yaml b/test/snapshots/tools/handles_tool_calling_errors.yaml index 33226722dd..3b85da9acd 100644 --- a/test/snapshots/tools/handles_tool_calling_errors.yaml +++ b/test/snapshots/tools/handles_tool_calling_errors.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_built_in_tools.yaml b/test/snapshots/tools/invokes_built_in_tools.yaml index 0fba134424..72d959be46 100644 --- a/test/snapshots/tools/invokes_built_in_tools.yaml +++ b/test/snapshots/tools/invokes_built_in_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_custom_tool.yaml b/test/snapshots/tools/invokes_custom_tool.yaml index 6f212e4a79..6362a5e4f7 100644 --- a/test/snapshots/tools/invokes_custom_tool.yaml +++ b/test/snapshots/tools/invokes_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml b/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml index fcb6fa7266..2cfccba989 100644 --- a/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml +++ b/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/low_level_tool_definition.yaml b/test/snapshots/tools/low_level_tool_definition.yaml index 03cb0748a2..5921d061cb 100644 --- a/test/snapshots/tools/low_level_tool_definition.yaml +++ b/test/snapshots/tools/low_level_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml b/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml index 5410d3f295..ec2fa0a9a7 100644 --- a/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml +++ b/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml b/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml index a9aae3aea5..949ffd18fa 100644 --- a/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml +++ b/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml b/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml index cf0cf564da..50494f2324 100644 --- a/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml +++ b/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml b/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml index dfdfa63fa7..fa7a5cfbe1 100644 --- a/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml +++ b/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system