diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b058535ba..57a0bbcd7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -28,6 +28,8 @@ - .NET: `cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj` - **.NET testing note:** Never add `InternalsVisibleTo` to any project file when writing tests. Tests must only access public APIs. - Java: `cd java && mvn clean verify` (full build + tests), `mvn spotless:apply` (format code before commit) + - Java single test: `cd java && mvn test -Dtest=CopilotClientTest` | single method: `mvn test -Dtest=ToolsTest#testToolInvocation` + - Java format check only: `mvn spotless:check` | Build without tests: `mvn clean package -DskipTests` - **Java testing note:** Always use `mvn verify` without `-q` and without piping through `grep`. Never add `InternalsVisibleTo` equivalent — tests must only access public APIs. ## Testing & E2E tips ⚙️ @@ -36,6 +38,7 @@ - Tests rely on YAML snapshot exchanges under `test/snapshots/` — to add test scenarios, add or edit the appropriate YAML files and update tests. - The harness prints `Listening: http://...` — tests parse this URL to configure CLI or proxy. - Java E2E tests use `E2ETestContext` which manages a `CapiProxy` (Node.js replaying proxy). The harness is cloned during Maven's `generate-test-resources` phase to `java/target/copilot-sdk/`. +- Java test method names are converted to lowercase snake_case for snapshot filenames (avoids case collisions on macOS/Windows). ## Project-specific conventions & patterns ✅ @@ -43,6 +46,8 @@ - Infinite sessions are enabled by default and persist workspace state to `~/.copilot/session-state/{sessionId}`; compaction events are emitted (`session.compaction_start`, `session.compaction_complete`). See language READMEs for usage. - Streaming: when `streaming`/`Streaming=true` you receive delta events (`assistant.message_delta`, `assistant.reasoning_delta`) and final events (`assistant.message`, `assistant.reasoning`) — tests expect this behavior. - Type generation is centralized in `nodejs/scripts/generate-session-types.ts` and requires the `@github/copilot` schema to be present (often via `npm link` or installed package). +- Java code style: 4-space indent (Spotless + Eclipse formatter), fluent setter pattern for config classes, Javadoc required on public APIs (enforced by Checkstyle, except `json`/`events` packages). +- Java handlers return `CompletableFuture` (the Java equivalent of C# `async/await`). When porting from .NET: convert properties → getters/fluent setters, use Jackson (`ObjectMapper`, `@JsonProperty`) for serialization. ## Integration & environment notes ⚠️ @@ -50,6 +55,7 @@ - Some scripts (typegen, formatting) call external tools: `gofmt`, `dotnet format`, `tsx` (available via npm), `quicktype`/`quicktype-core` (used by the Node typegen script), and `prettier` (provided as an npm devDependency). Most of these are available through the repo's package scripts or devDependencies—run `just install` (and `cd nodejs && npm ci`) to install them. Ensure the required tools are available in CI / developer machines. - Tests may assume `node >= 18`, `python >= 3.9`, platform differences handled (Windows uses `shell=True` for npx in harness). - Java requires JDK 17+ and Maven 3.9+. Java E2E tests also require Node.js (for the replay proxy). +- Java pre-commit hook runs `mvn spotless:check`. Enable with `git config core.hooksPath .githooks` (auto-enabled in Copilot coding agent environment via `copilot-setup-steps.yml`). ## Where to add new code or tests 🧭 @@ -57,3 +63,9 @@ - Unit tests: `nodejs/test`, `python/*`, `go/*`, `dotnet/test`, `rust/tests`, `java/src/test/java` - E2E tests: `*/e2e/` folders that use the shared replay proxy and `test/snapshots/`, `java/src/test/java/**/e2e/` - Generated types: update schema in `@github/copilot` then run `cd nodejs && npm run generate:session-types` and commit generated files in `src/generated` or language generated location. Java generated types: `java/src/generated/java` + +## Boundaries — files you must NOT hand-edit ⛔ + +- `java/src/generated/java/` — auto-generated by `scripts/codegen/java.ts`; regenerate with `cd java && mvn generate-sources -Pcodegen`. +- `nodejs/src/generated/` — auto-generated by `npm run generate:session-types`. +- `test/snapshots/` — authoritative test fixtures; add/edit YAML here to change E2E behavior, but don't delete without understanding downstream impact. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 5b0e39e3f..7b6d73867 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -76,7 +76,7 @@ jobs: # Install gh-aw extension for advanced GitHub CLI features - name: Install gh-aw extension - uses: github/gh-aw/actions/setup-cli@4d44d0e89851a877f4ddc0cb6c0197e42b1016c5 # v0.73.0 + uses: github/gh-aw/actions/setup-cli@0feed75a980b06f247abbbf80127f8eb2c19e2c5 # v0.74.8 with: version: v0.74.4 diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml index 1c92e0109..2f150f1b1 100644 --- a/.github/workflows/java-publish-maven.yml +++ b/.github/workflows/java-publish-maven.yml @@ -1,4 +1,4 @@ -name: Publish to Maven Central +name: "Java Publish to Maven Central" env: # Disable Husky Git hooks in CI to prevent local development hooks @@ -35,6 +35,10 @@ jobs: publish-maven: name: Publish Java SDK to Maven Central runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ./java outputs: version: ${{ steps.versions.outputs.release_version }} steps: @@ -114,15 +118,6 @@ jobs: run: | VERSION="${{ steps.versions.outputs.release_version }}" - # Read the reference implementation SDK commit hash that this release is synced to - REFERENCE_IMPL_HASH=$(cat .lastmerge) - REFERENCE_IMPL_SHORT="${REFERENCE_IMPL_HASH:0:7}" - REFERENCE_IMPL_URL="https://github.com/github/copilot-sdk/commit/${REFERENCE_IMPL_HASH}" - echo "Reference implementation SDK sync: ${REFERENCE_IMPL_SHORT} (${REFERENCE_IMPL_URL})" - - # Update CHANGELOG.md with release version and Reference implementation sync hash - ../.github/scripts/release/update-changelog.sh "${VERSION}" "${REFERENCE_IMPL_HASH}" - # Update version in README.md (supports any version qualifier like -java.N, -java-preview.N, -beta-java.N) sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|${VERSION}|g" README.md sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|copilot-sdk-java:${VERSION}|g" README.md @@ -136,7 +131,7 @@ jobs: sed -i 's|copilot-sdk-java:${project\.version}|copilot-sdk-java:'"${VERSION}"'|g' jbang-example.java # Commit the documentation changes before release:prepare (requires clean working directory) - git add CHANGELOG.md README.md jbang-example.java + git add README.md jbang-example.java git commit -m "docs: update version references to ${VERSION}" # Save the commit SHA for potential rollback @@ -150,7 +145,7 @@ jobs: mvn -B release:prepare \ -DreleaseVersion=${{ steps.versions.outputs.release_version }} \ -DdevelopmentVersion=${{ steps.versions.outputs.dev_version }} \ - -DtagNameFormat=v@{project.version} \ + -DtagNameFormat=java/v@{project.version} \ -DpushChanges=true \ -Darguments="-DskipTests" env: @@ -185,6 +180,10 @@ jobs: needs: publish-maven if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ./java steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -194,7 +193,7 @@ jobs: VERSION="${{ needs.publish-maven.outputs.version }}" GROUP_ID="com.github" ARTIFACT_ID="copilot-sdk-java" - CURRENT_TAG="v${VERSION}" + CURRENT_TAG="java/v${VERSION}" if gh release view "${CURRENT_TAG}" >/dev/null 2>&1; then echo "Release ${CURRENT_TAG} already exists. Skipping creation." @@ -203,11 +202,10 @@ jobs: # Generate release notes from template export VERSION GROUP_ID ARTIFACT_ID - RELEASE_NOTES=$(envsubst < .github/workflows/notes.template) + RELEASE_NOTES=$(envsubst < $GITHUB_WORKSPACE/.github/workflows/java.notes.template) # Get the previous tag for generating notes - PREV_TAG=$(git tag --list 'v*' --sort=-version:refname \ - | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-java(-preview)?\.[0-9]+)?$' \ + PREV_TAG=$(git tag --list 'java/v*' --sort=-version:refname \ | grep -Fxv "${CURRENT_TAG}" \ | head -n 1) @@ -229,28 +227,7 @@ jobs: gh release create "${GH_ARGS[@]}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Move 'latest' tag to new release - run: | - VERSION="${{ needs.publish-maven.outputs.version }}" - git tag -f latest "v${VERSION}" - git push origin latest --force - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - deploy-site: - name: Deploy Documentation - needs: [publish-maven, github-release] - runs-on: ubuntu-latest - permissions: - actions: write - contents: read - steps: - - name: Trigger site deployment - run: | - gh workflow run deploy-site.yml \ - --repo ${{ github.repository }} \ - -f version="${{ needs.publish-maven.outputs.version }}" \ - -f publish_as_latest=true + - name: Trigger changelog generation + run: gh workflow run release-changelog.lock.yml -f tag="java/v${{ needs.publish-maven.outputs.version }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml index ddc04c40b..7bc231c73 100644 --- a/.github/workflows/java-publish-snapshot.yml +++ b/.github/workflows/java-publish-snapshot.yml @@ -1,4 +1,4 @@ -name: Publish Snapshot to Maven Central +name: Java Publish Snapshot to Maven Central env: HUSKY: 0 @@ -19,6 +19,10 @@ jobs: publish-snapshot: name: Publish SNAPSHOT to Maven Central runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ./java steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/.github/workflows/java-smoke-test.yml b/.github/workflows/java-smoke-test.yml new file mode 100644 index 000000000..cffef0a35 --- /dev/null +++ b/.github/workflows/java-smoke-test.yml @@ -0,0 +1,161 @@ +name: "Java smoke test" + +on: + workflow_dispatch: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: true + +permissions: + contents: read + +jobs: + smoke-test-jdk17: + name: Build SDK and run smoke test (JDK 17) + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "17" + distribution: "microsoft" + cache: "maven" + + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v6 + with: + node-version: 22 + + - name: Read pinned @github/copilot version from pom.xml + id: cli-version + run: | + PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" + VERSION=$(sed -n "s|.*<${PROP}>\(.*\).*|\1|p" pom.xml | head -n 1 | tr -d '[:space:]') + if [[ -z "$VERSION" || "$VERSION" == "PRIMER_TO_REPLACE" ]]; then + echo "::error::Could not read pinned @github/copilot version from pom.xml property <${PROP}>" >&2 + exit 1 + fi + echo "Pinned @github/copilot version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Install Copilot CLI globally (pinned to pom.xml version) + run: npm install -g "@github/copilot@${{ steps.cli-version.outputs.version }}" + + - name: Verify CLI works + run: copilot --version + + - name: Build SDK and install to local repo + run: mvn -DskipTests -Pskip-test-harness clean install + + - name: Create and run smoke test via Copilot CLI + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + run: | + cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' + You are running inside the copilot-sdk monorepo, in the java/ subdirectory. + The SDK has already been built and installed into the local Maven repository. + JDK 17 and Maven are already installed and on PATH. + + Execute the prompt at `src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides: + + **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`. + + **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly. + + Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). + + If any step fails, exit with a non-zero exit code. Do not silently fix errors. + PROMPT_EOF + + copilot --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" + + - name: Run smoke test jar + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + run: | + cd smoke-test + java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar + echo "Smoke test passed (exit code 0)" + + smoke-test-java25: + name: Build SDK and run smoke test (JDK 25) + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "25" + distribution: "microsoft" + cache: "maven" + + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v6 + with: + node-version: 22 + + - name: Read pinned @github/copilot version from pom.xml + id: cli-version + run: | + PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" + VERSION=$(sed -n "s|.*<${PROP}>\(.*\).*|\1|p" pom.xml | head -n 1 | tr -d '[:space:]') + if [[ -z "$VERSION" || "$VERSION" == "PRIMER_TO_REPLACE" ]]; then + echo "::error::Could not read pinned @github/copilot version from pom.xml property <${PROP}>" >&2 + exit 1 + fi + echo "Pinned @github/copilot version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Install Copilot CLI globally (pinned to pom.xml version) + run: npm install -g "@github/copilot@${{ steps.cli-version.outputs.version }}" + + - name: Verify CLI works + run: copilot --version + + - name: Build SDK and install to local repo + run: mvn -DskipTests -Pskip-test-harness clean install + + - name: Create and run smoke test via Copilot CLI + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + run: | + cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' + You are running inside the copilot-sdk monorepo, in the java/ subdirectory. + The SDK has already been built and installed into the local Maven repository. + JDK 25 and Maven are already installed and on PATH. + + Execute the prompt at `src/test/prompts/PROMPT-smoke-test.md` with the following critical overrides: + + **Critical override — disable SNAPSHOT updates (but allow downloads):** The goal of this workflow is to validate the SDK SNAPSHOT that was just built and installed locally, not any newer SNAPSHOT that might exist in a remote repository. To ensure Maven does not download a newer timestamped SNAPSHOT of the SDK while still allowing it to download any missing plugins or dependencies, you must run the smoke-test Maven build without `-U` and with `--no-snapshot-updates`, so that it uses the locally installed SDK artifact. Use `mvn --no-snapshot-updates clean package` instead of `mvn -U clean package` or `mvn -o clean package`. + + **Critical override — do NOT run the jar:** Stop after the `mvn --no-snapshot-updates clean package` build succeeds. Do NOT execute Step 4 (java -jar) or Step 5 (verify exit code) from the prompt. The workflow will run the jar in a separate deterministic step to guarantee the exit code propagates correctly. + + **Critical override — enable Virtual Threads for JDK 25:** After creating the Java source file from the README "Quick Start" section but BEFORE building, you must modify the source file to enable virtual thread support. The Quick Start code contains inline comments that start with `// JDK 25+:` — these are instructions. Find every such comment and follow what it says (comment out lines it says to comment out, uncomment lines it says to uncomment). Add any imports required by the newly uncommented code (e.g. `java.util.concurrent.Executors`). + Also set `maven.compiler.source` and `maven.compiler.target` to `25` in the `pom.xml`. + + Follow steps 1-3 only: create the `smoke-test/` directory, create `pom.xml` and the Java source file exactly as specified, apply the JDK 25 virtual thread modifications described above, and build with `mvn --no-snapshot-updates clean package` (no SNAPSHOT updates and without `-U`). + + If any step fails, exit with a non-zero exit code. Do not silently fix errors. + PROMPT_EOF + + copilot --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" + + - name: Run smoke test jar + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + run: | + cd smoke-test + java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar + echo "Smoke test passed (exit code 0)" diff --git a/.github/workflows/java.notes.template b/.github/workflows/java.notes.template new file mode 100644 index 000000000..bec50a91b --- /dev/null +++ b/.github/workflows/java.notes.template @@ -0,0 +1,26 @@ +# Installation + +ℹ️ **Public Preview:** This is the official Java SDK for GitHub Copilot. This repository treats the official .NET and Node.js SDKs for GitHub Copilot as reference implementations. These SDKs are all officially supported as GitHub open source projects. The Java implementation follows the backward compatibility guarantees offered by the reference implementations. + +⚠️ **Artifact versioning plan:** Releases of this implementation track releases of the reference implementation. For each release of the reference implementation, there may follow a corresponding release of this implementation with the same number as the reference implementation. Release identifiers of the reference implementation are in the form `vMaj.Min.Micro`. For example v0.1.32. The corresponding maven version for the release will be `Maj.Min.Micro-java.N`, where `Maj`, `Min` and `Micro` are the corresponding numbers for the reference implementation release, and `N` is a monotonically increasing sequence number starting with 0 for each release. See the corresponding architectural decision record for more information in the `docs/adr` directory of the source code. + +📦 [View on Maven Central](https://central.sonatype.com/artifact/${GROUP_ID}/${ARTIFACT_ID}/${VERSION}) + +## Maven +```xml + + ${GROUP_ID} + ${ARTIFACT_ID} + ${VERSION} + +``` + +## Gradle (Kotlin DSL) +```kotlin +implementation("${GROUP_ID}:${ARTIFACT_ID}:${VERSION}") +``` + +## Gradle (Groovy DSL) +```groovy +implementation '${GROUP_ID}:${ARTIFACT_ID}:${VERSION}' +``` diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e78dbbda1..0c192b50c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -167,6 +167,14 @@ jobs: workspaces: "rust" - name: Set version run: sed -i -E 's/^version = ".*"$/version = "${{ needs.version.outputs.version }}"/' Cargo.toml + - name: Snapshot CLI version + hashes for build.rs + run: bash scripts/snapshot-bundled-cli-version.sh + - name: Verify snapshot file exists + run: | + if [[ ! -f bundled_cli_version.txt ]]; then + echo "::error::bundled_cli_version.txt was not generated. The Snapshot step must run before packaging." + exit 1 + fi - name: Package (dry run) run: cargo publish --dry-run --allow-dirty - name: Upload artifact diff --git a/.github/workflows/release-changelog.lock.yml b/.github/workflows/release-changelog.lock.yml index 2a82f1660..98cf18dc3 100644 --- a/.github/workflows/release-changelog.lock.yml +++ b/.github/workflows/release-changelog.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"c06cce5802b74e1280963eef2e92515d84870d76d9cfdefa84b56c038e2b8da1","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f56148e477b1349cf894dd5ee148dae8af3a90ab64cf708a41697d2c13b2da4b","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"d3abfe96a194bce3a523ed2093ddedd5704cdf62","version":"v0.74.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -58,7 +58,7 @@ on: required: false type: string tag: - description: Release tag to generate changelog for (e.g., v0.1.30) + description: Release tag to generate changelog for (e.g., v0.1.30, /v1.0.0) required: true type: string @@ -189,23 +189,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_41d0179c6df1e6c3_EOF' + cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' - GH_AW_PROMPT_41d0179c6df1e6c3_EOF + GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_41d0179c6df1e6c3_EOF' + cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' Tools: create_pull_request, update_release, missing_tool, missing_data, noop - GH_AW_PROMPT_41d0179c6df1e6c3_EOF + GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_41d0179c6df1e6c3_EOF' + cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' - GH_AW_PROMPT_41d0179c6df1e6c3_EOF + GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_41d0179c6df1e6c3_EOF' + cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -234,12 +234,12 @@ jobs: {{/if}} - GH_AW_PROMPT_41d0179c6df1e6c3_EOF + GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_41d0179c6df1e6c3_EOF' + cat << 'GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF' {{#runtime-import .github/workflows/release-changelog.md}} - GH_AW_PROMPT_41d0179c6df1e6c3_EOF + GH_AW_PROMPT_8ca4e2fb6c3e0923_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -446,9 +446,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_185484bc160cdce2_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6e92a7a47fdc567f_EOF' {"create_pull_request":{"draft":false,"labels":["automation","changelog"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"title_prefix":"[changelog] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_release":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_185484bc160cdce2_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_6e92a7a47fdc567f_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -687,7 +687,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_d0d73da3b3e2991f_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_432de5cac6e63f96_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -728,7 +728,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_d0d73da3b3e2991f_EOF + GH_AW_MCP_CONFIG_432de5cac6e63f96_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true diff --git a/.github/workflows/release-changelog.md b/.github/workflows/release-changelog.md index aba79d6f5..d82365ac5 100644 --- a/.github/workflows/release-changelog.md +++ b/.github/workflows/release-changelog.md @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: tag: - description: "Release tag to generate changelog for (e.g., v0.1.30)" + description: "Release tag to generate changelog for (e.g., v0.1.30, /v1.0.0)" required: true type: string permissions: @@ -54,8 +54,9 @@ Use the GitHub API to fetch the release corresponding to `${{ github.event.input 2. The **new version** is the release tag: `${{ github.event.inputs.tag }}` 3. Fetch the release metadata to determine if this is a **stable** or **prerelease** release. 4. Determine the **previous version** to diff against: - - **For stable releases**: find the previous **stable** release (skip prereleases). Check `CHANGELOG.md` for the most recent version heading (`## [vX.Y.Z](...)`), or fall back to listing releases via the API. This means stable changelogs include ALL changes since the last stable release, even if some were already mentioned in prerelease notes. - - **For prerelease releases**: find the most recent release of **any kind** (stable or prerelease) that precedes this one. This way prerelease notes only cover what's new since the last release. + - **Scoped tags**: If the tag has a language prefix (e.g., `java/v1.0.0` or `rust/v0.2.0`), the previous tag must use the **same prefix**. List tags matching that prefix (e.g., `java/v*` or `rust/v*`) sorted by version and pick the one immediately before the current tag. Only compare within the same scope. + - **For stable releases**: find the previous **stable** release (skip prereleases). Check `CHANGELOG.md` for the most recent version heading matching this scope (`## [vX.Y.Z](...)` for unscoped, `## [java/vX.Y.Z](...)` for Java, `## [rust/vX.Y.Z](...)` for Rust), or fall back to listing releases via the API. This means stable changelogs include ALL changes since the last stable release, even if some were already mentioned in prerelease notes. + - **For prerelease releases**: find the most recent release of **any kind** (stable or prerelease) that precedes this one within the same tag scope. This way prerelease notes only cover what's new since the last release. 5. If no previous release exists at all, use the first commit in the repo as the starting point. 6. After identifying the range, verify it by listing the commits in `PREVIOUS_TAG..NEW_TAG`. If the local result still looks suspiciously small or inconsistent, do **not** proceed based on local git alone — use the GitHub tools as the source of truth for the commits and PRs in the release. @@ -65,8 +66,9 @@ Use the GitHub API to fetch the release corresponding to `${{ github.event.input 2. Also list merged pull requests in that range. For each PR, note: - PR number and title - The PR author - - Which SDK(s) were affected (look for prefixes like `[C#]`, `[Python]`, `[Go]`, `[Node]` in the title, or infer from changed files) -3. Ignore: + - Which SDK(s) were affected (look for prefixes like `[C#]`, `[Python]`, `[Go]`, `[Node]`, `[Java]`, `[Rust]` in the title, or infer from changed files) +3. **For scoped tags** (e.g., `java/v*`, `rust/v*`): only include changes that touch the corresponding language directory (`java/`, `rust/`). Ignore changes to other languages unless they directly affect the scoped SDK. +4. Ignore: - Dependabot/bot PRs that only bump internal dependencies (like `Update @github/copilot to ...`) unless they bring user-facing changes - Merge commits with no meaningful content - Preview/prerelease-only changes that were already documented @@ -87,9 +89,9 @@ Additionally, identify **new contributors** — anyone whose first merged PR to **Skip this step entirely for prerelease releases.** 1. Read the current `CHANGELOG.md` file. -2. Add the new version entry **at the top** of the file, right after the title/header. +2. Add the new version entry **at the top** of the file, right after the title/header. Use the **full tag** as the version in the heading — e.g., `## [v0.2.3](...)` for unscoped tags, `## [java/v1.0.0](...)` for Java-scoped tags, `## [rust/v0.2.3](...)` for Rust-scoped tags. -**Format for each highlighted feature** — use an `### Feature:` or `### Fix:` heading, a 1-2 sentence description explaining what it does and why it matters, and at least one short code snippet (max 3 lines). Focus on **TypeScript** and **C#** as the primary languages. Only show Go/Python when giving a list of one-liner equivalents across all languages, or when their usage pattern is meaningfully different. +**Format for each highlighted feature** — use an `### Feature:` or `### Fix:` heading, a 1-2 sentence description explaining what it does and why it matters, and at least one short code snippet (max 3 lines). For unscoped releases, focus on **TypeScript** and **C#** as the primary languages; only show Go/Python when giving a list of one-liner equivalents across all languages, or when their usage pattern is meaningfully different. For **scoped releases** (e.g., `java/v*`), show code snippets in the scoped language only (e.g., Java for `java/v*`, Rust for `rust/v*`). **Format for other changes** — a single `### Other changes` section with a flat bulleted list. Each bullet has a lowercase prefix (`feature:`, `bugfix:`, `improvement:`) and a one-line description linking to the PR. **However, if there are no highlighted features above it, omit the `### Other changes` heading entirely** — just list the bullets directly under the version heading. @@ -107,6 +109,7 @@ Additionally, identify **new contributors** — anyone whose first merged PR to **Skip this step entirely for prerelease releases.** Use the `create-pull-request` output to submit your changes. The PR should: + - Have a clear title like "Add changelog for vX.Y.Z" - Include a brief body summarizing the number of changes @@ -160,6 +163,7 @@ While `session.rpc.models.setModel()` already worked, there is now a convenience ```` **Key rules visible in the example:** + - Highlighted features get their own `### Feature:` heading, a short description, and code snippets - Code snippets are TypeScript and C# primarily; Go/Python only when listing one-liner equivalents or when meaningfully different - The `### Other changes` section is a flat bulleted list with lowercase `bugfix:` / `feature:` / `improvement:` prefixes diff --git a/.github/workflows/rust-sdk-tests.yml b/.github/workflows/rust-sdk-tests.yml index 393db2d18..952294e13 100644 --- a/.github/workflows/rust-sdk-tests.yml +++ b/.github/workflows/rust-sdk-tests.yml @@ -101,15 +101,17 @@ jobs: RUST_E2E_CONCURRENCY: 4 COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.cli-path }} - run: cargo test --features test-support -- --test-threads=4 --nocapture - - # Validates the `embedded-cli` build path on all three supported - # platforms. This is the only place `build.rs` actually runs (the - # default `cargo test` job above has `COPILOT_CLI_VERSION` unset, so - # `build.rs` returns immediately). Catches regressions in the - # download / verify / extract / embed pipeline before they ship to - # crates.io and before bundling consumers (e.g. github-app's - # bundled-CLI release pipeline) hit them downstream. + # `--no-default-features` disables the bundled-cli download; the + # tests use the CLI provided by setup-copilot via COPILOT_CLI_PATH. + # The dedicated `bundle` job below exercises the bundling pipeline. + run: cargo test --no-default-features --features test-support -- --test-threads=4 --nocapture + + # Validates the bundled-CLI build path on all three supported + # platforms. While the regular `cargo test` job above also exercises + # build.rs (bundling is on by default now), this matrix job is the + # dedicated cross-platform smoke test for the download / verify / + # extract / embed pipeline. Catches regressions before they ship to + # crates.io and before bundling consumers hit them downstream. bundle: name: "Rust SDK Bundled CLI Build" if: github.event.repository.fork == false @@ -144,23 +146,21 @@ jobs: id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package.json').dependencies['@github/copilot'].replace(/^[\^~]/, '')") + version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" # Cache the downloaded archive across runs so we don't refetch - # ~130 MB on every CI invocation. Keyed by OS + CLI version; on - # cache miss the bundle job exercises the full ureq download + - # SHA-256 + retry path, which is exactly the regression surface - # we want validated. + # ~130 MB on every CI invocation. Keyed by OS + CLI version so old + # archives drop out when the pinned version bumps, keeping the + # cache bounded. - name: Cache bundled CLI tarball uses: actions/cache@v4 with: path: ./rust/.bundled-cli-cache key: bundled-cli-${{ matrix.os }}-${{ steps.cli-version.outputs.version }} - - name: cargo build --features embedded-cli + - name: cargo build (bundled-cli is the default feature) env: - COPILOT_CLI_VERSION: ${{ steps.cli-version.outputs.version }} BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo build --features embedded-cli + run: cargo build diff --git a/docs/auth/byok.md b/docs/auth/byok.md index cb2f8cb90..4afb149e8 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -372,8 +372,8 @@ const client = new CopilotClient({ from copilot import CopilotClient from copilot.client import ModelInfo, ModelCapabilities, ModelSupports, ModelLimits -client = CopilotClient({ - "on_list_models": lambda: [ +client = CopilotClient( + on_list_models=lambda: [ ModelInfo( id="my-custom-model", name="My Custom Model", @@ -383,7 +383,7 @@ client = CopilotClient({ ), ) ], -}) +) ``` diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index 3d93f7589..5329f163e 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -64,14 +64,13 @@ const session = await client.createSession({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", custom_agents=[ { @@ -104,6 +103,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -129,8 +129,8 @@ func main() { Prompt: "You are a code editor. Make minimal, surgical changes to files as requested.", }, }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) _ = session @@ -161,8 +161,8 @@ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Prompt: "You are a code editor. Make minimal, surgical changes to files as requested.", }, }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) ``` @@ -174,6 +174,7 @@ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig @@ -199,7 +200,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig }, }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); ``` @@ -520,6 +521,7 @@ import ( "context" "fmt" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -529,8 +531,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) diff --git a/docs/features/hooks.md b/docs/features/hooks.md index c88c6e605..bd55797dd 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -60,14 +60,13 @@ const session = await client.createSession({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={ "on_session_start": on_session_start, "on_pre_tool_use": on_pre_tool_use, @@ -89,6 +88,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func onSessionStart(input copilot.SessionStartHookInput, inv copilot.HookInvocation) (*copilot.SessionStartHookOutput, error) { @@ -113,8 +113,8 @@ func main() { OnPreToolUse: onPreToolUse, OnPostToolUse: onPostToolUse, }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) _ = session @@ -133,8 +133,8 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ OnPostToolUse: onPostToolUse, // ... add only the hooks you need }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) ``` @@ -147,6 +147,7 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class HooksExample { @@ -170,7 +171,7 @@ public static class HooksExample OnPostToolUse = onPostToolUse, }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); } } @@ -190,7 +191,7 @@ var session = await client.CreateSessionAsync(new SessionConfig // ... add only the hooks you need }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); ``` @@ -262,7 +263,7 @@ const session = await client.createSession({ Python ```python -from copilot.session import PermissionRequestResult +from copilot import PermissionDecisionApproveOnce READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"] @@ -276,7 +277,7 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "allow"} session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={"on_pre_tool_use": on_pre_tool_use}, ) ``` @@ -294,6 +295,7 @@ import ( "context" "fmt" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -314,8 +316,8 @@ func main() { return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil }, }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) _ = session @@ -349,6 +351,7 @@ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class PermissionControlExample { @@ -377,7 +380,7 @@ public static class PermissionControlExample }, }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); } } @@ -578,7 +581,7 @@ const session = await client.createSession({ ```python import json, aiofiles -from copilot.session import PermissionRequestResult +from copilot import PermissionDecisionApproveOnce audit_log = [] @@ -630,7 +633,7 @@ async def on_session_end(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, @@ -709,7 +712,7 @@ const session = await client.createSession({ ```python import subprocess -from copilot.session import PermissionRequestResult +from copilot import PermissionDecisionApproveOnce async def on_session_end(input_data, invocation): sid = invocation["session_id"][:8] @@ -728,7 +731,7 @@ async def on_error_occurred(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={ "on_session_end": on_session_end, "on_error_occurred": on_error_occurred, @@ -932,7 +935,7 @@ const session = await client.createSession({ Python ```python -from copilot.session import PermissionRequestResult +from copilot import PermissionDecisionApproveOnce session_metrics = {} @@ -963,7 +966,7 @@ async def on_session_end(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, diff --git a/docs/features/image-input.md b/docs/features/image-input.md index 4aa564558..cf2dee518 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -68,14 +68,13 @@ await session.send({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) @@ -102,6 +101,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -111,8 +111,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -137,8 +137,8 @@ client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -162,6 +162,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class ImageInputExample { @@ -172,7 +173,7 @@ public static class ImageInputExample { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); await session.SendAsync(new MessageOptions @@ -194,13 +195,14 @@ public static class ImageInputExample ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); await session.SendAsync(new MessageOptions @@ -286,14 +288,13 @@ await session.send({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) @@ -323,6 +324,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -332,8 +334,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -377,6 +379,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class BlobAttachmentExample { @@ -387,7 +390,7 @@ public static class BlobAttachmentExample { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); var base64ImageData = "..."; diff --git a/docs/features/remote-sessions.md b/docs/features/remote-sessions.md index 391bb762d..f58238eee 100644 --- a/docs/features/remote-sessions.md +++ b/docs/features/remote-sessions.md @@ -38,9 +38,9 @@ session.on("session.info", (event) => { ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient -client = CopilotClient(SubprocessConfig(remote=True)) +client = CopilotClient(enable_remote_sessions=True) session = await client.create_session( working_directory="/path/to/github-repo", on_permission_request=lambda req: {"allowed": True}, @@ -60,8 +60,8 @@ session.on(on_event) client, _ := copilot.NewClient(&copilot.ClientOptions{Remote: true}) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ WorkingDirectory: "/path/to/github-repo", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -81,7 +81,7 @@ var session = await client.CreateSessionAsync(new SessionConfig { WorkingDirectory = "/path/to/github-repo", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); session.On((SessionEvent e) => @@ -97,15 +97,16 @@ session.On((SessionEvent e) => ```rust -use github_copilot_sdk::{Client, ClientOptions, PermissionRequestResult, SessionConfig}; +use github_copilot_sdk::{Client, ClientOptions, SessionConfig}; +use github_copilot_sdk::handler::PermissionResult; let client = Client::start( - ClientOptions::new().with_remote(true) + ClientOptions::new().with_enable_remote_sessions(true) ).await?; let session = client.create_session( SessionConfig::new("/path/to/github-repo") .with_permission_handler(|_req, _inv| async { - Ok(PermissionRequestResult::approved()) + Ok(PermissionResult::approve_once()) }), ).await?; diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 5a1987227..3bfff10d0 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -70,6 +70,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -79,8 +80,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: "user-123-task-456", Model: "gpt-5.2-codex", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -202,6 +203,7 @@ session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What did we discuss ear ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class ResumeSessionExample { @@ -212,7 +214,7 @@ public static class ResumeSessionExample var session = await client.ResumeSessionAsync("user-123-task-456", new ResumeSessionConfig { OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); await session.SendAndWaitAsync(new MessageOptions { Prompt = "What did we discuss earlier?" }); diff --git a/docs/features/skills.md b/docs/features/skills.md index 516c11762..bac35e39e 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -42,15 +42,14 @@ await session.sendAndWait({ prompt: "Review this code for security issues" }); Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce async def main(): client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", skill_directories=[ "./skills/code-review", @@ -76,6 +75,7 @@ import ( "context" "log" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -92,8 +92,8 @@ func main() { "./skills/code-review", "./skills/documentation", }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -117,6 +117,7 @@ func main() { ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig @@ -128,7 +129,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig "./skills/documentation", }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); // Copilot now has access to skills in those directories @@ -212,6 +213,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -221,8 +223,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SkillDirectories: []string{"./skills"}, DisabledSkills: []string{"experimental-feature", "deprecated-tool"}, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) _ = session @@ -245,6 +247,7 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class SkillsExample { @@ -257,7 +260,7 @@ public static class SkillsExample SkillDirectories = new List { "./skills" }, DisabledSkills = new List { "experimental-feature", "deprecated-tool" }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); } } diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index ce4f4fba2..3b32f678d 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -69,15 +69,14 @@ await session.send({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce async def main(): client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) @@ -107,6 +106,7 @@ import ( "context" "log" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -119,8 +119,8 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -153,13 +153,14 @@ func main() { ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); // Start a long-running task @@ -261,15 +262,14 @@ await session.send({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce async def main(): client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) @@ -303,6 +303,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -312,8 +313,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -362,6 +363,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class QueueingExample { @@ -372,7 +374,7 @@ public static class QueueingExample { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); await session.SendAsync(new MessageOptions @@ -502,7 +504,7 @@ await session.send({ ```python session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index ebf7d5e30..9b61108ed 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -122,6 +122,7 @@ import ( "context" "fmt" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -131,8 +132,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", Streaming: copilot.Bool(true), - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) diff --git a/docs/getting-started.md b/docs/getting-started.md index 0d5e5887e..b5df45100 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -665,13 +665,12 @@ unsubscribeIdle(); ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionDecisionApproveOnce from copilot.generated.session_events import SessionEvent, SessionEventType -from copilot.session import PermissionRequestResult client = CopilotClient() -session = await client.create_session(on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once")) +session = await client.create_session(on_permission_request=lambda req, inv: PermissionDecisionApproveOnce()) # Subscribe to all events unsubscribe = session.on(lambda event: print(f"Event: {event.type}")) @@ -1909,7 +1908,7 @@ const session = await client.createSession({ }); ``` -Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`. +Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `runtime_instructions`, `last_instructions`. Each override supports four actions: `replace`, `remove`, `append`, and `prepend`. Unknown section IDs are handled gracefully—content is appended to additional instructions and a warning is emitted; `remove` on unknown sections is silently ignored. @@ -1968,12 +1967,10 @@ const session = await client.createSession({ onPermissionRequest: approveAll }); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler -client = CopilotClient({ - "cli_url": "localhost:4321" -}) +client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:4321")) await client.start() # Use the client normally @@ -2138,9 +2135,9 @@ Optional peer dependency: `@opentelemetry/api` ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, CopilotClientOptions -client = CopilotClient(SubprocessConfig( +client = CopilotClient(CopilotClientOptions( telemetry={ "otlp_endpoint": "http://localhost:4318", }, diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md index 42a5c6e96..1f9581ba5 100644 --- a/docs/observability/opentelemetry.md +++ b/docs/observability/opentelemetry.md @@ -27,13 +27,13 @@ const client = new CopilotClient({ ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient -client = CopilotClient(SubprocessConfig( +client = CopilotClient( telemetry={ "otlp_endpoint": "http://localhost:4318", }, -)) +) ``` diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index 0552ee36e..dfe9c19af 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -144,10 +144,12 @@ res.json({ content: response?.data.content }); Python ```python -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler -client = CopilotClient(ExternalServerConfig(url="localhost:4321")) +client = CopilotClient( + connection=RuntimeConnection.for_uri("localhost:4321"), +) await client.start() session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-{int(time.time())}") diff --git a/docs/troubleshooting/debugging.md b/docs/troubleshooting/debugging.md index 3beadb99f..95c9b3a7d 100644 --- a/docs/troubleshooting/debugging.md +++ b/docs/troubleshooting/debugging.md @@ -34,7 +34,7 @@ const client = new CopilotClient({ ```python from copilot import CopilotClient -client = CopilotClient({"log_level": "debug"}) +client = CopilotClient(log_level="debug") ``` @@ -131,7 +131,7 @@ const client = new CopilotClient({ ``` > [!NOTE] -> Python SDK logging configuration is limited. For advanced logging, run the CLI manually with `--log-dir` and connect via `cli_url`. +> Python SDK logging configuration is limited. For advanced logging, run the CLI manually with `--log-dir` and connect via `RuntimeConnection.for_uri(...)`. diff --git a/dotnet/README.md b/dotnet/README.md index f01d87474..a9527f447 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -1,4 +1,4 @@ -# Copilot SDK +# Copilot SDK SDK for programmatic control of GitHub Copilot CLI. @@ -627,18 +627,18 @@ var session = await client.CreateSessionAsync(new SessionConfig SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Customize, - Sections = new Dictionary + Sections = new Dictionary { - [SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = "Respond in a warm, professional tone. Be thorough in explanations." }, - [SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove }, - [SystemPromptSections.Guidelines] = new() { Action = SectionOverrideAction.Append, Content = "\n* Always cite data sources" }, + [SystemMessageSection.Tone] = new() { Action = SectionOverrideAction.Replace, Content = "Respond in a warm, professional tone. Be thorough in explanations." }, + [SystemMessageSection.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove }, + [SystemMessageSection.Guidelines] = new() { Action = SectionOverrideAction.Append, Content = "\n* Always cite data sources" }, }, Content = "Focus on financial analysis and reporting." } }); ``` -Available section IDs are defined as constants on `SystemPromptSections`: `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `LastInstructions`. +Available section IDs are defined as static properties on the `SystemMessageSection` struct: `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `RuntimeInstructions`, `LastInstructions`. Each section override supports four actions: `Replace`, `Remove`, `Append`, and `Prepend`. Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored. @@ -749,7 +749,7 @@ var session = await client.CreateSessionAsync(new SessionConfig ### Custom Permission Handler -Provide your own permission handler (`Func>`) to inspect each request and apply custom logic: +Provide your own permission handler (`Func>`) to inspect each request and apply custom logic: ```csharp var session = await client.CreateSessionAsync(new SessionConfig @@ -757,43 +757,29 @@ var session = await client.CreateSessionAsync(new SessionConfig Model = "gpt-5", OnPermissionRequest = async (request, invocation) => { - // request.Kind — string discriminator for the type of operation being requested: - // "shell" — executing a shell command - // "write" — writing or editing a file - // "read" — reading a file - // "mcp" — calling an MCP tool - // "custom_tool" — calling one of your registered tools - // "url" — fetching a URL - // "memory" — accessing or modifying assistant memory - // "hook" — invoking a registered hook - // request.ToolCallId — the tool call that triggered this request - // request.ToolName — name of the tool (for custom-tool / mcp) - // request.FileName — file being written (for write) - // request.FullCommandText — full shell command text (for shell) - - if (request.Kind == "shell") + // Pattern-match on the discriminated PermissionRequest union to access + // per-kind fields (FullCommandText, Path, ToolName, …). + return request switch { - // Deny shell commands - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Rejected }; - } - - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + PermissionRequestShell s => PermissionDecision.Reject($"Refusing shell: {s.FullCommandText}"), + _ => PermissionDecision.ApproveOnce(), + }; } }); ``` -### Permission Result Kinds +### Permission Decisions -The `Kind` property must be one of the canonical `PermissionRequestResultKind` values. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. +The handler returns a `PermissionDecision`. Use the static factories for common cases (returned types are the strongly-typed variant classes — full IntelliSense via `PermissionDecision.`): -| Value | Wire value | Meaning | -| ------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `PermissionRequestResultKind.Approved` | `"approve-once"` | Allow this single request | -| `PermissionRequestResultKind.Rejected` | `"reject"` | Deny the request | -| `PermissionRequestResultKind.UserNotAvailable` | `"user-not-available"` | Deny the request because no user is available to confirm it | -| `PermissionRequestResultKind.NoResult` | `"no-result"` | Leave the permission request unanswered (the SDK returns without calling the RPC). Not allowed for protocol v2 permission requests (will be rejected). | +| Factory | Meaning | +| -------------------------------------- | -------------------------------------------------------------------------------------------- | +| `PermissionDecision.ApproveOnce()` | Allow this single request | +| `PermissionDecision.Reject(feedback)` | Deny the request, optionally forwarding feedback to the LLM | +| `PermissionDecision.UserNotAvailable()`| Deny the request because no user is available to confirm it | +| `PermissionDecision.NoResult()` | Decline to respond, allowing another connected client to answer instead | -> The past-tense names `PermissionRequestResultKind.DeniedInteractivelyByUser`, `PermissionRequestResultKind.DeniedCouldNotRequestFromUser`, and `PermissionRequestResultKind.DeniedByRules` remain as `[Obsolete]` aliases for backward compatibility — prefer the canonical members above in new code. +For richer decisions that need an `Approval` payload — `PermissionDecisionApproveForSession`, `PermissionDecisionApproveForLocation`, `PermissionDecisionApprovePermanently` — instantiate the variant class directly. ### Resuming Sessions diff --git a/dotnet/src/Canvas.cs b/dotnet/src/Canvas.cs new file mode 100644 index 000000000..6a6134e18 --- /dev/null +++ b/dotnet/src/Canvas.cs @@ -0,0 +1,288 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using GitHub.Copilot.Rpc; + +namespace GitHub.Copilot; + +/// +/// Declarative metadata for a single canvas, sent over the wire on +/// session.create / session.resume. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasDeclaration +{ + /// Canvas identifier, unique within the declaring connection. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Human-readable name shown in host UI and canvas pickers. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Short, single-sentence description shown to the agent in canvas catalogs. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// JSON Schema for the input payload accepted by canvas.open. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } + + /// Agent-callable actions this canvas exposes. + [JsonPropertyName("actions")] + public IList? Actions { get; set; } +} + +/// +/// Stable extension identity for session participants that provide canvases. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class ExtensionInfo +{ + /// Extension namespace/source, e.g. "github-app". + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; + + /// Stable provider name within the source namespace. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// Response returned from . +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasOpenResponse +{ + /// URL the host should render. Optional for canvases with no visual surface. + [JsonPropertyName("url")] + public string? Url { get; set; } + + /// Provider-supplied title shown in host chrome. + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// Provider-supplied status text shown in host chrome. + [JsonPropertyName("status")] + public string? Status { get; set; } +} + +/// Host capabilities passed to canvas provider callbacks. +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasHostContext +{ + /// Host capability details. + [JsonPropertyName("capabilities")] + public CanvasHostCapabilities Capabilities { get; set; } = new(); +} + +/// Host capability details passed to canvas provider callbacks. +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasHostCapabilities +{ + /// Whether the host supports canvas rendering. + [JsonPropertyName("canvases")] + public bool Canvases { get; set; } +} + +/// Context handed to . +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasOpenContext +{ + /// Session that requested the canvas. + public string SessionId { get; init; } = string.Empty; + + /// Owning provider identifier. + public string ExtensionId { get; init; } = string.Empty; + + /// Canvas id from the declaring . + public string CanvasId { get; init; } = string.Empty; + + /// Stable instance id supplied by the runtime. + public string InstanceId { get; init; } = string.Empty; + + /// Validated input payload. + public JsonElement Input { get; init; } + + /// Host capabilities supplied by the runtime. + public CanvasHostContext? Host { get; init; } +} + +/// Context handed to . +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasActionContext +{ + /// Session that invoked the action. + public string SessionId { get; init; } = string.Empty; + + /// Owning provider identifier. + public string ExtensionId { get; init; } = string.Empty; + + /// Canvas id targeted by the action. + public string CanvasId { get; init; } = string.Empty; + + /// Instance id targeted by the action. + public string InstanceId { get; init; } = string.Empty; + + /// Action name from . + public string ActionName { get; init; } = string.Empty; + + /// Validated input payload. + public JsonElement Input { get; init; } + + /// Host capabilities supplied by the runtime. + public CanvasHostContext? Host { get; init; } +} + +/// Context handed to a canvas's close lifecycle hook. +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasLifecycleContext +{ + /// Session owning the canvas instance. + public string SessionId { get; init; } = string.Empty; + + /// Owning provider identifier. + public string ExtensionId { get; init; } = string.Empty; + + /// Canvas id from the declaring . + public string CanvasId { get; init; } = string.Empty; + + /// Instance id this lifecycle event applies to. + public string InstanceId { get; init; } = string.Empty; + + /// Host capabilities supplied by the runtime. + public CanvasHostContext? Host { get; init; } +} + +/// Structured error returned from canvas handlers. +/// +/// Throw this from implementations to surface a +/// machine-readable error code to the runtime. Any other exception is wrapped +/// in a generic canvas_handler_error envelope. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasError : Exception +{ + /// Initializes a new . + /// Machine-readable error code. + /// Human-readable message. + public CanvasError(string code, string message) : base(message) + { + Code = code; + } + + /// Machine-readable error code. + public string Code { get; } + + /// + /// Default error returned when a custom action has no handler. + /// + public static CanvasError NoHandler() => new( + "canvas_action_no_handler", + "No handler implemented for this canvas action"); +} + +/// +/// Internal helpers used by the session runtime to translate +/// (and other handler-thrown exceptions) into structured JSON-RPC error responses. +/// +internal static class CanvasErrorHelpers +{ + private const int InternalError = -32603; + + public static LocalRpcInvocationException HandlerUnset() => Build( + "canvas_handler_unset", + "No canvas handler is registered on this session"); + + public static LocalRpcInvocationException HandlerError(string message) => Build( + "canvas_handler_error", + message); + + public static LocalRpcInvocationException ToRpcException(CanvasError error) => Build(error.Code, error.Message); + + private static LocalRpcInvocationException Build(string code, string message) + { + var json = JsonSerializer.Serialize( + new CanvasErrorPayload { Code = code, Message = message }, + CanvasJsonContext.Default.CanvasErrorPayload); + using var doc = JsonDocument.Parse(json); + return new LocalRpcInvocationException(InternalError, message, doc.RootElement.Clone()); + } + + internal sealed class CanvasErrorPayload + { + [JsonPropertyName("code")] + public string Code { get; set; } = string.Empty; + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + } +} + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(CanvasErrorHelpers.CanvasErrorPayload))] +internal partial class CanvasJsonContext : JsonSerializerContext; + +/// +/// Provider-side canvas lifecycle handler. +/// +/// +/// A session installs a single via +/// SessionConfigBase.CanvasHandler. The handler receives every +/// inbound canvas.open / canvas.close / canvas.action.invoke +/// JSON-RPC request the runtime issues for this session and decides — typically +/// by inspecting — which +/// application-side canvas should handle the call. +/// +/// The SDK does not maintain a per-canvas registry; multiplexing across +/// declared canvases is the implementor's responsibility. +/// +/// +/// Implementations targeting netstandard2.0 cannot rely on default +/// interface methods; derive from to inherit +/// sensible defaults for and . +/// +/// +[Experimental(Diagnostics.Experimental)] +public interface ICanvasHandler +{ + /// Open a new canvas instance. + Task OnOpenAsync(CanvasOpenContext context, CancellationToken cancellationToken); + + /// Canvas was closed by the user or agent. Default: no-op. + Task OnCloseAsync(CanvasLifecycleContext context, CancellationToken cancellationToken); + + /// + /// Handle a non-lifecycle action declared by the canvas. + /// Default: throws . + /// + Task OnActionAsync(CanvasActionContext context, CancellationToken cancellationToken); +} + +/// +/// Convenience base class for that supplies +/// default no-op / no-handler implementations of the optional callbacks. +/// +[Experimental(Diagnostics.Experimental)] +public abstract class CanvasHandlerBase : ICanvasHandler +{ + /// + public abstract Task OnOpenAsync(CanvasOpenContext context, CancellationToken cancellationToken); + + /// + public virtual Task OnCloseAsync(CanvasLifecycleContext context, CancellationToken cancellationToken) +#if NET8_0_OR_GREATER + => Task.CompletedTask; +#else + => Task.FromResult(null); +#endif + + /// + public virtual Task OnActionAsync(CanvasActionContext context, CancellationToken cancellationToken) + => Task.FromException(CanvasError.NoHandler()); +} diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 285c97a56..0e7730690 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1,4 +1,4 @@ -/*--------------------------------------------------------------------------------------------- +/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ @@ -53,13 +53,10 @@ namespace GitHub.Copilot; /// public sealed partial class CopilotClient : IDisposable, IAsyncDisposable { - internal const string NoResultPermissionV2ErrorMessage = - "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."; - /// /// Minimum protocol version this SDK can communicate with. /// - private const int MinProtocolVersion = 2; + private const int MinProtocolVersion = 3; /// /// Provides a thread-safe collection of active Copilot sessions, indexed by session identifier. @@ -464,13 +461,13 @@ private static (SystemMessageConfig? wireConfig, Dictionary>>(); - var wireSections = new Dictionary(); + var wireSections = new Dictionary(); foreach (var (sectionId, sectionOverride) in systemMessage.Sections) { if (sectionOverride.Transform != null) { - callbacks[sectionId] = sectionOverride.Transform; + callbacks[sectionId.Value] = sectionOverride.Transform; wireSections[sectionId] = new SectionOverride { Action = SectionOverrideAction.Transform }; } else @@ -570,6 +567,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance session.On(config.OnEvent); } ConfigureSessionFsHandlers(session, config.CreateSessionFsProvider); + session.SetCanvasHandler(config.CanvasHandler); RegisterSession(session); session.StartProcessingEvents(); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, @@ -595,7 +593,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.ExcludedTools, config.Provider, config.EnableSessionTelemetry, - (bool?)true, + config.OnPermissionRequest != null ? true : null, config.OnUserInputRequest != null ? true : null, config.OnExitPlanModeRequest != null ? true : null, config.OnAutoModeSwitchRequest != null ? true : null, @@ -621,7 +619,11 @@ public async Task CreateSessionAsync(SessionConfig config, Cance GitHubToken: config.GitHubToken, RemoteSession: config.RemoteSession, Cloud: config.Cloud, - InstructionDirectories: config.InstructionDirectories); + InstructionDirectories: config.InstructionDirectories, + Canvases: config.Canvases, + RequestCanvasRenderer: config.RequestCanvasRenderer, + RequestExtensions: config.RequestExtensions, + ExtensionInfo: config.ExtensionInfo); var rpcTimestamp = Stopwatch.GetTimestamp(); var response = await InvokeRpcAsync( @@ -633,6 +635,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance session.WorkspacePath = response.WorkspacePath; session.SetCapabilities(response.Capabilities); + session.SetOpenCanvases(response.OpenCanvases); } catch (Exception ex) { @@ -729,6 +732,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes session.On(config.OnEvent); } ConfigureSessionFsHandlers(session, config.CreateSessionFsProvider); + session.SetCanvasHandler(config.CanvasHandler); RegisterSession(session); session.StartProcessingEvents(); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, @@ -754,7 +758,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.ExcludedTools, config.Provider, config.EnableSessionTelemetry, - (bool?)true, + config.OnPermissionRequest != null ? true : null, config.OnUserInputRequest != null ? true : null, config.OnExitPlanModeRequest != null ? true : null, config.OnAutoModeSwitchRequest != null ? true : null, @@ -781,7 +785,12 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes GitHubToken: config.GitHubToken, RemoteSession: config.RemoteSession, ContinuePendingWork: config.ContinuePendingWork, - InstructionDirectories: config.InstructionDirectories); + InstructionDirectories: config.InstructionDirectories, + Canvases: config.Canvases, + RequestCanvasRenderer: config.RequestCanvasRenderer, + RequestExtensions: config.RequestExtensions, + ExtensionInfo: config.ExtensionInfo, + OpenCanvases: config.OpenCanvases); var rpcTimestamp = Stopwatch.GetTimestamp(); var response = await InvokeRpcAsync( @@ -793,6 +802,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes session.WorkspacePath = response.WorkspacePath; session.SetCapabilities(response.Capabilities); + session.SetOpenCanvases(response.OpenCanvases); } catch (Exception ex) { @@ -1610,17 +1620,14 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? var handler = new RpcHandler(this); rpc.SetLocalRpcMethod("session.event", handler.OnSessionEvent); rpc.SetLocalRpcMethod("session.lifecycle", handler.OnSessionLifecycle); - // Protocol v3 servers send tool calls / permission requests as broadcast events. - // Protocol v2 servers use the older tool.call / permission.request RPC model. - // We always register v2 adapters because handlers are set up before version - // negotiation; a v3 server will simply never send these requests. - rpc.SetLocalRpcMethod("tool.call", handler.OnToolCallV2); - rpc.SetLocalRpcMethod("permission.request", handler.OnPermissionRequestV2); rpc.SetLocalRpcMethod("userInput.request", handler.OnUserInputRequest); rpc.SetLocalRpcMethod("exitPlanMode.request", handler.OnExitPlanModeRequest); rpc.SetLocalRpcMethod("autoModeSwitch.request", handler.OnAutoModeSwitchRequest); rpc.SetLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke); rpc.SetLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform); + rpc.SetLocalRpcMethod("canvas.open", handler.OnCanvasOpen); + rpc.SetLocalRpcMethod("canvas.close", handler.OnCanvasClose); + rpc.SetLocalRpcMethod("canvas.action.invoke", handler.OnCanvasInvokeAction); ClientSessionApiRegistration.RegisterClientSessionApiHandlers(rpc, sessionId => { var session = GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); @@ -1638,6 +1645,20 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? private static JsonSerializerOptions SerializerOptionsForMessageFormatter { get; } = CreateSerializerOptions(); + /// + /// Converts an arbitrary value into the representation that wire + /// DTOs use for opaque-JSON fields. Pass-through for , otherwise + /// serializes the runtime type using the shared JSON-RPC serializer options so that any + /// type registered in the SDK's source-generated contexts (e.g. primitives, + /// Dictionary<string, object>, generated DTOs) is supported. + /// + public static JsonElement? ToJsonElementForWire(object? value) => value switch + { + null => null, + JsonElement je => je, + _ => JsonSerializer.SerializeToElement(value, SerializerOptionsForMessageFormatter.GetTypeInfo(value.GetType())) + }; + private static JsonSerializerOptions CreateSerializerOptions() { var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) @@ -1800,110 +1821,46 @@ public async ValueTask OnSystemMessageTransfo return await session.HandleSystemMessageTransformAsync(sections); } - // Protocol v2 backward-compatibility adapters - - public async ValueTask OnToolCallV2(string sessionId, - string toolCallId, - string toolName, - object? arguments, - string? traceparent = null, - string? tracestate = null) +#pragma warning disable GHCP001 + public ValueTask OnCanvasOpen( + string sessionId, + string extensionId, + string canvasId, + string instanceId, + JsonElement? input = null, + CanvasHostContext? host = null) { - using var _ = TelemetryHelpers.RestoreTraceContext(traceparent, tracestate); - var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); - if (session.GetTool(toolName) is not { } tool) - { - // Support for not providing the tool handler is only available in the v3+ model. - // For v2, it must have been provided. - return new ToolCallResponseV2(new ToolResultObject - { - TextResultForLlm = $"Tool '{toolName}' is not supported.", - ResultType = "failure", - Error = $"tool '{toolName}' not supported" - }); - } - - try - { - var invocation = new ToolInvocation - { - SessionId = sessionId, - ToolCallId = toolCallId, - ToolName = toolName, - Arguments = arguments - }; - - var aiFunctionArgs = new AIFunctionArguments - { - Context = new Dictionary - { - [typeof(ToolInvocation)] = invocation - } - }; - - if (arguments is not null) - { - if (arguments is not JsonElement incomingJsonArgs) - { - throw new InvalidOperationException($"Incoming arguments must be a {nameof(JsonElement)}; received {arguments.GetType().Name}"); - } - - foreach (var prop in incomingJsonArgs.EnumerateObject()) - { - aiFunctionArgs[prop.Name] = prop.Value; - } - } - - var toolTimestamp = Stopwatch.GetTimestamp(); - var result = await tool.InvokeAsync(aiFunctionArgs); - LoggingHelpers.LogTiming(client._logger, LogLevel.Debug, null, - "RpcHandler.OnToolCallV2 tool dispatch. Elapsed={Elapsed}, SessionId={SessionId}, ToolCallId={ToolCallId}, Tool={ToolName}", - toolTimestamp, - sessionId, - toolCallId, - toolName); - - var toolResultObject = ToolResultObject.ConvertFromInvocationResult(result, tool.JsonSerializerOptions); - return new ToolCallResponseV2(toolResultObject); - } - catch (Exception ex) - { - return new ToolCallResponseV2(new ToolResultObject - { - TextResultForLlm = "Invoking this tool produced an error. Detailed information is not available.", - ResultType = "failure", - Error = ex.Message - }); - } + return session.HandleCanvasOpenAsync( + extensionId, canvasId, instanceId, input ?? default, host); } - public async ValueTask OnPermissionRequestV2(string sessionId, JsonElement permissionRequest) + public async ValueTask OnCanvasClose( + string sessionId, + string extensionId, + string canvasId, + string instanceId, + JsonElement? input = null, + CanvasHostContext? host = null) { - var session = client.GetSession(sessionId) - ?? throw new ArgumentException($"Unknown session {sessionId}"); + var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); + await session.HandleCanvasCloseAsync(extensionId, canvasId, instanceId, host); + } - try - { - var result = await session.HandlePermissionRequestAsync(permissionRequest); - if (result.Kind == new PermissionRequestResultKind("no-result")) - { - throw new InvalidOperationException(NoResultPermissionV2ErrorMessage); - } - return new PermissionRequestResponseV2(result); - } - catch (InvalidOperationException ex) when (ex.Message == NoResultPermissionV2ErrorMessage) - { - throw; - } - catch (Exception) - { - return new PermissionRequestResponseV2(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable - }); - } + public ValueTask OnCanvasInvokeAction( + string sessionId, + string extensionId, + string canvasId, + string instanceId, + string actionName, + JsonElement? input = null, + CanvasHostContext? host = null) + { + var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); + return session.HandleCanvasActionAsync( + extensionId, canvasId, instanceId, actionName, input ?? default, host); } +#pragma warning restore GHCP001 } private class Connection( @@ -1966,7 +1923,13 @@ internal record CreateSessionRequest( string? GitHubToken = null, RemoteSessionMode? RemoteSession = null, CloudSessionOptions? Cloud = null, - IList? InstructionDirectories = null); + IList? InstructionDirectories = null, +#pragma warning disable GHCP001 + IList? Canvases = null, + bool? RequestCanvasRenderer = null, + bool? RequestExtensions = null, + ExtensionInfo? ExtensionInfo = null); +#pragma warning restore GHCP001 internal record ToolDefinition( string Name, @@ -1988,7 +1951,10 @@ public static ToolDefinition FromAIFunction(AIFunctionDeclaration function) internal record CreateSessionResponse( string SessionId, string? WorkspacePath, - SessionCapabilities? Capabilities = null); + SessionCapabilities? Capabilities = null, +#pragma warning disable GHCP001 + IList? OpenCanvases = null); +#pragma warning restore GHCP001 internal record ResumeSessionRequest( string SessionId, @@ -2028,12 +1994,22 @@ internal record ResumeSessionRequest( string? GitHubToken = null, RemoteSessionMode? RemoteSession = null, bool? ContinuePendingWork = null, - IList? InstructionDirectories = null); + IList? InstructionDirectories = null, +#pragma warning disable GHCP001 + IList? Canvases = null, + bool? RequestCanvasRenderer = null, + bool? RequestExtensions = null, + ExtensionInfo? ExtensionInfo = null, + IList? OpenCanvases = null); +#pragma warning restore GHCP001 internal record ResumeSessionResponse( string SessionId, string? WorkspacePath, - SessionCapabilities? Capabilities = null); + SessionCapabilities? Capabilities = null, +#pragma warning disable GHCP001 + IList? OpenCanvases = null); +#pragma warning restore GHCP001 internal record CommandWireDefinition( string Name, @@ -2074,13 +2050,6 @@ internal record AutoModeSwitchRequestResponse( internal record HooksInvokeResponse( object? Output); - // Protocol v2 backward-compatibility response types - internal record ToolCallResponseV2( - ToolResultObject Result); - - internal record PermissionRequestResponseV2( - PermissionRequestResult Result); - [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -2103,9 +2072,6 @@ internal record PermissionRequestResponseV2( [JsonSerializable(typeof(GetSessionMetadataRequest))] [JsonSerializable(typeof(GetSessionMetadataResponse))] [JsonSerializable(typeof(ModelCapabilitiesOverride))] - [JsonSerializable(typeof(PermissionRequestResult))] - [JsonSerializable(typeof(PermissionRequestResultKind))] - [JsonSerializable(typeof(PermissionRequestResponseV2))] [JsonSerializable(typeof(ProviderConfig))] [JsonSerializable(typeof(ResumeSessionRequest))] [JsonSerializable(typeof(ResumeSessionResponse))] @@ -2116,7 +2082,6 @@ internal record PermissionRequestResponseV2( [JsonSerializable(typeof(SystemMessageConfig))] [JsonSerializable(typeof(SystemMessageTransformRpcResponse))] [JsonSerializable(typeof(CommandWireDefinition))] - [JsonSerializable(typeof(ToolCallResponseV2))] [JsonSerializable(typeof(ToolDefinition))] [JsonSerializable(typeof(ToolResultAIContent))] [JsonSerializable(typeof(ToolResultObject))] diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 09b16e0bf..f8e0f314c 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -66,6 +66,26 @@ internal sealed class ConnectRequest public string? Token { get; set; } } +/// Long context tier pricing (available for models with extended context windows). +public sealed class ModelBillingTokenPricesLongContext +{ + /// AI Credits cost per billing batch of cached tokens. + [JsonPropertyName("cachePrice")] + public double? CachePrice { get; set; } + + /// Maximum context window tokens for the long context tier. + [JsonPropertyName("contextMax")] + public long? ContextMax { get; set; } + + /// AI Credits cost per billing batch of input tokens. + [JsonPropertyName("inputPrice")] + public double? InputPrice { get; set; } + + /// AI Credits cost per billing batch of output tokens. + [JsonPropertyName("outputPrice")] + public double? OutputPrice { get; set; } +} + /// Token-level pricing information for this model. public sealed class ModelBillingTokenPrices { @@ -73,17 +93,25 @@ public sealed class ModelBillingTokenPrices [JsonPropertyName("batchSize")] public long? BatchSize { get; set; } - /// Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD). + /// AI Credits cost per billing batch of cached tokens. [JsonPropertyName("cachePrice")] - public long? CachePrice { get; set; } + public double? CachePrice { get; set; } + + /// Maximum context window tokens for the default tier. + [JsonPropertyName("contextMax")] + public long? ContextMax { get; set; } - /// Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD). + /// AI Credits cost per billing batch of input tokens. [JsonPropertyName("inputPrice")] - public long? InputPrice { get; set; } + public double? InputPrice { get; set; } - /// Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD). + /// Long context tier pricing (available for models with extended context windows). + [JsonPropertyName("longContext")] + public ModelBillingTokenPricesLongContext? LongContext { get; set; } + + /// AI Credits cost per billing batch of output tokens. [JsonPropertyName("outputPrice")] - public long? OutputPrice { get; set; } + public double? OutputPrice { get; set; } } /// Billing information. @@ -247,7 +275,7 @@ public sealed class Tool /// JSON Schema for the tool's input parameters. [JsonPropertyName("parameters")] - public IDictionary? Parameters { get; set; } + public IDictionary? Parameters { get; set; } } /// Built-in tools available for the requested model, with their parameters and instructions. @@ -352,7 +380,7 @@ public sealed class DiscoveredMcpServer [JsonPropertyName("source")] public McpServerSource Source { get; set; } - /// Server transport type: stdio, http, sse, or memory. + /// Server transport type: stdio, http, sse (deprecated), or memory. [JsonPropertyName("type")] public DiscoveredMcpServerType? Type { get; set; } } @@ -378,7 +406,7 @@ public sealed class McpConfigList { /// All MCP servers from user config, keyed by name. [JsonPropertyName("servers")] - public IDictionary Servers { get => field ??= new Dictionary(); set; } + public IDictionary Servers { get => field ??= new Dictionary(); set; } } /// MCP server name and configuration to add to user configuration. @@ -386,7 +414,7 @@ internal sealed class McpConfigAddRequest { /// MCP server configuration (stdio process or remote HTTP/SSE). [JsonPropertyName("config")] - public object Config { get; set; } = null!; + public JsonElement Config { get; set; } /// Unique name for the MCP server. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] @@ -401,7 +429,7 @@ internal sealed class McpConfigUpdateRequest { /// MCP server configuration (stdio process or remote HTTP/SSE). [JsonPropertyName("config")] - public object Config { get; set; } = null!; + public JsonElement Config { get; set; } /// Name of the MCP server to update. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] @@ -1074,7 +1102,7 @@ public sealed class InstalledPlugin /// Source for direct repo installs (when marketplace is empty). [JsonPropertyName("source")] - public object? Source { get; set; } + public JsonElement? Source { get; set; } /// Version installed (if available). [JsonPropertyName("version")] @@ -1345,8 +1373,9 @@ internal sealed class SendRequest public string SessionId { get; set; } = string.Empty; /// Optional provenance tag copied to the resulting user.message event. Supported values are `system`, `command-*`, and `schedule-*`. + [JsonInclude] [JsonPropertyName("source")] - public object? Source { get; set; } + internal JsonElement? Source { get; set; } /// W3C Trace Context traceparent header for distributed tracing of this agent turn. [JsonPropertyName("traceparent")] @@ -2007,6 +2036,207 @@ internal sealed class SessionSetCredentialsParams public string SessionId { get; set; } = string.Empty; } +/// Canvas action that the agent or host can invoke. To discover the input schema for a particular action, call the list_canvas_capabilities tool. +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasAction +{ + /// Description of the action. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// JSON Schema for the action input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } + + /// Action name exposed by the canvas provider. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// Canvas available in the current session. +[Experimental(Diagnostics.Experimental)] +public sealed class DiscoveredCanvas +{ + /// Actions the agent or host may invoke on an open instance. + [JsonPropertyName("actions")] + public IList? Actions { get; set; } + + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; + + /// Short, single-sentence description shown to the agent in canvas catalogs. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Human-readable canvas name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; + + /// Owning extension display name, when available. + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } + + /// JSON Schema for canvas open input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } +} + +/// Declared canvases available in this session. +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasList +{ + /// Declared canvases available in this session. + [JsonPropertyName("canvases")] + public IList Canvases { get => field ??= []; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionCanvasListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Open canvas instance snapshot. +[Experimental(Diagnostics.Experimental)] +public sealed class OpenCanvasInstance +{ + /// Runtime-controlled routing state for an open canvas instance. + [JsonPropertyName("availability")] + public CanvasInstanceAvailability Availability { get; set; } + + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; + + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; + + /// Owning extension display name, when available. + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } + + /// Input supplied when the instance was opened. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } + + /// Stable caller-supplied canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Whether this snapshot came from an idempotent reopen. + [JsonPropertyName("reopen")] + public bool Reopen { get; set; } + + /// Provider-supplied status text. + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// Rendered title. + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// URL for web-rendered canvases. + [JsonPropertyName("url")] + public string? Url { get; set; } +} + +/// Live open-canvas snapshot. +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasListOpenResult +{ + /// Currently open canvas instances. + [JsonPropertyName("openCanvases")] + public IList OpenCanvases { get => field ??= []; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionCanvasListOpenRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Canvas open parameters. +[Experimental(Diagnostics.Experimental)] +internal sealed class CanvasOpenRequest +{ + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; + + /// Owning provider identifier. Optional when the canvasId is unique across providers; required to disambiguate when multiple providers register the same canvasId. + [JsonPropertyName("extensionId")] + public string? ExtensionId { get; set; } + + /// Canvas open input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } + + /// Caller-supplied stable instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Canvas close parameters. +[Experimental(Diagnostics.Experimental)] +internal sealed class CanvasCloseRequest +{ + /// Open canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Canvas action invocation result. +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasInvokeActionResult +{ + /// Provider-supplied action result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } +} + +/// Canvas action invocation parameters. +[Experimental(Diagnostics.Experimental)] +internal sealed class CanvasInvokeActionRequest +{ + /// Action name to invoke. + [JsonPropertyName("actionName")] + public string ActionName { get; set; } = string.Empty; + + /// Action input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } + + /// Open canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// The currently selected model and reasoning effort for the session. [Experimental(Diagnostics.Experimental)] public sealed class CurrentModel @@ -2617,8 +2847,9 @@ public sealed class AgentInfo public string Id { get; set; } = string.Empty; /// MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. + [Experimental(Diagnostics.Experimental)] [JsonPropertyName("mcpServers")] - public IDictionary? McpServers { get; set; } + public IDictionary? McpServers { get; set; } /// Preferred model id for this agent. When omitted, inherits the outer agent's model. [JsonPropertyName("model")] @@ -3474,7 +3705,7 @@ internal sealed class McpExecuteSamplingParams { /// The original MCP JSON-RPC request ID (string or number). Used by the runtime to correlate the inference with the originating MCP request for telemetry; this is distinct from `requestId` (which is the schema-level cancellation handle). [JsonPropertyName("mcpRequestId")] - public object McpRequestId { get; set; } = null!; + public JsonElement McpRequestId { get; set; } /// Raw MCP CreateMessageRequest params, as received in the `sampling.requested` event. Treated as opaque at the schema layer; the runtime converts the embedded MCP messages into the OpenAI chat-completion shape internally. [JsonPropertyName("request")] @@ -3594,145 +3825,427 @@ internal sealed class McpOauthLoginRequest public string SessionId { get; set; } = string.Empty; } -/// Schema for the `Plugin` type. +/// Schema for the `McpAppsResourceContent` type. [Experimental(Diagnostics.Experimental)] -public sealed class Plugin +public sealed class McpAppsResourceContent { - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Resource-level metadata (CSP, permissions, etc.). + [JsonPropertyName("_meta")] + public IDictionary? _meta { get; set; } - /// Marketplace the plugin came from. - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; + /// Base64-encoded binary content. + [JsonPropertyName("blob")] + public string? Blob { get; set; } - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// Installed version. - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Text content (e.g. HTML). + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// The resource URI (typically ui://...). + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// Plugins installed for the session, with their enabled state and version metadata. +/// Resource contents returned by the MCP server. [Experimental(Diagnostics.Experimental)] -public sealed class PluginList +public sealed class McpAppsReadResourceResult { - /// Installed plugins. - [JsonPropertyName("plugins")] - public IList Plugins { get => field ??= []; set; } + /// Resource contents returned by the server. + [JsonPropertyName("contents")] + public IList Contents { get => field ??= []; set; } } -/// Identifies the target session. +/// MCP server and resource URI to fetch. [Experimental(Diagnostics.Experimental)] -internal sealed class SessionPluginsListRequest +internal sealed class McpAppsReadResourceRequest { + /// Name of the MCP server hosting the resource. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + + /// Resource URI (typically ui://...). + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// Indicates whether the session options patch was applied successfully. +/// App-callable tools from the named MCP server. [Experimental(Diagnostics.Experimental)] -public sealed class SessionUpdateOptionsResult +public sealed class McpAppsListToolsResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// App-callable tools from the server. + [JsonPropertyName("tools")] + public IList> Tools { get => field ??= []; set; } } -/// Schema for the `SessionInstalledPlugin` type. +/// MCP server to list app-callable tools for. [Experimental(Diagnostics.Experimental)] -public sealed class SessionInstalledPlugin +internal sealed class McpAppsListToolsRequest { - /// Path where the plugin is cached locally. - [JsonPropertyName("cache_path")] - public string? CachePath { get; set; } + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("originServerName")] + public string OriginServerName { get; set; } = string.Empty; - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// MCP server hosting the app. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Installation timestamp (ISO-8601). - [JsonPropertyName("installed_at")] - public string InstalledAt { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Marketplace the plugin came from (empty string for direct repo installs). - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; +/// MCP server, tool name, and arguments to invoke from an MCP App view. +[Experimental(Diagnostics.Experimental)] +internal sealed class McpAppsCallToolRequest +{ + /// Tool arguments. + [JsonPropertyName("arguments")] + public IDictionary? Arguments { get; set; } - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("originServerName")] + public string OriginServerName { get; set; } = string.Empty; - /// Source descriptor for direct repo installs (when marketplace is empty). - [JsonPropertyName("source")] - public object? Source { get; set; } + /// MCP server hosting the tool. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Installed version, if known. - [JsonPropertyName("version")] - public string? Version { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// MCP tool name. + [JsonPropertyName("toolName")] + public string ToolName { get; set; } = string.Empty; } -/// Patch of mutable session options to apply to the running session. +/// Host context advertised to MCP App guests. [Experimental(Diagnostics.Experimental)] -internal sealed class SessionUpdateOptionsParams +public sealed class McpAppsSetHostContextDetails { - /// Additional content-exclusion policies to merge into the session's policy set. Opaque shape; see `ContentExclusionApiResponse` in the runtime. - [JsonPropertyName("additionalContentExclusionPolicies")] - public IList? AdditionalContentExclusionPolicies { get; set; } + /// Display modes the host supports. + [JsonPropertyName("availableDisplayModes")] + public IList? AvailableDisplayModes { get; set; } - /// Runtime context discriminator (e.g., `cli`, `actions`). - [JsonPropertyName("agentContext")] - public string? AgentContext { get; set; } + /// Current display mode (SEP-1865). + [JsonPropertyName("displayMode")] + public McpAppsSetHostContextDetailsDisplayMode? DisplayMode { get; set; } - /// Whether to disable the `ask_user` tool (encourages autonomous behavior). - [JsonPropertyName("askUserDisabled")] - public bool? AskUserDisabled { get; set; } + /// BCP-47 locale, e.g. 'en-US'. + [JsonPropertyName("locale")] + public string? Locale { get; set; } - /// Allowlist of tool names available to this session. - [JsonPropertyName("availableTools")] - public IList? AvailableTools { get; set; } + /// Platform type for responsive design. + [JsonPropertyName("platform")] + public McpAppsSetHostContextDetailsPlatform? Platform { get; set; } - /// Identifier of the client driving the session. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// UI theme preference per SEP-1865. + [JsonPropertyName("theme")] + public McpAppsSetHostContextDetailsTheme? Theme { get; set; } - /// Whether to include the `Co-authored-by` trailer in commit messages. - [JsonPropertyName("coauthorEnabled")] - public bool? CoauthorEnabled { get; set; } + /// IANA timezone, e.g. 'America/New_York'. + [JsonPropertyName("timeZone")] + public string? TimeZone { get; set; } - /// Whether to allow auto-mode continuation across turns. - [JsonPropertyName("continueOnAutoMode")] - public bool? ContinueOnAutoMode { get; set; } + /// Host application identifier. + [JsonPropertyName("userAgent")] + public string? UserAgent { get; set; } +} - /// Override URL for the Copilot API endpoint. - [JsonPropertyName("copilotUrl")] - public string? CopilotUrl { get; set; } +/// Host context to advertise to MCP App guests. +[Experimental(Diagnostics.Experimental)] +internal sealed class McpAppsSetHostContextRequest +{ + /// Host context advertised to MCP App guests. + [JsonPropertyName("context")] + public McpAppsSetHostContextDetails Context { get => field ??= new(); set; } - /// Whether to default custom agents to local-only execution. - [JsonPropertyName("customAgentsLocalOnly")] - public bool? CustomAgentsLocalOnly { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Instruction source IDs to exclude from the system prompt. - [JsonPropertyName("disabledInstructionSources")] - public IList? DisabledInstructionSources { get; set; } +/// Current host context. +[Experimental(Diagnostics.Experimental)] +public sealed class McpAppsHostContextDetails +{ + /// Display modes the host supports. + [JsonPropertyName("availableDisplayModes")] + public IList? AvailableDisplayModes { get; set; } - /// Skill IDs that should be excluded from this session. - [JsonPropertyName("disabledSkills")] - public IList? DisabledSkills { get; set; } + /// Current display mode (SEP-1865). + [JsonPropertyName("displayMode")] + public McpAppsHostContextDetailsDisplayMode? DisplayMode { get; set; } - /// Whether to discover custom instructions on demand after successful file views (AGENTS.md / CLAUDE.md / .github/copilot-instructions.md surfacing). Combined with `skipCustomInstructions` and the runtime-side `ON_DEMAND_INSTRUCTIONS` feature flag. - [JsonPropertyName("enableOnDemandInstructionDiscovery")] - public bool? EnableOnDemandInstructionDiscovery { get; set; } + /// BCP-47 locale, e.g. 'en-US'. + [JsonPropertyName("locale")] + public string? Locale { get; set; } - /// Whether to surface reasoning-summary events from the model. - [JsonPropertyName("enableReasoningSummaries")] - public bool? EnableReasoningSummaries { get; set; } + /// Platform type for responsive design. + [JsonPropertyName("platform")] + public McpAppsHostContextDetailsPlatform? Platform { get; set; } - /// Whether shell-script safety heuristics are enabled. - [JsonPropertyName("enableScriptSafety")] + /// UI theme preference per SEP-1865. + [JsonPropertyName("theme")] + public McpAppsHostContextDetailsTheme? Theme { get; set; } + + /// IANA timezone, e.g. 'America/New_York'. + [JsonPropertyName("timeZone")] + public string? TimeZone { get; set; } + + /// Host application identifier. + [JsonPropertyName("userAgent")] + public string? UserAgent { get; set; } +} + +/// Current host context advertised to MCP App guests. +[Experimental(Diagnostics.Experimental)] +public sealed class McpAppsHostContext +{ + /// Current host context. + [JsonPropertyName("context")] + public McpAppsHostContextDetails Context { get => field ??= new(); set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionMcpAppsGetHostContextRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Capability negotiation snapshot. +[Experimental(Diagnostics.Experimental)] +public sealed class McpAppsDiagnoseCapability +{ + /// Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers. + [JsonPropertyName("advertised")] + public bool Advertised { get; set; } + + /// Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on. + [JsonPropertyName("featureFlagEnabled")] + public bool FeatureFlagEnabled { get; set; } + + /// Whether the session has the `mcp-apps` capability. + [JsonPropertyName("sessionHasMcpApps")] + public bool SessionHasMcpApps { get; set; } +} + +/// What the server returned for this session. +[Experimental(Diagnostics.Experimental)] +public sealed class McpAppsDiagnoseServer +{ + /// Whether the named server is currently connected. + [JsonPropertyName("connected")] + public bool Connected { get; set; } + + /// Up to 5 tool names with `_meta.ui` for quick inspection. + [JsonPropertyName("sampleToolNames")] + public IList SampleToolNames { get => field ??= []; set; } + + /// Total tools returned by the server's tools/list. + [JsonPropertyName("toolCount")] + public double ToolCount { get; set; } + + /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). + [JsonPropertyName("toolsWithUiMeta")] + public double ToolsWithUiMeta { get; set; } +} + +/// Diagnostic snapshot of MCP Apps wiring for the named server. +[Experimental(Diagnostics.Experimental)] +public sealed class McpAppsDiagnoseResult +{ + /// Capability negotiation snapshot. + [JsonPropertyName("capability")] + public McpAppsDiagnoseCapability Capability { get => field ??= new(); set; } + + /// What the server returned for this session. + [JsonPropertyName("server")] + public McpAppsDiagnoseServer Server { get => field ??= new(); set; } +} + +/// MCP server to diagnose MCP Apps wiring for. +[Experimental(Diagnostics.Experimental)] +internal sealed class McpAppsDiagnoseRequest +{ + /// MCP server to probe. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Schema for the `Plugin` type. +[Experimental(Diagnostics.Experimental)] +public sealed class Plugin +{ + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Marketplace the plugin came from. + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; + + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Installed version. + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +/// Plugins installed for the session, with their enabled state and version metadata. +[Experimental(Diagnostics.Experimental)] +public sealed class PluginList +{ + /// Installed plugins. + [JsonPropertyName("plugins")] + public IList Plugins { get => field ??= []; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionPluginsListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Indicates whether the session options patch was applied successfully. +[Experimental(Diagnostics.Experimental)] +public sealed class SessionUpdateOptionsResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Schema for the `SessionInstalledPlugin` type. +[Experimental(Diagnostics.Experimental)] +public sealed class SessionInstalledPlugin +{ + /// Path where the plugin is cached locally. + [JsonPropertyName("cache_path")] + public string? CachePath { get; set; } + + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Installation timestamp (ISO-8601). + [JsonPropertyName("installed_at")] + public string InstalledAt { get; set; } = string.Empty; + + /// Marketplace the plugin came from (empty string for direct repo installs). + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; + + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Source descriptor for direct repo installs (when marketplace is empty). + [JsonPropertyName("source")] + public JsonElement? Source { get; set; } + + /// Installed version, if known. + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +/// Patch of mutable session options to apply to the running session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionUpdateOptionsParams +{ + /// Additional content-exclusion policies to merge into the session's policy set. Opaque shape; see `ContentExclusionApiResponse` in the runtime. + [Experimental(Diagnostics.Experimental)] + [JsonPropertyName("additionalContentExclusionPolicies")] + public IList? AdditionalContentExclusionPolicies { get; set; } + + /// Runtime context discriminator (e.g., `cli`, `actions`). + [JsonPropertyName("agentContext")] + public string? AgentContext { get; set; } + + /// Whether to disable the `ask_user` tool (encourages autonomous behavior). + [JsonPropertyName("askUserDisabled")] + public bool? AskUserDisabled { get; set; } + + /// Allowlist of tool names available to this session. + [JsonPropertyName("availableTools")] + public IList? AvailableTools { get; set; } + + /// Identifier of the client driving the session. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } + + /// Whether to include the `Co-authored-by` trailer in commit messages. + [JsonPropertyName("coauthorEnabled")] + public bool? CoauthorEnabled { get; set; } + + /// Whether to allow auto-mode continuation across turns. + [JsonPropertyName("continueOnAutoMode")] + public bool? ContinueOnAutoMode { get; set; } + + /// Override URL for the Copilot API endpoint. + [JsonPropertyName("copilotUrl")] + public string? CopilotUrl { get; set; } + + /// Whether to default custom agents to local-only execution. + [JsonPropertyName("customAgentsLocalOnly")] + public bool? CustomAgentsLocalOnly { get; set; } + + /// Instruction source IDs to exclude from the system prompt. + [JsonPropertyName("disabledInstructionSources")] + public IList? DisabledInstructionSources { get; set; } + + /// Skill IDs that should be excluded from this session. + [JsonPropertyName("disabledSkills")] + public IList? DisabledSkills { get; set; } + + /// Whether to discover custom instructions on demand after successful file views (AGENTS.md / CLAUDE.md / .github/copilot-instructions.md surfacing). Combined with `skipCustomInstructions` and the runtime-side `ON_DEMAND_INSTRUCTIONS` feature flag. + [JsonPropertyName("enableOnDemandInstructionDiscovery")] + public bool? EnableOnDemandInstructionDiscovery { get; set; } + + /// Whether to surface reasoning-summary events from the model. + [JsonPropertyName("enableReasoningSummaries")] + public bool? EnableReasoningSummaries { get; set; } + + /// Whether shell-script safety heuristics are enabled. + [JsonPropertyName("enableScriptSafety")] public bool? EnableScriptSafety { get; set; } /// Whether to stream model responses. @@ -3784,8 +4297,9 @@ internal sealed class SessionUpdateOptionsParams public string? Model { get; set; } /// Custom model-provider configuration (BYOK). Opaque shape; see `ProviderConfig` in the runtime. + [Experimental(Diagnostics.Experimental)] [JsonPropertyName("provider")] - public object? Provider { get; set; } + public JsonElement? Provider { get; set; } /// Reasoning effort for the selected model (model-defined enum). [JsonPropertyName("reasoningEffort")] @@ -3796,8 +4310,9 @@ internal sealed class SessionUpdateOptionsParams public bool? RunningInInteractiveMode { get; set; } /// Sandbox configuration shape; opaque to SDK consumers. See `SandboxConfig` in the runtime. + [Experimental(Diagnostics.Experimental)] [JsonPropertyName("sandboxConfig")] - public object? SandboxConfig { get; set; } + public JsonElement? SandboxConfig { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] @@ -3950,7 +4465,7 @@ internal sealed class HandlePendingToolCallRequest /// Tool call result (string or expanded result object). [JsonPropertyName("result")] - public object? Result { get; set; } + public JsonElement? Result { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] @@ -4367,7 +4882,7 @@ public sealed class UIElicitationResponse /// The form values submitted by the user (present when action is 'accept'). [JsonPropertyName("content")] - public IDictionary? Content { get; set; } + public IDictionary? Content { get; set; } } /// JSON Schema describing the form fields to present to the user. @@ -4376,7 +4891,7 @@ public sealed class UIElicitationSchema { /// Form field definitions, keyed by field name. [JsonPropertyName("properties")] - public IDictionary Properties { get => field ??= new Dictionary(); set; } + public IDictionary Properties { get => field ??= new Dictionary(); set; } /// List of required field names. [JsonPropertyName("required")] @@ -4636,7 +5151,7 @@ public sealed class PermissionsConfigureAdditionalContentExclusionPolicy { /// Gets or sets the last_updated_at value. [JsonPropertyName("last_updated_at")] - public object LastUpdatedAt { get; set; } = null!; + public JsonElement LastUpdatedAt { get; set; } /// Gets or sets the rules value. [JsonPropertyName("rules")] @@ -6517,7 +7032,7 @@ internal sealed class EventLogReadRequest /// Either '*' to receive all event types, or a non-empty list of event types to receive. [JsonPropertyName("types")] - public object? Types { get; set; } + public JsonElement? Types { get; set; } /// Milliseconds to wait for new events when the cursor is at the tail of history. 0 (default) returns immediately even if no events are available. Capped at 30000ms. Ephemeral events that arrive during the wait are delivered in this batch but are NOT replayable on a subsequent read (use a non-zero waitMs in your next call to capture future ephemerals as they happen). [JsonConverter(typeof(MillisecondsTimeSpanConverter))] @@ -7139,7 +7654,7 @@ public sealed class SessionFsSqliteQueryResult /// For SELECT: array of row objects. For others: empty array. [JsonPropertyName("rows")] - public IList> Rows { get => field ??= []; set; } + public IList> Rows { get => field ??= []; set; } /// Number of rows affected (for INSERT/UPDATE/DELETE). [JsonPropertyName("rowsAffected")] @@ -7151,7 +7666,7 @@ public sealed class SessionFsSqliteQueryRequest { /// Optional named bind parameters. [JsonPropertyName("params")] - public IDictionary? Params { get; set; } + public IDictionary? Params { get; set; } /// SQL query to execute. [JsonPropertyName("query")] @@ -7380,7 +7895,7 @@ public override void Write(Utf8JsonWriter writer, ModelPolicyState value, JsonSe } -/// Server transport type: stdio, http, sse, or memory. +/// Server transport type: stdio, http, sse (deprecated), or memory. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct DiscoveredMcpServerType : IEquatable @@ -7405,7 +7920,7 @@ public DiscoveredMcpServerType(string value) /// Server communicates over streamable HTTP. public static DiscoveredMcpServerType Http { get; } = new("http"); - /// Server communicates over Server-Sent Events. + /// Server communicates over Server-Sent Events (deprecated). public static DiscoveredMcpServerType Sse { get; } = new("sse"); /// Server is backed by an in-memory runtime implementation. @@ -7978,6 +8493,69 @@ public override void Write(Utf8JsonWriter writer, AuthInfoType value, JsonSerial } +/// Runtime-controlled routing state for an open canvas instance. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CanvasInstanceAvailability : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CanvasInstanceAvailability(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The owning provider is currently connected and routing calls will be dispatched normally. + public static CanvasInstanceAvailability Ready { get; } = new("ready"); + + /// The owning provider is not currently connected. Routing calls fail with canvas_provider_unavailable until the agent re-issues open_canvas (which rehydrates via a fresh canvas.open) or the provider reconnects. + public static CanvasInstanceAvailability Stale { get; } = new("stale"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CanvasInstanceAvailability left, CanvasInstanceAvailability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CanvasInstanceAvailability left, CanvasInstanceAvailability right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is CanvasInstanceAvailability other && Equals(other); + + /// + public bool Equals(CanvasInstanceAvailability 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 CanvasInstanceAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, CanvasInstanceAvailability value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CanvasInstanceAvailability)); + } + } +} + + /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -8590,43 +9168,46 @@ public override void Write(Utf8JsonWriter writer, McpSetEnvValueModeDetails valu } -/// How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). +/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct OptionsUpdateEnvValueMode : IEquatable +public readonly struct McpAppsSetHostContextDetailsAvailableDisplayMode : 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 OptionsUpdateEnvValueMode(string value) + public McpAppsSetHostContextDetailsAvailableDisplayMode(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; - /// Pass MCP server environment values as literal strings. - public static OptionsUpdateEnvValueMode Direct { get; } = new("direct"); + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// Resolve MCP server environment values from host-side references. - public static OptionsUpdateEnvValueMode Indirect { get; } = new("indirect"); + /// Rendered as a fullscreen overlay. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(OptionsUpdateEnvValueMode left, OptionsUpdateEnvValueMode right) => left.Equals(right); + /// Rendered as a picture-in-picture floating panel. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(OptionsUpdateEnvValueMode left, OptionsUpdateEnvValueMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is OptionsUpdateEnvValueMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(OptionsUpdateEnvValueMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(McpAppsSetHostContextDetailsAvailableDisplayMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -8634,35 +9215,554 @@ public OptionsUpdateEnvValueMode(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 OptionsUpdateEnvValueMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override McpAppsSetHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, OptionsUpdateEnvValueMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(OptionsUpdateEnvValueMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); } } } -/// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/). +/// Current display mode (SEP-1865). [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ExtensionSource : IEquatable +public readonly struct McpAppsSetHostContextDetailsDisplayMode : 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 McpAppsSetHostContextDetailsDisplayMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsDisplayMode Inline { get; } = new("inline"); + + /// Rendered as a fullscreen overlay. + public static McpAppsSetHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + + /// Rendered as a picture-in-picture floating panel. + public static McpAppsSetHostContextDetailsDisplayMode Pip { get; } = new("pip"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); + + /// + public bool Equals(McpAppsSetHostContextDetailsDisplayMode 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 McpAppsSetHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsDisplayMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); + } + } +} + + +/// Platform type for responsive design. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpAppsSetHostContextDetailsPlatform : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpAppsSetHostContextDetailsPlatform(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Host runs in a web browser. + public static McpAppsSetHostContextDetailsPlatform Web { get; } = new("web"); + + /// Host runs as a desktop application. + public static McpAppsSetHostContextDetailsPlatform Desktop { get; } = new("desktop"); + + /// Host runs on a mobile device. + public static McpAppsSetHostContextDetailsPlatform Mobile { get; } = new("mobile"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsPlatform other && Equals(other); + + /// + public bool Equals(McpAppsSetHostContextDetailsPlatform 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 McpAppsSetHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsPlatform value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); + } + } +} + + +/// UI theme preference per SEP-1865. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpAppsSetHostContextDetailsTheme : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpAppsSetHostContextDetailsTheme(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Light UI theme. + public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); + + /// Dark UI theme. + public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsTheme other && Equals(other); + + /// + public bool Equals(McpAppsSetHostContextDetailsTheme 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 McpAppsSetHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpAppsSetHostContextDetailsTheme value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); + } + } +} + + +/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpAppsHostContextDetailsAvailableDisplayMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpAppsHostContextDetailsAvailableDisplayMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); + + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); + + /// Rendered as a picture-in-picture floating panel. + public static McpAppsHostContextDetailsAvailableDisplayMode Pip { get; } = new("pip"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); + + /// + public bool Equals(McpAppsHostContextDetailsAvailableDisplayMode 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 McpAppsHostContextDetailsAvailableDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); + } + } +} + + +/// Current display mode (SEP-1865). +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpAppsHostContextDetailsDisplayMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpAppsHostContextDetailsDisplayMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsDisplayMode Inline { get; } = new("inline"); + + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + + /// Rendered as a picture-in-picture floating panel. + public static McpAppsHostContextDetailsDisplayMode Pip { get; } = new("pip"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsDisplayMode other && Equals(other); + + /// + public bool Equals(McpAppsHostContextDetailsDisplayMode 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 McpAppsHostContextDetailsDisplayMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsDisplayMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); + } + } +} + + +/// Platform type for responsive design. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpAppsHostContextDetailsPlatform : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpAppsHostContextDetailsPlatform(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Host runs in a web browser. + public static McpAppsHostContextDetailsPlatform Web { get; } = new("web"); + + /// Host runs as a desktop application. + public static McpAppsHostContextDetailsPlatform Desktop { get; } = new("desktop"); + + /// Host runs on a mobile device. + public static McpAppsHostContextDetailsPlatform Mobile { get; } = new("mobile"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsPlatform other && Equals(other); + + /// + public bool Equals(McpAppsHostContextDetailsPlatform 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 McpAppsHostContextDetailsPlatform Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsPlatform value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); + } + } +} + + +/// UI theme preference per SEP-1865. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpAppsHostContextDetailsTheme : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpAppsHostContextDetailsTheme(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Light UI theme. + public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); + + /// Dark UI theme. + public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsTheme other && Equals(other); + + /// + public bool Equals(McpAppsHostContextDetailsTheme 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 McpAppsHostContextDetailsTheme Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpAppsHostContextDetailsTheme value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); + } + } +} + + +/// How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct OptionsUpdateEnvValueMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public OptionsUpdateEnvValueMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Pass MCP server environment values as literal strings. + public static OptionsUpdateEnvValueMode Direct { get; } = new("direct"); + + /// Resolve MCP server environment values from host-side references. + public static OptionsUpdateEnvValueMode Indirect { get; } = new("indirect"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(OptionsUpdateEnvValueMode left, OptionsUpdateEnvValueMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(OptionsUpdateEnvValueMode left, OptionsUpdateEnvValueMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is OptionsUpdateEnvValueMode other && Equals(other); + + /// + public bool Equals(OptionsUpdateEnvValueMode 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 OptionsUpdateEnvValueMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, OptionsUpdateEnvValueMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(OptionsUpdateEnvValueMode)); + } + } +} + + +/// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/). +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ExtensionSource : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] public ExtensionSource(string value) { @@ -10354,7 +11454,7 @@ public async Task AddAsync(string name, object config, CancellationToken cancell ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(config); - var request = new McpConfigAddRequest { Name = name, Config = config }; + var request = new McpConfigAddRequest { Name = name, Config = CopilotClient.ToJsonElementForWire(config)!.Value }; await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.add", [request], cancellationToken); } @@ -10367,7 +11467,7 @@ public async Task UpdateAsync(string name, object config, CancellationToken canc ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(config); - var request = new McpConfigUpdateRequest { Name = name, Config = config }; + var request = new McpConfigUpdateRequest { Name = name, Config = CopilotClient.ToJsonElementForWire(config)!.Value }; await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.update", [request], cancellationToken); } @@ -10737,6 +11837,12 @@ internal SessionRpc(CopilotSession session) Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + /// Canvas APIs. + public CanvasApi Canvas => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// Model APIs. public ModelApi Model => field ?? @@ -10938,7 +12044,7 @@ public async Task SendAsync(string prompt, string? displayPrompt = n ArgumentNullException.ThrowIfNull(prompt); _session.ThrowIfDisposed(); - var request = new SendRequest { SessionId = _session.SessionId, Prompt = prompt, DisplayPrompt = displayPrompt, Attachments = attachments, Mode = mode, Prepend = prepend, Billable = billable, RequiredTool = requiredTool, Source = source, AgentMode = agentMode, RequestHeaders = requestHeaders, Traceparent = traceparent, Tracestate = tracestate, Wait = wait }; + var request = new SendRequest { SessionId = _session.SessionId, Prompt = prompt, DisplayPrompt = displayPrompt, Attachments = attachments, Mode = mode, Prepend = prepend, Billable = billable, RequiredTool = requiredTool, Source = CopilotClient.ToJsonElementForWire(source), AgentMode = agentMode, RequestHeaders = requestHeaders, Traceparent = traceparent, Tracestate = tracestate, Wait = wait }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.send", [request], cancellationToken); } @@ -11023,6 +12129,85 @@ public async Task SetCredentialsAsync(AuthInfo? cre } } +/// Provides session-scoped Canvas APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class CanvasApi +{ + private readonly CopilotSession _session; + + internal CanvasApi(CopilotSession session) + { + _session = session; + } + + /// Lists canvases declared for the session. + /// The to monitor for cancellation requests. The default is . + /// Declared canvases available in this session. + public async Task ListAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionCanvasListRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.canvas.list", [request], cancellationToken); + } + + /// Lists currently open canvas instances for the live session. + /// The to monitor for cancellation requests. The default is . + /// Live open-canvas snapshot. + public async Task ListOpenAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionCanvasListOpenRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.canvas.listOpen", [request], cancellationToken); + } + + /// Opens or focuses a canvas instance. + /// Provider-local canvas identifier. + /// Caller-supplied stable instance identifier. + /// Owning provider identifier. Optional when the canvasId is unique across providers; required to disambiguate when multiple providers register the same canvasId. + /// Canvas open input. + /// The to monitor for cancellation requests. The default is . + /// Open canvas instance snapshot. + public async Task OpenAsync(string canvasId, string instanceId, string? extensionId = null, object? input = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(canvasId); + ArgumentNullException.ThrowIfNull(instanceId); + _session.ThrowIfDisposed(); + + var request = new CanvasOpenRequest { SessionId = _session.SessionId, CanvasId = canvasId, InstanceId = instanceId, ExtensionId = extensionId, Input = CopilotClient.ToJsonElementForWire(input) }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.canvas.open", [request], cancellationToken); + } + + /// Closes an open canvas instance. + /// Open canvas instance identifier. + /// The to monitor for cancellation requests. The default is . + public async Task CloseAsync(string instanceId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instanceId); + _session.ThrowIfDisposed(); + + var request = new CanvasCloseRequest { SessionId = _session.SessionId, InstanceId = instanceId }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.canvas.close", [request], cancellationToken); + } + + /// Invokes an action on an open canvas instance. + /// Open canvas instance identifier. + /// Action name to invoke. + /// Action input. + /// The to monitor for cancellation requests. The default is . + /// Canvas action invocation result. + public async Task InvokeActionAsync(string instanceId, string actionName, object? input = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(instanceId); + ArgumentNullException.ThrowIfNull(actionName); + _session.ThrowIfDisposed(); + + var request = new CanvasInvokeActionRequest { SessionId = _session.SessionId, InstanceId = instanceId, ActionName = actionName, Input = CopilotClient.ToJsonElementForWire(input) }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.canvas.invokeAction", [request], cancellationToken); + } +} + /// Provides session-scoped Model APIs. [Experimental(Diagnostics.Experimental)] public sealed class ModelApi @@ -11718,7 +12903,7 @@ public async Task ExecuteSamplingAsync(string reques ArgumentNullException.ThrowIfNull(request); _session.ThrowIfDisposed(); - var rpcRequest = new McpExecuteSamplingParams { SessionId = _session.SessionId, RequestId = requestId, ServerName = serverName, McpRequestId = mcpRequestId, Request = request }; + var rpcRequest = new McpExecuteSamplingParams { SessionId = _session.SessionId, RequestId = requestId, ServerName = serverName, McpRequestId = CopilotClient.ToJsonElementForWire(mcpRequestId)!.Value, Request = request }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.executeSampling", [rpcRequest], cancellationToken); } @@ -11763,6 +12948,12 @@ public async Task RemoveGitHubAsync(CancellationToken can field ?? Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + + /// Apps APIs. + public McpAppsApi Apps => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; } /// Provides session-scoped McpOauth APIs. @@ -11793,6 +12984,102 @@ public async Task LoginAsync(string serverName, bool? force } } +/// Provides session-scoped McpApps APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class McpAppsApi +{ + private readonly CopilotSession _session; + + internal McpAppsApi(CopilotSession session) + { + _session = session; + } + + /// Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. + /// Name of the MCP server hosting the resource. + /// Resource URI (typically ui://...). + /// The to monitor for cancellation requests. The default is . + /// Resource contents returned by the MCP server. + public async Task ReadResourceAsync(string serverName, string uri, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + ArgumentNullException.ThrowIfNull(uri); + _session.ThrowIfDisposed(); + + var request = new McpAppsReadResourceRequest { SessionId = _session.SessionId, ServerName = serverName, Uri = uri }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.apps.readResource", [request], cancellationToken); + } + + /// List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. + /// MCP server hosting the app. + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + /// The to monitor for cancellation requests. The default is . + /// App-callable tools from the named MCP server. + public async Task ListToolsAsync(string serverName, string originServerName, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + ArgumentNullException.ThrowIfNull(originServerName); + _session.ThrowIfDisposed(); + + var request = new McpAppsListToolsRequest { SessionId = _session.SessionId, ServerName = serverName, OriginServerName = originServerName }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.apps.listTools", [request], cancellationToken); + } + + /// Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. + /// MCP server hosting the tool. + /// MCP tool name. + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + /// Tool arguments. + /// The to monitor for cancellation requests. The default is . + /// Standard MCP CallToolResult. + public async Task> CallToolAsync(string serverName, string toolName, string originServerName, IDictionary? arguments = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + ArgumentNullException.ThrowIfNull(toolName); + ArgumentNullException.ThrowIfNull(originServerName); + _session.ThrowIfDisposed(); + + var request = new McpAppsCallToolRequest { SessionId = _session.SessionId, ServerName = serverName, ToolName = toolName, OriginServerName = originServerName, Arguments = arguments }; + return await CopilotClient.InvokeRpcAsync>(_session.Rpc, "session.mcp.apps.callTool", [request], cancellationToken); + } + + /// Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. + /// Host context advertised to MCP App guests. + /// The to monitor for cancellation requests. The default is . + public async Task SetHostContextAsync(McpAppsSetHostContextDetails context, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(context); + _session.ThrowIfDisposed(); + + var request = new McpAppsSetHostContextRequest { SessionId = _session.SessionId, Context = context }; + await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.apps.setHostContext", [request], cancellationToken); + } + + /// Read the current host context advertised to MCP App guests. + /// The to monitor for cancellation requests. The default is . + /// Current host context advertised to MCP App guests. + public async Task GetHostContextAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionMcpAppsGetHostContextRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.apps.getHostContext", [request], cancellationToken); + } + + /// Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. + /// MCP server to probe. + /// The to monitor for cancellation requests. The default is . + /// Diagnostic snapshot of MCP Apps wiring for the named server. + public async Task DiagnoseAsync(string serverName, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(serverName); + _session.ThrowIfDisposed(); + + var request = new McpAppsDiagnoseRequest { SessionId = _session.SessionId, ServerName = serverName }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.apps.diagnose", [request], cancellationToken); + } +} + /// Provides session-scoped Plugins APIs. [Experimental(Diagnostics.Experimental)] public sealed class PluginsApi @@ -11866,11 +13153,11 @@ internal OptionsApi(CopilotSession session) /// Whether to expose the `manage_schedule` tool to the agent. The runtime always owns the per-session schedule registry; this flag only controls tool exposure (typically gated to staff users). /// The to monitor for cancellation requests. The default is . /// Indicates whether the session options patch was applied successfully. - public async Task UpdateAsync(string? model = null, string? reasoningEffort = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, object? provider = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, bool? enableScriptSafety = null, string? shellInitProfile = null, IList? shellProcessFlags = null, object? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, CancellationToken cancellationToken = default) + public async Task UpdateAsync(string? model = null, string? reasoningEffort = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, object? provider = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, bool? enableScriptSafety = null, string? shellInitProfile = null, IList? shellProcessFlags = null, object? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ReasoningEffort = reasoningEffort, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = provider, WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, EnableScriptSafety = enableScriptSafety, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = sandboxConfig, LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled }; + var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ReasoningEffort = reasoningEffort, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = CopilotClient.ToJsonElementForWire(provider), WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, EnableScriptSafety = enableScriptSafety, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = CopilotClient.ToJsonElementForWire(sandboxConfig), LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies?.Select(static v => CopilotClient.ToJsonElementForWire(v)!.Value).ToList(), ManageScheduleEnabled = manageScheduleEnabled }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.options.update", [request], cancellationToken); } } @@ -11979,7 +13266,7 @@ public async Task HandlePendingToolCallAsync(string ArgumentNullException.ThrowIfNull(requestId); _session.ThrowIfDisposed(); - var request = new HandlePendingToolCallRequest { SessionId = _session.SessionId, RequestId = requestId, Result = result, Error = error }; + var request = new HandlePendingToolCallRequest { SessionId = _session.SessionId, RequestId = requestId, Result = CopilotClient.ToJsonElementForWire(result), Error = error }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tools.handlePendingToolCall", [request], cancellationToken); } @@ -12836,7 +14123,7 @@ public async Task ReadAsync(string? cursor = null, int? max = { _session.ThrowIfDisposed(); - var request = new EventLogReadRequest { SessionId = _session.SessionId, Cursor = cursor, Max = max, Wait = waitMs, Types = types, AgentScope = agentScope }; + var request = new EventLogReadRequest { SessionId = _session.SessionId, Cursor = cursor, Max = max, Wait = waitMs, Types = CopilotClient.ToJsonElementForWire(types), AgentScope = agentScope }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.eventLog.read", [request], cancellationToken); } @@ -13172,16 +14459,17 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, FuncSchema for the `CanvasOpenedData` type. +/// Represents the session.canvas.opened event. +public sealed partial class SessionCanvasOpenedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.canvas.opened"; + + /// The session.canvas.opened event payload. + [JsonPropertyName("data")] + public required SessionCanvasOpenedData Data { get; set; } +} + +/// Schema for the `CanvasRegistryChangedData` type. +/// Represents the session.canvas.registry_changed event. +public sealed partial class SessionCanvasRegistryChangedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.canvas.registry_changed"; + + /// The session.canvas.registry_changed event payload. + [JsonPropertyName("data")] + public required SessionCanvasRegistryChangedData Data { get; set; } +} + +/// MCP App view called a tool on a connected MCP server (SEP-1865). +/// Represents the mcp_app.tool_call_complete event. +public sealed partial class McpAppToolCallCompleteEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "mcp_app.tool_call_complete"; + + /// The mcp_app.tool_call_complete event payload. + [JsonPropertyName("data")] + public required McpAppToolCallCompleteData Data { get; set; } +} + /// Session initialization metadata including context and configuration. public sealed partial class SessionStartData { @@ -1345,6 +1387,11 @@ public sealed partial class SessionErrorData [JsonPropertyName("providerCallId")] public string? ProviderCallId { get; set; } + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("serviceRequestId")] + public string? ServiceRequestId { get; set; } + /// Error stack trace, when available. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("stack")] @@ -1460,6 +1507,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("cause")] public string? Cause { get; set; } + /// Context tier after the model change; null explicitly clears a previously selected tier. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("contextTier")] + public SessionModelChangeDataContextTier? ContextTier { get; set; } + /// Newly selected model identifier. [JsonPropertyName("newModel")] public required string NewModel { get; set; } @@ -1667,14 +1719,16 @@ public sealed partial class SessionShutdownData public required TimeSpan TotalApiDuration { get; set; } /// Session-wide accumulated nano-AI units cost. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("totalNanoAiu")] public double? TotalNanoAiu { get; set; } /// Total number of premium API requests used during the session. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("totalPremiumRequests")] - public double? TotalPremiumRequests { get; set; } + internal double? TotalPremiumRequests { get; set; } } /// Working directory and git context at session start. @@ -1833,6 +1887,11 @@ public sealed partial class SessionCompactionCompleteData [JsonPropertyName("requestId")] public string? RequestId { get; set; } + /// Copilot service request ID (x-copilot-service-request-id header) for the compaction LLM call. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("serviceRequestId")] + public string? ServiceRequestId { get; set; } + /// Whether compaction completed successfully. [JsonPropertyName("success")] public required bool Success { get; set; } @@ -1987,11 +2046,13 @@ public sealed partial class AssistantStreamingDeltaData public sealed partial class AssistantMessageData { /// Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("anthropicAdvisorBlocks")] - public object[]? AnthropicAdvisorBlocks { get; set; } + public JsonElement[]? AnthropicAdvisorBlocks { get; set; } /// Anthropic advisor model ID used for this response, for timeline display on replay. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("anthropicAdvisorModel")] public string? AnthropicAdvisorModel { get; set; } @@ -2051,6 +2112,11 @@ public sealed partial class AssistantMessageData [JsonPropertyName("requestId")] public string? RequestId { get; set; } + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("serviceRequestId")] + public string? ServiceRequestId { get; set; } + /// Tool invocations requested by the assistant in this message. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolRequests")] @@ -2127,10 +2193,12 @@ public sealed partial class AssistantUsageData /// Per-request cost and usage data from the CAPI copilot_usage response field. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("copilotUsage")] - public AssistantUsageCopilotUsage? CopilotUsage { get; set; } + internal AssistantUsageCopilotUsage? CopilotUsage { get; set; } /// Model multiplier cost for billing purposes. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cost")] public double? Cost { get; set; } @@ -2180,8 +2248,9 @@ public sealed partial class AssistantUsageData /// Per-quota resource usage snapshots, keyed by quota identifier. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("quotaSnapshots")] - public IDictionary? QuotaSnapshots { get; set; } + internal IDictionary? QuotaSnapshots { get; set; } /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2193,6 +2262,11 @@ public sealed partial class AssistantUsageData [JsonPropertyName("reasoningTokens")] public long? ReasoningTokens { get; set; } + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("serviceRequestId")] + public string? ServiceRequestId { get; set; } + /// Time to first token in milliseconds. Only available for streaming requests. [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2234,6 +2308,11 @@ public sealed partial class ModelCallFailureData [JsonPropertyName("providerCallId")] public string? ProviderCallId { get; set; } + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("serviceRequestId")] + public string? ServiceRequestId { get; set; } + /// Where the failed model call originated. [JsonPropertyName("source")] public required ModelCallFailureSource Source { get; set; } @@ -2258,7 +2337,7 @@ public sealed partial class ToolUserRequestedData /// Arguments for the tool invocation. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } /// Unique identifier for this tool call. [JsonPropertyName("toolCallId")] @@ -2275,7 +2354,7 @@ public sealed partial class ToolExecutionStartData /// Arguments passed to the tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } /// Name of the MCP server hosting this tool, when the tool is an MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2380,10 +2459,15 @@ public sealed partial class ToolExecutionCompleteData [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Tool definition metadata, present for MCP tools with MCP Apps support. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolDescription")] + public ToolExecutionCompleteToolDescription? ToolDescription { get; set; } + /// Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolTelemetry")] - public IDictionary? ToolTelemetry { get; set; } + public IDictionary? ToolTelemetry { get; set; } /// Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2425,6 +2509,16 @@ public sealed partial class SkillInvokedData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [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), personal-claude (~/.claude/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("source")] + public string? Source { get; set; } + + /// 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). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("trigger")] + public SkillInvokedTrigger? Trigger { get; set; } } /// Sub-agent startup details including parent tool call and agent information. @@ -2565,7 +2659,7 @@ public sealed partial class HookStartData /// Input data passed to the hook. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("input")] - public object? Input { get; set; } + public JsonElement? Input { get; set; } } /// Hook invocation completion details including output, success status, and error information. @@ -2587,7 +2681,7 @@ public sealed partial class HookEndData /// Output data produced by the hook. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("output")] - public object? Output { get; set; } + public JsonElement? Output { get; set; } /// Whether the hook completed successfully. [JsonPropertyName("success")] @@ -2760,7 +2854,7 @@ public sealed partial class ElicitationCompletedData /// The submitted form data when action is 'accept'; keys match the requested schema fields. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("content")] - public IDictionary? Content { get; set; } + public IDictionary? Content { get; set; } /// Request ID of the resolved elicitation request; clients should dismiss any UI for this request. [JsonPropertyName("requestId")] @@ -2772,7 +2866,7 @@ public sealed partial class SamplingRequestedData { /// The JSON-RPC request ID from the MCP protocol. [JsonPropertyName("mcpRequestId")] - public required object McpRequestId { get; set; } + public required JsonElement McpRequestId { get; set; } /// Unique identifier for this sampling request; used to respond via session.respondToSampling(). [JsonPropertyName("requestId")] @@ -2831,7 +2925,7 @@ public sealed partial class SessionCustomNotificationData /// Source-defined JSON payload for the custom notification. [JsonPropertyName("payload")] - public required object Payload { get; set; } + public required JsonElement Payload { get; set; } /// Namespace for the custom notification producer. [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] @@ -2856,7 +2950,7 @@ public sealed partial class ExternalToolRequestedData /// Arguments to pass to the external tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } /// Unique identifier for this request; used to respond via session.respondToExternalTool(). [JsonPropertyName("requestId")] @@ -3080,6 +3174,11 @@ public sealed partial class SessionMcpServersLoadedData /// Schema for the `McpServerStatusChangedData` type. public sealed partial class SessionMcpServerStatusChangedData { + /// Error message if the server entered a failed state. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } + /// Name of the MCP server whose status changed. [JsonPropertyName("serverName")] public required string ServerName { get; set; } @@ -3097,6 +3196,103 @@ public sealed partial class SessionExtensionsLoadedData public required ExtensionsLoadedExtension[] Extensions { get; set; } } +/// Schema for the `CanvasOpenedData` type. +public sealed partial class SessionCanvasOpenedData +{ + /// Runtime-controlled routing state for the instance. "ready" when the provider connection is live; "stale" when the provider has gone away and the instance is awaiting rebinding. + [JsonPropertyName("availability")] + public required CanvasOpenedAvailability Availability { get; set; } + + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public required string CanvasId { get; set; } + + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public required string ExtensionId { get; set; } + + /// Owning extension display name, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } + + /// Input supplied when the instance was opened. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } + + /// Stable caller-supplied canvas instance identifier. + [JsonPropertyName("instanceId")] + public required string InstanceId { get; set; } + + /// Whether this notification represents an idempotent reopen. + [JsonPropertyName("reopen")] + public required bool Reopen { get; set; } + + /// Provider-supplied status text. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// Rendered title. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// URL for web-rendered canvases. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("url")] + public string? Url { get; set; } +} + +/// Schema for the `CanvasRegistryChangedData` type. +public sealed partial class SessionCanvasRegistryChangedData +{ + /// Canvas declarations currently available. + [JsonPropertyName("canvases")] + public required CanvasRegistryChangedCanvas[] Canvases { get; set; } +} + +/// MCP App view called a tool on a connected MCP server (SEP-1865). +public sealed partial class McpAppToolCallCompleteData +{ + /// Arguments passed to the tool by the app view, if any. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("arguments")] + public IDictionary? Arguments { get; set; } + + /// Wall-clock duration of the underlying tools/call in milliseconds. + [JsonPropertyName("durationMs")] + public required double DurationMs { get; set; } + + /// Set when the underlying tools/call threw an error before returning a CallToolResult. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public McpAppToolCallCompleteError? Error { get; set; } + + /// Standard MCP CallToolResult returned by the server. Present whether or not the call set isError. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public IDictionary? Result { get; set; } + + /// Name of the MCP server hosting the tool. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// True when the call completed without throwing AND the MCP CallToolResult did not set isError. + [JsonPropertyName("success")] + public required bool Success { get; set; } + + /// The tool's `_meta.ui` block at the time of the call, so consumers can decide whether to forward the result to the model without re-listing tools. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolMeta")] + public McpAppToolCallCompleteToolMeta? ToolMeta { get; set; } + + /// MCP tool name that was invoked. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } +} + /// Working directory and git context at session start. /// Nested data type for WorkingDirectoryContext. public sealed partial class WorkingDirectoryContext @@ -3181,11 +3377,13 @@ public sealed partial class ShutdownCodeChanges public sealed partial class ShutdownModelMetricRequests { /// Cumulative cost multiplier for requests to this model. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cost")] public double? Cost { get; set; } /// Total number of API requests made to this model. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("count")] public long? Count { get; set; } @@ -3240,6 +3438,7 @@ public sealed partial class ShutdownModelMetric public IDictionary? TokenDetails { get; set; } /// Accumulated nano-AI units cost for this model. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("totalNanoAiu")] public double? TotalNanoAiu { get; set; } @@ -3281,7 +3480,7 @@ public sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsageTo /// Per-request cost and usage data from the CAPI copilot_usage response field. /// Nested data type for CompactionCompleteCompactionTokensUsedCopilotUsage. -public sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsage +internal sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsage { /// Itemized token usage breakdown. [JsonPropertyName("tokenDetails")] @@ -3308,8 +3507,9 @@ public sealed partial class CompactionCompleteCompactionTokensUsed /// Per-request cost and usage data from the CAPI copilot_usage response field. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("copilotUsage")] - public CompactionCompleteCompactionTokensUsedCopilotUsage? CopilotUsage { get; set; } + internal CompactionCompleteCompactionTokensUsedCopilotUsage? CopilotUsage { get; set; } /// Duration of the compaction LLM call in milliseconds. [JsonConverter(typeof(MillisecondsTimeSpanConverter))] @@ -3526,7 +3726,7 @@ public sealed partial class AssistantMessageToolRequest /// Arguments to pass to the tool, format depends on the tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } /// Resolved intention summary describing what this specific call does. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -3585,7 +3785,7 @@ public sealed partial class AssistantUsageCopilotUsageTokenDetail /// Per-request cost and usage data from the CAPI copilot_usage response field. /// Nested data type for AssistantUsageCopilotUsage. -public sealed partial class AssistantUsageCopilotUsage +internal sealed partial class AssistantUsageCopilotUsage { /// Itemized token usage breakdown. [JsonPropertyName("tokenDetails")] @@ -3598,40 +3798,48 @@ public sealed partial class AssistantUsageCopilotUsage /// Schema for the `AssistantUsageQuotaSnapshot` type. /// Nested data type for AssistantUsageQuotaSnapshot. -public sealed partial class AssistantUsageQuotaSnapshot +internal sealed partial class AssistantUsageQuotaSnapshot { /// Total requests allowed by the entitlement. + [JsonInclude] [JsonPropertyName("entitlementRequests")] - public required long EntitlementRequests { get; set; } + internal required long EntitlementRequests { get; set; } /// Whether the user has an unlimited usage entitlement. + [JsonInclude] [JsonPropertyName("isUnlimitedEntitlement")] - public required bool IsUnlimitedEntitlement { get; set; } + internal required bool IsUnlimitedEntitlement { get; set; } /// Number of additional usage requests made this period. + [JsonInclude] [JsonPropertyName("overage")] - public required double Overage { get; set; } + internal required double Overage { get; set; } /// Whether additional usage is allowed when quota is exhausted. + [JsonInclude] [JsonPropertyName("overageAllowedWithExhaustedQuota")] - public required bool OverageAllowedWithExhaustedQuota { get; set; } + internal required bool OverageAllowedWithExhaustedQuota { get; set; } /// Percentage of quota remaining (0 to 100). + [JsonInclude] [JsonPropertyName("remainingPercentage")] - public required double RemainingPercentage { get; set; } + internal required double RemainingPercentage { get; set; } /// Date when the quota resets. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("resetDate")] - public DateTimeOffset? ResetDate { get; set; } + internal DateTimeOffset? ResetDate { get; set; } /// Whether usage is still permitted after quota exhaustion. + [JsonInclude] [JsonPropertyName("usageAllowedWithExhaustedQuota")] - public required bool UsageAllowedWithExhaustedQuota { get; set; } + internal required bool UsageAllowedWithExhaustedQuota { get; set; } /// Number of requests already consumed. + [JsonInclude] [JsonPropertyName("usedRequests")] - public required long UsedRequests { get; set; } + internal required long UsedRequests { get; set; } } /// Error details when the tool execution failed. @@ -3933,6 +4141,143 @@ public partial class ToolExecutionCompleteContent } +/// Schema for the `ToolExecutionCompleteUIResourceMetaUICsp` type. +/// Nested data type for ToolExecutionCompleteUIResourceMetaUICsp. +public sealed partial class ToolExecutionCompleteUIResourceMetaUICsp +{ + /// Gets or sets the baseUriDomains value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("baseUriDomains")] + public string[]? BaseUriDomains { get; set; } + + /// Gets or sets the connectDomains value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("connectDomains")] + public string[]? ConnectDomains { get; set; } + + /// Gets or sets the frameDomains value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("frameDomains")] + public string[]? FrameDomains { get; set; } + + /// Gets or sets the resourceDomains value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resourceDomains")] + public string[]? ResourceDomains { get; set; } +} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsCamera` type. +/// Nested data type for ToolExecutionCompleteUIResourceMetaUIPermissionsCamera. +public sealed partial class ToolExecutionCompleteUIResourceMetaUIPermissionsCamera +{ +} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite` type. +/// Nested data type for ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite. +public sealed partial class ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite +{ +} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation` type. +/// Nested data type for ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation. +public sealed partial class ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation +{ +} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone` type. +/// Nested data type for ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone. +public sealed partial class ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone +{ +} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissions` type. +/// Nested data type for ToolExecutionCompleteUIResourceMetaUIPermissions. +public sealed partial class ToolExecutionCompleteUIResourceMetaUIPermissions +{ + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsCamera` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("camera")] + public ToolExecutionCompleteUIResourceMetaUIPermissionsCamera? Camera { get; set; } + + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("clipboardWrite")] + public ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite? ClipboardWrite { get; set; } + + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("geolocation")] + public ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation? Geolocation { get; set; } + + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("microphone")] + public ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone? Microphone { get; set; } +} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUI` type. +/// Nested data type for ToolExecutionCompleteUIResourceMetaUI. +public sealed partial class ToolExecutionCompleteUIResourceMetaUI +{ + /// Schema for the `ToolExecutionCompleteUIResourceMetaUICsp` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("csp")] + public ToolExecutionCompleteUIResourceMetaUICsp? Csp { get; set; } + + /// Gets or sets the domain value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("domain")] + public string? Domain { get; set; } + + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissions` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("permissions")] + public ToolExecutionCompleteUIResourceMetaUIPermissions? Permissions { get; set; } + + /// Gets or sets the prefersBorder value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("prefersBorder")] + public bool? PrefersBorder { get; set; } +} + +/// Resource-level UI metadata (CSP, permissions, visual preferences). +/// Nested data type for ToolExecutionCompleteUIResourceMeta. +public sealed partial class ToolExecutionCompleteUIResourceMeta +{ + /// Schema for the `ToolExecutionCompleteUIResourceMetaUI` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ui")] + public ToolExecutionCompleteUIResourceMetaUI? Ui { get; set; } +} + +/// MCP Apps UI resource content for rendering in a sandboxed iframe. +/// Nested data type for ToolExecutionCompleteUIResource. +public sealed partial class ToolExecutionCompleteUIResource +{ + /// Resource-level UI metadata (CSP, permissions, visual preferences). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("_meta")] + public ToolExecutionCompleteUIResourceMeta? _meta { get; set; } + + /// Base64-encoded HTML content. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("blob")] + public string? Blob { get; set; } + + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public required string MimeType { get; set; } + + /// HTML content as a string. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// The ui:// URI of the resource. + [JsonPropertyName("uri")] + public required string Uri { get; set; } +} + /// Tool execution result on success. /// Nested data type for ToolExecutionCompleteResult. public sealed partial class ToolExecutionCompleteResult @@ -3950,6 +4295,55 @@ public sealed partial class ToolExecutionCompleteResult [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("detailedContent")] public string? DetailedContent { get; set; } + + /// MCP Apps UI resource content for rendering in a sandboxed iframe. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("uiResource")] + public ToolExecutionCompleteUIResource? UiResource { get; set; } +} + +/// Schema for the `ToolExecutionCompleteToolDescriptionMetaUI` type. +/// Nested data type for ToolExecutionCompleteToolDescriptionMetaUI. +public sealed partial class ToolExecutionCompleteToolDescriptionMetaUI +{ + /// URI of the UI resource. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resourceUri")] + public string? ResourceUri { get; set; } + + /// Who can access this tool. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("visibility")] + public ToolExecutionCompleteToolDescriptionMetaUIVisibility[]? Visibility { get; set; } +} + +/// MCP Apps metadata for UI resource association. +/// Nested data type for ToolExecutionCompleteToolDescriptionMeta. +public sealed partial class ToolExecutionCompleteToolDescriptionMeta +{ + /// Schema for the `ToolExecutionCompleteToolDescriptionMetaUI` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ui")] + public ToolExecutionCompleteToolDescriptionMetaUI? Ui { get; set; } +} + +/// Tool definition metadata, present for MCP tools with MCP Apps support. +/// Nested data type for ToolExecutionCompleteToolDescription. +public sealed partial class ToolExecutionCompleteToolDescription +{ + /// MCP Apps metadata for UI resource association. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("_meta")] + public ToolExecutionCompleteToolDescriptionMeta? _meta { get; set; } + + /// Tool description. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Tool name. + [JsonPropertyName("name")] + public required string Name { get; set; } } /// Error details when the hook failed. @@ -3978,7 +4372,7 @@ public sealed partial class SystemMessageMetadata /// Template variables used when constructing the prompt. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("variables")] - public IDictionary? Variables { get; set; } + public IDictionary? Variables { get; set; } } /// Schema for the `SystemNotificationAgentCompleted` type. @@ -4282,7 +4676,7 @@ public sealed partial class PermissionRequestMcp : PermissionRequest /// Arguments to pass to the MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonElement? Args { get; set; } /// Whether this MCP tool is read-only (no side effects). [JsonPropertyName("readOnly")] @@ -4382,7 +4776,7 @@ public sealed partial class PermissionRequestCustomTool : PermissionRequest /// Arguments to pass to the custom tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonElement? Args { get; set; } /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -4414,7 +4808,7 @@ public sealed partial class PermissionRequestHook : PermissionRequest /// Arguments of the tool call being gated. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolArgs")] - public object? ToolArgs { get; set; } + public JsonElement? ToolArgs { get; set; } /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -4597,7 +4991,7 @@ public sealed partial class PermissionPromptRequestMcp : PermissionPromptRequest /// Arguments to pass to the MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonElement? Args { get; set; } /// Name of the MCP server providing the tool. [JsonPropertyName("serverName")] @@ -4693,7 +5087,7 @@ public sealed partial class PermissionPromptRequestCustomTool : PermissionPrompt /// Arguments to pass to the custom tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonElement? Args { get; set; } /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -4747,7 +5141,7 @@ public sealed partial class PermissionPromptRequestHook : PermissionPromptReques /// Arguments of the tool call being gated. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolArgs")] - public object? ToolArgs { get; set; } + public JsonElement? ToolArgs { get; set; } /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -5117,7 +5511,7 @@ public sealed partial class ElicitationRequestedSchema { /// Form field definitions, keyed by field name. [JsonPropertyName("properties")] - public required IDictionary Properties { get; set; } + public required IDictionary Properties { get; set; } /// List of required field names. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -5166,10 +5560,20 @@ public sealed partial class CommandsChangedCommand /// Nested data type for CapabilitiesChangedUI. public sealed partial class CapabilitiesChangedUI { + /// Whether canvas rendering is now supported. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("canvases")] + public bool? Canvases { get; set; } + /// Whether elicitation is now supported. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("elicitation")] public bool? Elicitation { get; set; } + + /// Whether MCP Apps (SEP-1865) UI passthrough is now supported. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mcpApps")] + public bool? McpApps { get; set; } } /// Schema for the `SkillsLoadedSkill` type. @@ -5253,6 +5657,16 @@ public sealed partial class McpServersLoadedServer [JsonPropertyName("name")] public required string Name { get; set; } + /// Name of the plugin that supplied the effective MCP server config, only when source is plugin. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pluginName")] + public string? PluginName { get; set; } + + /// Version of the plugin that supplied the effective MCP server config, only when source is plugin. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pluginVersion")] + public string? PluginVersion { get; set; } + /// Configuration source: user, workspace, plugin, or builtin. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] @@ -5261,6 +5675,11 @@ public sealed partial class McpServersLoadedServer /// Connection status: connected, failed, needs-auth, pending, disabled, or not_configured. [JsonPropertyName("status")] public required McpServerStatus Status { get; set; } + + /// Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("transport")] + public McpServerTransport? Transport { get; set; } } /// Schema for the `ExtensionsLoadedExtension` type. @@ -5284,6 +5703,97 @@ public sealed partial class ExtensionsLoadedExtension public required ExtensionsLoadedExtensionStatus Status { get; set; } } +/// Schema for the `CanvasRegistryChangedCanvasAction` type. +/// Nested data type for CanvasRegistryChangedCanvasAction. +public sealed partial class CanvasRegistryChangedCanvasAction +{ + /// Action description. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// JSON Schema for action input. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } + + /// Action name. + [JsonPropertyName("name")] + public required string Name { get; set; } +} + +/// Schema for the `CanvasRegistryChangedCanvas` type. +/// Nested data type for CanvasRegistryChangedCanvas. +public sealed partial class CanvasRegistryChangedCanvas +{ + /// Actions the agent or host may invoke. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("actions")] + public CanvasRegistryChangedCanvasAction[]? Actions { get; set; } + + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public required string CanvasId { get; set; } + + /// Short, single-sentence description shown to the agent in canvas catalogs. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Human-readable canvas name. + [JsonPropertyName("displayName")] + public required string DisplayName { get; set; } + + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public required string ExtensionId { get; set; } + + /// Owning extension display name, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } + + /// JSON Schema for canvas open input. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } +} + +/// Set when the underlying tools/call threw an error before returning a CallToolResult. +/// Nested data type for McpAppToolCallCompleteError. +public sealed partial class McpAppToolCallCompleteError +{ + /// Human-readable error message. + [JsonPropertyName("message")] + public required string Message { get; set; } +} + +/// Schema for the `McpAppToolCallCompleteToolMetaUI` type. +/// Nested data type for McpAppToolCallCompleteToolMetaUI. +public sealed partial class McpAppToolCallCompleteToolMetaUI +{ + /// `ui://` URI declared by the tool's `_meta.ui.resourceUri`. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resourceUri")] + public string? ResourceUri { get; set; } + + /// Tool visibility per SEP-1865 (typically a subset of `["model","app"]`). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("visibility")] + public string[]? Visibility { get; set; } +} + +/// The tool's `_meta.ui` block at the time of the call, so consumers can decide whether to forward the result to the model without re-listing tools. +/// Nested data type for McpAppToolCallCompleteToolMeta. +public sealed partial class McpAppToolCallCompleteToolMeta +{ + /// Schema for the `McpAppToolCallCompleteToolMetaUI` type. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ui")] + public McpAppToolCallCompleteToolMetaUI? Ui { get; set; } +} + /// Hosting platform type of the repository (github or ado). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -5409,6 +5919,67 @@ public override void Write(Utf8JsonWriter writer, ReasoningSummary value, JsonSe } } +/// Defines the allowed values. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SessionModelChangeDataContextTier : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SessionModelChangeDataContextTier(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Default context tier with standard context window size. + public static SessionModelChangeDataContextTier Default { get; } = new("default"); + + /// Extended context tier with a larger context window. + public static SessionModelChangeDataContextTier LongContext { get; } = new("long_context"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SessionModelChangeDataContextTier left, SessionModelChangeDataContextTier right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SessionModelChangeDataContextTier left, SessionModelChangeDataContextTier right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SessionModelChangeDataContextTier other && Equals(other); + + /// + public bool Equals(SessionModelChangeDataContextTier 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 SessionModelChangeDataContextTier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SessionModelChangeDataContextTier value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionModelChangeDataContextTier)); + } + } +} + /// The session mode the agent is operating in. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -6168,6 +6739,131 @@ public override void Write(Utf8JsonWriter writer, ToolExecutionCompleteContentRe } } +/// Allowed values for the `ToolExecutionCompleteToolDescriptionMetaUIVisibility` enumeration. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ToolExecutionCompleteToolDescriptionMetaUIVisibility : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ToolExecutionCompleteToolDescriptionMetaUIVisibility(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Tool is callable by the model (LLM tool surface). + public static ToolExecutionCompleteToolDescriptionMetaUIVisibility Model { get; } = new("model"); + + /// Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool. + public static ToolExecutionCompleteToolDescriptionMetaUIVisibility App { get; } = new("app"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ToolExecutionCompleteToolDescriptionMetaUIVisibility left, ToolExecutionCompleteToolDescriptionMetaUIVisibility right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ToolExecutionCompleteToolDescriptionMetaUIVisibility left, ToolExecutionCompleteToolDescriptionMetaUIVisibility right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ToolExecutionCompleteToolDescriptionMetaUIVisibility other && Equals(other); + + /// + public bool Equals(ToolExecutionCompleteToolDescriptionMetaUIVisibility 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 ToolExecutionCompleteToolDescriptionMetaUIVisibility Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ToolExecutionCompleteToolDescriptionMetaUIVisibility value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ToolExecutionCompleteToolDescriptionMetaUIVisibility)); + } + } +} + +/// 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). +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SkillInvokedTrigger : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SkillInvokedTrigger(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Skill invocation requested explicitly by the user, such as via a slash command or UI affordance. + public static SkillInvokedTrigger UserInvoked { get; } = new("user-invoked"); + + /// Skill invocation requested by the agent. + public static SkillInvokedTrigger AgentInvoked { get; } = new("agent-invoked"); + + /// Skill content loaded as part of another context, such as a configured custom agent or subagent. + public static SkillInvokedTrigger ContextLoad { get; } = new("context-load"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SkillInvokedTrigger left, SkillInvokedTrigger right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SkillInvokedTrigger left, SkillInvokedTrigger right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SkillInvokedTrigger other && Equals(other); + + /// + public bool Equals(SkillInvokedTrigger 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 SkillInvokedTrigger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SkillInvokedTrigger value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInvokedTrigger)); + } + } +} + /// Message role: "system" for system prompts, "developer" for developer-injected instructions. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -6948,6 +7644,73 @@ public override void Write(Utf8JsonWriter writer, McpServerStatus value, JsonSer } } +/// Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server). +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpServerTransport : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpServerTransport(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Server communicates over stdio with a local child process. + public static McpServerTransport Stdio { get; } = new("stdio"); + + /// Server communicates over streamable HTTP. + public static McpServerTransport Http { get; } = new("http"); + + /// Server communicates over Server-Sent Events (deprecated). + public static McpServerTransport Sse { get; } = new("sse"); + + /// Server is backed by an in-memory runtime implementation. + public static McpServerTransport Memory { get; } = new("memory"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpServerTransport left, McpServerTransport right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpServerTransport left, McpServerTransport right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpServerTransport other && Equals(other); + + /// + public bool Equals(McpServerTransport 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 McpServerTransport Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpServerTransport value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpServerTransport)); + } + } +} + /// Discovery source. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -7076,6 +7839,67 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu } } +/// Runtime-controlled routing state for the instance. "ready" when the provider connection is live; "stale" when the provider has gone away and the instance is awaiting rebinding. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CanvasOpenedAvailability : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CanvasOpenedAvailability(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Provider connection is live; actions can be invoked. + public static CanvasOpenedAvailability Ready { get; } = new("ready"); + + /// Provider has gone away; the instance is awaiting rebinding. + public static CanvasOpenedAvailability Stale { get; } = new("stale"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CanvasOpenedAvailability left, CanvasOpenedAvailability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CanvasOpenedAvailability left, CanvasOpenedAvailability right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is CanvasOpenedAvailability other && Equals(other); + + /// + public bool Equals(CanvasOpenedAvailability 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 CanvasOpenedAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, CanvasOpenedAvailability value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CanvasOpenedAvailability)); + } + } +} + [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -7111,6 +7935,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(AutoModeSwitchCompletedEvent))] [JsonSerializable(typeof(AutoModeSwitchRequestedData))] [JsonSerializable(typeof(AutoModeSwitchRequestedEvent))] +[JsonSerializable(typeof(CanvasRegistryChangedCanvas))] +[JsonSerializable(typeof(CanvasRegistryChangedCanvasAction))] [JsonSerializable(typeof(CapabilitiesChangedData))] [JsonSerializable(typeof(CapabilitiesChangedEvent))] [JsonSerializable(typeof(CapabilitiesChangedUI))] @@ -7149,6 +7975,11 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(HookEndEvent))] [JsonSerializable(typeof(HookStartData))] [JsonSerializable(typeof(HookStartEvent))] +[JsonSerializable(typeof(McpAppToolCallCompleteData))] +[JsonSerializable(typeof(McpAppToolCallCompleteError))] +[JsonSerializable(typeof(McpAppToolCallCompleteEvent))] +[JsonSerializable(typeof(McpAppToolCallCompleteToolMeta))] +[JsonSerializable(typeof(McpAppToolCallCompleteToolMetaUI))] [JsonSerializable(typeof(McpOauthCompletedData))] [JsonSerializable(typeof(McpOauthCompletedEvent))] [JsonSerializable(typeof(McpOauthRequiredData))] @@ -7205,6 +8036,10 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SamplingRequestedEvent))] [JsonSerializable(typeof(SessionBackgroundTasksChangedData))] [JsonSerializable(typeof(SessionBackgroundTasksChangedEvent))] +[JsonSerializable(typeof(SessionCanvasOpenedData))] +[JsonSerializable(typeof(SessionCanvasOpenedEvent))] +[JsonSerializable(typeof(SessionCanvasRegistryChangedData))] +[JsonSerializable(typeof(SessionCanvasRegistryChangedEvent))] [JsonSerializable(typeof(SessionCompactionCompleteData))] [JsonSerializable(typeof(SessionCompactionCompleteEvent))] [JsonSerializable(typeof(SessionCompactionStartData))] @@ -7310,6 +8145,18 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolExecutionCompleteError))] [JsonSerializable(typeof(ToolExecutionCompleteEvent))] [JsonSerializable(typeof(ToolExecutionCompleteResult))] +[JsonSerializable(typeof(ToolExecutionCompleteToolDescription))] +[JsonSerializable(typeof(ToolExecutionCompleteToolDescriptionMeta))] +[JsonSerializable(typeof(ToolExecutionCompleteToolDescriptionMetaUI))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResource))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResourceMeta))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResourceMetaUI))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResourceMetaUICsp))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResourceMetaUIPermissions))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResourceMetaUIPermissionsCamera))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation))] +[JsonSerializable(typeof(ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone))] [JsonSerializable(typeof(ToolExecutionPartialResultData))] [JsonSerializable(typeof(ToolExecutionPartialResultEvent))] [JsonSerializable(typeof(ToolExecutionProgressData))] diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index 2b69cbb6f..7a9fa2bdc 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -68,7 +68,7 @@ - + <_VersionPropsContent> diff --git a/dotnet/src/JsonRpc.cs b/dotnet/src/JsonRpc.cs index 866bb868f..df7170373 100644 --- a/dotnet/src/JsonRpc.cs +++ b/dotnet/src/JsonRpc.cs @@ -486,13 +486,21 @@ private async Task HandleIncomingMethodAsync(string methodName, JsonElement mess } catch (Exception ex) when (ex is not OperationCanceledException) { + var actual = ex is TargetInvocationException tie && tie.InnerException != null ? tie.InnerException : ex; if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Error handling JSON-RPC method {Method}: {Error}", methodName, ex.Message); + _logger.LogDebug("Error handling JSON-RPC method {Method}: {Error}", methodName, actual.Message); } if (requestId.HasValue) { - await SendErrorResponseAsync(requestId.Value, ErrorCodeInternalError, ex.Message, cancellationToken).ConfigureAwait(false); + if (actual is LocalRpcInvocationException lre) + { + await SendErrorResponseAsync(requestId.Value, lre.Code, lre.Message, lre.Data, cancellationToken).ConfigureAwait(false); + } + else + { + await SendErrorResponseAsync(requestId.Value, ErrorCodeInternalError, actual.Message, cancellationToken).ConfigureAwait(false); + } } } } @@ -718,13 +726,16 @@ await SendMessageAsync(new JsonRpcResponse } private async Task SendErrorResponseAsync(JsonElement id, int code, string message, CancellationToken cancellationToken) + => await SendErrorResponseAsync(id, code, message, data: null, cancellationToken).ConfigureAwait(false); + + private async Task SendErrorResponseAsync(JsonElement id, int code, string message, JsonElement? data, CancellationToken cancellationToken) { try { await SendMessageAsync(new JsonRpcErrorResponse { Id = id, - Error = new JsonRpcError { Code = code, Message = message }, + Error = new JsonRpcError { Code = code, Message = message, Data = data }, }, JsonRpcWireContext.Default.JsonRpcErrorResponse, cancellationToken).ConfigureAwait(false); } catch (Exception ex) when (ex is IOException or ObjectDisposedException or OperationCanceledException) @@ -852,6 +863,10 @@ private sealed class JsonRpcError [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; + + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonElement? Data { get; set; } } private sealed class JsonRpcNotification @@ -891,3 +906,20 @@ internal sealed class RemoteRpcException(string message, int errorCode, Exceptio public int ErrorCode { get; } = errorCode; } + +/// +/// Allows handler methods registered via JsonRpcConnection.SetLocalRpcMethod +/// to surface a structured JSON-RPC error response (code, message, and optional +/// data payload) instead of the default ErrorCodeInternalError envelope. +/// +internal sealed class LocalRpcInvocationException : Exception +{ + public LocalRpcInvocationException(int code, string message, JsonElement? data = null) : base(message) + { + Code = code; + Data = data; + } + + public int Code { get; } + public new JsonElement? Data { get; } +} diff --git a/dotnet/src/PermissionDecision.cs b/dotnet/src/PermissionDecision.cs new file mode 100644 index 000000000..54e123791 --- /dev/null +++ b/dotnet/src/PermissionDecision.cs @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Text.Json.Serialization; + +namespace GitHub.Copilot.Rpc; + +/// +/// SDK-only value indicating the handler +/// declines to respond to this permission request. The SDK then suppresses +/// the response so another connected client can answer instead. +/// +public sealed class PermissionDecisionNoResult : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "no-result"; +} + +/// +/// Static factories for the common variants +/// returned by OnPermissionRequest handlers. Use these for quick +/// discoverability via PermissionDecision.<dot>. For richer +/// decisions (per-session, per-location, permanent) that need an +/// Approval payload, instantiate the variant class directly. +/// +[JsonDerivedType(typeof(PermissionDecisionNoResult), "no-result")] +public partial class PermissionDecision +{ + /// Approve this single request. + public static PermissionDecision ApproveOnce() => new PermissionDecisionApproveOnce(); + + /// Reject the request, optionally forwarding feedback to the LLM. + public static PermissionDecision Reject(string? feedback = null) => + new PermissionDecisionReject { Feedback = feedback }; + + /// Deny the request because no user is available to confirm it. + public static PermissionDecision UserNotAvailable() => new PermissionDecisionUserNotAvailable(); + + /// + /// Decline to respond to this permission request, allowing another + /// connected client to answer instead. + /// + public static PermissionDecision NoResult() => new PermissionDecisionNoResult(); +} diff --git a/dotnet/src/PermissionHandlers.cs b/dotnet/src/PermissionHandlers.cs index 0e4af7eac..4386e8ba6 100644 --- a/dotnet/src/PermissionHandlers.cs +++ b/dotnet/src/PermissionHandlers.cs @@ -2,12 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; + namespace GitHub.Copilot; /// Provides pre-built permission request handlers. public static class PermissionHandler { /// A permission handler that approves all permission requests. - public static Func> ApproveAll { get; } = - (_, _) => Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + public static Func> ApproveAll { get; } = + (_, _) => Task.FromResult(PermissionDecision.ApproveOnce()); } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 0916a7b21..bd2309187 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -60,7 +61,7 @@ public sealed partial class CopilotSession : IAsyncDisposable private readonly ILogger _logger; private readonly CopilotClient _parentClient; - private volatile Func>? _permissionHandler; + private volatile Func>? _permissionHandler; private volatile Func>? _userInputHandler; private volatile Func>? _elicitationHandler; private volatile Func>? _exitPlanModeHandler; @@ -75,6 +76,11 @@ private sealed record EventSubscription(Type EventType, Action Han private Dictionary>>? _transformCallbacks; private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1); +#pragma warning disable GHCP001 + private volatile ICanvasHandler? _canvasHandler; + private IReadOnlyList _openCanvases = Array.Empty(); +#pragma warning restore GHCP001 + private int _isDisposed; /// @@ -121,6 +127,19 @@ public SessionCapabilities Capabilities private set; } +#pragma warning disable GHCP001 + /// + /// Canvas instances currently known to be open for this session. + /// + /// + /// Populated from the most recent session.create / session.resume + /// response. This snapshot is not refreshed automatically when canvases open or + /// close after the session is established. + /// + [Experimental(Diagnostics.Experimental)] + public IReadOnlyList OpenCanvases => _openCanvases; +#pragma warning restore GHCP001 + /// /// Gets the UI API for eliciting information from the user during this session. /// @@ -535,7 +554,7 @@ internal void RegisterTools(ICollection tools) /// When the assistant needs permission to perform certain actions (e.g., file operations), /// this handler is called to approve or deny the request. /// - internal void RegisterPermissionHandler(Func>? handler) + internal void RegisterPermissionHandler(Func>? handler) { _permissionHandler = handler; } @@ -545,16 +564,13 @@ internal void RegisterPermissionHandler(Func /// The permission request data from the CLI. /// A task that resolves with the permission decision. - internal async Task HandlePermissionRequestAsync(JsonElement permissionRequestData) + internal async Task HandlePermissionRequestAsync(JsonElement permissionRequestData) { var handler = _permissionHandler; if (handler == null) { - return new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable - }; + return PermissionDecision.UserNotAvailable(); } var request = JsonSerializer.Deserialize(permissionRequestData.GetRawText(), SessionEventsJsonContext.Default.PermissionRequest) @@ -640,7 +656,7 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) ? new ElicitationSchema { Type = data.RequestedSchema.Type, - Properties = data.RequestedSchema.Properties, + Properties = data.RequestedSchema.Properties.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value), Required = data.RequestedSchema.Required?.ToList() } : null; @@ -690,7 +706,7 @@ await HandleElicitationRequestAsync( /// /// Executes a tool handler and sends the result back via the HandlePendingToolCall RPC. /// - private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, string toolCallId, object? arguments, AIFunction tool) + private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, string toolCallId, JsonElement? arguments, AIFunction tool) { try { @@ -710,13 +726,8 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, } }; - if (arguments is not null) + if (arguments is JsonElement incomingJsonArgs) { - if (arguments is not JsonElement incomingJsonArgs) - { - throw new InvalidOperationException($"Incoming arguments must be a {nameof(JsonElement)}; received {arguments.GetType().Name}"); - } - foreach (var prop in incomingJsonArgs.EnumerateObject()) { aiFunctionArgs[prop.Name] = prop.Value; @@ -765,7 +776,7 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, /// /// Executes a permission handler and sends the result back via the HandlePendingPermissionRequest RPC. /// - private async Task ExecutePermissionAndRespondAsync(string requestId, PermissionRequest permissionRequest, Func> handler) + private async Task ExecutePermissionAndRespondAsync(string requestId, PermissionRequest permissionRequest, Func> handler) { try { @@ -775,20 +786,17 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission }; var permissionTimestamp = Stopwatch.GetTimestamp(); - var result = await handler(permissionRequest, invocation); + var decision = await handler(permissionRequest, invocation); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotSession.ExecutePermissionAndRespondAsync dispatch. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}", permissionTimestamp, SessionId, requestId); - if (result.Kind == new PermissionRequestResultKind("no-result")) + if (decision is PermissionDecisionNoResult) { return; } var responseRpcTimestamp = Stopwatch.GetTimestamp(); - PermissionDecision decision = result.Kind == PermissionRequestResultKind.Rejected - ? new PermissionDecisionReject { Feedback = result.Feedback } - : new PermissionDecision { Kind = result.Kind.Value }; await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, decision); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotSession.ExecutePermissionAndRespondAsync response sent successfully. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}", @@ -800,10 +808,7 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission { try { - await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, new PermissionDecision - { - Kind = PermissionRequestResultKind.UserNotAvailable.Value - }); + await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, PermissionDecision.UserNotAvailable()); } catch (IOException) { @@ -875,6 +880,125 @@ internal void SetCapabilities(SessionCapabilities? capabilities) Capabilities = capabilities ?? new SessionCapabilities(); } +#pragma warning disable GHCP001 + internal void SetOpenCanvases(IList? canvases) + { + _openCanvases = canvases is { Count: > 0 } + ? new List(canvases).AsReadOnly() + : Array.Empty(); + } + + internal void SetCanvasHandler(ICanvasHandler? handler) + { + _canvasHandler = handler; + } + + internal async ValueTask HandleCanvasOpenAsync( + string extensionId, + string canvasId, + string instanceId, + JsonElement input, + CanvasHostContext? host) + { + var handler = _canvasHandler ?? throw CanvasErrorHelpers.HandlerUnset(); + var ctx = new CanvasOpenContext + { + SessionId = SessionId, + ExtensionId = extensionId, + CanvasId = canvasId, + InstanceId = instanceId, + Input = input, + Host = host, + }; + try + { + return await handler.OnOpenAsync(ctx, CancellationToken.None).ConfigureAwait(false); + } + catch (CanvasError ce) + { + throw CanvasErrorHelpers.ToRpcException(ce); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw CanvasErrorHelpers.HandlerError(ex.Message); + } + } + + internal async ValueTask HandleCanvasCloseAsync( + string extensionId, + string canvasId, + string instanceId, + CanvasHostContext? host) + { + var handler = _canvasHandler ?? throw CanvasErrorHelpers.HandlerUnset(); + var ctx = new CanvasLifecycleContext + { + SessionId = SessionId, + ExtensionId = extensionId, + CanvasId = canvasId, + InstanceId = instanceId, + Host = host, + }; + try + { + await handler.OnCloseAsync(ctx, CancellationToken.None).ConfigureAwait(false); + } + catch (CanvasError ce) + { + throw CanvasErrorHelpers.ToRpcException(ce); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw CanvasErrorHelpers.HandlerError(ex.Message); + } + } + + internal async ValueTask HandleCanvasActionAsync( + string extensionId, + string canvasId, + string instanceId, + string actionName, + JsonElement input, + CanvasHostContext? host) + { + var handler = _canvasHandler ?? throw CanvasErrorHelpers.HandlerUnset(); + var ctx = new CanvasActionContext + { + SessionId = SessionId, + ExtensionId = extensionId, + CanvasId = canvasId, + InstanceId = instanceId, + ActionName = actionName, + Input = input, + Host = host, + }; + try + { + var result = await handler.OnActionAsync(ctx, CancellationToken.None).ConfigureAwait(false); + return SerializeActionResult(result); + } + catch (CanvasError ce) + { + throw CanvasErrorHelpers.ToRpcException(ce); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw CanvasErrorHelpers.HandlerError(ex.Message); + } + } + + private static JsonElement SerializeActionResult(object? value) + { + var element = CopilotClient.ToJsonElementForWire(value); + if (element.HasValue) + { + return element.Value; + } + using var doc = JsonDocument.Parse("null"); + return doc.RootElement.Clone(); + } +#pragma warning restore GHCP001 + /// /// Dispatches a command.execute event to the registered handler and /// responds via the commands.handlePendingCommand RPC. @@ -957,7 +1081,9 @@ private async Task HandleElicitationRequestAsync(ElicitationContext context, str await Rpc.Ui.HandlePendingElicitationAsync(requestId, new UIElicitationResponse { Action = result.Action, - Content = result.Content + Content = result.Content?.ToDictionary( + kvp => kvp.Key, + kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value) }); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotSession.HandleElicitationRequestAsync response sent successfully. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}", @@ -1000,6 +1126,15 @@ private void AssertElicitation() /// private sealed class SessionUiApiImpl(CopilotSession session) : ISessionUiApi { + // Parses a JSON string and returns a detached JsonElement. Using `using` + // ensures the pooled buffers backing the JsonDocument are released + // promptly; the cloned RootElement is independent of the document. + private static JsonElement ParseJsonElement(string json) + { + using var doc = JsonDocument.Parse(json); + return doc.RootElement.Clone(); + } + public async Task ElicitAsync(ElicitationParams elicitationParams, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(elicitationParams); @@ -1009,12 +1144,18 @@ public async Task ElicitAsync(ElicitationParams elicitationPa var schema = new UIElicitationSchema { Type = elicitationParams.RequestedSchema.Type, - Properties = elicitationParams.RequestedSchema.Properties, + Properties = elicitationParams.RequestedSchema.Properties.ToDictionary( + kvp => kvp.Key, + kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value), Required = elicitationParams.RequestedSchema.Required }; var result = await session.Rpc.Ui.ElicitationAsync(elicitationParams.Message, schema, cancellationToken); - return new ElicitationResult { Action = result.Action, Content = result.Content }; + return new ElicitationResult + { + Action = result.Action, + Content = result.Content?.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value) + }; } public async Task ConfirmAsync(string message, CancellationToken cancellationToken) @@ -1026,9 +1167,9 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat var schema = new UIElicitationSchema { Type = "object", - Properties = new Dictionary + Properties = new Dictionary { - ["confirmed"] = new Dictionary { ["type"] = "boolean", ["default"] = true } + ["confirmed"] = ParseJsonElement("""{"type":"boolean","default":true}""") }, Required = ["confirmed"] }; @@ -1038,11 +1179,10 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat && result.Content != null && result.Content.TryGetValue("confirmed", out var val)) { - return val switch + return val.ValueKind switch { - bool b => b, - JsonElement { ValueKind: JsonValueKind.True } => true, - JsonElement { ValueKind: JsonValueKind.False } => false, + JsonValueKind.True => true, + JsonValueKind.False => false, _ => false }; } @@ -1057,12 +1197,13 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat session.ThrowIfDisposed(); session.AssertElicitation(); + var enumJson = JsonSerializer.Serialize(options, TypesJsonContext.Default.StringArray); var schema = new UIElicitationSchema { Type = "object", - Properties = new Dictionary + Properties = new Dictionary { - ["selection"] = new Dictionary { ["type"] = "string", ["enum"] = options } + ["selection"] = ParseJsonElement($$"""{"type":"string","enum":{{enumJson}}}""") }, Required = ["selection"] }; @@ -1072,12 +1213,7 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat && result.Content != null && result.Content.TryGetValue("selection", out var val)) { - return val switch - { - string s => s, - JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(), - _ => val.ToString() - }; + return val.ValueKind == JsonValueKind.String ? val.GetString() : val.ToString(); } return null; @@ -1089,18 +1225,21 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat session.ThrowIfDisposed(); session.AssertElicitation(); - var field = new Dictionary { ["type"] = "string" }; - if (options?.Title != null) field["title"] = options.Title; - if (options?.Description != null) field["description"] = options.Description; - if (options?.MinLength != null) field["minLength"] = options.MinLength; - if (options?.MaxLength != null) field["maxLength"] = options.MaxLength; - if (options?.Format != null) field["format"] = options.Format; - if (options?.Default != null) field["default"] = options.Default; + var fieldNode = new System.Text.Json.Nodes.JsonObject { ["type"] = "string" }; + if (options?.Title != null) fieldNode["title"] = options.Title; + if (options?.Description != null) fieldNode["description"] = options.Description; + if (options?.MinLength != null) fieldNode["minLength"] = options.MinLength; + if (options?.MaxLength != null) fieldNode["maxLength"] = options.MaxLength; + if (options?.Format != null) fieldNode["format"] = options.Format; + if (options?.Default != null) fieldNode["default"] = options.Default; var schema = new UIElicitationSchema { Type = "object", - Properties = new Dictionary { ["value"] = field }, + Properties = new Dictionary + { + ["value"] = ParseJsonElement(fieldNode.ToJsonString()) + }, Required = ["value"] }; @@ -1109,12 +1248,7 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat && result.Content != null && result.Content.TryGetValue("value", out var val)) { - return val switch - { - string s => s, - JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(), - _ => val.ToString() - }; + return val.ValueKind == JsonValueKind.String ? val.GetString() : val.ToString(); } return null; diff --git a/dotnet/src/SessionFsProvider.cs b/dotnet/src/SessionFsProvider.cs index 12dfc8770..fbb8df507 100644 --- a/dotnet/src/SessionFsProvider.cs +++ b/dotnet/src/SessionFsProvider.cs @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ using GitHub.Copilot.Rpc; +using System.Text.Json; namespace GitHub.Copilot; @@ -44,7 +45,7 @@ public interface ISessionFsSqliteProvider Task QueryAsync( SessionFsSqliteQueryType queryType, string query, - IDictionary? bindParams, + IDictionary? bindParams, CancellationToken cancellationToken); /// @@ -287,11 +288,16 @@ async Task ISessionFsHandler.SqliteQueryAsync(Sessio try { - var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, request.Params, cancellationToken).ConfigureAwait(false); + var bindParams = request.Params?.ToDictionary( + kvp => kvp.Key, + kvp => JsonElementToValue(kvp.Value)); + var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, bindParams, cancellationToken).ConfigureAwait(false); return new SessionFsSqliteQueryResult { - Rows = result?.Rows ?? [], + Rows = result?.Rows?.Select(row => (IDictionary)row.ToDictionary( + kvp => kvp.Key, + kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [], Columns = result?.Columns ?? [], RowsAffected = result?.RowsAffected ?? 0, LastInsertRowid = result?.LastInsertRowid, @@ -329,4 +335,14 @@ private static SessionFsError ToSessionFsError(Exception ex) : SessionFsErrorCode.UNKNOWN; return new SessionFsError { Code = code, Message = ex.Message }; } + + private static object? JsonElementToValue(JsonElement element) => element.ValueKind switch + { + JsonValueKind.Null => null, + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(), + _ => element.GetRawText(), + }; } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index bb72820e5..a02a5db3a 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -672,112 +672,7 @@ public sealed class ToolInvocation /// /// Arguments passed to the tool by the language model. /// - public object? Arguments { get; set; } -} - -/// Describes the kind of a permission request result. -[JsonConverter(typeof(PermissionRequestResultKind.Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionRequestResultKind : IEquatable -{ - /// Gets the kind indicating the permission was approved for this one instance. - public static PermissionRequestResultKind Approved { get; } = new("approve-once"); - - /// Gets the kind indicating the permission was denied interactively by the user. - public static PermissionRequestResultKind Rejected { get; } = new("reject"); - - /// Gets the kind indicating the permission was denied because user confirmation was unavailable. - public static PermissionRequestResultKind UserNotAvailable { get; } = new("user-not-available"); - - /// Gets the kind indicating no permission decision was made. - public static PermissionRequestResultKind NoResult { get; } = new("no-result"); - - /// Gets the underlying string value of this . - public string Value => _value ?? string.Empty; - - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The string value for this kind. - [JsonConstructor] - public PermissionRequestResultKind(string value) => _value = value; - - /// - public static bool operator ==(PermissionRequestResultKind left, PermissionRequestResultKind right) => left.Equals(right); - - /// - public static bool operator !=(PermissionRequestResultKind left, PermissionRequestResultKind right) => !left.Equals(right); - - /// - public override bool Equals([NotNullWhen(true)] object? obj) => obj is PermissionRequestResultKind other && Equals(other); - - /// - public bool Equals(PermissionRequestResultKind 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 PermissionRequestResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException("Expected string for PermissionRequestResultKind."); - } - - var value = reader.GetString(); - if (value is null) - { - throw new JsonException("PermissionRequestResultKind value cannot be null."); - } - - return new PermissionRequestResultKind(value); - } - - /// - public override void Write(Utf8JsonWriter writer, PermissionRequestResultKind value, JsonSerializerOptions options) => - writer.WriteStringValue(value.Value); - } -} - -/// -/// Result of a permission request evaluation. -/// -public sealed class PermissionRequestResult -{ - /// - /// Permission decision kind. Construct values with the static members on - /// : - /// - /// — allow this single request. - /// — deny the request. - /// — deny because no user is available to confirm. - /// — leave the pending request unanswered (protocol v1 only; rejected by protocol v2 servers). - /// - /// - [JsonPropertyName("kind")] - public PermissionRequestResultKind Kind { get; set; } - - /// - /// Permission rules to apply for the decision. - /// - [JsonPropertyName("rules")] - public IList? Rules { get; set; } - - /// - /// Optional human-readable feedback to forward to the LLM along with the - /// decision. Mirrors the feedback field on the RPC-level - /// type. - /// - [JsonPropertyName("feedback")] - public string? Feedback { get; set; } + public JsonElement? Arguments { get; set; } } /// @@ -1228,7 +1123,7 @@ public sealed class PreToolUseHookInput /// Arguments that will be passed to the tool. /// [JsonPropertyName("toolArgs")] - public object? ToolArgs { get; set; } + public JsonElement? ToolArgs { get; set; } } /// @@ -1383,13 +1278,13 @@ public sealed class PostToolUseHookInput /// Arguments that were passed to the tool. /// [JsonPropertyName("toolArgs")] - public object? ToolArgs { get; set; } + public JsonElement? ToolArgs { get; set; } /// /// Result returned by the tool execution. /// [JsonPropertyName("toolResult")] - public object? ToolResult { get; set; } + public JsonElement? ToolResult { get; set; } } /// @@ -1748,7 +1643,7 @@ public enum SystemMessageMode } /// -/// Specifies the operation to perform on a system prompt section. +/// Specifies the operation to perform on a system message section. /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum SectionOverrideAction @@ -1771,7 +1666,7 @@ public enum SectionOverrideAction } /// -/// Override operation for a single system prompt section. +/// Override operation for a single system message section. /// public sealed class SectionOverride { @@ -1797,30 +1692,95 @@ public sealed class SectionOverride } /// -/// Known system prompt section identifiers for the "customize" mode. +/// Identifies a system message section for the "customize" mode. /// -public static class SystemPromptSections +[JsonConverter(typeof(SystemMessageSection.Converter))] +public readonly struct SystemMessageSection : IEquatable { /// Agent identity preamble and mode statement. - public const string Identity = "identity"; + public static SystemMessageSection Identity { get; } = new("identity"); /// Response style, conciseness rules, output formatting preferences. - public const string Tone = "tone"; + public static SystemMessageSection Tone { get; } = new("tone"); /// Tool usage patterns, parallel calling, batching guidelines. - public const string ToolEfficiency = "tool_efficiency"; + public static SystemMessageSection ToolEfficiency { get; } = new("tool_efficiency"); /// CWD, OS, git root, directory listing, available tools. - public const string EnvironmentContext = "environment_context"; + public static SystemMessageSection EnvironmentContext { get; } = new("environment_context"); /// Coding rules, linting/testing, ecosystem tools, style. - public const string CodeChangeRules = "code_change_rules"; + public static SystemMessageSection CodeChangeRules { get; } = new("code_change_rules"); /// Tips, behavioral best practices, behavioral guidelines. - public const string Guidelines = "guidelines"; + public static SystemMessageSection Guidelines { get; } = new("guidelines"); /// Environment limitations, prohibited actions, security policies. - public const string Safety = "safety"; + public static SystemMessageSection Safety { get; } = new("safety"); /// Per-tool usage instructions. - public const string ToolInstructions = "tool_instructions"; + public static SystemMessageSection ToolInstructions { get; } = new("tool_instructions"); /// Repository and organization custom instructions. - public const string CustomInstructions = "custom_instructions"; + public static SystemMessageSection CustomInstructions { get; } = new("custom_instructions"); + /// Runtime-provided context and instructions (e.g. system notifications, memories, workspace context, mode-specific instructions, content-exclusion policy). + public static SystemMessageSection RuntimeInstructions { get; } = new("runtime_instructions"); /// End-of-prompt instructions: parallel tool calling, persistence, task completion. - public const string LastInstructions = "last_instructions"; + public static SystemMessageSection LastInstructions { get; } = new("last_instructions"); + + /// Gets the underlying string value of this . + public string Value => _value ?? string.Empty; + + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The string value for this section identifier. + [JsonConstructor] + public SystemMessageSection(string value) => _value = value; + + /// + public static bool operator ==(SystemMessageSection left, SystemMessageSection right) => left.Equals(right); + + /// + public static bool operator !=(SystemMessageSection left, SystemMessageSection right) => !left.Equals(right); + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => obj is SystemMessageSection other && Equals(other); + + /// + public bool Equals(SystemMessageSection 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 SystemMessageSection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException("Expected string for SystemMessageSection."); + } + + var value = reader.GetString(); + if (value is null) + { + throw new JsonException("SystemMessageSection value cannot be null."); + } + + return new SystemMessageSection(value); + } + + /// + public override void Write(Utf8JsonWriter writer, SystemMessageSection value, JsonSerializerOptions options) => + writer.WriteStringValue(value.Value); + + /// + public override SystemMessageSection ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + new(reader.GetString()!); + + /// + public override void WriteAsPropertyName(Utf8JsonWriter writer, SystemMessageSection value, JsonSerializerOptions options) => + writer.WritePropertyName(value.Value); + } } /// @@ -1841,9 +1801,9 @@ public sealed class SystemMessageConfig /// /// Section-level overrides for customize mode. - /// Keys are section identifiers (see ). + /// Keys are section identifiers (see ). /// - public IDictionary? Sections { get; set; } + public IDictionary? Sections { get; set; } } /// @@ -2258,6 +2218,13 @@ protected SessionConfigBase(SessionConfigBase? other) CreateSessionFsProvider = other.CreateSessionFsProvider; GitHubToken = other.GitHubToken; RemoteSession = other.RemoteSession; +#pragma warning disable GHCP001 + Canvases = other.Canvases is not null ? [.. other.Canvases] : null; + RequestCanvasRenderer = other.RequestCanvasRenderer; + RequestExtensions = other.RequestExtensions; + ExtensionInfo = other.ExtensionInfo; + CanvasHandler = other.CanvasHandler; +#pragma warning restore GHCP001 SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; InstructionDirectories = other.InstructionDirectories is not null ? [.. other.InstructionDirectories] : null; Streaming = other.Streaming; @@ -2333,7 +2300,7 @@ protected SessionConfigBase(SessionConfigBase? other) public bool? EnableSessionTelemetry { get; set; } /// Handler for permission requests from the server. - public Func>? OnPermissionRequest { get; set; } + public Func>? OnPermissionRequest { get; set; } /// Handler for user input requests from the agent. public Func>? OnUserInputRequest { get; set; } @@ -2440,6 +2407,47 @@ protected SessionConfigBase(SessionConfigBase? other) /// /// public RemoteSessionMode? RemoteSession { get; set; } + +#pragma warning disable GHCP001 + /// + /// Canvas declarations advertised by this connection. The runtime forwards + /// these to the agent and routes inbound canvas.* requests for any + /// declared canvas to . + /// + [Experimental(Diagnostics.Experimental)] + public IList? Canvases { get; set; } + + /// + /// When , asks the host to expose canvas renderer tools + /// for this session. The host typically grants this only to trusted clients. + /// + [Experimental(Diagnostics.Experimental)] + public bool? RequestCanvasRenderer { get; set; } + + /// + /// When , asks the host to expose extension-discovery + /// tools for this session. The host typically grants this only to trusted clients. + /// + [Experimental(Diagnostics.Experimental)] + public bool? RequestExtensions { get; set; } + + /// + /// Stable extension identity for canvas/tool providers on this connection. + /// Required when is set so the runtime can attribute + /// declared canvases back to this provider. + /// + [Experimental(Diagnostics.Experimental)] + public ExtensionInfo? ExtensionInfo { get; set; } + + /// + /// Provider-side canvas lifecycle handler. The SDK routes inbound + /// canvas.open / canvas.close / canvas.action.invoke + /// requests to this handler. + /// + [Experimental(Diagnostics.Experimental)] + [JsonIgnore] + public ICanvasHandler? CanvasHandler { get; set; } +#pragma warning restore GHCP001 } /// @@ -2502,6 +2510,7 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other) SuppressResumeEvent = other.SuppressResumeEvent; ContinuePendingWork = other.ContinuePendingWork; + OpenCanvases = other.OpenCanvases is not null ? [.. other.OpenCanvases] : null; } /// @@ -2524,6 +2533,16 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other) /// public bool? ContinuePendingWork { get; set; } +#pragma warning disable GHCP001 + /// + /// Snapshot of canvases that were already open when the session was suspended. + /// When provided on resume, the runtime can rehydrate canvas state so consumers + /// do not need to re-open canvases that were active before the previous shutdown. + /// + [Experimental(Diagnostics.Experimental)] + public IList? OpenCanvases { get; set; } +#pragma warning restore GHCP001 + /// /// Creates a shallow clone of this instance. /// @@ -3001,7 +3020,7 @@ public sealed class SetForegroundSessionResponse } /// -/// Content data for a single system prompt section in a transform RPC call. +/// Content data for a single system message section in a transform RPC call. /// public sealed class SystemMessageTransformSection { @@ -3049,8 +3068,6 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(ModelPolicy))] [JsonSerializable(typeof(ModelSupports))] [JsonSerializable(typeof(ModelVisionLimits))] -[JsonSerializable(typeof(PermissionRequestResult))] -[JsonSerializable(typeof(PermissionRequestResultKind))] [JsonSerializable(typeof(PingRequest))] [JsonSerializable(typeof(PingResponse))] [JsonSerializable(typeof(ProviderConfig))] @@ -3071,4 +3088,11 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(object))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(string[]))] +#pragma warning disable GHCP001 +[JsonSerializable(typeof(CanvasDeclaration))] +[JsonSerializable(typeof(CanvasOpenResponse))] +[JsonSerializable(typeof(CanvasHostContext))] +[JsonSerializable(typeof(CanvasHostCapabilities))] +[JsonSerializable(typeof(ExtensionInfo))] +#pragma warning restore GHCP001 internal partial class TypesJsonContext : JsonSerializerContext; diff --git a/dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs b/dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs index 0ae9c9a7d..4e573ff5c 100644 --- a/dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs +++ b/dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs @@ -43,7 +43,7 @@ private SqliteConnection GetOrCreateDb() public Task QueryAsync( SessionFsSqliteQueryType queryType, string query, - IDictionary? bindParams, + IDictionary? bindParams, CancellationToken cancellationToken) { sqliteCalls.Add(new SqliteCall(sessionId, queryType.Value, query)); @@ -125,7 +125,7 @@ public Task ExistsAsync(CancellationToken cancellationToken) return Task.FromResult(_db is not null); } - private static void AddParams(SqliteCommand cmd, IDictionary? bindParams) + private static void AddParams(SqliteCommand cmd, IDictionary? bindParams) { if (bindParams is null) return; foreach (var (key, value) in bindParams) diff --git a/dotnet/test/E2E/MultiClientE2ETests.cs b/dotnet/test/E2E/MultiClientE2ETests.cs index 34efd09b2..faaf38393 100644 --- a/dotnet/test/E2E/MultiClientE2ETests.cs +++ b/dotnet/test/E2E/MultiClientE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.AI; using System.Collections.Concurrent; @@ -152,17 +153,14 @@ public async Task One_Client_Approves_Permission_And_Both_See_The_Result() OnPermissionRequest = (request, _) => { client1PermissionRequests.Add(request); - return Task.FromResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.Approved, - }); + return Task.FromResult(PermissionDecision.ApproveOnce()); }, }); // Client 2 resumes — its handler never completes, so only client 1's approval takes effect var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig { - OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, + OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, }); var client1Events = new ConcurrentBag(); @@ -204,16 +202,13 @@ public async Task One_Client_Rejects_Permission_And_Both_See_The_Result() { var session1 = await Client1.CreateSessionAsync(new SessionConfig { - OnPermissionRequest = (_, _) => Task.FromResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.Rejected, - }), + OnPermissionRequest = (_, _) => Task.FromResult(PermissionDecision.Reject()), }); // Client 2 resumes — its handler never completes var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig { - OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, + OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, }); var client1Events = new ConcurrentBag(); diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index 889dc0050..9cc0785bf 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -1,10 +1,12 @@ -/*--------------------------------------------------------------------------------------------- +/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.AI; using System.ComponentModel; +using System.Text.Json; using Xunit; using Xunit.Abstractions; using RpcPermissionDecisionApproveOnce = GitHub.Copilot.Rpc.PermissionDecisionApproveOnce; @@ -21,7 +23,7 @@ public class PendingWorkResumeE2ETests(E2ETestFixture fixture, ITestOutputHelper public async Task Should_Continue_Pending_Permission_Request_After_Resume() { var originalPermissionRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var releaseOriginalPermission = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseOriginalPermission = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var resumedToolInvoked = false; await using var server = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForTcp(connectionToken: SharedToken) }); @@ -59,10 +61,7 @@ await session1.SendAsync(new MessageOptions var session2 = await resumedTcpClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig { ContinuePendingWork = true, - OnPermissionRequest = (_, _) => Task.FromResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.NoResult - }), + OnPermissionRequest = (_, _) => Task.FromResult(PermissionDecision.NoResult()), Tools = [ AIFunctionFactory.Create( @@ -90,10 +89,7 @@ await session1.SendAsync(new MessageOptions } finally { - releaseOriginalPermission.TrySetResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable, - }); + releaseOriginalPermission.TrySetResult(PermissionDecision.UserNotAvailable()); } [Description("Transforms a value after permission is granted")] @@ -141,7 +137,7 @@ await session1.SendAsync(new MessageOptions var toolResult = await session2.Rpc.Tools.HandlePendingToolCallAsync( toolEvent.Data.RequestId, - result: "EXTERNAL_RESUMED_BETA"); + result: JsonDocument.Parse("\"EXTERNAL_RESUMED_BETA\"").RootElement.Clone()); Assert.True(toolResult.Success); var answer = await TestHelper.GetFinalAssistantMessageAsync(session2, PendingWorkTimeout); @@ -210,7 +206,7 @@ await session1.SendAsync(new MessageOptions var resumedResult = await session2.Rpc.Tools.HandlePendingToolCallAsync( toolEvent.Data.RequestId, - result: "EXTERNAL_RESUMED_BETA"); + result: JsonDocument.Parse("\"EXTERNAL_RESUMED_BETA\"").RootElement.Clone()); Assert.True(resumedResult.Success); // continuePendingWork=false may interrupt agent continuation before this response, @@ -287,11 +283,11 @@ await Task.WhenAll( var toolB = toolEvents["pending_lookup_b"]; var resultB = await session2.Rpc.Tools.HandlePendingToolCallAsync( toolB.Data.RequestId, - result: "PARALLEL_B_BETA"); + result: JsonDocument.Parse("\"PARALLEL_B_BETA\"").RootElement.Clone()); Assert.True(resultB.Success); var resultA = await session2.Rpc.Tools.HandlePendingToolCallAsync( toolA.Data.RequestId, - result: "PARALLEL_A_ALPHA"); + result: JsonDocument.Parse("\"PARALLEL_A_ALPHA\"").RootElement.Clone()); Assert.True(resultA.Success); await session2.DisposeAsync(); diff --git a/dotnet/test/E2E/PermissionE2ETests.cs b/dotnet/test/E2E/PermissionE2ETests.cs index 953ab1469..53b52c31c 100644 --- a/dotnet/test/E2E/PermissionE2ETests.cs +++ b/dotnet/test/E2E/PermissionE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.AI; using System.Text.Json; @@ -43,20 +44,20 @@ public async Task Should_Invoke_Permission_Handler_For_Write_Operations() { writePermissionRequestReceived.TrySetResult(writeRequest); } - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); } }); await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "test.txt"), "original content"); - await session.SendAsync(new MessageOptions + var sendTask = session.SendAndWaitAsync(new MessageOptions { Prompt = "Edit test.txt and replace 'original' with 'modified'" }); var readRequest = await readPermissionRequestReceived.Task.WaitAsync(TimeSpan.FromSeconds(30)); var writeRequest = await writePermissionRequestReceived.Task.WaitAsync(TimeSpan.FromSeconds(30)); - await TestHelper.GetFinalAssistantMessageAsync(session); + await sendTask; List observedPermissionRequests; lock (permissionRequestsLock) @@ -86,10 +87,7 @@ public async Task Should_Deny_Permission_When_Handler_Returns_Denied() { OnPermissionRequest = (request, invocation) => { - return Task.FromResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.Rejected - }); + return Task.FromResult(PermissionDecision.Reject()); } }); @@ -135,7 +133,7 @@ public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies() var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = (_, _) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.UserNotAvailable }) + Task.FromResult(PermissionDecision.UserNotAvailable()) }); var permissionDenied = false; @@ -181,7 +179,7 @@ public async Task Should_Handle_Async_Permission_Handler() { permissionRequestReceived = true; await Task.Yield(); - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + return PermissionDecision.ApproveOnce(); } }); @@ -212,7 +210,7 @@ public async Task Should_Resume_Session_With_Permission_Handler() OnPermissionRequest = (request, invocation) => { permissionRequestReceived = true; - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); } }); @@ -262,7 +260,7 @@ public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies_Aft var session2 = await Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig { OnPermissionRequest = (_, _) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.UserNotAvailable }) + Task.FromResult(PermissionDecision.UserNotAvailable()) }); var permissionDenied = false; @@ -297,7 +295,7 @@ public async Task Should_Receive_ToolCallId_In_Permission_Requests() { receivedToolCallId = true; } - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); } }); @@ -340,7 +338,7 @@ void AddLifecycleEvent(string phase, string? toolCallId) handlerEntered.TrySetResult(); await releaseHandler.Task.WaitAsync(TimeSpan.FromSeconds(30)); AddLifecycleEvent("permission-complete", shellRequest.ToolCallId); - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + return PermissionDecision.ApproveOnce(); } }); @@ -438,7 +436,7 @@ public async Task Should_Handle_Concurrent_Permission_Requests_From_Parallel_Too } await bothPermissionRequestsStarted.Task.WaitAsync(TimeSpan.FromSeconds(30)); - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + return PermissionDecision.ApproveOnce(); } }); @@ -515,7 +513,7 @@ public async Task Should_Deny_Permission_With_NoResult_Kind() OnPermissionRequest = (_, _) => { permissionCalled.TrySetResult(true); - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.NoResult }); + return Task.FromResult(PermissionDecision.NoResult()); } }); @@ -541,7 +539,7 @@ public async Task Should_Short_Circuit_Permission_Handler_When_Set_Approve_All_E OnPermissionRequest = (_, _) => { Interlocked.Increment(ref handlerCallCount); - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); }, }); diff --git a/dotnet/test/E2E/PreMcpToolCallHookE2ETests.cs b/dotnet/test/E2E/PreMcpToolCallHookE2ETests.cs index f825c4254..8e5240a9a 100644 --- a/dotnet/test/E2E/PreMcpToolCallHookE2ETests.cs +++ b/dotnet/test/E2E/PreMcpToolCallHookE2ETests.cs @@ -15,7 +15,7 @@ namespace GitHub.Copilot.Test.E2E; public class PreMcpToolCallHookE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "pre_mcp_tool_call_hook", output) { - private static string FindTestHarnessDir() + private static string FindMetaEchoTestHarnessDir() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null) @@ -42,7 +42,7 @@ private static string FindTestHarnessDir() [Fact] public async Task Should_Set_Meta_Via_PreMcpToolCall_Hook() { - var testHarnessDir = FindTestHarnessDir(); + var testHarnessDir = FindMetaEchoTestHarnessDir(); var hookInputs = new List(); var session = await CreateSessionAsync(new SessionConfig @@ -84,7 +84,7 @@ public async Task Should_Set_Meta_Via_PreMcpToolCall_Hook() [Fact] public async Task Should_Replace_Meta_Via_PreMcpToolCall_Hook() { - var testHarnessDir = FindTestHarnessDir(); + var testHarnessDir = FindMetaEchoTestHarnessDir(); var hookInputs = new List(); var session = await CreateSessionAsync(new SessionConfig @@ -125,7 +125,7 @@ public async Task Should_Replace_Meta_Via_PreMcpToolCall_Hook() [Fact] public async Task Should_Remove_Meta_Via_PreMcpToolCall_Hook() { - var testHarnessDir = FindTestHarnessDir(); + var testHarnessDir = FindMetaEchoTestHarnessDir(); var hookInputs = new List(); var session = await CreateSessionAsync(new SessionConfig diff --git a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs index a3d08a1cb..0ba7d620e 100644 --- a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs +++ b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using Xunit; using Xunit.Abstractions; using RpcSkill = GitHub.Copilot.Rpc.Skill; @@ -67,21 +68,14 @@ public async Task Should_List_Mcp_Servers_With_Configured_Server() const string serverName = "rpc-list-mcp-server"; var session = await CreateSessionAsync(new SessionConfig { - McpServers = new Dictionary - { - [serverName] = new McpStdioServerConfig - { - Command = "echo", - Args = ["rpc-list-mcp-server"], - Tools = ["*"], - }, - }, + McpServers = CreateTestMcpServers(serverName), }); + await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); var result = await session.Rpc.Mcp.ListAsync(); var server = Assert.Single(result.Servers, server => string.Equals(server.Name, serverName, StringComparison.Ordinal)); - Assert.False(string.IsNullOrWhiteSpace(server.Status.Value)); + Assert.Equal(McpServerStatus.Connected, server.Status); } [Fact] @@ -143,16 +137,9 @@ public async Task Should_Report_Error_When_Mcp_Oauth_Server_Is_Not_Configured() { var session = await CreateSessionAsync(new SessionConfig { - McpServers = new Dictionary - { - ["configured-stdio-server"] = new McpStdioServerConfig - { - Command = "echo", - Args = ["configured-stdio-server"], - Tools = ["*"], - }, - }, + McpServers = CreateTestMcpServers("configured-stdio-server"), }); + await WaitForMcpServerStatusAsync(session, "configured-stdio-server", McpServerStatus.Connected); await AssertFailureAsync( () => session.Rpc.Mcp.Oauth.LoginAsync("missing-server"), @@ -165,16 +152,9 @@ 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 { - McpServers = new Dictionary - { - [serverName] = new McpStdioServerConfig - { - Command = "echo", - Args = [serverName], - Tools = ["*"], - }, - }, + McpServers = CreateTestMcpServers(serverName), }); + await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); await AssertFailureAsync( () => session.Rpc.Mcp.Oauth.LoginAsync(serverName, forceReauth: true, clientName: "SDK E2E", callbackSuccessMessage: "Done"), diff --git a/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs b/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs index 2ed338129..61c8d5878 100644 --- a/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs +++ b/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs @@ -4,6 +4,7 @@ using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; +using System.Text.Json; using Xunit; using Xunit.Abstractions; @@ -143,7 +144,7 @@ public async Task Should_Return_Expected_Results_For_Missing_Pending_Handler_Req var tool = await session.Rpc.Tools.HandlePendingToolCallAsync( requestId: "missing-tool-request", - result: "tool result"); + result: JsonDocument.Parse("\"tool result\"").RootElement.Clone()); Assert.False(tool.Success); var command = await session.Rpc.Commands.HandlePendingCommandAsync( diff --git a/dotnet/test/E2E/SessionConfigE2ETests.cs b/dotnet/test/E2E/SessionConfigE2ETests.cs index 64f5518fb..a763b6b72 100644 --- a/dotnet/test/E2E/SessionConfigE2ETests.cs +++ b/dotnet/test/E2E/SessionConfigE2ETests.cs @@ -435,12 +435,17 @@ public async Task Should_Apply_AvailableTools_On_Session_Resume() AvailableTools = ["view"], }); - await session2.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); - - var exchange = Assert.Single(await Ctx.GetExchangesAsync()); - Assert.Equal(["view"], GetToolNames(exchange)); - - await session2.DisposeAsync(); + try + { + var exchange = Assert.Single(await SendAndWaitForExchangesAsync( + session2, + new MessageOptions { Prompt = "What is 1+1?" })); + Assert.Equal(["view"], GetToolNames(exchange)); + } + finally + { + await session2.DisposeAsync(); + } } [Fact] diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index d2c611e07..06868d70a 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -1,4 +1,4 @@ -/*--------------------------------------------------------------------------------------------- +/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ @@ -101,10 +101,10 @@ public async Task Should_Create_A_Session_With_Customized_SystemMessage_Config() SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Customize, - Sections = new Dictionary + Sections = new Dictionary { - [SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = customTone }, - [SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove }, + [SystemMessageSection.Tone] = new() { Action = SectionOverrideAction.Replace, Content = customTone }, + [SystemMessageSection.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove }, }, Content = appendedContent } @@ -130,16 +130,20 @@ public async Task Should_Create_A_Session_With_AvailableTools() AvailableTools = ["view", "edit"] }); - await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); - await TestHelper.GetFinalAssistantMessageAsync(session); - - var traffic = await Ctx.GetExchangesAsync(); - Assert.NotEmpty(traffic); - - var toolNames = GetToolNames(traffic[0]); - Assert.Equal(2, toolNames.Count); - Assert.Contains("view", toolNames); - Assert.Contains("edit", toolNames); + try + { + var traffic = await SendAndWaitForExchangesAsync( + session, + new MessageOptions { Prompt = "What is 1+1?" }); + var toolNames = GetToolNames(traffic[0]); + Assert.Equal(2, toolNames.Count); + Assert.Contains("view", toolNames); + Assert.Contains("edit", toolNames); + } + finally + { + await session.DisposeAsync(); + } } [Fact] @@ -150,16 +154,20 @@ public async Task Should_Create_A_Session_With_ExcludedTools() ExcludedTools = ["view"] }); - await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); - await TestHelper.GetFinalAssistantMessageAsync(session); - - var traffic = await Ctx.GetExchangesAsync(); - Assert.NotEmpty(traffic); - - var toolNames = GetToolNames(traffic[0]); - Assert.DoesNotContain("view", toolNames); - Assert.Contains("edit", toolNames); - Assert.Contains("grep", toolNames); + try + { + var traffic = await SendAndWaitForExchangesAsync( + session, + new MessageOptions { Prompt = "What is 1+1?" }); + var toolNames = GetToolNames(traffic[0]); + Assert.DoesNotContain("view", toolNames); + Assert.Contains("edit", toolNames); + Assert.Contains("grep", toolNames); + } + finally + { + await session.DisposeAsync(); + } } [Fact] @@ -180,14 +188,18 @@ public async Task Should_Create_A_Session_With_DefaultAgent_ExcludedTools() }, }); - await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); - await TestHelper.GetFinalAssistantMessageAsync(session); - - var traffic = await Ctx.GetExchangesAsync(); - Assert.NotEmpty(traffic); - - var toolNames = GetToolNames(traffic[0]); - Assert.DoesNotContain("secret_tool", toolNames); + try + { + var traffic = await SendAndWaitForExchangesAsync( + session, + new MessageOptions { Prompt = "What is 1+1?" }); + var toolNames = GetToolNames(traffic[0]); + Assert.DoesNotContain("secret_tool", toolNames); + } + finally + { + await session.DisposeAsync(); + } } [Fact] @@ -539,11 +551,17 @@ public async Task Should_Create_Session_With_Custom_Config_Dir() Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); - // Session should work normally with custom config dir - await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); - var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session); - Assert.NotNull(assistantMessage); - Assert.Contains("2", assistantMessage!.Data.Content); + try + { + // Session should work normally with custom config dir. + var assistantMessage = await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); + Assert.NotNull(assistantMessage); + Assert.Contains("2", assistantMessage!.Data.Content); + } + finally + { + await session.DisposeAsync(); + } } [Fact] diff --git a/dotnet/test/E2E/SessionFsE2ETests.cs b/dotnet/test/E2E/SessionFsE2ETests.cs index b2e41f024..1d0157658 100644 --- a/dotnet/test/E2E/SessionFsE2ETests.cs +++ b/dotnet/test/E2E/SessionFsE2ETests.cs @@ -609,7 +609,7 @@ protected override Task RemoveAsync(string path, bool recursive, bool force, Can protected override Task RenameAsync(string src, string dest, CancellationToken cancellationToken) => Task.FromException(exception); - Task ISessionFsSqliteProvider.QueryAsync(SessionFsSqliteQueryType queryType, string query, IDictionary? bindParams, CancellationToken cancellationToken) => + Task ISessionFsSqliteProvider.QueryAsync(SessionFsSqliteQueryType queryType, string query, IDictionary? bindParams, CancellationToken cancellationToken) => Task.FromException(exception); Task ISessionFsSqliteProvider.ExistsAsync(CancellationToken cancellationToken) => diff --git a/dotnet/test/E2E/SessionMcpAndAgentConfigE2ETests.cs b/dotnet/test/E2E/SessionMcpAndAgentConfigE2ETests.cs index 9567c98be..18a8835a6 100644 --- a/dotnet/test/E2E/SessionMcpAndAgentConfigE2ETests.cs +++ b/dotnet/test/E2E/SessionMcpAndAgentConfigE2ETests.cs @@ -14,22 +14,13 @@ public class SessionMcpAndAgentConfigE2ETests(E2ETestFixture fixture, ITestOutpu [Fact] public async Task Should_Accept_MCP_Server_Configuration_On_Session_Create() { - var mcpServers = new Dictionary - { - ["test-server"] = new McpStdioServerConfig - { - Command = "echo", - Args = ["hello"], - Tools = ["*"] - } - }; - var session = await CreateSessionAsync(new SessionConfig { - McpServers = mcpServers + McpServers = CreateTestMcpServers("test-server") }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); + await WaitForMcpServerStatusAsync(session, "test-server", McpServerStatus.Connected); // Simple interaction to verify session works await session.SendAsync(new MessageOptions { Prompt = "What is 2+2?" }); @@ -48,7 +39,7 @@ public async Task Should_Accept_MCP_Server_Configuration_Without_Args() { ["test-server"] = new McpStdioServerConfig { - Command = "true", + Command = "dotnet", Tools = ["*"] } }; @@ -60,12 +51,6 @@ public async Task Should_Accept_MCP_Server_Configuration_Without_Args() Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); - await session.SendAsync(new MessageOptions { Prompt = "What is 2+2?" }); - - var message = await TestHelper.GetFinalAssistantMessageAsync(session); - Assert.NotNull(message); - Assert.Contains("4", message!.Data.Content); - await session.DisposeAsync(); } @@ -79,26 +64,13 @@ public async Task Should_Accept_MCP_Server_Configuration_On_Session_Resume() await session1.DisposeAsync(); // Resume with MCP servers - var mcpServers = new Dictionary - { - ["test-server"] = new McpStdioServerConfig - { - Command = "echo", - Args = ["hello"], - Tools = ["*"] - } - }; - var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig { - McpServers = mcpServers + McpServers = CreateTestMcpServers("test-server") }); Assert.Equal(sessionId, session2.SessionId); - - var message = await session2.SendAndWaitAsync(new MessageOptions { Prompt = "What is 3+3?" }); - Assert.NotNull(message); - Assert.Contains("6", message!.Data.Content); + await WaitForMcpServerStatusAsync(session2, "test-server", McpServerStatus.Connected); await session2.DisposeAsync(); } @@ -106,28 +78,14 @@ public async Task Should_Accept_MCP_Server_Configuration_On_Session_Resume() [Fact] public async Task Should_Handle_Multiple_MCP_Servers() { - var mcpServers = new Dictionary - { - ["server1"] = new McpStdioServerConfig - { - Command = "echo", - Args = ["server1"], - Tools = ["*"] - }, - ["server2"] = new McpStdioServerConfig - { - Command = "echo", - Args = ["server2"], - Tools = ["*"] - } - }; - var session = await CreateSessionAsync(new SessionConfig { - McpServers = mcpServers + McpServers = CreateTestMcpServers("server1", "server2") }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); + await WaitForMcpServerStatusAsync(session, "server1", McpServerStatus.Connected); + await WaitForMcpServerStatusAsync(session, "server2", McpServerStatus.Connected); await session.DisposeAsync(); } @@ -234,15 +192,7 @@ public async Task Should_Handle_Custom_Agent_With_MCP_Servers() DisplayName = "MCP Agent", Description = "An agent with its own MCP servers", Prompt = "You are an agent with MCP servers.", - McpServers = new Dictionary - { - ["agent-server"] = new McpStdioServerConfig - { - Command = "echo", - Args = ["agent-mcp"], - Tools = ["*"] - } - } + McpServers = CreateTestMcpServers("agent-server") } }; @@ -401,16 +351,6 @@ await File.WriteAllTextAsync( [Fact] public async Task Should_Accept_Both_MCP_Servers_And_Custom_Agents() { - var mcpServers = new Dictionary - { - ["shared-server"] = new McpStdioServerConfig - { - Command = "echo", - Args = ["shared"], - Tools = ["*"] - } - }; - var customAgents = new List { new CustomAgentConfig @@ -424,42 +364,13 @@ public async Task Should_Accept_Both_MCP_Servers_And_Custom_Agents() var session = await CreateSessionAsync(new SessionConfig { - McpServers = mcpServers, + McpServers = CreateTestMcpServers("shared-server"), CustomAgents = customAgents }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); + await WaitForMcpServerStatusAsync(session, "shared-server", McpServerStatus.Connected); await session.DisposeAsync(); } - private static string FindTestHarnessDir() - { - var dir = new DirectoryInfo(AppContext.BaseDirectory); - while (dir != null) - { - var candidate = Path.Combine(dir.FullName, "test", "harness", "test-mcp-server.mjs"); - if (File.Exists(candidate)) - return Path.GetDirectoryName(candidate)!; - dir = dir.Parent; - } - throw new InvalidOperationException("Could not find test/harness/test-mcp-server.mjs"); - } - - private static async Task WaitForMcpServerStatusAsync( - CopilotSession session, - string serverName, - McpServerStatus expectedStatus) - { - await TestHelper.WaitForConditionAsync( - async () => - { - var result = await session.Rpc.Mcp.ListAsync(); - return result.Servers.Any(server => - string.Equals(server.Name, serverName, StringComparison.Ordinal) - && server.Status == expectedStatus); - }, - timeout: TimeSpan.FromSeconds(60), - pollInterval: TimeSpan.FromMilliseconds(200), - timeoutMessage: $"{serverName} reaching {expectedStatus}"); - } } diff --git a/dotnet/test/E2E/SuspendE2ETests.cs b/dotnet/test/E2E/SuspendE2ETests.cs index d3aa8067d..f531f0e59 100644 --- a/dotnet/test/E2E/SuspendE2ETests.cs +++ b/dotnet/test/E2E/SuspendE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using Microsoft.Extensions.AI; using System.ComponentModel; using Xunit; @@ -100,7 +101,7 @@ public async Task Should_Cancel_Pending_Permission_Request_When_Suspending() // and the underlying tool function is never invoked because the cancelled // permission means the runtime never grants execution. var permissionHandlerEntered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var releasePermissionHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releasePermissionHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var toolInvoked = false; var session = await CreateSessionAsync(new SessionConfig @@ -142,10 +143,7 @@ public async Task Should_Cancel_Pending_Permission_Request_When_Suspending() { // Defensive: release the dangling SDK-side handler task so it doesn't keep // a stray TaskCompletionSource alive after the test ends. - releasePermissionHandler.TrySetResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable, - }); + releasePermissionHandler.TrySetResult(PermissionDecision.UserNotAvailable()); } await session.DisposeAsync(); diff --git a/dotnet/test/E2E/SystemMessageTransformE2ETests.cs b/dotnet/test/E2E/SystemMessageTransformE2ETests.cs index 91f942190..79210e61b 100644 --- a/dotnet/test/E2E/SystemMessageTransformE2ETests.cs +++ b/dotnet/test/E2E/SystemMessageTransformE2ETests.cs @@ -22,9 +22,9 @@ public async Task Should_Invoke_Transform_Callbacks_With_Section_Content() SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Customize, - Sections = new Dictionary + Sections = new Dictionary { - ["identity"] = new SectionOverride + [SystemMessageSection.Identity] = new SectionOverride { Transform = async (content) => { @@ -33,7 +33,7 @@ public async Task Should_Invoke_Transform_Callbacks_With_Section_Content() return content; } }, - ["tone"] = new SectionOverride + [SystemMessageSection.Tone] = new SectionOverride { Transform = async (content) => { @@ -68,9 +68,9 @@ public async Task Should_Apply_Transform_Modifications_To_Section_Content() SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Customize, - Sections = new Dictionary + Sections = new Dictionary { - ["identity"] = new SectionOverride + [SystemMessageSection.Identity] = new SectionOverride { Transform = async (content) => { @@ -108,13 +108,13 @@ public async Task Should_Work_With_Static_Overrides_And_Transforms_Together() SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Customize, - Sections = new Dictionary + Sections = new Dictionary { - ["safety"] = new SectionOverride + [SystemMessageSection.Safety] = new SectionOverride { Action = SectionOverrideAction.Remove }, - ["identity"] = new SectionOverride + [SystemMessageSection.Identity] = new SectionOverride { Transform = async (content) => { diff --git a/dotnet/test/E2E/ToolResultsE2ETests.cs b/dotnet/test/E2E/ToolResultsE2ETests.cs index 75fd9488e..103c7ffe2 100644 --- a/dotnet/test/E2E/ToolResultsE2ETests.cs +++ b/dotnet/test/E2E/ToolResultsE2ETests.cs @@ -113,8 +113,8 @@ static ToolResultAIContent AnalyzeCode([Description("File to analyze")] string f ResultType = "success", ToolTelemetry = new Dictionary { - ["metrics"] = new Dictionary { ["analysisTimeMs"] = 150 }, - ["properties"] = new Dictionary { ["analyzer"] = "eslint" }, + ["metrics"] = JsonDocument.Parse("""{"analysisTimeMs":150}""").RootElement.Clone(), + ["properties"] = JsonDocument.Parse("""{"analyzer":"eslint"}""").RootElement.Clone(), }, }); } diff --git a/dotnet/test/E2E/ToolsE2ETests.cs b/dotnet/test/E2E/ToolsE2ETests.cs index c36bf2294..57ed6be2d 100644 --- a/dotnet/test/E2E/ToolsE2ETests.cs +++ b/dotnet/test/E2E/ToolsE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.AI; using System.Collections.ObjectModel; @@ -201,7 +202,7 @@ static string SafeLookup([Description("Lookup ID")] string id) OnPermissionRequest = (_, _) => { didRunPermissionRequest = true; - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.NoResult }); + return Task.FromResult(PermissionDecision.NoResult()); } }); @@ -258,7 +259,7 @@ public async Task Invokes_Custom_Tool_With_Permission_Handler() OnPermissionRequest = (request, invocation) => { permissionRequests.Add(request); - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); }, }); @@ -289,7 +290,7 @@ public async Task Denies_Custom_Tool_When_Permission_Denied() var session = await Client.CreateSessionAsync(new SessionConfig { Tools = [AIFunctionFactory.Create(EncryptStringDenied, "encrypt_string")], - OnPermissionRequest = async (request, invocation) => new() { Kind = PermissionRequestResultKind.Rejected }, + OnPermissionRequest = async (request, invocation) => PermissionDecision.Reject(), }); await session.SendAsync(new MessageOptions diff --git a/dotnet/test/Harness/E2ETestBase.cs b/dotnet/test/Harness/E2ETestBase.cs index 592253bda..ddd1b894b 100644 --- a/dotnet/test/Harness/E2ETestBase.cs +++ b/dotnet/test/Harness/E2ETestBase.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.Logging; using System.Data; @@ -107,4 +108,99 @@ protected static List GetToolNames(ParsedHttpExchange exchange) { return exchange.Request.Tools?.Select(t => t.Function.Name).ToList() ?? []; } + + protected async Task> WaitForExchangesAsync(int minimumCount = 1) + { + List exchanges = []; + await TestHelper.WaitForConditionAsync( + async () => + { + exchanges = await Ctx.GetExchangesAsync(); + return exchanges.Count >= minimumCount; + }, + timeoutMessage: $"Timed out waiting for {minimumCount} chat completion request(s)"); + return exchanges; + } + + protected async Task> SendAndWaitForExchangesAsync( + CopilotSession session, + MessageOptions options, + int minimumCount = 1) + { + using var cts = new CancellationTokenSource(); + var sendTask = session.SendAndWaitAsync(options, TimeSpan.FromMinutes(3), cts.Token); + var exchangesTask = WaitForExchangesAsync(minimumCount); + + try + { + var completedTask = await Task.WhenAny(exchangesTask, sendTask); + if (completedTask == sendTask) + { + await sendTask; + } + + return await exchangesTask; + } + finally + { + if (!sendTask.IsCompleted) + { + cts.Cancel(); + try + { + await sendTask; + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + // Expected when cleanup cancels the send task. + } + } + } + } + + protected static Dictionary CreateTestMcpServers(params string[] serverNames) + { + var testHarnessDir = FindTestHarnessDir(); + return serverNames.ToDictionary( + name => name, + _ => (McpServerConfig)new McpStdioServerConfig + { + Command = "node", + Args = [Path.Join(testHarnessDir, "test-mcp-server.mjs")], + WorkingDirectory = testHarnessDir, + Tools = ["*"] + }); + } + + protected static string FindTestHarnessDir() + { + var relativePath = Path.Join("test", "harness", "test-mcp-server.mjs"); + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = Path.Join(dir.FullName, relativePath); + if (File.Exists(candidate)) + return Path.GetDirectoryName(candidate)!; + dir = dir.Parent; + } + throw new InvalidOperationException("Could not find test/harness/test-mcp-server.mjs"); + } + + protected static async Task WaitForMcpServerStatusAsync( + CopilotSession session, + string serverName, + McpServerStatus expectedStatus) + { + await TestHelper.WaitForConditionAsync( + async () => + { + var result = await session.Rpc.Mcp.ListAsync(); + return result.Servers.Any(server => + string.Equals(server.Name, serverName, StringComparison.Ordinal) + && server.Status == expectedStatus); + }, + timeout: TimeSpan.FromSeconds(60), + pollInterval: TimeSpan.FromMilliseconds(200), + timeoutMessage: $"{serverName} reaching {expectedStatus}"); + } } diff --git a/dotnet/test/Unit/CanvasTests.cs b/dotnet/test/Unit/CanvasTests.cs new file mode 100644 index 000000000..fc5db84aa --- /dev/null +++ b/dotnet/test/Unit/CanvasTests.cs @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System; +using System.Reflection; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using GitHub.Copilot; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public class CanvasTests +{ + private static JsonSerializerOptions GetSerializerOptions() + { + var prop = typeof(CopilotClient).GetProperty( + "SerializerOptionsForMessageFormatter", + BindingFlags.NonPublic | BindingFlags.Static); + var options = (JsonSerializerOptions?)prop?.GetValue(null); + Assert.NotNull(options); + return options!; + } + + [Fact] + public void CanvasDeclaration_Serializes_CamelCase_SkippingNulls() + { + var options = GetSerializerOptions(); + var decl = new CanvasDeclaration + { + Id = "report", + DisplayName = "Quarterly Report", + Description = "Renders the latest report", + }; + + var json = JsonSerializer.Serialize(decl, options); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + Assert.Equal("report", root.GetProperty("id").GetString()); + Assert.Equal("Quarterly Report", root.GetProperty("displayName").GetString()); + Assert.Equal("Renders the latest report", root.GetProperty("description").GetString()); + Assert.False(root.TryGetProperty("inputSchema", out _)); + Assert.False(root.TryGetProperty("actions", out _)); + } + + [Fact] + public void CanvasOpenResponse_Roundtrips_WithCamelCaseFields() + { + var options = GetSerializerOptions(); + var response = new CanvasOpenResponse + { + Url = "https://example.com/c/1", + Title = "Demo", + Status = "ready" + }; + + var json = JsonSerializer.Serialize(response, options); + var parsed = JsonSerializer.Deserialize(json, options); + + Assert.NotNull(parsed); + Assert.Equal("https://example.com/c/1", parsed!.Url); + Assert.Equal("Demo", parsed.Title); + Assert.Equal("ready", parsed.Status); + } + + [Fact] + public void ExtensionInfo_Serializes_SourceAndName() + { + var options = GetSerializerOptions(); + var info = new ExtensionInfo { Source = "github-app", Name = "demo" }; + var json = JsonSerializer.Serialize(info, options); + using var doc = JsonDocument.Parse(json); + Assert.Equal("github-app", doc.RootElement.GetProperty("source").GetString()); + Assert.Equal("demo", doc.RootElement.GetProperty("name").GetString()); + } + + [Fact] + public async Task CanvasHandlerBase_DefaultOnClose_Completes() + { + var handler = new TestHandler(); + await handler.OnCloseAsync(new CanvasLifecycleContext(), CancellationToken.None); + } + + [Fact] + public async Task CanvasHandlerBase_DefaultOnAction_ThrowsNoHandlerCanvasError() + { + var handler = new TestHandler(); + var ex = await Assert.ThrowsAsync( + () => handler.OnActionAsync(new CanvasActionContext(), CancellationToken.None)); + Assert.Equal("canvas_action_no_handler", ex.Code); + } + + [Fact] + public void CanvasError_NoHandler_HasExpectedCode() + { + var err = CanvasError.NoHandler(); + Assert.Equal("canvas_action_no_handler", err.Code); + Assert.False(string.IsNullOrEmpty(err.Message)); + } + + [Fact] + public void SessionConfig_Clone_CopiesCanvasFields() + { + var handler = new TestHandler(); + var declaration = new CanvasDeclaration { Id = "c1", DisplayName = "C", Description = "d" }; + var config = new SessionConfig + { + Canvases = new[] { declaration }, + RequestCanvasRenderer = true, + RequestExtensions = true, + ExtensionInfo = new ExtensionInfo { Source = "github-app", Name = "demo" }, + CanvasHandler = handler + }; + + var clone = config.Clone(); + + Assert.NotNull(clone.Canvases); + Assert.Single(clone.Canvases!); + Assert.Equal("c1", clone.Canvases![0].Id); + Assert.True(clone.RequestCanvasRenderer); + Assert.True(clone.RequestExtensions); + Assert.NotNull(clone.ExtensionInfo); + Assert.Equal("github-app", clone.ExtensionInfo!.Source); + Assert.Same(handler, clone.CanvasHandler); + + // Mutating the clone's list does not affect the original. + clone.Canvases!.Add(new CanvasDeclaration { Id = "c2", DisplayName = "C2", Description = "d2" }); + Assert.Single(config.Canvases!); + } + + [Fact] + public void ResumeSessionConfig_Clone_CopiesCanvasFields() + { + var handler = new TestHandler(); + var config = new ResumeSessionConfig + { + Canvases = new[] { new CanvasDeclaration { Id = "c1", DisplayName = "C", Description = "d" } }, + RequestCanvasRenderer = true, + ExtensionInfo = new ExtensionInfo { Source = "s", Name = "n" }, + CanvasHandler = handler + }; + + var clone = config.Clone(); + + Assert.NotNull(clone.Canvases); + Assert.Single(clone.Canvases!); + Assert.True(clone.RequestCanvasRenderer); + Assert.NotNull(clone.ExtensionInfo); + Assert.Same(handler, clone.CanvasHandler); + } + + private sealed class TestHandler : CanvasHandlerBase + { + public override Task OnOpenAsync(CanvasOpenContext context, CancellationToken cancellationToken) + => Task.FromResult(new CanvasOpenResponse { Url = "https://example.com" }); + } +} diff --git a/dotnet/test/Unit/PermissionRequestResultKindTests.cs b/dotnet/test/Unit/PermissionRequestResultKindTests.cs deleted file mode 100644 index ce828e6ef..000000000 --- a/dotnet/test/Unit/PermissionRequestResultKindTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -using System.Text.Json; -using Xunit; - -namespace GitHub.Copilot.Test.Unit; - -public class PermissionRequestResultKindTests -{ - private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) - { - TypeInfoResolver = TestJsonContext.Default, - }; - - [Fact] - public void WellKnownKinds_HaveExpectedValues() - { - Assert.Equal("approve-once", PermissionRequestResultKind.Approved.Value); - Assert.Equal("reject", PermissionRequestResultKind.Rejected.Value); - Assert.Equal("user-not-available", PermissionRequestResultKind.UserNotAvailable.Value); - Assert.Equal("no-result", PermissionRequestResultKind.NoResult.Value); - } - - [Fact] - public void Equals_SameValue_ReturnsTrue() - { - var a = new PermissionRequestResultKind("approve-once"); - Assert.True(a == PermissionRequestResultKind.Approved); - Assert.True(a.Equals(PermissionRequestResultKind.Approved)); - Assert.True(a.Equals((object)PermissionRequestResultKind.Approved)); - } - - [Fact] - public void Equals_DifferentValue_ReturnsFalse() - { - Assert.True(PermissionRequestResultKind.Approved != PermissionRequestResultKind.Rejected); - Assert.False(PermissionRequestResultKind.Approved.Equals(PermissionRequestResultKind.Rejected)); - } - - [Fact] - public void Equals_IsCaseInsensitive() - { - var upper = new PermissionRequestResultKind("APPROVE-ONCE"); - Assert.Equal(PermissionRequestResultKind.Approved, upper); - } - - [Fact] - public void GetHashCode_IsCaseInsensitive() - { - var upper = new PermissionRequestResultKind("APPROVE-ONCE"); - Assert.Equal(PermissionRequestResultKind.Approved.GetHashCode(), upper.GetHashCode()); - } - - [Fact] - public void ToString_ReturnsValue() - { - Assert.Equal("approve-once", PermissionRequestResultKind.Approved.ToString()); - Assert.Equal("reject", PermissionRequestResultKind.Rejected.ToString()); - } - - [Fact] - public void CustomValue_IsPreserved() - { - var custom = new PermissionRequestResultKind("custom-kind"); - Assert.Equal("custom-kind", custom.Value); - Assert.Equal("custom-kind", custom.ToString()); - } - - [Fact] - public void Constructor_NullValue_TreatedAsEmpty() - { - var kind = new PermissionRequestResultKind(null!); - Assert.Equal(string.Empty, kind.Value); - } - - [Fact] - public void Default_HasEmptyStringValue() - { - var defaultKind = default(PermissionRequestResultKind); - Assert.Equal(string.Empty, defaultKind.Value); - Assert.Equal(string.Empty, defaultKind.ToString()); - Assert.Equal(defaultKind.GetHashCode(), defaultKind.GetHashCode()); - } - - [Fact] - public void Equals_NonPermissionRequestResultKindObject_ReturnsFalse() - { - Assert.False(PermissionRequestResultKind.Approved.Equals("approve-once")); - } - - [Fact] - public void JsonSerialize_WritesStringValue() - { - var result = new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; - var json = JsonSerializer.Serialize(result, s_jsonOptions); - Assert.Contains("\"kind\":\"approve-once\"", json); - } - - [Fact] - public void JsonDeserialize_ReadsStringValue() - { - var json = """{"kind":"reject"}"""; - var result = JsonSerializer.Deserialize(json, s_jsonOptions)!; - Assert.Equal(PermissionRequestResultKind.Rejected, result.Kind); - } - - [Fact] - public void JsonRoundTrip_PreservesAllKinds() - { - var kinds = new[] - { - PermissionRequestResultKind.Approved, - PermissionRequestResultKind.Rejected, - PermissionRequestResultKind.UserNotAvailable, - PermissionRequestResultKind.NoResult, - }; - - foreach (var kind in kinds) - { - var result = new PermissionRequestResult { Kind = kind }; - var json = JsonSerializer.Serialize(result, s_jsonOptions); - var deserialized = JsonSerializer.Deserialize(json, s_jsonOptions)!; - Assert.Equal(kind, deserialized.Kind); - } - } - - [Fact] - public void JsonRoundTrip_CustomValue() - { - var result = new PermissionRequestResult { Kind = new PermissionRequestResultKind("custom") }; - var json = JsonSerializer.Serialize(result, s_jsonOptions); - var deserialized = JsonSerializer.Deserialize(json, s_jsonOptions)!; - Assert.Equal("custom", deserialized.Kind.Value); - } -} - -[System.Text.Json.Serialization.JsonSerializable(typeof(PermissionRequestResult))] -internal partial class TestJsonContext : System.Text.Json.Serialization.JsonSerializerContext; diff --git a/dotnet/test/Unit/PublicDtoTests.cs b/dotnet/test/Unit/PublicDtoTests.cs index 473c312ba..c81a8a7a6 100644 --- a/dotnet/test/Unit/PublicDtoTests.cs +++ b/dotnet/test/Unit/PublicDtoTests.cs @@ -174,15 +174,17 @@ private static bool TryCreateGenericCollection(Type type, HashSet visited, .FirstOrDefault(candidate => candidate.IsGenericType && (candidate.GetGenericTypeDefinition() == typeof(IDictionary<,>) || - candidate.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) && - candidate.GetGenericArguments()[0] == typeof(string)); + candidate.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))); if (dictionaryInterface is not null) { + var keyType = dictionaryInterface.GetGenericArguments()[0]; var valueType = dictionaryInterface.GetGenericArguments()[1]; + TryCreateSampleValue(keyType, visited, out var sampleKey); TryCreateSampleValue(valueType, visited, out var sampleValue); - var dictionary = (IDictionary)Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeof(string), valueType))!; - dictionary["key"] = sampleValue; + var dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); + var dictionary = (IDictionary)Activator.CreateInstance(dictionaryType)!; + dictionary[sampleKey!] = sampleValue; value = dictionary; return true; } diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 6a3802d0c..f225fe4ee 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -203,6 +203,34 @@ public void ResumeSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptio Assert.False(root.GetProperty("enableSessionTelemetry").GetBoolean()); } + [Fact] + public void ResumeSessionRequest_CanSerializeOpenCanvases_WithSdkOptions() + { + var options = GetSerializerOptions(); + var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); + var instances = new List + { + new() + { + CanvasId = "canvas-id", + ExtensionId = "ext-id", + InstanceId = "instance-1", + Availability = CanvasInstanceAvailability.Ready, + }, + }; + var request = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("OpenCanvases", instances)); + + var json = JsonSerializer.Serialize(request, requestType, options); + using var document = JsonDocument.Parse(json); + var root = document.RootElement; + var openCanvases = root.GetProperty("openCanvases"); + Assert.Equal(1, openCanvases.GetArrayLength()); + Assert.Equal("canvas-id", openCanvases[0].GetProperty("canvasId").GetString()); + } + [Fact] public void ResumeSessionRequest_CanSerializeModeRequestFlags_WithSdkOptions() { @@ -295,12 +323,9 @@ public void QueuedCommandResult_SerializesHandledAsBoolean_WithSdkOptions() public void PermissionDecision_SerializesBaseDiscriminator_WithSdkOptions() { var options = GetSerializerOptions(); - var original = new PermissionDecision - { - Kind = PermissionRequestResultKind.Approved.Value - }; + var original = PermissionDecision.ApproveOnce(); - var json = JsonSerializer.Serialize(original, options); + var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); Assert.Equal("approve-once", document.RootElement.GetProperty("kind").GetString()); diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 3e6d4661f..47b4ac3f7 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -66,7 +66,7 @@ public class SessionEventSerializationTests Content = "ok", DetailedContent = "ok", }, - ToolTelemetry = new Dictionary + ToolTelemetry = new Dictionary { ["properties"] = ParseJsonElement("""{"command":"view"}"""), ["metrics"] = ParseJsonElement("""{"resultLength":2}"""), @@ -84,7 +84,6 @@ public class SessionEventSerializationTests Data = new SessionShutdownData { ShutdownType = ShutdownType.Routine, - TotalPremiumRequests = 1, TotalApiDuration = TimeSpan.FromMilliseconds(100), SessionStartTime = 1773609948932, CodeChanges = new ShutdownCodeChanges diff --git a/go/README.md b/go/README.md index 8dcffb1a8..da77033f8 100644 --- a/go/README.md +++ b/go/README.md @@ -160,7 +160,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `SystemMessage` (\*SystemMessageConfig): System message configuration. Supports three modes: - **append** (default): Appends `Content` after the SDK-managed prompt - **replace**: Replaces the entire prompt with `Content` - - **customize**: Selectively override individual sections via `Sections` map (keys: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionLastInstructions`; values: `SectionOverride` with `Action` and optional `Content`) + - **customize**: Selectively override individual sections via `Sections` map (keys: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionRuntimeInstructions`, `SectionLastInstructions`; values: `SectionOverride` with `Action` and optional `Content`) - `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. - `Streaming` (*bool): Enable streaming delta events (nil = runtime default) - `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration @@ -233,7 +233,7 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ }) ``` -Available section constants: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionLastInstructions`. +Available section constants: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionRuntimeInstructions`, `SectionLastInstructions`. Each section override supports four actions: @@ -594,45 +594,39 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi Provide your own `PermissionHandlerFunc` to inspect each request and apply custom logic: ```go +import ( + "fmt" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" +) + session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ Model: "gpt-5", - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - // request.Kind — what type of operation is being requested: - // copilot.KindShell — executing a shell command - // copilot.Write — writing or editing a file - // copilot.Read — reading a file - // copilot.MCP — calling an MCP tool - // copilot.CustomTool — calling one of your registered tools - // copilot.URL — fetching a URL - // copilot.Memory — accessing or updating Copilot-managed memory - // copilot.Hook — invoking a registered hook - // request.ToolCallID — pointer to the tool call that triggered this request - // request.ToolName — pointer to the name of the tool (for custom-tool / mcp) - // request.FileName — pointer to the file being written (for write) - // request.FullCommandText — pointer to the full shell command (for shell) - - if request.Kind == copilot.KindShell { - // Deny shell commands - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + // Type-switch on the discriminated PermissionRequest variants to + // access per-kind fields: + if shell, ok := request.(*copilot.PermissionRequestShell); ok { + feedback := fmt.Sprintf("Refusing shell: %s", shell.FullCommandText) + return &rpc.PermissionDecisionReject{Feedback: &feedback}, nil } - - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) ``` -### Permission Result Kinds +### Permission Decisions -The `Kind` field must be one of the canonical `PermissionRequestResultKind` constants. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. +The handler returns an `rpc.PermissionDecision` — a sealed interface implemented by every decision variant: -| Constant | Wire value | Meaning | -| --------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------- | -| `PermissionRequestResultKindApproved` | `"approve-once"` | Allow this single request | -| `PermissionRequestResultKindRejected` | `"reject"` | Deny the request | -| `PermissionRequestResultKindUserNotAvailable` | `"user-not-available"` | Deny the request because no user is available to confirm it | -| `PermissionRequestResultKindNoResult` | `"no-result"` | Leave the permission request unanswered (protocol v1 only; rejected by protocol v2 servers) | +| Variant | Meaning | +| ---------------------------------------- | ---------------------------------------------------------------------------------- | +| `&rpc.PermissionDecisionApproveOnce{}` | Allow this single request | +| `&rpc.PermissionDecisionReject{...}` | Deny the request (set `Feedback` to forward a message to the LLM) | +| `&rpc.PermissionDecisionUserNotAvailable{}` | Deny because no user is available to confirm | +| `&rpc.PermissionDecisionNoResult{}` | Decline to respond, allowing another connected client to answer instead | -> The past-tense names `PermissionRequestResultKindDeniedInteractivelyByUser`, `PermissionRequestResultKindDeniedCouldNotRequestFromUser`, and `PermissionRequestResultKindDeniedByRules` remain as deprecated aliases for backward compatibility — prefer the canonical constants above in new code. +Richer decisions (`PermissionDecisionApproveForSession`, `PermissionDecisionApproveForLocation`, `PermissionDecisionApprovePermanently`) carry per-kind approval payloads — instantiate the variant struct directly. ### Resuming Sessions diff --git a/go/canvas.go b/go/canvas.go new file mode 100644 index 000000000..2d122db29 --- /dev/null +++ b/go/canvas.go @@ -0,0 +1,232 @@ +// Canvas declarations, provider callbacks, and host-side canvas RPC types. +// +// This file mirrors rust/src/canvas.rs. The SDK does not maintain a per-canvas +// registry; multiplexing across declared canvases is the CanvasHandler +// implementor's responsibility (typically by switching on CanvasOpenContext.CanvasID). + +package copilot + +import ( + "context" + + "github.com/github/copilot-sdk/go/rpc" +) + +// CanvasDeclaration is the declarative metadata for a single canvas, sent over +// the wire on `session.create` / `session.resume`. +type CanvasDeclaration struct { + // ID is the canvas identifier, unique within the declaring connection. + ID string `json:"id"` + // DisplayName is the human-readable name shown in host UI and canvas pickers. + DisplayName string `json:"displayName"` + // Description is a short, single-sentence description shown to the agent in canvas catalogs. + Description string `json:"description"` + // InputSchema is the JSON Schema for the `input` payload accepted by `canvas.open`. + InputSchema map[string]any `json:"inputSchema,omitempty"` + // Actions are the agent-callable actions this canvas exposes. + Actions []rpc.CanvasAction `json:"actions,omitempty"` +} + +// CanvasOpenResponse is the response returned from CanvasHandler.OnOpen. +type CanvasOpenResponse struct { + // URL the host should render. Optional for canvases with no visual surface. + URL *string `json:"url,omitempty"` + // Title is the provider-supplied title shown in host chrome. + Title *string `json:"title,omitempty"` + // Status is the provider-supplied status text shown in host chrome. + Status *string `json:"status,omitempty"` +} + +// CanvasHostContext carries host capability hints passed to canvas provider callbacks. +type CanvasHostContext struct { + // Capabilities describes host feature support relevant to canvases. + Capabilities CanvasHostCapabilities `json:"capabilities"` +} + +// CanvasHostCapabilities describes host capability details passed to canvas provider callbacks. +type CanvasHostCapabilities struct { + // Canvases indicates whether the host supports canvas rendering. + Canvases bool `json:"canvases"` +} + +// CanvasOpenContext is the context handed to CanvasHandler.OnOpen. +type CanvasOpenContext struct { + // SessionID is the session that requested the canvas. + SessionID string + // ExtensionID is the owning provider identifier. + ExtensionID string + // CanvasID is the canvas id from the declaring CanvasDeclaration. + CanvasID string + // InstanceID is the stable instance id supplied by the runtime. + InstanceID string + // Input is the validated input payload. + Input any + // Host carries host capabilities supplied by the runtime. + Host *CanvasHostContext +} + +// CanvasActionContext is the context handed to CanvasHandler.OnAction. +type CanvasActionContext struct { + // SessionID is the session that invoked the action. + SessionID string + // ExtensionID is the owning provider identifier. + ExtensionID string + // CanvasID is the canvas id targeted by the action. + CanvasID string + // InstanceID is the instance id targeted by the action. + InstanceID string + // ActionName is the action name from CanvasAction.Name. + ActionName string + // Input is the validated input payload. + Input any + // Host carries host capabilities supplied by the runtime. + Host *CanvasHostContext +} + +// CanvasLifecycleContext is the context handed to a canvas's close lifecycle hook. +type CanvasLifecycleContext struct { + // SessionID is the session owning the canvas instance. + SessionID string + // ExtensionID is the owning provider identifier. + ExtensionID string + // CanvasID is the canvas id from the declaring CanvasDeclaration. + CanvasID string + // InstanceID is the instance id this lifecycle event applies to. + InstanceID string + // Host carries host capabilities supplied by the runtime. + Host *CanvasHostContext +} + +// CanvasError is a structured error returned from canvas handlers. +// +// Wire envelope: +// +// { "code": "", "message": "" } +type CanvasError struct { + // Code is the machine-readable error code. + Code string `json:"code"` + // Message is the human-readable message. + Message string `json:"message"` +} + +// Error implements the error interface. +func (e *CanvasError) Error() string { + return e.Code + ": " + e.Message +} + +// NewCanvasError constructs a new error envelope with the given code and message. +func NewCanvasError(code, message string) *CanvasError { + return &CanvasError{Code: code, Message: message} +} + +// CanvasErrorNoHandler is the default error returned when a custom action has no handler. +func CanvasErrorNoHandler() *CanvasError { + return NewCanvasError( + "canvas_action_no_handler", + "No handler implemented for this canvas action", + ) +} + +// CanvasHandler is the provider-side canvas lifecycle handler. +// +// A session installs a single CanvasHandler (via SessionConfig.CanvasHandler). +// The handler receives every inbound `canvas.open` / `canvas.close` / +// `canvas.action.invoke` JSON-RPC request the runtime issues for this session +// and decides — typically by inspecting CanvasOpenContext.CanvasID — which +// application-side canvas should handle the call. +// +// The SDK does not maintain a per-canvas registry; multiplexing across declared +// canvases is the implementor's responsibility. +// +// Embed CanvasHandlerDefaults to inherit no-op defaults for OnClose and a +// "no handler" error for OnAction. +type CanvasHandler interface { + OnOpen(ctx context.Context, c CanvasOpenContext) (CanvasOpenResponse, error) + OnClose(ctx context.Context, c CanvasLifecycleContext) error + OnAction(ctx context.Context, c CanvasActionContext) (any, error) +} + +// CanvasHandlerDefaults supplies default OnClose / OnAction implementations +// that consumers can inherit by embedding it in their CanvasHandler. +// +// Example: +// +// type myHandler struct { +// copilot.CanvasHandlerDefaults +// } +// func (h *myHandler) OnOpen(ctx context.Context, c copilot.CanvasOpenContext) (copilot.CanvasOpenResponse, error) { ... } +type CanvasHandlerDefaults struct{} + +// OnClose returns nil by default. +func (CanvasHandlerDefaults) OnClose(ctx context.Context, c CanvasLifecycleContext) error { + return nil +} + +// OnAction returns CanvasErrorNoHandler() by default. +func (CanvasHandlerDefaults) OnAction(ctx context.Context, c CanvasActionContext) (any, error) { + return nil, CanvasErrorNoHandler() +} + +// canvasProviderRequestParams is the wire shape of the common fields sent by +// direct `canvas.*` provider callbacks (canvas.open / canvas.close). +type canvasProviderRequestParams struct { + SessionID string `json:"sessionId"` + ExtensionID string `json:"extensionId"` + CanvasID string `json:"canvasId"` + InstanceID string `json:"instanceId"` + Input any `json:"input,omitempty"` + Host *CanvasHostContext `json:"host,omitempty"` +} + +func (p *canvasProviderRequestParams) toOpenContext() CanvasOpenContext { + return CanvasOpenContext{ + SessionID: p.SessionID, + ExtensionID: p.ExtensionID, + CanvasID: p.CanvasID, + InstanceID: p.InstanceID, + Input: p.Input, + Host: p.Host, + } +} + +func (p *canvasProviderRequestParams) toLifecycleContext() CanvasLifecycleContext { + return CanvasLifecycleContext{ + SessionID: p.SessionID, + ExtensionID: p.ExtensionID, + CanvasID: p.CanvasID, + InstanceID: p.InstanceID, + Host: p.Host, + } +} + +// canvasInvokeParams is the wire shape for `canvas.action.invoke`. +type canvasInvokeParams struct { + SessionID string `json:"sessionId"` + ExtensionID string `json:"extensionId"` + CanvasID string `json:"canvasId"` + InstanceID string `json:"instanceId"` + ActionName string `json:"actionName"` + Input any `json:"input,omitempty"` + Host *CanvasHostContext `json:"host,omitempty"` +} + +func (p *canvasInvokeParams) toActionContext() CanvasActionContext { + return CanvasActionContext{ + SessionID: p.SessionID, + ExtensionID: p.ExtensionID, + CanvasID: p.CanvasID, + InstanceID: p.InstanceID, + ActionName: p.ActionName, + Input: p.Input, + Host: p.Host, + } +} + +// ExtensionInfo carries stable extension identity for session participants +// that provide canvases. +type ExtensionInfo struct { + // Source is the extension namespace/source, e.g. "github-app". + Source string `json:"source"` + // Name is the stable provider name within the source namespace. + Name string `json:"name"` +} diff --git a/go/canvas_test.go b/go/canvas_test.go new file mode 100644 index 000000000..be0538d58 --- /dev/null +++ b/go/canvas_test.go @@ -0,0 +1,319 @@ +package copilot + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +func TestCanvasDeclaration_JSONShape(t *testing.T) { + desc := "bump" + decl := CanvasDeclaration{ + ID: "counter", + DisplayName: "Counter", + Description: "Count things", + Actions: []rpc.CanvasAction{ + {Name: "increment", Description: &desc}, + }, + } + + data, err := json.Marshal(decl) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + if decoded["id"] != "counter" { + t.Fatalf("expected id=counter, got %v", decoded["id"]) + } + if decoded["displayName"] != "Counter" { + t.Fatalf("expected displayName=Counter, got %v", decoded["displayName"]) + } + if decoded["description"] != "Count things" { + t.Fatalf("expected description, got %v", decoded["description"]) + } + if _, present := decoded["inputSchema"]; present { + t.Fatalf("inputSchema should be omitted when nil, got %v", decoded["inputSchema"]) + } + actions, ok := decoded["actions"].([]any) + if !ok || len(actions) != 1 { + t.Fatalf("expected actions array of length 1, got %v", decoded["actions"]) + } + first, _ := actions[0].(map[string]any) + if first["name"] != "increment" { + t.Fatalf("expected first action name=increment, got %v", first["name"]) + } +} + +func TestCanvasDeclaration_OmitsEmptyActions(t *testing.T) { + decl := CanvasDeclaration{ID: "x", DisplayName: "X", Description: "y"} + data, err := json.Marshal(decl) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + var decoded map[string]any + _ = json.Unmarshal(data, &decoded) + if _, present := decoded["actions"]; present { + t.Fatalf("actions should be omitted when nil, got %v", decoded["actions"]) + } +} + +func TestCanvasHandlerDefaults_OnAction_ReturnsNoHandler(t *testing.T) { + d := CanvasHandlerDefaults{} + _, err := d.OnAction(context.Background(), CanvasActionContext{}) + if err == nil { + t.Fatalf("expected error from default OnAction") + } + cerr, ok := err.(*CanvasError) + if !ok { + t.Fatalf("expected *CanvasError, got %T", err) + } + if cerr.Code != "canvas_action_no_handler" { + t.Fatalf("expected code=canvas_action_no_handler, got %q", cerr.Code) + } +} + +func TestCanvasHandlerDefaults_OnClose_ReturnsNil(t *testing.T) { + d := CanvasHandlerDefaults{} + if err := d.OnClose(context.Background(), CanvasLifecycleContext{}); err != nil { + t.Fatalf("expected nil from default OnClose, got %v", err) + } +} + +func TestCanvasError_ErrorString(t *testing.T) { + e := NewCanvasError("foo_code", "bar message") + if got := e.Error(); got != "foo_code: bar message" { + t.Fatalf("unexpected Error() output: %q", got) + } +} + +// recordingCanvasHandler captures calls for assertion. +type recordingCanvasHandler struct { + CanvasHandlerDefaults + openCtx *CanvasOpenContext + openResult CanvasOpenResponse + openErr error +} + +func (h *recordingCanvasHandler) OnOpen(ctx context.Context, c CanvasOpenContext) (CanvasOpenResponse, error) { + h.openCtx = &c + return h.openResult, h.openErr +} + +func TestClient_HandleCanvasOpen_DispatchesToHandler(t *testing.T) { + title := "Echo" + url := "https://example.test/echo" + handler := &recordingCanvasHandler{ + openResult: CanvasOpenResponse{URL: &url, Title: &title}, + } + + session := &Session{SessionID: "s1"} + session.registerCanvasHandler(handler) + + c := &Client{sessions: map[string]*Session{"s1": session}} + + params := canvasProviderRequestParams{ + SessionID: "s1", + ExtensionID: "project:echo", + CanvasID: "echo", + InstanceID: "echo-1", + Input: map[string]any{"x": float64(1)}, + } + resp, rpcErr := c.handleCanvasOpen(params) + if rpcErr != nil { + t.Fatalf("unexpected rpc error: %+v", rpcErr) + } + if handler.openCtx == nil { + t.Fatalf("handler.OnOpen was not called") + } + if handler.openCtx.CanvasID != "echo" || handler.openCtx.InstanceID != "echo-1" { + t.Fatalf("unexpected ctx: %+v", handler.openCtx) + } + if resp.URL == nil || *resp.URL != url { + t.Fatalf("response URL not propagated: %+v", resp) + } +} + +func TestClient_HandleCanvasOpen_NoHandler_ReturnsUnsetError(t *testing.T) { + session := &Session{SessionID: "s1"} + c := &Client{sessions: map[string]*Session{"s1": session}} + + _, rpcErr := c.handleCanvasOpen(canvasProviderRequestParams{SessionID: "s1"}) + if rpcErr == nil { + t.Fatalf("expected error when no canvas handler installed") + } + if rpcErr.Code != -32603 { + t.Fatalf("expected internal-error code, got %d", rpcErr.Code) + } + var data map[string]string + if err := json.Unmarshal(rpcErr.Data, &data); err != nil { + t.Fatalf("invalid error data: %v", err) + } + if data["code"] != "canvas_handler_unset" { + t.Fatalf("expected code=canvas_handler_unset, got %q", data["code"]) + } +} + +func TestClient_HandleCanvasOpen_HandlerCanvasError_Wired(t *testing.T) { + handler := &recordingCanvasHandler{ + openErr: NewCanvasError("permission_denied", "nope"), + } + session := &Session{SessionID: "s1"} + session.registerCanvasHandler(handler) + c := &Client{sessions: map[string]*Session{"s1": session}} + + _, rpcErr := c.handleCanvasOpen(canvasProviderRequestParams{SessionID: "s1"}) + if rpcErr == nil { + t.Fatalf("expected error") + } + var data map[string]string + _ = json.Unmarshal(rpcErr.Data, &data) + if data["code"] != "permission_denied" { + t.Fatalf("expected propagated code, got %q", data["code"]) + } +} + +func TestClient_HandleCanvasOpen_HandlerGenericError_WrappedAsCanvasHandlerError(t *testing.T) { + handler := &recordingCanvasHandler{openErr: errors.New("boom")} + session := &Session{SessionID: "s1"} + session.registerCanvasHandler(handler) + c := &Client{sessions: map[string]*Session{"s1": session}} + + _, rpcErr := c.handleCanvasOpen(canvasProviderRequestParams{SessionID: "s1"}) + if rpcErr == nil { + t.Fatalf("expected error") + } + var data map[string]string + _ = json.Unmarshal(rpcErr.Data, &data) + if data["code"] != "canvas_handler_error" { + t.Fatalf("expected code=canvas_handler_error, got %q", data["code"]) + } + if data["message"] != "boom" { + t.Fatalf("expected message=boom, got %q", data["message"]) + } +} + +// Ensure the JSON-RPC inbound parsing wires through RequestHandlerFor correctly. +func TestClient_HandleCanvasOpen_RawJSONRoundTrip(t *testing.T) { + handler := &recordingCanvasHandler{ + openResult: CanvasOpenResponse{Status: strPtr("ready")}, + } + session := &Session{SessionID: "s1"} + session.registerCanvasHandler(handler) + c := &Client{sessions: map[string]*Session{"s1": session}} + + rpcHandler := jsonrpc2.RequestHandlerFor(c.handleCanvasOpen) + raw := []byte(`{"sessionId":"s1","extensionId":"ext","canvasId":"echo","instanceId":"i1","input":{"k":"v"},"host":{"capabilities":{"canvases":true}}}`) + out, rpcErr := rpcHandler(raw) + if rpcErr != nil { + t.Fatalf("unexpected rpc error: %v", rpcErr) + } + if handler.openCtx == nil { + t.Fatalf("handler not invoked") + } + if handler.openCtx.Host == nil || !handler.openCtx.Host.Capabilities.Canvases { + t.Fatalf("host capabilities not parsed: %+v", handler.openCtx.Host) + } + var decoded map[string]any + if err := json.Unmarshal(out, &decoded); err != nil { + t.Fatalf("bad output JSON: %v", err) + } + if decoded["status"] != "ready" { + t.Fatalf("expected status=ready, got %v", decoded["status"]) + } +} + +func TestResumeSessionResponse_OpenCanvasesParse(t *testing.T) { + raw := []byte(`{ + "sessionId": "s1", + "workspacePath": "/tmp/ws", + "openCanvases": [ + { + "availability": "ready", + "canvasId": "echo", + "extensionId": "project:echo", + "instanceId": "echo-1", + "reopen": false + } + ] + }`) + + var resp resumeSessionResponse + if err := json.Unmarshal(raw, &resp); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + if len(resp.OpenCanvases) != 1 { + t.Fatalf("expected 1 open canvas, got %d", len(resp.OpenCanvases)) + } + if resp.OpenCanvases[0].CanvasID != "echo" { + t.Fatalf("unexpected canvasId: %q", resp.OpenCanvases[0].CanvasID) + } + + session := &Session{SessionID: "s1"} + session.setOpenCanvases(resp.OpenCanvases) + got := session.OpenCanvases() + if len(got) != 1 || got[0].InstanceID != "echo-1" { + t.Fatalf("OpenCanvases did not surface snapshot: %+v", got) + } +} + +func TestResumeSessionRequest_OpenCanvasesWireShape(t *testing.T) { + req := resumeSessionRequest{ + SessionID: "s1", + OpenCanvases: []rpc.OpenCanvasInstance{ + { + Availability: "ready", + CanvasID: "echo", + ExtensionID: "project:echo", + InstanceID: "echo-1", + Reopen: false, + }, + }, + } + + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + raw, ok := decoded["openCanvases"].([]any) + if !ok || len(raw) != 1 { + t.Fatalf("expected openCanvases array of length 1, got %v", decoded["openCanvases"]) + } + first, _ := raw[0].(map[string]any) + if first["canvasId"] != "echo" { + t.Fatalf("expected canvasId=echo, got %v", first["canvasId"]) + } + if first["instanceId"] != "echo-1" { + t.Fatalf("expected instanceId=echo-1, got %v", first["instanceId"]) + } + + // Omitted when nil + empty := resumeSessionRequest{SessionID: "s1"} + emptyData, err := json.Marshal(empty) + if err != nil { + t.Fatalf("marshal empty failed: %v", err) + } + var emptyDecoded map[string]any + if err := json.Unmarshal(emptyData, &emptyDecoded); err != nil { + t.Fatalf("unmarshal empty failed: %v", err) + } + if _, present := emptyDecoded["openCanvases"]; present { + t.Fatalf("openCanvases should be omitted when nil") + } +} + +func strPtr(s string) *string { return &s } diff --git a/go/client.go b/go/client.go index 3c99cd555..6e7557b0b 100644 --- a/go/client.go +++ b/go/client.go @@ -52,8 +52,6 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) -const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server" - func validateSessionFsConfig(config *SessionFsConfig) error { if config == nil { return nil @@ -628,6 +626,10 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.GitHubToken = config.GitHubToken req.RemoteSession = config.RemoteSession req.Cloud = config.Cloud + req.Canvases = config.Canvases + req.RequestCanvasRenderer = config.RequestCanvasRenderer + req.RequestExtensions = config.RequestExtensions + req.ExtensionInfo = config.ExtensionInfo if len(config.Commands) > 0 { cmds := make([]wireCommand, 0, len(config.Commands)) @@ -666,7 +668,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses config.Hooks.OnErrorOccurred != nil) { req.Hooks = Bool(true) } - req.RequestPermission = Bool(true) + if config.OnPermissionRequest != nil { + req.RequestPermission = Bool(true) + } traceparent, tracestate := getTraceContext(ctx) req.Traceparent = traceparent @@ -708,6 +712,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.OnAutoModeSwitchRequest != nil { session.registerAutoModeSwitchHandler(config.OnAutoModeSwitchRequest) } + if config.CanvasHandler != nil { + session.registerCanvasHandler(config.CanvasHandler) + } c.sessionsMux.Lock() c.sessions[sessionID] = session @@ -841,7 +848,14 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.InfiniteSessions = config.InfiniteSessions req.GitHubToken = config.GitHubToken req.RemoteSession = config.RemoteSession - req.RequestPermission = Bool(true) + req.Canvases = config.Canvases + req.OpenCanvases = config.OpenCanvases + req.RequestCanvasRenderer = config.RequestCanvasRenderer + req.RequestExtensions = config.RequestExtensions + req.ExtensionInfo = config.ExtensionInfo + if config.OnPermissionRequest != nil { + req.RequestPermission = Bool(true) + } if len(config.Commands) > 0 { cmds := make([]wireCommand, 0, len(config.Commands)) @@ -894,6 +908,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.OnAutoModeSwitchRequest != nil { session.registerAutoModeSwitchHandler(config.OnAutoModeSwitchRequest) } + if config.CanvasHandler != nil { + session.registerCanvasHandler(config.CanvasHandler) + } c.sessionsMux.Lock() c.sessions[sessionID] = session @@ -936,6 +953,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, session.workspacePath = response.WorkspacePath session.setCapabilities(response.Capabilities) + session.setOpenCanvases(response.OpenCanvases) return session, nil } @@ -1380,7 +1398,7 @@ func (c *Client) ListModels(ctx context.Context) ([]ModelInfo, error) { } // minProtocolVersion is the minimum protocol version this SDK can communicate with. -const minProtocolVersion = 2 +const minProtocolVersion = 3 // verifyProtocolVersion sends the `connect` handshake (carrying the optional token) and // verifies the server's protocol version. Falls back to `ping` against legacy servers @@ -1736,20 +1754,17 @@ func (c *Client) connectViaTcp(ctx context.Context) error { } // setupNotificationHandler configures handlers for session events and RPC requests. -// Protocol v3 servers send tool calls and permission requests as broadcast session events. -// Protocol v2 servers use the older tool.call / permission.request RPC model. -// We always register v2 adapters because handlers are set up before version negotiation; -// a v3 server will simply never send these requests. func (c *Client) setupNotificationHandler() { c.client.SetRequestHandler("session.event", jsonrpc2.NotificationHandlerFor(c.handleSessionEvent)) c.client.SetRequestHandler("session.lifecycle", jsonrpc2.NotificationHandlerFor(c.handleLifecycleEvent)) - c.client.SetRequestHandler("tool.call", jsonrpc2.RequestHandlerFor(c.handleToolCallRequestV2)) - c.client.SetRequestHandler("permission.request", jsonrpc2.RequestHandlerFor(c.handlePermissionRequestV2)) c.client.SetRequestHandler("userInput.request", jsonrpc2.RequestHandlerFor(c.handleUserInputRequest)) c.client.SetRequestHandler("exitPlanMode.request", jsonrpc2.RequestHandlerFor(c.handleExitPlanModeRequest)) c.client.SetRequestHandler("autoModeSwitch.request", jsonrpc2.RequestHandlerFor(c.handleAutoModeSwitchRequest)) c.client.SetRequestHandler("hooks.invoke", jsonrpc2.RequestHandlerFor(c.handleHooksInvoke)) c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform)) + c.client.SetRequestHandler("canvas.open", jsonrpc2.RequestHandlerFor(c.handleCanvasOpen)) + c.client.SetRequestHandler("canvas.close", jsonrpc2.RequestHandlerFor(c.handleCanvasClose)) + c.client.SetRequestHandler("canvas.action.invoke", jsonrpc2.RequestHandlerFor(c.handleCanvasActionInvoke)) rpc.RegisterClientSessionApiHandlers(c.client, func(sessionID string) *rpc.ClientSessionApiHandlers { c.sessionsMux.Lock() defer c.sessionsMux.Unlock() @@ -1899,119 +1914,88 @@ func (c *Client) handleSystemMessageTransform(req systemMessageTransformRequest) return resp, nil } -// ======================================================================== -// Protocol v2 backward-compatibility adapters -// ======================================================================== - -// toolCallRequestV2 is the v2 RPC request payload for tool.call. -type toolCallRequestV2 struct { - SessionID string `json:"sessionId"` - ToolCallID string `json:"toolCallId"` - ToolName string `json:"toolName"` - Arguments any `json:"arguments"` - Traceparent string `json:"traceparent,omitempty"` - Tracestate string `json:"tracestate,omitempty"` -} - -// toolCallResponseV2 is the v2 RPC response payload for tool.call. -type toolCallResponseV2 struct { - Result ToolResult `json:"result"` -} - -// permissionRequestV2 is the v2 RPC request payload for permission.request. -type permissionRequestV2 struct { - SessionID string `json:"sessionId"` - Request PermissionRequest `json:"permissionRequest"` -} - -// permissionResponseV2 is the v2 RPC response payload for permission.request. -type permissionResponseV2 struct { - Result PermissionRequestResult `json:"result"` -} - -// handleToolCallRequestV2 handles a v2-style tool.call RPC request from the server. -func (c *Client) handleToolCallRequestV2(req toolCallRequestV2) (*toolCallResponseV2, *jsonrpc2.Error) { - if req.SessionID == "" || req.ToolCallID == "" || req.ToolName == "" { - return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid tool call payload"} +// canvasJSONRPCError converts a CanvasError into the structured JSON-RPC error +// envelope used by all canvas.* dispatch responses. +func canvasJSONRPCError(cerr *CanvasError) *jsonrpc2.Error { + data, _ := json.Marshal(map[string]string{ + "code": cerr.Code, + "message": cerr.Message, + }) + return &jsonrpc2.Error{ + Code: -32603, + Message: cerr.Message, + Data: data, } +} +// resolveCanvasSession looks up a session and its installed CanvasHandler, +// returning the canvas_handler_unset error envelope if either is missing. +func (c *Client) resolveCanvasSession(sessionID string) (*Session, CanvasHandler, *jsonrpc2.Error) { c.sessionsMux.Lock() - session, ok := c.sessions[req.SessionID] + session, ok := c.sessions[sessionID] c.sessionsMux.Unlock() if !ok { - return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)} + return nil, nil, canvasJSONRPCError(NewCanvasError( + "canvas_handler_unset", + fmt.Sprintf("unknown session %s", sessionID), + )) } - - handler, ok := session.getToolHandler(req.ToolName) - if !ok { - return &toolCallResponseV2{Result: ToolResult{ - TextResultForLLM: fmt.Sprintf("Tool '%s' is not supported by this client instance.", req.ToolName), - ResultType: "failure", - Error: fmt.Sprintf("tool '%s' not supported", req.ToolName), - ToolTelemetry: map[string]any{}, - }}, nil + handler := session.getCanvasHandler() + if handler == nil { + return session, nil, canvasJSONRPCError(NewCanvasError( + "canvas_handler_unset", + "No CanvasHandler installed on this session; install one via SessionConfig.CanvasHandler before creating the session.", + )) } + return session, handler, nil +} - ctx := contextWithTraceParent(context.Background(), req.Traceparent, req.Tracestate) - - invocation := ToolInvocation{ - SessionID: req.SessionID, - ToolCallID: req.ToolCallID, - ToolName: req.ToolName, - Arguments: req.Arguments, - TraceContext: ctx, +// canvasResultError normalizes any error returned from a CanvasHandler method +// into the structured JSON-RPC error envelope. +func canvasResultError(err error) *jsonrpc2.Error { + if err == nil { + return nil } - - result, err := handler(invocation) - if err != nil { - return &toolCallResponseV2{Result: ToolResult{ - TextResultForLLM: "Invoking this tool produced an error. Detailed information is not available.", - ResultType: "failure", - Error: err.Error(), - ToolTelemetry: map[string]any{}, - }}, nil + if cerr, ok := err.(*CanvasError); ok { + return canvasJSONRPCError(cerr) } - - return &toolCallResponseV2{Result: result}, nil + return canvasJSONRPCError(NewCanvasError("canvas_handler_error", err.Error())) } -// handlePermissionRequestV2 handles a v2-style permission.request RPC request from the server. -func (c *Client) handlePermissionRequestV2(req permissionRequestV2) (*permissionResponseV2, *jsonrpc2.Error) { - if req.SessionID == "" { - return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid permission request payload"} +// handleCanvasOpen dispatches an inbound canvas.open request to the session's CanvasHandler. +func (c *Client) handleCanvasOpen(params canvasProviderRequestParams) (CanvasOpenResponse, *jsonrpc2.Error) { + _, handler, rpcErr := c.resolveCanvasSession(params.SessionID) + if rpcErr != nil { + return CanvasOpenResponse{}, rpcErr } - - c.sessionsMux.Lock() - session, ok := c.sessions[req.SessionID] - c.sessionsMux.Unlock() - if !ok { - return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)} + resp, err := handler.OnOpen(context.Background(), params.toOpenContext()) + if err != nil { + return CanvasOpenResponse{}, canvasResultError(err) } + return resp, nil +} - handler := session.getPermissionHandler() - if handler == nil { - return &permissionResponseV2{ - Result: PermissionRequestResult{ - Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser, - }, - }, nil +// handleCanvasClose dispatches an inbound canvas.close request to the session's CanvasHandler. +func (c *Client) handleCanvasClose(params canvasProviderRequestParams) (any, *jsonrpc2.Error) { + _, handler, rpcErr := c.resolveCanvasSession(params.SessionID) + if rpcErr != nil { + return nil, rpcErr } - - invocation := PermissionInvocation{ - SessionID: session.SessionID, + if err := handler.OnClose(context.Background(), params.toLifecycleContext()); err != nil { + return nil, canvasResultError(err) } + return nil, nil +} - result, err := handler(req.Request, invocation) - if err != nil { - return &permissionResponseV2{ - Result: PermissionRequestResult{ - Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser, - }, - }, nil +// handleCanvasActionInvoke dispatches an inbound canvas.action.invoke request to the session's CanvasHandler. +func (c *Client) handleCanvasActionInvoke(params canvasInvokeParams) (any, *jsonrpc2.Error) { + _, handler, rpcErr := c.resolveCanvasSession(params.SessionID) + if rpcErr != nil { + return nil, rpcErr } - if result.Kind == "no-result" { - return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionV2Error} + result, err := handler.OnAction(context.Background(), params.toActionContext()) + if err != nil { + return nil, canvasResultError(err) } - - return &permissionResponseV2{Result: result}, nil + return result, nil } diff --git a/go/internal/e2e/mcp_and_agents_e2e_test.go b/go/internal/e2e/mcp_and_agents_e2e_test.go index 87b25a533..4c7f29bc8 100644 --- a/go/internal/e2e/mcp_and_agents_e2e_test.go +++ b/go/internal/e2e/mcp_and_agents_e2e_test.go @@ -7,6 +7,7 @@ import ( copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) func TestMCPServersE2E(t *testing.T) { @@ -17,13 +18,7 @@ func TestMCPServersE2E(t *testing.T) { t.Run("accept MCP server config on create", func(t *testing.T) { ctx.ConfigureForTest(t) - mcpServers := map[string]copilot.MCPServerConfig{ - "test-server": copilot.MCPStdioServerConfig{ - Command: "echo", - Args: []string{"hello"}, - Tools: &[]string{"*"}, - }, - } + mcpServers := testMCPServers(t, "test-server") session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, @@ -36,6 +31,7 @@ func TestMCPServersE2E(t *testing.T) { if session.SessionID == "" { t.Error("Expected non-empty session ID") } + waitForMCPServerStatus(t, session, "test-server", rpc.McpServerStatusConnected) // Simple interaction to verify session works _, err = session.Send(t.Context(), copilot.MessageOptions{ @@ -62,7 +58,7 @@ func TestMCPServersE2E(t *testing.T) { mcpServers := map[string]copilot.MCPServerConfig{ "test-server": copilot.MCPStdioServerConfig{ - Command: "echo", + Command: "git", Tools: &[]string{"*"}, }, } @@ -79,22 +75,6 @@ func TestMCPServersE2E(t *testing.T) { t.Error("Expected non-empty session ID") } - _, err = session.Send(t.Context(), copilot.MessageOptions{ - Prompt: "What is 2+2?", - }) - if err != nil { - t.Fatalf("Failed to send message: %v", err) - } - - message, err := testharness.GetFinalAssistantMessage(t.Context(), session) - if err != nil { - t.Fatalf("Failed to get final message: %v", err) - } - - if md, ok := message.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(md.Content, "4") { - t.Errorf("Expected message to contain '4', got: %v", message.Data) - } - session.Disconnect() }) @@ -114,13 +94,7 @@ func TestMCPServersE2E(t *testing.T) { } // Resume with MCP servers - mcpServers := map[string]copilot.MCPServerConfig{ - "test-server": copilot.MCPStdioServerConfig{ - Command: "echo", - Args: []string{"hello"}, - Tools: &[]string{"*"}, - }, - } + mcpServers := testMCPServers(t, "test-server") session2, err := client.ResumeSessionWithOptions(t.Context(), sessionID, &copilot.ResumeSessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, @@ -133,15 +107,7 @@ func TestMCPServersE2E(t *testing.T) { if session2.SessionID != sessionID { t.Errorf("Expected session ID %s, got %s", sessionID, session2.SessionID) } - - message, err := session2.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "What is 3+3?"}) - if err != nil { - t.Fatalf("Failed to send message: %v", err) - } - - if md, ok := message.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(md.Content, "6") { - t.Errorf("Expected message to contain '6', got: %v", message.Data) - } + waitForMCPServerStatus(t, session2, "test-server", rpc.McpServerStatusConnected) session2.Disconnect() }) @@ -176,6 +142,7 @@ func TestMCPServersE2E(t *testing.T) { if session.SessionID == "" { t.Error("Expected non-empty session ID") } + waitForMCPServerStatus(t, session, "env-echo", rpc.McpServerStatusConnected) message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ Prompt: "Use the env-echo/get_env tool to read the TEST_SECRET environment variable. Reply with just the value, nothing else.", @@ -194,18 +161,7 @@ func TestMCPServersE2E(t *testing.T) { t.Run("handle multiple MCP servers", func(t *testing.T) { ctx.ConfigureForTest(t) - mcpServers := map[string]copilot.MCPServerConfig{ - "server1": copilot.MCPStdioServerConfig{ - Command: "echo", - Args: []string{"server1"}, - Tools: &[]string{"*"}, - }, - "server2": copilot.MCPStdioServerConfig{ - Command: "echo", - Args: []string{"server2"}, - Tools: &[]string{"*"}, - }, - } + mcpServers := testMCPServers(t, "server1", "server2") session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, @@ -218,6 +174,8 @@ func TestMCPServersE2E(t *testing.T) { if session.SessionID == "" { t.Error("Expected non-empty session ID") } + waitForMCPServerStatus(t, session, "server1", rpc.McpServerStatusConnected) + waitForMCPServerStatus(t, session, "server2", rpc.McpServerStatusConnected) session.Disconnect() }) @@ -362,13 +320,7 @@ func TestCustomAgentsE2E(t *testing.T) { DisplayName: "MCP Agent", Description: "An agent with its own MCP servers", Prompt: "You are an agent with MCP servers.", - MCPServers: map[string]copilot.MCPServerConfig{ - "agent-server": copilot.MCPStdioServerConfig{ - Command: "echo", - Args: []string{"agent-mcp"}, - Tools: &[]string{"*"}, - }, - }, + MCPServers: testMCPServers(t, "agent-server"), }, } @@ -433,13 +385,7 @@ func TestCombinedConfigurationE2E(t *testing.T) { t.Run("accept MCP servers and custom agents", func(t *testing.T) { ctx.ConfigureForTest(t) - mcpServers := map[string]copilot.MCPServerConfig{ - "shared-server": copilot.MCPStdioServerConfig{ - Command: "echo", - Args: []string{"shared"}, - Tools: &[]string{"*"}, - }, - } + mcpServers := testMCPServers(t, "shared-server") customAgents := []copilot.CustomAgentConfig{ { @@ -462,6 +408,7 @@ func TestCombinedConfigurationE2E(t *testing.T) { if session.SessionID == "" { t.Error("Expected non-empty session ID") } + waitForMCPServerStatus(t, session, "shared-server", rpc.McpServerStatusConnected) session.Disconnect() }) diff --git a/go/internal/e2e/mcp_server_helpers_test.go b/go/internal/e2e/mcp_server_helpers_test.go new file mode 100644 index 000000000..f6cf2ad6b --- /dev/null +++ b/go/internal/e2e/mcp_server_helpers_test.go @@ -0,0 +1,59 @@ +package e2e + +import ( + "path/filepath" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" +) + +func testMCPServers(t *testing.T, serverNames ...string) map[string]copilot.MCPServerConfig { + t.Helper() + + mcpServerPath, err := filepath.Abs("../../../test/harness/test-mcp-server.mjs") + if err != nil { + t.Fatalf("Failed to resolve test-mcp-server path: %v", err) + } + + mcpServerDir := filepath.Dir(mcpServerPath) + mcpServers := make(map[string]copilot.MCPServerConfig, len(serverNames)) + for _, serverName := range serverNames { + mcpServers[serverName] = copilot.MCPStdioServerConfig{ + Command: "node", + Args: []string{mcpServerPath}, + Tools: &[]string{"*"}, + WorkingDirectory: mcpServerDir, + } + } + return mcpServers +} + +func waitForMCPServerStatus(t *testing.T, session *copilot.Session, serverName string, expectedStatus rpc.McpServerStatus) { + t.Helper() + + var lastStatus string + deadline := time.Now().Add(60 * time.Second) + for time.Now().Before(deadline) { + result, err := session.RPC.Mcp.List(t.Context()) + if err != nil { + lastStatus = err.Error() + } else { + lastStatus = "" + for _, server := range result.Servers { + if server.Name != serverName { + continue + } + if server.Status == expectedStatus { + return + } + lastStatus = string(server.Status) + break + } + } + time.Sleep(200 * time.Millisecond) + } + + t.Fatalf("%s did not reach %s; last status was %s", serverName, expectedStatus, lastStatus) +} diff --git a/go/internal/e2e/multi_client_e2e_test.go b/go/internal/e2e/multi_client_e2e_test.go index 9dd8a15bf..a5c852bc8 100644 --- a/go/internal/e2e/multi_client_e2e_test.go +++ b/go/internal/e2e/multi_client_e2e_test.go @@ -11,6 +11,7 @@ import ( copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) func TestMultiClientE2E(t *testing.T) { @@ -139,11 +140,11 @@ func TestMultiClientE2E(t *testing.T) { // Client 1 creates a session and manually approves permission requests session1, err := client1.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { mu.Lock() client1PermissionRequests = append(client1PermissionRequests, request) mu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -152,8 +153,8 @@ func TestMultiClientE2E(t *testing.T) { // Client 2 observes the permission request but leaves the decision to client 1. session2, err := client2.ResumeSession(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionNoResult{}, nil }, }) if err != nil { @@ -239,8 +240,8 @@ func TestMultiClientE2E(t *testing.T) { // Client 1 creates a session and denies all permission requests session1, err := client1.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionReject{}, nil }, }) if err != nil { @@ -249,8 +250,8 @@ func TestMultiClientE2E(t *testing.T) { // Client 2 observes the permission request but leaves the decision to client 1. session2, err := client2.ResumeSession(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionNoResult{}, nil }, }) if err != nil { diff --git a/go/internal/e2e/pending_work_resume_e2e_test.go b/go/internal/e2e/pending_work_resume_e2e_test.go index 41ca83021..552886413 100644 --- a/go/internal/e2e/pending_work_resume_e2e_test.go +++ b/go/internal/e2e/pending_work_resume_e2e_test.go @@ -40,14 +40,14 @@ func TestPendingWorkResumeE2E(t *testing.T) { }) permissionRequested := make(chan copilot.PermissionRequest, 1) - releasePermission := make(chan copilot.PermissionRequestResult, 1) + releasePermission := make(chan rpc.PermissionDecision, 1) suspendedClient := ctx.NewClient(func(opts *copilot.ClientOptions) { opts.Connection = copilot.UriConnection{URL: cliURL, ConnectionToken: sharedTcpToken} }) session1, err := suspendedClient.CreateSession(t.Context(), &copilot.SessionConfig{ Tools: []copilot.Tool{originalTool}, - OnPermissionRequest: func(req copilot.PermissionRequest, _ copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, _ copilot.PermissionInvocation) (rpc.PermissionDecision, error) { select { case permissionRequested <- req: default: @@ -114,8 +114,8 @@ func TestPendingWorkResumeE2E(t *testing.T) { session2, err := resumedClient.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ ContinuePendingWork: true, - OnPermissionRequest: func(_ copilot.PermissionRequest, _ copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + OnPermissionRequest: func(_ copilot.PermissionRequest, _ copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionNoResult{}, nil }, Tools: []copilot.Tool{resumedTool}, }) @@ -154,7 +154,7 @@ func TestPendingWorkResumeE2E(t *testing.T) { // Allow original handler to unblock so cleanup proceeds. select { - case releasePermission <- copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindUserNotAvailable}: + case releasePermission <- &rpc.PermissionDecisionUserNotAvailable{}: default: } diff --git a/go/internal/e2e/permissions_e2e_test.go b/go/internal/e2e/permissions_e2e_test.go index bcc6fe278..5cbbfba1b 100644 --- a/go/internal/e2e/permissions_e2e_test.go +++ b/go/internal/e2e/permissions_e2e_test.go @@ -25,7 +25,7 @@ func TestPermissionsE2E(t *testing.T) { var permissionRequests []copilot.PermissionRequest var mu sync.Mutex - onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { mu.Lock() permissionRequests = append(permissionRequests, request) mu.Unlock() @@ -34,7 +34,7 @@ func TestPermissionsE2E(t *testing.T) { t.Error("Expected non-empty session ID in invocation") } - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ @@ -80,12 +80,12 @@ func TestPermissionsE2E(t *testing.T) { var permissionRequests []copilot.PermissionRequest var mu sync.Mutex - onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { mu.Lock() permissionRequests = append(permissionRequests, request) mu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ @@ -119,8 +119,8 @@ func TestPermissionsE2E(t *testing.T) { t.Run("deny permission", func(t *testing.T) { ctx.ConfigureForTest(t) - onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil + onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionReject{}, nil } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ @@ -190,8 +190,8 @@ func TestPermissionsE2E(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindUserNotAvailable}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionUserNotAvailable{}, nil }, }) if err != nil { @@ -240,8 +240,8 @@ func TestPermissionsE2E(t *testing.T) { } session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindUserNotAvailable}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionUserNotAvailable{}, nil }, }) if err != nil { @@ -309,9 +309,9 @@ func TestPermissionsE2E(t *testing.T) { var permissionRequestReceived atomicBool session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { permissionRequestReceived.Set(true) - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -348,9 +348,9 @@ func TestPermissionsE2E(t *testing.T) { var permissionRequestReceived atomicBool session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { permissionRequestReceived.Set(true) - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -372,8 +372,8 @@ func TestPermissionsE2E(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{}, fmt.Errorf("handler error") + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return nil, fmt.Errorf("handler error") }, }) if err != nil { @@ -409,11 +409,11 @@ func TestPermissionsE2E(t *testing.T) { var receivedToolCallID atomicBool session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { if shellReq, ok := req.(*copilot.PermissionRequestShell); ok && shellReq.ToolCallID != nil && *shellReq.ToolCallID != "" { receivedToolCallID.Set(true) } - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -450,10 +450,10 @@ func TestPermissionsE2E(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { shellReq, ok := req.(*copilot.PermissionRequestShell) if !ok { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil } toolCallID := "" if shellReq.ToolCallID != nil { @@ -470,7 +470,7 @@ func TestPermissionsE2E(t *testing.T) { } <-releaseHandler addLifecycle("permission-complete", toolCallID) - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -607,7 +607,7 @@ func TestPermissionsE2E(t *testing.T) { }), }, AvailableTools: []string{"first_permission_tool", "second_permission_tool"}, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { permissionRequestsMu.Lock() permissionRequestCount++ permissionRequests = append(permissionRequests, req) @@ -620,7 +620,7 @@ func TestPermissionsE2E(t *testing.T) { case <-bothStarted: case <-time.After(30 * time.Second): } - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -725,12 +725,12 @@ func TestPermissionsE2E(t *testing.T) { permissionCalled := make(chan struct{}, 1) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { select { case permissionCalled <- struct{}{}: default: } - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + return &rpc.PermissionDecisionNoResult{}, nil }, }) if err != nil { @@ -761,11 +761,11 @@ func TestPermissionsE2E(t *testing.T) { var handlerCallCountMu sync.Mutex session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { handlerCallCountMu.Lock() handlerCallCount++ handlerCallCountMu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { diff --git a/go/internal/e2e/rpc_mcp_and_skills_e2e_test.go b/go/internal/e2e/rpc_mcp_and_skills_e2e_test.go index c636201da..6063dd162 100644 --- a/go/internal/e2e/rpc_mcp_and_skills_e2e_test.go +++ b/go/internal/e2e/rpc_mcp_and_skills_e2e_test.go @@ -108,18 +108,13 @@ func TestRpcMcpAndSkillsE2E(t *testing.T) { const serverName = "rpc-list-mcp-server" session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - MCPServers: map[string]copilot.MCPServerConfig{ - serverName: copilot.MCPStdioServerConfig{ - Command: "echo", - Args: []string{"rpc-list-mcp-server"}, - Tools: &[]string{"*"}, - }, - }, + MCPServers: testMCPServers(t, serverName), }) if err != nil { t.Fatalf("CreateSession failed: %v", err) } + waitForMCPServerStatus(t, session, serverName, rpc.McpServerStatusConnected) result, err := session.RPC.Mcp.List(t.Context()) if err != nil { t.Fatalf("Mcp.List failed: %v", err) diff --git a/go/internal/e2e/session_config_e2e_test.go b/go/internal/e2e/session_config_e2e_test.go index d932ae31b..e5daf931b 100644 --- a/go/internal/e2e/session_config_e2e_test.go +++ b/go/internal/e2e/session_config_e2e_test.go @@ -635,15 +635,12 @@ func TestSessionConfigExtrasE2E(t *testing.T) { } t.Cleanup(func() { _ = session2.Disconnect() }) - _, err = session2.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"}) + _, err = session2.Send(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"}) if err != nil { - t.Fatalf("SendAndWait failed: %v", err) + t.Fatalf("Send failed: %v", err) } - exchanges, err := ctx.GetExchanges() - if err != nil { - t.Fatalf("GetExchanges failed: %v", err) - } + exchanges := ctx.WaitForExchanges(t, 1) if len(exchanges) != 1 { t.Fatalf("Expected exactly 1 exchange, got %d", len(exchanges)) } diff --git a/go/internal/e2e/session_e2e_test.go b/go/internal/e2e/session_e2e_test.go index f45a74616..fb5ecbd6e 100644 --- a/go/internal/e2e/session_e2e_test.go +++ b/go/internal/e2e/session_e2e_test.go @@ -245,26 +245,15 @@ func TestSessionE2E(t *testing.T) { if err != nil { t.Fatalf("Failed to create session: %v", err) } + t.Cleanup(func() { _ = session.Disconnect() }) _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"}) if err != nil { t.Fatalf("Failed to send message: %v", err) } - _, err = testharness.GetFinalAssistantMessage(t.Context(), session) - if err != nil { - t.Fatalf("Failed to get assistant message: %v", err) - } - // Validate that only the specified tools are present - traffic, err := ctx.GetExchanges() - if err != nil { - t.Fatalf("Failed to get exchanges: %v", err) - } - if len(traffic) == 0 { - t.Fatal("Expected at least one exchange") - } - + traffic := ctx.WaitForExchanges(t, 1) toolNames := getToolNames(traffic[0]) if len(toolNames) != 2 { t.Errorf("Expected exactly 2 tools, got %d: %v", len(toolNames), toolNames) @@ -284,26 +273,15 @@ func TestSessionE2E(t *testing.T) { if err != nil { t.Fatalf("Failed to create session: %v", err) } + t.Cleanup(func() { _ = session.Disconnect() }) _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"}) if err != nil { t.Fatalf("Failed to send message: %v", err) } - _, err = testharness.GetFinalAssistantMessage(t.Context(), session) - if err != nil { - t.Fatalf("Failed to get assistant message: %v", err) - } - // Validate that excluded tool is not present but others are - traffic, err := ctx.GetExchanges() - if err != nil { - t.Fatalf("Failed to get exchanges: %v", err) - } - if len(traffic) == 0 { - t.Fatal("Expected at least one exchange") - } - + traffic := ctx.WaitForExchanges(t, 1) toolNames := getToolNames(traffic[0]) if contains(toolNames, "view") { t.Errorf("Expected 'view' to be excluded, got %v", toolNames) @@ -338,26 +316,15 @@ func TestSessionE2E(t *testing.T) { if err != nil { t.Fatalf("Failed to create session: %v", err) } + t.Cleanup(func() { _ = session.Disconnect() }) _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"}) if err != nil { t.Fatalf("Failed to send message: %v", err) } - _, err = testharness.GetFinalAssistantMessage(t.Context(), session) - if err != nil { - t.Fatalf("Failed to get assistant message: %v", err) - } - // The real assertion: verify the runtime excluded the tool from the CAPI request - traffic, err := ctx.GetExchanges() - if err != nil { - t.Fatalf("Failed to get exchanges: %v", err) - } - if len(traffic) == 0 { - t.Fatal("Expected at least one exchange") - } - + traffic := ctx.WaitForExchanges(t, 1) toolNames := getToolNames(traffic[0]) if contains(toolNames, "secret_tool") { t.Errorf("Expected 'secret_tool' to be excluded from default agent, got %v", toolNames) diff --git a/go/internal/e2e/suspend_e2e_test.go b/go/internal/e2e/suspend_e2e_test.go index 8ce0c1fb1..672481a4f 100644 --- a/go/internal/e2e/suspend_e2e_test.go +++ b/go/internal/e2e/suspend_e2e_test.go @@ -9,6 +9,7 @@ import ( copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) const suspendTimeout = 60 * time.Second @@ -108,7 +109,7 @@ func TestSuspendE2E(t *testing.T) { } permissionRequested := make(chan copilot.PermissionRequest, 1) - releasePermission := make(chan copilot.PermissionRequestResult, 1) + releasePermission := make(chan rpc.PermissionDecision, 1) var toolInvoked atomic.Bool tool := copilot.DefineTool("suspend_cancel_permission_tool", "Transforms a value (should not run when suspend cancels permission)", @@ -119,7 +120,7 @@ func TestSuspendE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ Tools: []copilot.Tool{tool}, - OnPermissionRequest: func(request copilot.PermissionRequest, _ copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(request copilot.PermissionRequest, _ copilot.PermissionInvocation) (rpc.PermissionDecision, error) { select { case permissionRequested <- request: default: @@ -132,7 +133,7 @@ func TestSuspendE2E(t *testing.T) { } defer func() { select { - case releasePermission <- copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindUserNotAvailable}: + case releasePermission <- &rpc.PermissionDecisionUserNotAvailable{}: default: } }() diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index c1331fbe9..cae966667 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -8,6 +8,7 @@ import ( "strings" "sync" "testing" + "time" copilot "github.com/github/copilot-sdk/go" ) @@ -169,6 +170,30 @@ func (c *TestContext) GetExchanges() ([]ParsedHttpExchange, error) { return c.proxy.GetExchanges() } +// WaitForExchanges waits until the proxy has captured at least the requested exchanges. +func (c *TestContext) WaitForExchanges(t *testing.T, minimumCount int) []ParsedHttpExchange { + t.Helper() + + deadline := time.Now().Add(120 * time.Second) + var lastErr error + var exchanges []ParsedHttpExchange + for time.Now().Before(deadline) { + var err error + exchanges, err = c.GetExchanges() + if err == nil && len(exchanges) >= minimumCount { + return exchanges + } + lastErr = err + time.Sleep(100 * time.Millisecond) + } + + if lastErr != nil { + t.Fatalf("Timed out waiting for %d chat completion request(s): %v", minimumCount, lastErr) + } + t.Fatalf("Timed out waiting for %d chat completion request(s); captured %d", minimumCount, len(exchanges)) + return nil +} + // SetCopilotUserByToken registers a per-token user configuration on the proxy. func (c *TestContext) SetCopilotUserByToken(token string, response map[string]interface{}) error { return c.proxy.SetCopilotUserByToken(token, response) diff --git a/go/internal/e2e/tools_e2e_test.go b/go/internal/e2e/tools_e2e_test.go index 014ced56f..621f7758d 100644 --- a/go/internal/e2e/tools_e2e_test.go +++ b/go/internal/e2e/tools_e2e_test.go @@ -10,6 +10,7 @@ import ( copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) func TestToolsE2E(t *testing.T) { @@ -284,9 +285,9 @@ func TestToolsE2E(t *testing.T) { didRunPermissionRequest := false session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { didRunPermissionRequest = true - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + return &rpc.PermissionDecisionNoResult{}, nil }, Tools: []copilot.Tool{ safeLookupTool, @@ -496,11 +497,11 @@ func TestToolsE2E(t *testing.T) { return strings.ToUpper(params.Input), nil }), }, - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { mu.Lock() permissionRequests = append(permissionRequests, request) mu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -555,8 +556,8 @@ func TestToolsE2E(t *testing.T) { return strings.ToUpper(params.Input), nil }), }, - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionReject{}, nil }, }) if err != nil { diff --git a/go/permissions.go b/go/permissions.go index fb28851e3..f86a72683 100644 --- a/go/permissions.go +++ b/go/permissions.go @@ -1,11 +1,15 @@ package copilot +import ( + "github.com/github/copilot-sdk/go/rpc" +) + // PermissionHandler provides pre-built OnPermissionRequest implementations. var PermissionHandler = struct { // ApproveAll approves all permission requests. ApproveAll PermissionHandlerFunc }{ - ApproveAll: func(_ PermissionRequest, _ PermissionInvocation) (PermissionRequestResult, error) { - return PermissionRequestResult{Kind: PermissionRequestResultKindApproved}, nil + ApproveAll: func(_ PermissionRequest, _ PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, } diff --git a/go/rpc/permission_decision_no_result.go b/go/rpc/permission_decision_no_result.go new file mode 100644 index 000000000..3337120bc --- /dev/null +++ b/go/rpc/permission_decision_no_result.go @@ -0,0 +1,26 @@ +// Copyright (c) GitHub. All rights reserved. + +package rpc + +import "encoding/json" + +// PermissionDecisionNoResult is an SDK-only [PermissionDecision] value +// returned by a permission handler when it declines to respond to a +// request, allowing another connected client to answer instead. The SDK +// suppresses the response on the wire when it sees this variant. +type PermissionDecisionNoResult struct{} + +func (PermissionDecisionNoResult) permissionDecision() {} +func (PermissionDecisionNoResult) Kind() PermissionDecisionKind { + return PermissionDecisionKind("no-result") +} + +// MarshalJSON emits {"kind":"no-result"} for serialization symmetry with +// the other PermissionDecision variants. The SDK normally suppresses this +// value before it reaches the wire, but a stable representation is useful +// for tests and logging. +func (PermissionDecisionNoResult) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Kind string `json:"kind"` + }{Kind: "no-result"}) +} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index d2a332e49..fdd71f24b 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -81,6 +81,7 @@ type AgentInfo struct { ID string `json:"id"` // MCP server configurations attached to this agent, keyed by server name. Server config // shape mirrors the MCP `mcpServers` schema. + // Experimental: McpServers is part of an experimental API and may change or be removed. McpServers map[string]any `json:"mcpServers,omitempty"` // Preferred model id for this agent. When omitted, inherits the outer agent's model. Model *string `json:"model,omitempty"` @@ -284,6 +285,81 @@ func (UserAuthInfo) Type() AuthInfoType { return AuthInfoTypeUser } +// Canvas action that the agent or host can invoke. To discover the input schema for a +// particular action, call the list_canvas_capabilities tool. +// Experimental: CanvasAction is part of an experimental API and may change or be removed. +type CanvasAction struct { + // Description of the action + Description *string `json:"description,omitempty"` + // JSON Schema for the action input + InputSchema any `json:"inputSchema,omitempty"` + // Action name exposed by the canvas provider + Name string `json:"name"` +} + +// Canvas close parameters. +// Experimental: CanvasCloseRequest is part of an experimental API and may change or be +// removed. +type CanvasCloseRequest struct { + // Open canvas instance identifier + InstanceID string `json:"instanceId"` +} + +// Canvas action invocation parameters. +// Experimental: CanvasInvokeActionRequest is part of an experimental API and may change or +// be removed. +type CanvasInvokeActionRequest struct { + // Action name to invoke + ActionName string `json:"actionName"` + // Action input + Input any `json:"input,omitempty"` + // Open canvas instance identifier + InstanceID string `json:"instanceId"` +} + +// Canvas action invocation result. +// Experimental: CanvasInvokeActionResult is part of an experimental API and may change or +// be removed. +type CanvasInvokeActionResult struct { + // Provider-supplied action result + Result any `json:"result,omitempty"` +} + +// JSON Schema for canvas open input +// Experimental: CanvasJSONSchema is part of an experimental API and may change or be +// removed. +type CanvasJSONSchema any + +// Declared canvases available in this session. +// Experimental: CanvasList is part of an experimental API and may change or be removed. +type CanvasList struct { + // Declared canvases available in this session + Canvases []DiscoveredCanvas `json:"canvases"` +} + +// Live open-canvas snapshot. +// Experimental: CanvasListOpenResult is part of an experimental API and may change or be +// removed. +type CanvasListOpenResult struct { + // Currently open canvas instances + OpenCanvases []OpenCanvasInstance `json:"openCanvases"` +} + +// Canvas open parameters. +// Experimental: CanvasOpenRequest is part of an experimental API and may change or be +// removed. +type CanvasOpenRequest struct { + // Provider-local canvas identifier + CanvasID string `json:"canvasId"` + // Owning provider identifier. Optional when the canvasId is unique across providers; + // required to disambiguate when multiple providers register the same canvasId. + ExtensionID *string `json:"extensionId,omitempty"` + // Canvas open input + Input any `json:"input,omitempty"` + // Caller-supplied stable instance identifier + InstanceID string `json:"instanceId"` +} + // Slash commands available in the session, after applying any include/exclude filters. // Experimental: CommandList is part of an experimental API and may change or be removed. type CommandList struct { @@ -542,6 +618,26 @@ type CurrentModel struct { ReasoningEffort *string `json:"reasoningEffort,omitempty"` } +// Canvas available in the current session. +// Experimental: DiscoveredCanvas is part of an experimental API and may change or be +// removed. +type DiscoveredCanvas struct { + // Actions the agent or host may invoke on an open instance + Actions []CanvasAction `json:"actions,omitempty"` + // Provider-local canvas identifier + CanvasID string `json:"canvasId"` + // Short, single-sentence description shown to the agent in canvas catalogs. + Description string `json:"description"` + // Human-readable canvas name + DisplayName string `json:"displayName"` + // Owning provider identifier + ExtensionID string `json:"extensionId"` + // Owning extension display name, when available + ExtensionName *string `json:"extensionName,omitempty"` + // JSON Schema for canvas open input + InputSchema any `json:"inputSchema,omitempty"` +} + // Schema for the `DiscoveredMcpServer` type. type DiscoveredMcpServer struct { // Whether the server is enabled (not in the disabled list) @@ -550,7 +646,7 @@ type DiscoveredMcpServer struct { Name string `json:"name"` // Configuration source: user, workspace, plugin, or builtin Source McpServerSource `json:"source"` - // Server transport type: stdio, http, sse, or memory + // Server transport type: stdio, http, sse (deprecated), or memory Type *DiscoveredMcpServerType `json:"type,omitempty"` } @@ -746,6 +842,8 @@ type ExternalToolTextResultForLlmBinaryResultsForLlm struct { Data string `json:"data"` // Human-readable description of the binary data Description *string `json:"description,omitempty"` + // Optional metadata from the producing tool. + Metadata map[string]any `json:"metadata,omitempty"` // MIME type of the binary data MIMEType string `json:"mimeType"` // Binary result type discriminator. Use "image" for images and "resource" for other binary @@ -1221,6 +1319,176 @@ type LspInitializeRequest struct { WorkingDirectory *string `json:"workingDirectory,omitempty"` } +// MCP server, tool name, and arguments to invoke from an MCP App view. +// Experimental: McpAppsCallToolRequest is part of an experimental API and may change or be +// removed. +type McpAppsCallToolRequest struct { + // Tool arguments + Arguments map[string]any `json:"arguments,omitempty"` + // **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the + // app from this server only'), the call is rejected when this differs from `serverName`, + // and rejected outright when missing. + OriginServerName string `json:"originServerName"` + // MCP server hosting the tool + ServerName string `json:"serverName"` + // MCP tool name + ToolName string `json:"toolName"` +} + +// Capability negotiation snapshot +// Experimental: McpAppsDiagnoseCapability is part of an experimental API and may change or +// be removed. +type McpAppsDiagnoseCapability struct { + // Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers + Advertised bool `json:"advertised"` + // Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on + FeatureFlagEnabled bool `json:"featureFlagEnabled"` + // Whether the session has the `mcp-apps` capability + SessionHasMcpApps bool `json:"sessionHasMcpApps"` +} + +// MCP server to diagnose MCP Apps wiring for. +// Experimental: McpAppsDiagnoseRequest is part of an experimental API and may change or be +// removed. +type McpAppsDiagnoseRequest struct { + // MCP server to probe + ServerName string `json:"serverName"` +} + +// Diagnostic snapshot of MCP Apps wiring for the named server. +// Experimental: McpAppsDiagnoseResult is part of an experimental API and may change or be +// removed. +type McpAppsDiagnoseResult struct { + // Capability negotiation snapshot + Capability McpAppsDiagnoseCapability `json:"capability"` + // What the server returned for this session + Server McpAppsDiagnoseServer `json:"server"` +} + +// What the server returned for this session +// Experimental: McpAppsDiagnoseServer is part of an experimental API and may change or be +// removed. +type McpAppsDiagnoseServer struct { + // Whether the named server is currently connected + Connected bool `json:"connected"` + // Up to 5 tool names with `_meta.ui` for quick inspection + SampleToolNames []string `json:"sampleToolNames"` + // Total tools returned by the server's tools/list + ToolCount float64 `json:"toolCount"` + // Tools whose `_meta.ui` is populated (resourceUri and/or visibility set) + ToolsWithUIMeta float64 `json:"toolsWithUiMeta"` +} + +// Current host context advertised to MCP App guests. +// Experimental: McpAppsHostContext is part of an experimental API and may change or be +// removed. +type McpAppsHostContext struct { + // Current host context + Context McpAppsHostContextDetails `json:"context"` +} + +// Current host context +// Experimental: McpAppsHostContextDetails is part of an experimental API and may change or +// be removed. +type McpAppsHostContextDetails struct { + // Display modes the host supports + AvailableDisplayModes []McpAppsHostContextDetailsAvailableDisplayMode `json:"availableDisplayModes,omitempty"` + // Current display mode (SEP-1865) + DisplayMode *McpAppsHostContextDetailsDisplayMode `json:"displayMode,omitempty"` + // BCP-47 locale, e.g. 'en-US' + Locale *string `json:"locale,omitempty"` + // Platform type for responsive design + Platform *McpAppsHostContextDetailsPlatform `json:"platform,omitempty"` + // UI theme preference per SEP-1865 + Theme *McpAppsHostContextDetailsTheme `json:"theme,omitempty"` + // IANA timezone, e.g. 'America/New_York' + TimeZone *string `json:"timeZone,omitempty"` + // Host application identifier + UserAgent *string `json:"userAgent,omitempty"` +} + +// MCP server to list app-callable tools for. +// Experimental: McpAppsListToolsRequest is part of an experimental API and may change or be +// removed. +type McpAppsListToolsRequest struct { + // **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the + // app from this server only'), the call is rejected when this differs from `serverName`, + // and rejected outright when missing. + OriginServerName string `json:"originServerName"` + // MCP server hosting the app + ServerName string `json:"serverName"` +} + +// App-callable tools from the named MCP server. +// Experimental: McpAppsListToolsResult is part of an experimental API and may change or be +// removed. +type McpAppsListToolsResult struct { + // App-callable tools from the server + Tools []map[string]any `json:"tools"` +} + +// MCP server and resource URI to fetch. +// Experimental: McpAppsReadResourceRequest is part of an experimental API and may change or +// be removed. +type McpAppsReadResourceRequest struct { + // Name of the MCP server hosting the resource + ServerName string `json:"serverName"` + // Resource URI (typically ui://...) + URI string `json:"uri"` +} + +// Resource contents returned by the MCP server. +// Experimental: McpAppsReadResourceResult is part of an experimental API and may change or +// be removed. +type McpAppsReadResourceResult struct { + // Resource contents returned by the server + Contents []McpAppsResourceContent `json:"contents"` +} + +// Schema for the `McpAppsResourceContent` type. +// Experimental: McpAppsResourceContent is part of an experimental API and may change or be +// removed. +type McpAppsResourceContent struct { + // Base64-encoded binary content + Blob *string `json:"blob,omitempty"` + // Resource-level metadata (CSP, permissions, etc.) + Meta map[string]any `json:"_meta,omitempty"` + // MIME type of the content + MIMEType *string `json:"mimeType,omitempty"` + // Text content (e.g. HTML) + Text *string `json:"text,omitempty"` + // The resource URI (typically ui://...) + URI string `json:"uri"` +} + +// Host context advertised to MCP App guests +// Experimental: McpAppsSetHostContextDetails is part of an experimental API and may change +// or be removed. +type McpAppsSetHostContextDetails struct { + // Display modes the host supports + AvailableDisplayModes []McpAppsSetHostContextDetailsAvailableDisplayMode `json:"availableDisplayModes,omitempty"` + // Current display mode (SEP-1865) + DisplayMode *McpAppsSetHostContextDetailsDisplayMode `json:"displayMode,omitempty"` + // BCP-47 locale, e.g. 'en-US' + Locale *string `json:"locale,omitempty"` + // Platform type for responsive design + Platform *McpAppsSetHostContextDetailsPlatform `json:"platform,omitempty"` + // UI theme preference per SEP-1865 + Theme *McpAppsSetHostContextDetailsTheme `json:"theme,omitempty"` + // IANA timezone, e.g. 'America/New_York' + TimeZone *string `json:"timeZone,omitempty"` + // Host application identifier + UserAgent *string `json:"userAgent,omitempty"` +} + +// Host context to advertise to MCP App guests. +// Experimental: McpAppsSetHostContextRequest is part of an experimental API and may change +// or be removed. +type McpAppsSetHostContextRequest struct { + // Host context advertised to MCP App guests + Context McpAppsSetHostContextDetails `json:"context"` +} + // The requestId previously passed to executeSampling that should be cancelled. // Experimental: McpCancelSamplingExecutionParams is part of an experimental API and may // change or be removed. @@ -1696,15 +1964,28 @@ type ModelBilling struct { type ModelBillingTokenPrices struct { // Number of tokens per standard billing batch BatchSize *int64 `json:"batchSize,omitempty"` - // Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 - // AIU = $0.01 USD) - CachePrice *int64 `json:"cachePrice,omitempty"` - // Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU - // = $0.01 USD) - InputPrice *int64 `json:"inputPrice,omitempty"` - // Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 - // AIU = $0.01 USD) - OutputPrice *int64 `json:"outputPrice,omitempty"` + // AI Credits cost per billing batch of cached tokens + CachePrice *float64 `json:"cachePrice,omitempty"` + // Maximum context window tokens for the default tier + ContextMax *int64 `json:"contextMax,omitempty"` + // AI Credits cost per billing batch of input tokens + InputPrice *float64 `json:"inputPrice,omitempty"` + // Long context tier pricing (available for models with extended context windows) + LongContext *ModelBillingTokenPricesLongContext `json:"longContext,omitempty"` + // AI Credits cost per billing batch of output tokens + OutputPrice *float64 `json:"outputPrice,omitempty"` +} + +// Long context tier pricing (available for models with extended context windows) +type ModelBillingTokenPricesLongContext struct { + // AI Credits cost per billing batch of cached tokens + CachePrice *float64 `json:"cachePrice,omitempty"` + // Maximum context window tokens for the long context tier + ContextMax *int64 `json:"contextMax,omitempty"` + // AI Credits cost per billing batch of input tokens + InputPrice *float64 `json:"inputPrice,omitempty"` + // AI Credits cost per billing batch of output tokens + OutputPrice *float64 `json:"outputPrice,omitempty"` } // Model capabilities and limits @@ -1893,6 +2174,32 @@ type NameSetRequest struct { Name string `json:"name"` } +// Open canvas instance snapshot. +// Experimental: OpenCanvasInstance is part of an experimental API and may change or be +// removed. +type OpenCanvasInstance struct { + // Runtime-controlled routing state for an open canvas instance. + Availability CanvasInstanceAvailability `json:"availability"` + // Provider-local canvas identifier + CanvasID string `json:"canvasId"` + // Owning provider identifier + ExtensionID string `json:"extensionId"` + // Owning extension display name, when available + ExtensionName *string `json:"extensionName,omitempty"` + // Input supplied when the instance was opened + Input any `json:"input,omitempty"` + // Stable caller-supplied canvas instance identifier + InstanceID string `json:"instanceId"` + // Whether this snapshot came from an idempotent reopen + Reopen bool `json:"reopen"` + // Provider-supplied status text + Status *string `json:"status,omitempty"` + // Rendered title + Title *string `json:"title,omitempty"` + // URL for web-rendered canvases + URL *string `json:"url,omitempty"` +} + // Schema for the `PendingPermissionRequest` type. // Experimental: PendingPermissionRequest is part of an experimental API and may change or // be removed. @@ -3403,6 +3710,8 @@ type SendRequest struct { RequiredTool *string `json:"requiredTool,omitempty"` // Optional provenance tag copied to the resulting user.message event. Supported values are // `system`, `command-*`, and `schedule-*`. + // Internal: Source is part of the SDK's internal API surface and is not intended for + // external use. Source any `json:"source,omitempty"` // W3C Trace Context traceparent header for distributed tracing of this agent turn Traceparent *string `json:"traceparent,omitempty"` @@ -3478,6 +3787,11 @@ type SessionBulkDeleteResult struct { FreedBytes map[string]int64 `json:"freedBytes"` } +// Experimental: SessionCanvasCloseResult is part of an experimental API and may change or +// be removed. +type SessionCanvasCloseResult struct { +} + // Schema for the `SessionContext` type. // Experimental: SessionContext is part of an experimental API and may change or be removed. type SessionContext struct { @@ -3873,6 +4187,16 @@ type SessionLoadDeferredRepoHooksResult struct { type SessionLspInitializeResult struct { } +// Standard MCP CallToolResult +// Experimental: SessionMcpAppsCallToolResult is part of an experimental API and may change +// or be removed. +type SessionMcpAppsCallToolResult map[string]any + +// Experimental: SessionMcpAppsSetHostContextResult is part of an experimental API and may +// change or be removed. +type SessionMcpAppsSetHostContextResult struct { +} + // Experimental: SessionMcpDisableResult is part of an experimental API and may change or be // removed. type SessionMcpDisableResult struct { @@ -4322,6 +4646,8 @@ type SessionTelemetrySetFeatureOverridesResult struct { type SessionUpdateOptionsParams struct { // Additional content-exclusion policies to merge into the session's policy set. Opaque // shape; see `ContentExclusionApiResponse` in the runtime. + // Experimental: AdditionalContentExclusionPolicies is part of an experimental API and may + // change or be removed. AdditionalContentExclusionPolicies []any `json:"additionalContentExclusionPolicies,omitempty"` // Runtime context discriminator (e.g., `cli`, `actions`). AgentContext *string `json:"agentContext,omitempty"` @@ -4382,12 +4708,14 @@ type SessionUpdateOptionsParams struct { Model *string `json:"model,omitempty"` // Custom model-provider configuration (BYOK). Opaque shape; see `ProviderConfig` in the // runtime. + // Experimental: Provider is part of an experimental API and may change or be removed. Provider any `json:"provider,omitempty"` // Reasoning effort for the selected model (model-defined enum). ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Whether the session is running in an interactive UI. RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Sandbox configuration shape; opaque to SDK consumers. See `SandboxConfig` in the runtime. + // Experimental: SandboxConfig is part of an experimental API and may change or be removed. SandboxConfig any `json:"sandboxConfig,omitempty"` // Shell init profile (`None` or `NonInteractive`). ShellInitProfile *string `json:"shellInitProfile,omitempty"` @@ -5901,6 +6229,20 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) +// Runtime-controlled routing state for an open canvas instance. +// Experimental: CanvasInstanceAvailability is part of an experimental API and may change or +// be removed. +type CanvasInstanceAvailability string + +const ( + // The owning provider is currently connected and routing calls will be dispatched normally. + CanvasInstanceAvailabilityReady CanvasInstanceAvailability = "ready" + // The owning provider is not currently connected. Routing calls fail with + // canvas_provider_unavailable until the agent re-issues open_canvas (which rehydrates via a + // fresh canvas.open) or the provider reconnects. + CanvasInstanceAvailabilityStale CanvasInstanceAvailability = "stale" +) + // Neutral SDK discriminator for the connected remote session kind. // Experimental: ConnectedRemoteSessionMetadataKind is part of an experimental API and may // change or be removed. @@ -5934,7 +6276,7 @@ const ( CopilotAPITokenAuthInfoHostHTTPSGithubCom CopilotAPITokenAuthInfoHost = "https://github.com" ) -// Server transport type: stdio, http, sse, or memory +// Server transport type: stdio, http, sse (deprecated), or memory type DiscoveredMcpServerType string const ( @@ -5942,7 +6284,7 @@ const ( DiscoveredMcpServerTypeHTTP DiscoveredMcpServerType = "http" // Server is backed by an in-memory runtime implementation. DiscoveredMcpServerTypeMemory DiscoveredMcpServerType = "memory" - // Server communicates over Server-Sent Events. + // Server communicates over Server-Sent Events (deprecated). DiscoveredMcpServerTypeSse DiscoveredMcpServerType = "sse" // Server communicates over stdio with a local child process. DiscoveredMcpServerTypeStdio DiscoveredMcpServerType = "stdio" @@ -6112,6 +6454,114 @@ const ( InstructionsSourcesTypeVscode InstructionsSourcesType = "vscode" ) +// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. +// Experimental: McpAppsHostContextDetailsAvailableDisplayMode is part of an experimental +// API and may change or be removed. +type McpAppsHostContextDetailsAvailableDisplayMode string + +const ( + // Rendered as a fullscreen overlay + McpAppsHostContextDetailsAvailableDisplayModeFullscreen McpAppsHostContextDetailsAvailableDisplayMode = "fullscreen" + // Rendered inline within the host conversation surface + McpAppsHostContextDetailsAvailableDisplayModeInline McpAppsHostContextDetailsAvailableDisplayMode = "inline" + // Rendered as a picture-in-picture floating panel + McpAppsHostContextDetailsAvailableDisplayModePip McpAppsHostContextDetailsAvailableDisplayMode = "pip" +) + +// Current display mode (SEP-1865) +// Experimental: McpAppsHostContextDetailsDisplayMode is part of an experimental API and may +// change or be removed. +type McpAppsHostContextDetailsDisplayMode string + +const ( + // Rendered as a fullscreen overlay + McpAppsHostContextDetailsDisplayModeFullscreen McpAppsHostContextDetailsDisplayMode = "fullscreen" + // Rendered inline within the host conversation surface + McpAppsHostContextDetailsDisplayModeInline McpAppsHostContextDetailsDisplayMode = "inline" + // Rendered as a picture-in-picture floating panel + McpAppsHostContextDetailsDisplayModePip McpAppsHostContextDetailsDisplayMode = "pip" +) + +// Platform type for responsive design +// Experimental: McpAppsHostContextDetailsPlatform is part of an experimental API and may +// change or be removed. +type McpAppsHostContextDetailsPlatform string + +const ( + // Host runs as a desktop application + McpAppsHostContextDetailsPlatformDesktop McpAppsHostContextDetailsPlatform = "desktop" + // Host runs on a mobile device + McpAppsHostContextDetailsPlatformMobile McpAppsHostContextDetailsPlatform = "mobile" + // Host runs in a web browser + McpAppsHostContextDetailsPlatformWeb McpAppsHostContextDetailsPlatform = "web" +) + +// UI theme preference per SEP-1865 +// Experimental: McpAppsHostContextDetailsTheme is part of an experimental API and may +// change or be removed. +type McpAppsHostContextDetailsTheme string + +const ( + // Dark UI theme + McpAppsHostContextDetailsThemeDark McpAppsHostContextDetailsTheme = "dark" + // Light UI theme + McpAppsHostContextDetailsThemeLight McpAppsHostContextDetailsTheme = "light" +) + +// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. +// Experimental: McpAppsSetHostContextDetailsAvailableDisplayMode is part of an experimental +// API and may change or be removed. +type McpAppsSetHostContextDetailsAvailableDisplayMode string + +const ( + // Rendered as a fullscreen overlay + McpAppsSetHostContextDetailsAvailableDisplayModeFullscreen McpAppsSetHostContextDetailsAvailableDisplayMode = "fullscreen" + // Rendered inline within the host conversation surface + McpAppsSetHostContextDetailsAvailableDisplayModeInline McpAppsSetHostContextDetailsAvailableDisplayMode = "inline" + // Rendered as a picture-in-picture floating panel + McpAppsSetHostContextDetailsAvailableDisplayModePip McpAppsSetHostContextDetailsAvailableDisplayMode = "pip" +) + +// Current display mode (SEP-1865) +// Experimental: McpAppsSetHostContextDetailsDisplayMode is part of an experimental API and +// may change or be removed. +type McpAppsSetHostContextDetailsDisplayMode string + +const ( + // Rendered as a fullscreen overlay + McpAppsSetHostContextDetailsDisplayModeFullscreen McpAppsSetHostContextDetailsDisplayMode = "fullscreen" + // Rendered inline within the host conversation surface + McpAppsSetHostContextDetailsDisplayModeInline McpAppsSetHostContextDetailsDisplayMode = "inline" + // Rendered as a picture-in-picture floating panel + McpAppsSetHostContextDetailsDisplayModePip McpAppsSetHostContextDetailsDisplayMode = "pip" +) + +// Platform type for responsive design +// Experimental: McpAppsSetHostContextDetailsPlatform is part of an experimental API and may +// change or be removed. +type McpAppsSetHostContextDetailsPlatform string + +const ( + // Host runs as a desktop application + McpAppsSetHostContextDetailsPlatformDesktop McpAppsSetHostContextDetailsPlatform = "desktop" + // Host runs on a mobile device + McpAppsSetHostContextDetailsPlatformMobile McpAppsSetHostContextDetailsPlatform = "mobile" + // Host runs in a web browser + McpAppsSetHostContextDetailsPlatformWeb McpAppsSetHostContextDetailsPlatform = "web" +) + +// UI theme preference per SEP-1865 +// Experimental: McpAppsSetHostContextDetailsTheme is part of an experimental API and may +// change or be removed. +type McpAppsSetHostContextDetailsTheme string + +const ( + // Dark UI theme + McpAppsSetHostContextDetailsThemeDark McpAppsSetHostContextDetailsTheme = "dark" + // Light UI theme + McpAppsSetHostContextDetailsThemeLight McpAppsSetHostContextDetailsTheme = "light" +) + // Outcome of the sampling inference. 'success' produced a response; 'failure' encountered // an error (including agent-side rejection by content filter or criteria); 'cancelled' the // caller cancelled this execution via cancelSamplingExecution. @@ -7797,6 +8247,123 @@ func (a *AuthApi) SetCredentials(ctx context.Context, params *SessionSetCredenti return &result, nil } +// Experimental: CanvasApi contains experimental APIs that may change or be removed. +type CanvasApi sessionApi + +// Closes an open canvas instance. +// +// RPC method: session.canvas.close. +// +// Parameters: Canvas close parameters. +func (a *CanvasApi) Close(ctx context.Context, params *CanvasCloseRequest) (*SessionCanvasCloseResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["instanceId"] = params.InstanceID + } + raw, err := a.client.Request("session.canvas.close", req) + if err != nil { + return nil, err + } + var result SessionCanvasCloseResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// InvokeAction invokes an action on an open canvas instance. +// +// RPC method: session.canvas.invokeAction. +// +// Parameters: Canvas action invocation parameters. +// +// Returns: Canvas action invocation result. +func (a *CanvasApi) InvokeAction(ctx context.Context, params *CanvasInvokeActionRequest) (*CanvasInvokeActionResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["actionName"] = params.ActionName + if params.Input != nil { + req["input"] = params.Input + } + req["instanceId"] = params.InstanceID + } + raw, err := a.client.Request("session.canvas.invokeAction", req) + if err != nil { + return nil, err + } + var result CanvasInvokeActionResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Lists canvases declared for the session. +// +// RPC method: session.canvas.list. +// +// Returns: Declared canvases available in this session. +func (a *CanvasApi) List(ctx context.Context) (*CanvasList, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request("session.canvas.list", req) + if err != nil { + return nil, err + } + var result CanvasList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListOpen lists currently open canvas instances for the live session. +// +// RPC method: session.canvas.listOpen. +// +// Returns: Live open-canvas snapshot. +func (a *CanvasApi) ListOpen(ctx context.Context) (*CanvasListOpenResult, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request("session.canvas.listOpen", req) + if err != nil { + return nil, err + } + var result CanvasListOpenResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Opens or focuses a canvas instance. +// +// RPC method: session.canvas.open. +// +// Parameters: Canvas open parameters. +// +// Returns: Open canvas instance snapshot. +func (a *CanvasApi) Open(ctx context.Context, params *CanvasOpenRequest) (*OpenCanvasInstance, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["canvasId"] = params.CanvasID + if params.ExtensionID != nil { + req["extensionId"] = *params.ExtensionID + } + if params.Input != nil { + req["input"] = params.Input + } + req["instanceId"] = params.InstanceID + } + raw, err := a.client.Request("session.canvas.open", req) + if err != nil { + return nil, err + } + var result OpenCanvasInstance + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: CommandsApi contains experimental APIs that may change or be removed. type CommandsApi sessionApi @@ -8519,6 +9086,159 @@ func (a *McpApi) SetEnvValueMode(ctx context.Context, params *McpSetEnvValueMode return &result, nil } +// Experimental: McpAppsApi contains experimental APIs that may change or be removed. +type McpAppsApi sessionApi + +// CallTool call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check +// that prevents an app iframe from invoking model-only tools. Returns the standard MCP +// `CallToolResult`. +// +// RPC method: session.mcp.apps.callTool. +// +// Parameters: MCP server, tool name, and arguments to invoke from an MCP App view. +// +// Returns: Standard MCP CallToolResult +func (a *McpAppsApi) CallTool(ctx context.Context, params *McpAppsCallToolRequest) (*SessionMcpAppsCallToolResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Arguments != nil { + req["arguments"] = params.Arguments + } + req["originServerName"] = params.OriginServerName + req["serverName"] = params.ServerName + req["toolName"] = params.ToolName + } + raw, err := a.client.Request("session.mcp.apps.callTool", req) + if err != nil { + return nil, err + } + var result SessionMcpAppsCallToolResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, +// feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. +// +// RPC method: session.mcp.apps.diagnose. +// +// Parameters: MCP server to diagnose MCP Apps wiring for. +// +// Returns: Diagnostic snapshot of MCP Apps wiring for the named server. +func (a *McpAppsApi) Diagnose(ctx context.Context, params *McpAppsDiagnoseRequest) (*McpAppsDiagnoseResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["serverName"] = params.ServerName + } + raw, err := a.client.Request("session.mcp.apps.diagnose", req) + if err != nil { + return nil, err + } + var result McpAppsDiagnoseResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetHostContext read the current host context advertised to MCP App guests. +// +// RPC method: session.mcp.apps.getHostContext. +// +// Returns: Current host context advertised to MCP App guests. +func (a *McpAppsApi) GetHostContext(ctx context.Context) (*McpAppsHostContext, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request("session.mcp.apps.getHostContext", req) + if err != nil { + return nil, err + } + var result McpAppsHostContext + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListTools list tools that an MCP App view is allowed to call (SEP-1865 visibility +// filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) +// or includes `"app"`. +// +// RPC method: session.mcp.apps.listTools. +// +// Parameters: MCP server to list app-callable tools for. +// +// Returns: App-callable tools from the named MCP server. +func (a *McpAppsApi) ListTools(ctx context.Context, params *McpAppsListToolsRequest) (*McpAppsListToolsResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["originServerName"] = params.OriginServerName + req["serverName"] = params.ServerName + } + raw, err := a.client.Request("session.mcp.apps.listTools", req) + if err != nil { + return nil, err + } + var result McpAppsListToolsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ReadResource fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) +// from a connected server. Requires the `mcp-apps` session capability. +// +// RPC method: session.mcp.apps.readResource. +// +// Parameters: MCP server and resource URI to fetch. +// +// Returns: Resource contents returned by the MCP server. +func (a *McpAppsApi) ReadResource(ctx context.Context, params *McpAppsReadResourceRequest) (*McpAppsReadResourceResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["serverName"] = params.ServerName + req["uri"] = params.URI + } + raw, err := a.client.Request("session.mcp.apps.readResource", req) + if err != nil { + return nil, err + } + var result McpAppsReadResourceResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// SetHostContext replace the host context returned to MCP App guests on `ui/initialize`. +// Hosts use this to advertise theme, locale, or other metadata to the guest UI. +// +// RPC method: session.mcp.apps.setHostContext. +// +// Parameters: Host context to advertise to MCP App guests. +func (a *McpAppsApi) SetHostContext(ctx context.Context, params *McpAppsSetHostContextRequest) (*SessionMcpAppsSetHostContextResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["context"] = params.Context + } + raw, err := a.client.Request("session.mcp.apps.setHostContext", req) + if err != nil { + return nil, err + } + var result SessionMcpAppsSetHostContextResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Apps returns experimental APIs that may change or be removed. +func (s *McpApi) Apps() *McpAppsApi { + return (*McpAppsApi)(s) +} + // Experimental: McpOauthApi contains experimental APIs that may change or be removed. type McpOauthApi sessionApi @@ -10697,6 +11417,7 @@ type SessionRpc struct { Agent *AgentApi Auth *AuthApi + Canvas *CanvasApi Commands *CommandsApi EventLog *EventLogApi Extensions *ExtensionsApi @@ -10906,6 +11627,7 @@ func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { r.common = sessionApi{client: client, sessionID: sessionID} r.Agent = (*AgentApi)(&r.common) r.Auth = (*AuthApi)(&r.common) + r.Canvas = (*CanvasApi)(&r.common) r.Commands = (*CommandsApi)(&r.common) r.EventLog = (*EventLogApi)(&r.common) r.Extensions = (*ExtensionsApi)(&r.common) diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index cc61228ab..cc344851b 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -191,6 +191,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeMcpAppToolCallComplete: + var d McpAppToolCallCompleteData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeMcpOauthCompleted: var d McpOauthCompletedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -245,6 +251,18 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionCanvasOpened: + var d SessionCanvasOpenedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeSessionCanvasRegistryChanged: + var d SessionCanvasRegistryChangedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionCompactionComplete: var d SessionCompactionCompleteData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -904,9 +922,10 @@ func (r ToolExecutionCompleteContentText) MarshalJSON() ([]byte, error) { func (r *ToolExecutionCompleteResult) UnmarshalJSON(data []byte) error { type rawToolExecutionCompleteResult struct { - Content string `json:"content"` - Contents []json.RawMessage `json:"contents,omitempty"` - DetailedContent *string `json:"detailedContent,omitempty"` + Content string `json:"content"` + Contents []json.RawMessage `json:"contents,omitempty"` + DetailedContent *string `json:"detailedContent,omitempty"` + UIResource *ToolExecutionCompleteUIResource `json:"uiResource,omitempty"` } var raw rawToolExecutionCompleteResult if err := json.Unmarshal(data, &raw); err != nil { @@ -924,6 +943,7 @@ func (r *ToolExecutionCompleteResult) UnmarshalJSON(data []byte) error { } } r.DetailedContent = raw.DetailedContent + r.UIResource = raw.UIResource return nil } diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index fc3a8de60..d56371750 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -79,6 +79,7 @@ const ( SessionEventTypeExternalToolRequested SessionEventType = "external_tool.requested" SessionEventTypeHookEnd SessionEventType = "hook.end" SessionEventTypeHookStart SessionEventType = "hook.start" + SessionEventTypeMcpAppToolCallComplete SessionEventType = "mcp_app.tool_call_complete" SessionEventTypeMcpOauthCompleted SessionEventType = "mcp.oauth_completed" SessionEventTypeMcpOauthRequired SessionEventType = "mcp.oauth_required" SessionEventTypeModelCallFailure SessionEventType = "model.call_failure" @@ -88,6 +89,8 @@ const ( SessionEventTypeSamplingCompleted SessionEventType = "sampling.completed" SessionEventTypeSamplingRequested SessionEventType = "sampling.requested" SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" + SessionEventTypeSessionCanvasOpened SessionEventType = "session.canvas.opened" + SessionEventTypeSessionCanvasRegistryChanged SessionEventType = "session.canvas.registry_changed" SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" SessionEventTypeSessionContextChanged SessionEventType = "session.context_changed" @@ -170,8 +173,10 @@ func (*AssistantReasoningData) Type() SessionEventType { return SessionEventType // Assistant response containing text content, optional tool requests, and interaction metadata type AssistantMessageData struct { // Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping + // Experimental: AnthropicAdvisorBlocks is part of an experimental API and may change or be removed. AnthropicAdvisorBlocks []any `json:"anthropicAdvisorBlocks,omitempty"` // Anthropic advisor model ID used for this response, for timeline display on replay + // Experimental: AnthropicAdvisorModel is part of an experimental API and may change or be removed. AnthropicAdvisorModel *string `json:"anthropicAdvisorModel,omitempty"` // The assistant's text response content Content string `json:"content"` @@ -196,6 +201,8 @@ type AssistantMessageData struct { ReasoningText *string `json:"reasoningText,omitempty"` // GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs RequestID *string `json:"requestId,omitempty"` + // Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + ServiceRequestID *string `json:"serviceRequestId,omitempty"` // Tool invocations requested by the assistant in this message ToolRequests []AssistantMessageToolRequest `json:"toolRequests,omitempty"` // Identifier for the agent loop turn that produced this message, matching the corresponding assistant.turn_start event @@ -272,6 +279,8 @@ type SessionCompactionCompleteData struct { PreCompactionTokens *int64 `json:"preCompactionTokens,omitempty"` // GitHub request tracing ID (x-github-request-id header) for the compaction LLM call RequestID *string `json:"requestId,omitempty"` + // Copilot service request ID (x-copilot-service-request-id header) for the compaction LLM call + ServiceRequestID *string `json:"serviceRequestId,omitempty"` // Whether compaction completed successfully Success bool `json:"success"` // LLM-generated summary of the compacted conversation history @@ -408,6 +417,8 @@ type SessionErrorData struct { Message string `json:"message"` // GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs ProviderCallID *string `json:"providerCallId,omitempty"` + // Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + ServiceRequestID *string `json:"serviceRequestId,omitempty"` // Error stack trace, when available Stack *string `json:"stack,omitempty"` // HTTP status code from the upstream request, if applicable @@ -467,6 +478,8 @@ type ModelCallFailureData struct { Model *string `json:"model,omitempty"` // GitHub request tracing ID (x-github-request-id header) for server-side log correlation ProviderCallID *string `json:"providerCallId,omitempty"` + // Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + ServiceRequestID *string `json:"serviceRequestId,omitempty"` // Where the failed model call originated Source ModelCallFailureSource `json:"source"` // HTTP status code from the failed request @@ -532,8 +545,10 @@ type AssistantUsageData struct { // Number of tokens written to prompt cache CacheWriteTokens *int64 `json:"cacheWriteTokens,omitempty"` // Per-request cost and usage data from the CAPI copilot_usage response field + // Internal: CopilotUsage is part of the SDK's internal API surface and is not intended for external use. CopilotUsage *AssistantUsageCopilotUsage `json:"copilotUsage,omitempty"` // Model multiplier cost for billing purposes + // Experimental: Cost is part of an experimental API and may change or be removed. Cost *float64 `json:"cost,omitempty"` // Duration of the API call in milliseconds Duration *int64 `json:"duration,omitempty"` @@ -553,11 +568,14 @@ type AssistantUsageData struct { // GitHub request tracing ID (x-github-request-id header) for server-side log correlation ProviderCallID *string `json:"providerCallId,omitempty"` // Per-quota resource usage snapshots, keyed by quota identifier + // Internal: QuotaSnapshots is part of the SDK's internal API surface and is not intended for external use. QuotaSnapshots map[string]AssistantUsageQuotaSnapshot `json:"quotaSnapshots,omitempty"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Number of output tokens used for reasoning (e.g., chain-of-thought) ReasoningTokens *int64 `json:"reasoningTokens,omitempty"` + // Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + ServiceRequestID *string `json:"serviceRequestId,omitempty"` // Time to first token in milliseconds. Only available for streaming requests TimeToFirstTokenMs *int64 `json:"timeToFirstTokenMs,omitempty"` } @@ -565,6 +583,31 @@ type AssistantUsageData struct { func (*AssistantUsageData) sessionEventData() {} func (*AssistantUsageData) Type() SessionEventType { return SessionEventTypeAssistantUsage } +// MCP App view called a tool on a connected MCP server (SEP-1865) +type McpAppToolCallCompleteData struct { + // Arguments passed to the tool by the app view, if any + Arguments map[string]any `json:"arguments,omitempty"` + // Wall-clock duration of the underlying tools/call in milliseconds + DurationMs float64 `json:"durationMs"` + // Set when the underlying tools/call threw an error before returning a CallToolResult + Error *McpAppToolCallCompleteError `json:"error,omitempty"` + // Standard MCP CallToolResult returned by the server. Present whether or not the call set isError. + Result map[string]any `json:"result,omitempty"` + // Name of the MCP server hosting the tool + ServerName string `json:"serverName"` + // True when the call completed without throwing AND the MCP CallToolResult did not set isError + Success bool `json:"success"` + // The tool's `_meta.ui` block at the time of the call, so consumers can decide whether to forward the result to the model without re-listing tools. + ToolMeta *McpAppToolCallCompleteToolMeta `json:"toolMeta,omitempty"` + // MCP tool name that was invoked + ToolName string `json:"toolName"` +} + +func (*McpAppToolCallCompleteData) sessionEventData() {} +func (*McpAppToolCallCompleteData) Type() SessionEventType { + return SessionEventTypeMcpAppToolCallComplete +} + // MCP OAuth request completion notification type McpOauthCompletedData struct { // Request ID of the resolved OAuth request @@ -578,6 +621,8 @@ func (*McpOauthCompletedData) Type() SessionEventType { return SessionEventTypeM type SessionModelChangeData struct { // Reason the change happened, when not user-initiated. Currently `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path. UI clients can use this to render contextual copy. Cause *string `json:"cause,omitempty"` + // Context tier after the model change; null explicitly clears a previously selected tier + ContextTier *SessionModelChangeDataContextTier `json:"contextTier,omitempty"` // Newly selected model identifier NewModel string `json:"newModel"` // Model that was previously selected, if any @@ -829,6 +874,44 @@ func (*SessionBackgroundTasksChangedData) Type() SessionEventType { return SessionEventTypeSessionBackgroundTasksChanged } +// Schema for the `CanvasOpenedData` type. +type SessionCanvasOpenedData struct { + // Runtime-controlled routing state for the instance. "ready" when the provider connection is live; "stale" when the provider has gone away and the instance is awaiting rebinding. + Availability CanvasOpenedAvailability `json:"availability"` + // Provider-local canvas identifier + CanvasID string `json:"canvasId"` + // Owning provider identifier + ExtensionID string `json:"extensionId"` + // Owning extension display name, when available + ExtensionName *string `json:"extensionName,omitempty"` + // Input supplied when the instance was opened + Input any `json:"input,omitempty"` + // Stable caller-supplied canvas instance identifier + InstanceID string `json:"instanceId"` + // Whether this notification represents an idempotent reopen + Reopen bool `json:"reopen"` + // Provider-supplied status text + Status *string `json:"status,omitempty"` + // Rendered title + Title *string `json:"title,omitempty"` + // URL for web-rendered canvases + URL *string `json:"url,omitempty"` +} + +func (*SessionCanvasOpenedData) sessionEventData() {} +func (*SessionCanvasOpenedData) Type() SessionEventType { return SessionEventTypeSessionCanvasOpened } + +// Schema for the `CanvasRegistryChangedData` type. +type SessionCanvasRegistryChangedData struct { + // Canvas declarations currently available + Canvases []CanvasRegistryChangedCanvas `json:"canvases"` +} + +func (*SessionCanvasRegistryChangedData) sessionEventData() {} +func (*SessionCanvasRegistryChangedData) Type() SessionEventType { + return SessionEventTypeSessionCanvasRegistryChanged +} + // Schema for the `CustomAgentsUpdatedData` type. type SessionCustomAgentsUpdatedData struct { // Array of loaded custom agent metadata @@ -857,6 +940,8 @@ func (*SessionExtensionsLoadedData) Type() SessionEventType { // Schema for the `McpServerStatusChangedData` type. type SessionMcpServerStatusChangedData struct { + // Error message if the server entered a failed state + Error *string `json:"error,omitempty"` // Name of the MCP server whose status changed ServerName string `json:"serverName"` // Connection status: connected, failed, needs-auth, pending, disabled, or not_configured @@ -1052,8 +1137,10 @@ type SessionShutdownData struct { // Cumulative time spent in API calls during the session, in milliseconds TotalAPIDurationMs int64 `json:"totalApiDurationMs"` // Session-wide accumulated nano-AI units cost + // Experimental: TotalNanoAiu is part of an experimental API and may change or be removed. TotalNanoAiu *float64 `json:"totalNanoAiu,omitempty"` // Total number of premium API requests used during the session + // Internal: TotalPremiumRequests is part of the SDK's internal API surface and is not intended for external use. TotalPremiumRequests *float64 `json:"totalPremiumRequests,omitempty"` } @@ -1085,6 +1172,10 @@ type SkillInvokedData struct { 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), personal-claude (~/.claude/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise 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"` } func (*SkillInvokedData) sessionEventData() {} @@ -1275,6 +1366,8 @@ type ToolExecutionCompleteData struct { Success bool `json:"success"` // Unique identifier for the completed tool call ToolCallID string `json:"toolCallId"` + // Tool definition metadata, present for MCP tools with MCP Apps support + ToolDescription *ToolExecutionCompleteToolDescription `json:"toolDescription,omitempty"` // Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) ToolTelemetry map[string]any `json:"toolTelemetry,omitempty"` // Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event @@ -1465,6 +1558,7 @@ type AssistantMessageToolRequest struct { } // Per-request cost and usage data from the CAPI copilot_usage response field +// Internal: AssistantUsageCopilotUsage is an internal SDK API and is not part of the public surface. type AssistantUsageCopilotUsage struct { // Itemized token usage breakdown TokenDetails []AssistantUsageCopilotUsageTokenDetail `json:"tokenDetails"` @@ -1485,29 +1579,70 @@ type AssistantUsageCopilotUsageTokenDetail struct { } // Schema for the `AssistantUsageQuotaSnapshot` type. +// Internal: AssistantUsageQuotaSnapshot is an internal SDK API and is not part of the public surface. type AssistantUsageQuotaSnapshot struct { // Total requests allowed by the entitlement + // Internal: EntitlementRequests is part of the SDK's internal API surface and is not intended for external use. EntitlementRequests int64 `json:"entitlementRequests"` // Whether the user has an unlimited usage entitlement + // Internal: IsUnlimitedEntitlement is part of the SDK's internal API surface and is not intended for external use. IsUnlimitedEntitlement bool `json:"isUnlimitedEntitlement"` // Number of additional usage requests made this period + // Internal: Overage is part of the SDK's internal API surface and is not intended for external use. Overage float64 `json:"overage"` // Whether additional usage is allowed when quota is exhausted + // Internal: OverageAllowedWithExhaustedQuota is part of the SDK's internal API surface and is not intended for external use. OverageAllowedWithExhaustedQuota bool `json:"overageAllowedWithExhaustedQuota"` // Percentage of quota remaining (0 to 100) + // Internal: RemainingPercentage is part of the SDK's internal API surface and is not intended for external use. RemainingPercentage float64 `json:"remainingPercentage"` // Date when the quota resets + // Internal: ResetDate is part of the SDK's internal API surface and is not intended for external use. ResetDate *time.Time `json:"resetDate,omitempty"` // Whether usage is still permitted after quota exhaustion + // Internal: UsageAllowedWithExhaustedQuota is part of the SDK's internal API surface and is not intended for external use. UsageAllowedWithExhaustedQuota bool `json:"usageAllowedWithExhaustedQuota"` // Number of requests already consumed + // Internal: UsedRequests is part of the SDK's internal API surface and is not intended for external use. UsedRequests int64 `json:"usedRequests"` } +// Schema for the `CanvasRegistryChangedCanvas` type. +type CanvasRegistryChangedCanvas struct { + // Actions the agent or host may invoke + Actions []CanvasRegistryChangedCanvasAction `json:"actions,omitempty"` + // Provider-local canvas identifier + CanvasID string `json:"canvasId"` + // Short, single-sentence description shown to the agent in canvas catalogs. + Description string `json:"description"` + // Human-readable canvas name + DisplayName string `json:"displayName"` + // Owning provider identifier + ExtensionID string `json:"extensionId"` + // Owning extension display name, when available + ExtensionName *string `json:"extensionName,omitempty"` + // JSON Schema for canvas open input + InputSchema map[string]any `json:"inputSchema,omitempty"` +} + +// Schema for the `CanvasRegistryChangedCanvasAction` type. +type CanvasRegistryChangedCanvasAction struct { + // Action description + Description *string `json:"description,omitempty"` + // JSON Schema for action input + InputSchema map[string]any `json:"inputSchema,omitempty"` + // Action name + Name string `json:"name"` +} + // UI capability changes type CapabilitiesChangedUI struct { + // Whether canvas rendering is now supported + Canvases *bool `json:"canvases,omitempty"` // Whether elicitation is now supported Elicitation *bool `json:"elicitation,omitempty"` + // Whether MCP Apps (SEP-1865) UI passthrough is now supported + McpApps *bool `json:"mcpApps,omitempty"` } // Schema for the `CommandsChangedCommand` type. @@ -1525,6 +1660,7 @@ type CompactionCompleteCompactionTokensUsed struct { // Tokens written to prompt cache in the compaction LLM call CacheWriteTokens *int64 `json:"cacheWriteTokens,omitempty"` // Per-request cost and usage data from the CAPI copilot_usage response field + // Internal: CopilotUsage is part of the SDK's internal API surface and is not intended for external use. CopilotUsage *CompactionCompleteCompactionTokensUsedCopilotUsage `json:"copilotUsage,omitempty"` // Duration of the compaction LLM call in milliseconds Duration *int64 `json:"duration,omitempty"` @@ -1537,6 +1673,7 @@ type CompactionCompleteCompactionTokensUsed struct { } // Per-request cost and usage data from the CAPI copilot_usage response field +// Internal: CompactionCompleteCompactionTokensUsedCopilotUsage is an internal SDK API and is not part of the public surface. type CompactionCompleteCompactionTokensUsedCopilotUsage struct { // Itemized token usage breakdown TokenDetails []CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail `json:"tokenDetails"` @@ -1646,6 +1783,26 @@ type HookEndError struct { Stack *string `json:"stack,omitempty"` } +// Set when the underlying tools/call threw an error before returning a CallToolResult +type McpAppToolCallCompleteError struct { + // Human-readable error message + Message string `json:"message"` +} + +// The tool's `_meta.ui` block at the time of the call, so consumers can decide whether to forward the result to the model without re-listing tools. +type McpAppToolCallCompleteToolMeta struct { + // Schema for the `McpAppToolCallCompleteToolMetaUI` type. + UI *McpAppToolCallCompleteToolMetaUI `json:"ui,omitempty"` +} + +// Schema for the `McpAppToolCallCompleteToolMetaUI` type. +type McpAppToolCallCompleteToolMetaUI struct { + // `ui://` URI declared by the tool's `_meta.ui.resourceUri` + ResourceURI *string `json:"resourceUri,omitempty"` + // Tool visibility per SEP-1865 (typically a subset of `["model","app"]`) + Visibility []string `json:"visibility,omitempty"` +} + // Static OAuth client configuration, if the server specifies one type McpOauthRequiredStaticClientConfig struct { // OAuth client ID for the server @@ -1662,10 +1819,16 @@ type McpServersLoadedServer struct { Error *string `json:"error,omitempty"` // Server name (config key) Name string `json:"name"` + // Name of the plugin that supplied the effective MCP server config, only when source is plugin + PluginName *string `json:"pluginName,omitempty"` + // Version of the plugin that supplied the effective MCP server config, only when source is plugin + PluginVersion *string `json:"pluginVersion,omitempty"` // Configuration source: user, workspace, plugin, or builtin Source *McpServerSource `json:"source,omitempty"` // Connection status: connected, failed, needs-auth, pending, disabled, or not_configured Status McpServerStatus `json:"status"` + // Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server) + Transport *McpServerTransport `json:"transport,omitempty"` } // Derived user-facing permission prompt details for UI consumers @@ -2229,6 +2392,7 @@ type ShutdownModelMetric struct { // Token count details per type TokenDetails map[string]ShutdownModelMetricTokenDetail `json:"tokenDetails,omitempty"` // Accumulated nano-AI units cost for this model + // Experimental: TotalNanoAiu is part of an experimental API and may change or be removed. TotalNanoAiu *float64 `json:"totalNanoAiu,omitempty"` // Token usage breakdown Usage ShutdownModelMetricUsage `json:"usage"` @@ -2237,8 +2401,10 @@ type ShutdownModelMetric struct { // Request count and cost metrics type ShutdownModelMetricRequests struct { // Cumulative cost multiplier for requests to this model + // Experimental: Cost is part of an experimental API and may change or be removed. Cost *float64 `json:"cost,omitempty"` // Total number of API requests made to this model + // Experimental: Count is part of an experimental API and may change or be removed. Count *int64 `json:"count,omitempty"` } @@ -2540,6 +2706,98 @@ type ToolExecutionCompleteResult struct { Contents []ToolExecutionCompleteContent `json:"contents,omitempty"` // Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. DetailedContent *string `json:"detailedContent,omitempty"` + // MCP Apps UI resource content for rendering in a sandboxed iframe + UIResource *ToolExecutionCompleteUIResource `json:"uiResource,omitempty"` +} + +// Tool definition metadata, present for MCP tools with MCP Apps support +type ToolExecutionCompleteToolDescription struct { + // Tool description + Description *string `json:"description,omitempty"` + // MCP Apps metadata for UI resource association + Meta *ToolExecutionCompleteToolDescriptionMeta `json:"_meta,omitempty"` + // Tool name + Name string `json:"name"` +} + +// MCP Apps metadata for UI resource association +type ToolExecutionCompleteToolDescriptionMeta struct { + // Schema for the `ToolExecutionCompleteToolDescriptionMetaUI` type. + UI *ToolExecutionCompleteToolDescriptionMetaUI `json:"ui,omitempty"` +} + +// Schema for the `ToolExecutionCompleteToolDescriptionMetaUI` type. +type ToolExecutionCompleteToolDescriptionMetaUI struct { + // URI of the UI resource + ResourceURI *string `json:"resourceUri,omitempty"` + // Who can access this tool + Visibility []ToolExecutionCompleteToolDescriptionMetaUIVisibility `json:"visibility,omitempty"` +} + +// MCP Apps UI resource content for rendering in a sandboxed iframe +type ToolExecutionCompleteUIResource struct { + // Base64-encoded HTML content + Blob *string `json:"blob,omitempty"` + // Resource-level UI metadata (CSP, permissions, visual preferences) + Meta *ToolExecutionCompleteUIResourceMeta `json:"_meta,omitempty"` + // MIME type of the content + MIMEType string `json:"mimeType"` + // HTML content as a string + Text *string `json:"text,omitempty"` + // The ui:// URI of the resource + URI string `json:"uri"` +} + +// Resource-level UI metadata (CSP, permissions, visual preferences) +type ToolExecutionCompleteUIResourceMeta struct { + // Schema for the `ToolExecutionCompleteUIResourceMetaUI` type. + UI *ToolExecutionCompleteUIResourceMetaUI `json:"ui,omitempty"` +} + +// Schema for the `ToolExecutionCompleteUIResourceMetaUI` type. +type ToolExecutionCompleteUIResourceMetaUI struct { + // Schema for the `ToolExecutionCompleteUIResourceMetaUICsp` type. + Csp *ToolExecutionCompleteUIResourceMetaUICsp `json:"csp,omitempty"` + Domain *string `json:"domain,omitempty"` + // Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissions` type. + Permissions *ToolExecutionCompleteUIResourceMetaUIPermissions `json:"permissions,omitempty"` + PrefersBorder *bool `json:"prefersBorder,omitempty"` +} + +// Schema for the `ToolExecutionCompleteUIResourceMetaUICsp` type. +type ToolExecutionCompleteUIResourceMetaUICsp struct { + BaseURIDomains []string `json:"baseUriDomains,omitempty"` + ConnectDomains []string `json:"connectDomains,omitempty"` + FrameDomains []string `json:"frameDomains,omitempty"` + ResourceDomains []string `json:"resourceDomains,omitempty"` +} + +// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissions` type. +type ToolExecutionCompleteUIResourceMetaUIPermissions struct { + // Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsCamera` type. + Camera *ToolExecutionCompleteUIResourceMetaUIPermissionsCamera `json:"camera,omitempty"` + // Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite` type. + ClipboardWrite *ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite `json:"clipboardWrite,omitempty"` + // Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation` type. + Geolocation *ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation `json:"geolocation,omitempty"` + // Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone` type. + Microphone *ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone `json:"microphone,omitempty"` +} + +// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsCamera` type. +type ToolExecutionCompleteUIResourceMetaUIPermissionsCamera struct { +} + +// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite` type. +type ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite struct { +} + +// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation` type. +type ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation struct { +} + +// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone` type. +type ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone struct { } // A user message attachment — a file, directory, code selection, blob, or GitHub reference @@ -2725,6 +2983,16 @@ const ( AutoModeSwitchResponseYesAlways AutoModeSwitchResponse = "yes_always" ) +// Runtime-controlled routing state for the instance. "ready" when the provider connection is live; "stale" when the provider has gone away and the instance is awaiting rebinding. +type CanvasOpenedAvailability string + +const ( + // Provider connection is live; actions can be invoked. + CanvasOpenedAvailabilityReady CanvasOpenedAvailability = "ready" + // Provider has gone away; the instance is awaiting rebinding. + CanvasOpenedAvailabilityStale CanvasOpenedAvailability = "stale" +) + // The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) type ElicitationCompletedAction string @@ -2809,6 +3077,20 @@ const ( McpOauthRequiredStaticClientConfigGrantTypeClientCredentials McpOauthRequiredStaticClientConfigGrantType = "client_credentials" ) +// Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server) +type McpServerTransport string + +const ( + // Server communicates over streamable HTTP. + McpServerTransportHTTP McpServerTransport = "http" + // Server is backed by an in-memory runtime implementation. + McpServerTransportMemory McpServerTransport = "memory" + // Server communicates over Server-Sent Events (deprecated). + McpServerTransportSse McpServerTransport = "sse" + // Server communicates over stdio with a local child process. + McpServerTransportStdio McpServerTransport = "stdio" +) + // Where the failed model call originated type ModelCallFailureSource string @@ -2913,6 +3195,27 @@ const ( PlanChangedOperationUpdate PlanChangedOperation = "update" ) +type SessionModelChangeDataContextTier string + +const ( + // Default context tier with standard context window size. + SessionModelChangeDataContextTierDefault SessionModelChangeDataContextTier = "default" + // Extended context tier with a larger context window. + SessionModelChangeDataContextTierLongContext SessionModelChangeDataContextTier = "long_context" +) + +// 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) +type SkillInvokedTrigger string + +const ( + // Skill invocation requested by the agent. + SkillInvokedTriggerAgentInvoked SkillInvokedTrigger = "agent-invoked" + // Skill content loaded as part of another context, such as a configured custom agent or subagent. + SkillInvokedTriggerContextLoad SkillInvokedTrigger = "context-load" + // Skill invocation requested explicitly by the user, such as via a slash command or UI affordance. + SkillInvokedTriggerUserInvoked SkillInvokedTrigger = "user-invoked" +) + // Message role: "system" for system prompts, "developer" for developer-injected instructions type SystemMessageRole string @@ -2967,6 +3270,16 @@ const ( ToolExecutionCompleteContentTypeText ToolExecutionCompleteContentType = "text" ) +// Allowed values for the `ToolExecutionCompleteToolDescriptionMetaUIVisibility` enumeration. +type ToolExecutionCompleteToolDescriptionMetaUIVisibility string + +const ( + // Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool + ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp ToolExecutionCompleteToolDescriptionMetaUIVisibility = "app" + // Tool is callable by the model (LLM tool surface) + ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel ToolExecutionCompleteToolDescriptionMetaUIVisibility = "model" +) + // The agent mode that was active when this message was sent type UserMessageAgentMode string diff --git a/go/session.go b/go/session.go index 8119a1bf5..eca928c19 100644 --- a/go/session.go +++ b/go/session.go @@ -75,6 +75,10 @@ type Session struct { commandHandlersMu sync.RWMutex elicitationHandler ElicitationHandler elicitationMu sync.RWMutex + canvasHandler CanvasHandler + canvasMu sync.RWMutex + openCanvases []rpc.OpenCanvasInstance + openCanvasesMu sync.RWMutex capabilities SessionCapabilities capabilitiesMu sync.RWMutex @@ -94,6 +98,38 @@ func (s *Session) WorkspacePath() string { return s.workspacePath } +// OpenCanvases returns the open-canvas snapshot last reported by the runtime +// (currently populated from the session.resume response). The returned slice +// is a copy and is safe to mutate by the caller. +func (s *Session) OpenCanvases() []rpc.OpenCanvasInstance { + s.openCanvasesMu.RLock() + defer s.openCanvasesMu.RUnlock() + if len(s.openCanvases) == 0 { + return nil + } + out := make([]rpc.OpenCanvasInstance, len(s.openCanvases)) + copy(out, s.openCanvases) + return out +} + +func (s *Session) setOpenCanvases(canvases []rpc.OpenCanvasInstance) { + s.openCanvasesMu.Lock() + defer s.openCanvasesMu.Unlock() + s.openCanvases = canvases +} + +func (s *Session) registerCanvasHandler(handler CanvasHandler) { + s.canvasMu.Lock() + defer s.canvasMu.Unlock() + s.canvasHandler = handler +} + +func (s *Session) getCanvasHandler() CanvasHandler { + s.canvasMu.RLock() + defer s.canvasMu.RUnlock() + return s.canvasHandler +} + // newSession creates a new session wrapper with the given session ID and client. func newSession(sessionID string, client *jsonrpc2.Client, workspacePath string) *Session { s := &Session{ @@ -1143,7 +1179,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques SessionID: s.SessionID, } - result, err := handler(permissionRequest, invocation) + decision, err := handler(permissionRequest, invocation) if err != nil { s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.PermissionDecisionRequest{ RequestID: requestID, @@ -1151,29 +1187,28 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques }) return } - if result.Kind == "no-result" { + if decision == nil { + // Handler returned (nil, nil); treat as user-not-available rather + // than sending null on the wire. + s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.PermissionDecisionRequest{ + RequestID: requestID, + Result: &rpc.PermissionDecisionUserNotAvailable{}, + }) + return + } + if _, ok := decision.(*rpc.PermissionDecisionNoResult); ok { + return + } + if _, ok := decision.(rpc.PermissionDecisionNoResult); ok { return } s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.PermissionDecisionRequest{ RequestID: requestID, - Result: rpcPermissionDecisionFromKind(rpc.PermissionDecisionKind(result.Kind)), + Result: decision, }) } -func rpcPermissionDecisionFromKind(kind rpc.PermissionDecisionKind) rpc.PermissionDecision { - switch kind { - case rpc.PermissionDecisionKindApproveOnce: - return &rpc.PermissionDecisionApproveOnce{} - case rpc.PermissionDecisionKindReject: - return &rpc.PermissionDecisionReject{} - case rpc.PermissionDecisionKindUserNotAvailable: - return &rpc.PermissionDecisionUserNotAvailable{} - default: - return &rpc.RawPermissionDecisionData{Discriminator: kind} - } -} - // GetEvents retrieves all events from this session's history. // // This returns the complete conversation history including user messages, diff --git a/go/session_test.go b/go/session_test.go index 0b7de5ac9..16ac64273 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -8,8 +8,6 @@ import ( "sync/atomic" "testing" "time" - - "github.com/github/copilot-sdk/go/rpc" ) // newTestSession creates a session with an event channel and starts the consumer goroutine. @@ -28,24 +26,6 @@ func newTestEvent() SessionEvent { return SessionEvent{Data: &SessionIdleData{}} } -func TestRPCPermissionDecisionFromKindPreservesUnknownKind(t *testing.T) { - kind := rpc.PermissionDecisionKind("future-decision") - decision := rpcPermissionDecisionFromKind(kind) - - data, err := json.Marshal(decision) - if err != nil { - t.Fatalf("marshal permission decision: %v", err) - } - - var serialized map[string]any - if err := json.Unmarshal(data, &serialized); err != nil { - t.Fatalf("unmarshal serialized permission decision: %v", err) - } - if serialized["kind"] != string(kind) { - t.Fatalf("expected kind %q to round-trip, got %v in %s", kind, serialized["kind"], data) - } -} - func TestSession_On(t *testing.T) { t.Run("multiple handlers all receive events", func(t *testing.T) { session, cleanup := newTestSession() diff --git a/go/types.go b/go/types.go index 2760e9b2b..fe7f9d93c 100644 --- a/go/types.go +++ b/go/types.go @@ -194,21 +194,36 @@ func Int(v int) *int { return &v } -// Known system prompt section identifiers for the "customize" mode. +// Known system message section identifiers for the "customize" mode. const ( - SectionIdentity = "identity" - SectionTone = "tone" - SectionToolEfficiency = "tool_efficiency" + // SectionIdentity is the agent identity preamble and mode statement. + SectionIdentity = "identity" + // SectionTone covers response style, conciseness rules, and output formatting preferences. + SectionTone = "tone" + // SectionToolEfficiency covers tool usage patterns, parallel calling, and batching guidelines. + SectionToolEfficiency = "tool_efficiency" + // SectionEnvironmentContext covers CWD, OS, git root, directory listing, and available tools. SectionEnvironmentContext = "environment_context" - SectionCodeChangeRules = "code_change_rules" - SectionGuidelines = "guidelines" - SectionSafety = "safety" - SectionToolInstructions = "tool_instructions" + // SectionCodeChangeRules covers coding rules, linting/testing, ecosystem tools, and style. + SectionCodeChangeRules = "code_change_rules" + // SectionGuidelines covers tips, behavioral best practices, and behavioral guidelines. + SectionGuidelines = "guidelines" + // SectionSafety covers environment limitations, prohibited actions, and security policies. + SectionSafety = "safety" + // SectionToolInstructions covers per-tool usage instructions. + SectionToolInstructions = "tool_instructions" + // SectionCustomInstructions covers repository and organization custom instructions. SectionCustomInstructions = "custom_instructions" - SectionLastInstructions = "last_instructions" + // SectionRuntimeInstructions targets runtime-provided context and instructions + // (e.g. system notifications, memories, workspace context, mode-specific instructions, + // content-exclusion policy). + SectionRuntimeInstructions = "runtime_instructions" + // SectionLastInstructions covers end-of-prompt instructions: parallel tool calling, + // persistence, and task completion. + SectionLastInstructions = "last_instructions" ) -// SectionOverrideAction represents the action to perform on a system prompt section. +// SectionOverrideAction represents the action to perform on a system message section. type SectionOverrideAction string const ( @@ -222,12 +237,12 @@ const ( SectionActionPrepend SectionOverrideAction = "prepend" ) -// SectionTransformFn is a callback that receives the current content of a system prompt section +// SectionTransformFn is a callback that receives the current content of a system message section // and returns the transformed content. Used with the "transform" action to read-then-write // modify sections at runtime. type SectionTransformFn func(currentContent string) (string, error) -// SectionOverride defines an override operation for a single system prompt section. +// SectionOverride defines an override operation for a single system message section. type SectionOverride struct { // Action is the operation to perform: "replace", "remove", "append", "prepend", or "transform". Action SectionOverrideAction `json:"action,omitempty"` @@ -268,42 +283,17 @@ type SystemMessageConfig struct { Sections map[string]SectionOverride `json:"sections,omitempty"` } -// PermissionRequestResultKind represents the kind of a permission request result. -type PermissionRequestResultKind string - -const ( - // PermissionRequestResultKindApproved indicates the permission was approved for this one instance. - PermissionRequestResultKindApproved PermissionRequestResultKind = "approve-once" - - // PermissionRequestResultKindRejected indicates the permission was denied interactively by the user. - PermissionRequestResultKindRejected PermissionRequestResultKind = "reject" - - // PermissionRequestResultKindUserNotAvailable indicates the permission was denied because - // user confirmation was unavailable. - PermissionRequestResultKindUserNotAvailable PermissionRequestResultKind = "user-not-available" - - // PermissionRequestResultKindNoResult indicates no permission decision was made. - PermissionRequestResultKindNoResult PermissionRequestResultKind = "no-result" - - // Deprecated: Use PermissionRequestResultKindRejected instead. - PermissionRequestResultKindDeniedInteractivelyByUser = PermissionRequestResultKindRejected - - // Deprecated: Use PermissionRequestResultKindUserNotAvailable instead. - PermissionRequestResultKindDeniedCouldNotRequestFromUser = PermissionRequestResultKindUserNotAvailable - - // Deprecated: Use PermissionRequestResultKindUserNotAvailable instead. - PermissionRequestResultKindDeniedByRules = PermissionRequestResultKindUserNotAvailable -) - -// PermissionRequestResult represents the result of a permission request -type PermissionRequestResult struct { - Kind PermissionRequestResultKind `json:"kind"` - Rules []any `json:"rules,omitempty"` -} - -// PermissionHandlerFunc executes a permission request -// The handler should return a PermissionRequestResult. Returning an error denies the permission. -type PermissionHandlerFunc func(request PermissionRequest, invocation PermissionInvocation) (PermissionRequestResult, error) +// PermissionHandlerFunc executes a permission request. +// The handler should return a [rpc.PermissionDecision]. Returning an error +// causes the SDK to respond with [rpc.PermissionDecisionUserNotAvailable]. +// +// Use the variant types directly: +// +// &rpc.PermissionDecisionApproveOnce{} +// &rpc.PermissionDecisionReject{Feedback: &feedback} +// &rpc.PermissionDecisionUserNotAvailable{} +// &rpc.PermissionDecisionNoResult{} // decline to respond; another client may answer +type PermissionHandlerFunc func(request PermissionRequest, invocation PermissionInvocation) (rpc.PermissionDecision, error) // PermissionInvocation provides context about a permission request type PermissionInvocation struct { @@ -937,6 +927,21 @@ type SessionConfig struct { // Cloud creates a remote session in the cloud instead of a local session. // The optional repository is associated with the cloud session. Cloud *CloudSessionOptions + // Canvases declares canvases this session provides. Sent over the wire on + // `session.create`. CanvasHandler must be set when this is non-empty (the + // SDK does not enforce this — declarations without a handler will surface + // canvas RPCs that return a canvas_handler_unset error envelope). + Canvases []CanvasDeclaration + // RequestCanvasRenderer asks the host to enable canvas rendering for this session. + RequestCanvasRenderer *bool + // RequestExtensions asks the host to surface declared canvases as agent-visible extensions. + RequestExtensions *bool + // CanvasHandler receives inbound canvas.open / canvas.close / canvas.action.invoke + // requests for this session. The SDK does not maintain a per-canvas registry; + // the handler must dispatch on CanvasOpenContext.CanvasID itself. + CanvasHandler CanvasHandler `json:"-"` + // ExtensionInfo identifies the stable extension providing this session's canvases. + ExtensionInfo *ExtensionInfo } type Tool struct { Name string `json:"name"` @@ -1185,6 +1190,21 @@ type ResumeSessionConfig struct { // OnAutoModeSwitchRequest is a handler for auto-mode-switch requests from the server. // See SessionConfig.OnAutoModeSwitchRequest. OnAutoModeSwitchRequest AutoModeSwitchRequestHandler + // Canvases declares canvases this session provides. Sent over the wire on + // `session.resume`. See SessionConfig.Canvases. + Canvases []CanvasDeclaration + // OpenCanvases declares canvas instances the caller knows were open before + // this resume so the runtime can re-attach them. Sent over the wire on + // `session.resume` as `openCanvases`. + OpenCanvases []rpc.OpenCanvasInstance + // RequestCanvasRenderer asks the host to enable canvas rendering for this session. + RequestCanvasRenderer *bool + // RequestExtensions asks the host to surface declared canvases as agent-visible extensions. + RequestExtensions *bool + // CanvasHandler receives inbound canvas.* requests for this session. See SessionConfig.CanvasHandler. + CanvasHandler CanvasHandler `json:"-"` + // ExtensionInfo identifies the stable extension providing this session's canvases. + ExtensionInfo *ExtensionInfo } type ProviderConfig struct { // Type is the provider type: "openai", "azure", or "anthropic". Defaults to "openai". @@ -1409,6 +1429,10 @@ type createSessionRequest struct { GitHubToken string `json:"gitHubToken,omitempty"` RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` Cloud *CloudSessionOptions `json:"cloud,omitempty"` + Canvases []CanvasDeclaration `json:"canvases,omitempty"` + RequestCanvasRenderer *bool `json:"requestCanvasRenderer,omitempty"` + RequestExtensions *bool `json:"requestExtensions,omitempty"` + ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` } @@ -1464,15 +1488,21 @@ type resumeSessionRequest struct { RequestElicitation *bool `json:"requestElicitation,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` + Canvases []CanvasDeclaration `json:"canvases,omitempty"` + OpenCanvases []rpc.OpenCanvasInstance `json:"openCanvases,omitempty"` + RequestCanvasRenderer *bool `json:"requestCanvasRenderer,omitempty"` + RequestExtensions *bool `json:"requestExtensions,omitempty"` + ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` } // resumeSessionResponse is the response from session.resume type resumeSessionResponse struct { - SessionID string `json:"sessionId"` - WorkspacePath string `json:"workspacePath"` - Capabilities *SessionCapabilities `json:"capabilities,omitempty"` + SessionID string `json:"sessionId"` + WorkspacePath string `json:"workspacePath"` + Capabilities *SessionCapabilities `json:"capabilities,omitempty"` + OpenCanvases []rpc.OpenCanvasInstance `json:"openCanvases,omitempty"` } type hooksInvokeRequest struct { diff --git a/go/types_test.go b/go/types_test.go index 1d201d2b8..6d83c8ec0 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -5,96 +5,6 @@ import ( "testing" ) -func TestPermissionRequestResultKind_Constants(t *testing.T) { - tests := []struct { - name string - kind PermissionRequestResultKind - expected string - }{ - {"Approved", PermissionRequestResultKindApproved, "approve-once"}, - {"Rejected", PermissionRequestResultKindRejected, "reject"}, - {"UserNotAvailable", PermissionRequestResultKindUserNotAvailable, "user-not-available"}, - {"NoResult", PermissionRequestResultKindNoResult, "no-result"}, - // Deprecated aliases - {"DeprecatedDeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser, "reject"}, - {"DeprecatedDeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser, "user-not-available"}, - {"DeprecatedDeniedByRules", PermissionRequestResultKindDeniedByRules, "user-not-available"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if string(tt.kind) != tt.expected { - t.Errorf("expected %q, got %q", tt.expected, string(tt.kind)) - } - }) - } -} - -func TestPermissionRequestResultKind_CustomValue(t *testing.T) { - custom := PermissionRequestResultKind("custom-kind") - if string(custom) != "custom-kind" { - t.Errorf("expected %q, got %q", "custom-kind", string(custom)) - } -} - -func TestPermissionRequestResult_JSONRoundTrip(t *testing.T) { - tests := []struct { - name string - kind PermissionRequestResultKind - }{ - {"Approved", PermissionRequestResultKindApproved}, - {"DeniedByRules", PermissionRequestResultKindDeniedByRules}, - {"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser}, - {"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser}, - {"NoResult", PermissionRequestResultKind("no-result")}, - {"Custom", PermissionRequestResultKind("custom")}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - original := PermissionRequestResult{Kind: tt.kind} - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("failed to marshal: %v", err) - } - - var decoded PermissionRequestResult - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } - - if decoded.Kind != tt.kind { - t.Errorf("expected kind %q, got %q", tt.kind, decoded.Kind) - } - }) - } -} - -func TestPermissionRequestResult_JSONDeserialize(t *testing.T) { - jsonStr := `{"kind":"reject"}` - var result PermissionRequestResult - if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } - - if result.Kind != PermissionRequestResultKindRejected { - t.Errorf("expected %q, got %q", PermissionRequestResultKindRejected, result.Kind) - } -} - -func TestPermissionRequestResult_JSONSerialize(t *testing.T) { - result := PermissionRequestResult{Kind: PermissionRequestResultKindApproved} - data, err := json.Marshal(result) - if err != nil { - t.Fatalf("failed to marshal: %v", err) - } - - expected := `{"kind":"approve-once"}` - if string(data) != expected { - t.Errorf("expected %s, got %s", expected, string(data)) - } -} - func TestProviderConfig_JSONIncludesHeaders(t *testing.T) { config := ProviderConfig{ BaseURL: "https://example.com/provider", diff --git a/go/zsession_events.go b/go/zsession_events.go index b9cd90a83..7f99e6eac 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -7,243 +7,265 @@ import "github.com/github/copilot-sdk/go/rpc" // Session-event types are generated in the rpc package and aliased here for source compatibility. type ( - AbortData = rpc.AbortData - AbortReason = rpc.AbortReason - AssistantIntentData = rpc.AssistantIntentData - AssistantMessageData = rpc.AssistantMessageData - AssistantMessageDeltaData = rpc.AssistantMessageDeltaData - AssistantMessageStartData = rpc.AssistantMessageStartData - AssistantMessageToolRequest = rpc.AssistantMessageToolRequest - AssistantMessageToolRequestType = rpc.AssistantMessageToolRequestType - AssistantReasoningData = rpc.AssistantReasoningData - AssistantReasoningDeltaData = rpc.AssistantReasoningDeltaData - AssistantStreamingDeltaData = rpc.AssistantStreamingDeltaData - AssistantTurnEndData = rpc.AssistantTurnEndData - AssistantTurnStartData = rpc.AssistantTurnStartData - AssistantUsageAPIEndpoint = rpc.AssistantUsageAPIEndpoint - AssistantUsageCopilotUsage = rpc.AssistantUsageCopilotUsage - AssistantUsageCopilotUsageTokenDetail = rpc.AssistantUsageCopilotUsageTokenDetail - AssistantUsageData = rpc.AssistantUsageData - AssistantUsageQuotaSnapshot = rpc.AssistantUsageQuotaSnapshot - Attachment = rpc.Attachment - AttachmentType = rpc.AttachmentType - AutoModeSwitchCompletedData = rpc.AutoModeSwitchCompletedData - AutoModeSwitchRequestedData = rpc.AutoModeSwitchRequestedData - AutoModeSwitchResponse = rpc.AutoModeSwitchResponse - CapabilitiesChangedData = rpc.CapabilitiesChangedData - CapabilitiesChangedUI = rpc.CapabilitiesChangedUI - CommandCompletedData = rpc.CommandCompletedData - CommandExecuteData = rpc.CommandExecuteData - CommandQueuedData = rpc.CommandQueuedData - CommandsChangedCommand = rpc.CommandsChangedCommand - CommandsChangedData = rpc.CommandsChangedData - CompactionCompleteCompactionTokensUsed = rpc.CompactionCompleteCompactionTokensUsed - CompactionCompleteCompactionTokensUsedCopilotUsage = rpc.CompactionCompleteCompactionTokensUsedCopilotUsage - CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail = rpc.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail - CustomAgentsUpdatedAgent = rpc.CustomAgentsUpdatedAgent - CustomNotificationPayload = rpc.CustomNotificationPayload - ElicitationCompletedAction = rpc.ElicitationCompletedAction - ElicitationCompletedBooleanContent = rpc.ElicitationCompletedBooleanContent - ElicitationCompletedContent = rpc.ElicitationCompletedContent - ElicitationCompletedData = rpc.ElicitationCompletedData - ElicitationCompletedNumberContent = rpc.ElicitationCompletedNumberContent - ElicitationCompletedStringArrayContent = rpc.ElicitationCompletedStringArrayContent - ElicitationCompletedStringContent = rpc.ElicitationCompletedStringContent - ElicitationRequestedData = rpc.ElicitationRequestedData - ElicitationRequestedMode = rpc.ElicitationRequestedMode - ElicitationRequestedSchema = rpc.ElicitationRequestedSchema - ElicitationRequestedSchemaType = rpc.ElicitationRequestedSchemaType - EmbeddedBlobResourceContents = rpc.EmbeddedBlobResourceContents - EmbeddedTextResourceContents = rpc.EmbeddedTextResourceContents - ExitPlanModeAction = rpc.ExitPlanModeAction - ExitPlanModeCompletedData = rpc.ExitPlanModeCompletedData - ExitPlanModeRequestedData = rpc.ExitPlanModeRequestedData - ExtensionsLoadedExtension = rpc.ExtensionsLoadedExtension - ExtensionsLoadedExtensionSource = rpc.ExtensionsLoadedExtensionSource - ExtensionsLoadedExtensionStatus = rpc.ExtensionsLoadedExtensionStatus - ExternalToolCompletedData = rpc.ExternalToolCompletedData - ExternalToolRequestedData = rpc.ExternalToolRequestedData - HandoffRepository = rpc.HandoffRepository - HandoffSourceType = rpc.HandoffSourceType - HookEndData = rpc.HookEndData - HookEndError = rpc.HookEndError - HookStartData = rpc.HookStartData - McpOauthCompletedData = rpc.McpOauthCompletedData - McpOauthRequiredData = rpc.McpOauthRequiredData - McpOauthRequiredStaticClientConfig = rpc.McpOauthRequiredStaticClientConfig - McpOauthRequiredStaticClientConfigGrantType = rpc.McpOauthRequiredStaticClientConfigGrantType - McpServersLoadedServer = rpc.McpServersLoadedServer - McpServerSource = rpc.McpServerSource - McpServerStatus = rpc.McpServerStatus - ModelCallFailureData = rpc.ModelCallFailureData - ModelCallFailureSource = rpc.ModelCallFailureSource - PendingMessagesModifiedData = rpc.PendingMessagesModifiedData - PermissionApproved = rpc.PermissionApproved - PermissionApprovedForLocation = rpc.PermissionApprovedForLocation - PermissionApprovedForSession = rpc.PermissionApprovedForSession - PermissionCancelled = rpc.PermissionCancelled - PermissionCompletedData = rpc.PermissionCompletedData - PermissionDeniedByContentExclusionPolicy = rpc.PermissionDeniedByContentExclusionPolicy - PermissionDeniedByPermissionRequestHook = rpc.PermissionDeniedByPermissionRequestHook - PermissionDeniedByRules = rpc.PermissionDeniedByRules - PermissionDeniedInteractivelyByUser = rpc.PermissionDeniedInteractivelyByUser - PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser = rpc.PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser - PermissionPromptRequest = rpc.PermissionPromptRequest - PermissionPromptRequestCommands = rpc.PermissionPromptRequestCommands - PermissionPromptRequestCustomTool = rpc.PermissionPromptRequestCustomTool - PermissionPromptRequestExtensionManagement = rpc.PermissionPromptRequestExtensionManagement - PermissionPromptRequestExtensionPermissionAccess = rpc.PermissionPromptRequestExtensionPermissionAccess - PermissionPromptRequestHook = rpc.PermissionPromptRequestHook - PermissionPromptRequestKind = rpc.PermissionPromptRequestKind - PermissionPromptRequestMcp = rpc.PermissionPromptRequestMcp - PermissionPromptRequestMemory = rpc.PermissionPromptRequestMemory - PermissionPromptRequestPath = rpc.PermissionPromptRequestPath - PermissionPromptRequestPathAccessKind = rpc.PermissionPromptRequestPathAccessKind - PermissionPromptRequestRead = rpc.PermissionPromptRequestRead - PermissionPromptRequestURL = rpc.PermissionPromptRequestURL - PermissionPromptRequestWrite = rpc.PermissionPromptRequestWrite - PermissionRequest = rpc.PermissionRequest - PermissionRequestCommand = rpc.PermissionRequestCommand - PermissionRequestCustomTool = rpc.PermissionRequestCustomTool - PermissionRequestedData = rpc.PermissionRequestedData - PermissionRequestExtensionManagement = rpc.PermissionRequestExtensionManagement - PermissionRequestExtensionPermissionAccess = rpc.PermissionRequestExtensionPermissionAccess - PermissionRequestHook = rpc.PermissionRequestHook - PermissionRequestKind = rpc.PermissionRequestKind - PermissionRequestMcp = rpc.PermissionRequestMcp - PermissionRequestMemory = rpc.PermissionRequestMemory - PermissionRequestMemoryAction = rpc.PermissionRequestMemoryAction - PermissionRequestMemoryDirection = rpc.PermissionRequestMemoryDirection - PermissionRequestRead = rpc.PermissionRequestRead - PermissionRequestShell = rpc.PermissionRequestShell - PermissionRequestShellCommand = rpc.PermissionRequestShellCommand - PermissionRequestShellPossibleURL = rpc.PermissionRequestShellPossibleURL - PermissionRequestURL = rpc.PermissionRequestURL - PermissionRequestWrite = rpc.PermissionRequestWrite - PermissionResult = rpc.PermissionResult - PermissionResultKind = rpc.PermissionResultKind - PermissionRule = rpc.PermissionRule - PlanChangedOperation = rpc.PlanChangedOperation - PossibleURL = rpc.PossibleURL - RawPermissionPromptRequest = rpc.RawPermissionPromptRequest - RawPermissionRequest = rpc.RawPermissionRequest - RawPermissionResult = rpc.RawPermissionResult - RawSessionEventData = rpc.RawSessionEventData - RawSystemNotification = rpc.RawSystemNotification - RawToolExecutionCompleteContent = rpc.RawToolExecutionCompleteContent - RawUserMessageAttachment = rpc.RawUserMessageAttachment - ReasoningSummary = rpc.ReasoningSummary - SamplingCompletedData = rpc.SamplingCompletedData - SamplingRequestedData = rpc.SamplingRequestedData - SessionBackgroundTasksChangedData = rpc.SessionBackgroundTasksChangedData - SessionCompactionCompleteData = rpc.SessionCompactionCompleteData - SessionCompactionStartData = rpc.SessionCompactionStartData - SessionContextChangedData = rpc.SessionContextChangedData - SessionCustomAgentsUpdatedData = rpc.SessionCustomAgentsUpdatedData - SessionCustomNotificationData = rpc.SessionCustomNotificationData - SessionErrorData = rpc.SessionErrorData - SessionEvent = rpc.SessionEvent - SessionEventData = rpc.SessionEventData - SessionEventType = rpc.SessionEventType - SessionExtensionsLoadedData = rpc.SessionExtensionsLoadedData - SessionHandoffData = rpc.SessionHandoffData - SessionIdleData = rpc.SessionIdleData - SessionInfoData = rpc.SessionInfoData - SessionMcpServersLoadedData = rpc.SessionMcpServersLoadedData - SessionMcpServerStatusChangedData = rpc.SessionMcpServerStatusChangedData - SessionMode = rpc.SessionMode - SessionModeChangedData = rpc.SessionModeChangedData - SessionModelChangeData = rpc.SessionModelChangeData - SessionPlanChangedData = rpc.SessionPlanChangedData - SessionRemoteSteerableChangedData = rpc.SessionRemoteSteerableChangedData - SessionResumeData = rpc.SessionResumeData - SessionScheduleCancelledData = rpc.SessionScheduleCancelledData - SessionScheduleCreatedData = rpc.SessionScheduleCreatedData - SessionShutdownData = rpc.SessionShutdownData - SessionSkillsLoadedData = rpc.SessionSkillsLoadedData - SessionSnapshotRewindData = rpc.SessionSnapshotRewindData - SessionStartData = rpc.SessionStartData - SessionTaskCompleteData = rpc.SessionTaskCompleteData - SessionTitleChangedData = rpc.SessionTitleChangedData - SessionToolsUpdatedData = rpc.SessionToolsUpdatedData - SessionTruncationData = rpc.SessionTruncationData - SessionUsageInfoData = rpc.SessionUsageInfoData - SessionWarningData = rpc.SessionWarningData - SessionWorkspaceFileChangedData = rpc.SessionWorkspaceFileChangedData - ShutdownCodeChanges = rpc.ShutdownCodeChanges - ShutdownModelMetric = rpc.ShutdownModelMetric - ShutdownModelMetricRequests = rpc.ShutdownModelMetricRequests - ShutdownModelMetricTokenDetail = rpc.ShutdownModelMetricTokenDetail - ShutdownModelMetricUsage = rpc.ShutdownModelMetricUsage - ShutdownTokenDetail = rpc.ShutdownTokenDetail - ShutdownType = rpc.ShutdownType - SkillInvokedData = rpc.SkillInvokedData - SkillsLoadedSkill = rpc.SkillsLoadedSkill - SkillSource = rpc.SkillSource - SubagentCompletedData = rpc.SubagentCompletedData - SubagentDeselectedData = rpc.SubagentDeselectedData - SubagentFailedData = rpc.SubagentFailedData - SubagentSelectedData = rpc.SubagentSelectedData - SubagentStartedData = rpc.SubagentStartedData - SystemMessageData = rpc.SystemMessageData - SystemMessageMetadata = rpc.SystemMessageMetadata - SystemMessageRole = rpc.SystemMessageRole - SystemNotification = rpc.SystemNotification - SystemNotificationAgentCompleted = rpc.SystemNotificationAgentCompleted - SystemNotificationAgentCompletedStatus = rpc.SystemNotificationAgentCompletedStatus - SystemNotificationAgentIdle = rpc.SystemNotificationAgentIdle - SystemNotificationData = rpc.SystemNotificationData - SystemNotificationInstructionDiscovered = rpc.SystemNotificationInstructionDiscovered - SystemNotificationNewInboxMessage = rpc.SystemNotificationNewInboxMessage - SystemNotificationShellCompleted = rpc.SystemNotificationShellCompleted - SystemNotificationShellDetachedCompleted = rpc.SystemNotificationShellDetachedCompleted - SystemNotificationType = rpc.SystemNotificationType - ToolExecutionCompleteContent = rpc.ToolExecutionCompleteContent - ToolExecutionCompleteContentAudio = rpc.ToolExecutionCompleteContentAudio - ToolExecutionCompleteContentImage = rpc.ToolExecutionCompleteContentImage - ToolExecutionCompleteContentResource = rpc.ToolExecutionCompleteContentResource - ToolExecutionCompleteContentResourceDetails = rpc.ToolExecutionCompleteContentResourceDetails - ToolExecutionCompleteContentResourceLink = rpc.ToolExecutionCompleteContentResourceLink - ToolExecutionCompleteContentResourceLinkIcon = rpc.ToolExecutionCompleteContentResourceLinkIcon - ToolExecutionCompleteContentResourceLinkIconTheme = rpc.ToolExecutionCompleteContentResourceLinkIconTheme - ToolExecutionCompleteContentTerminal = rpc.ToolExecutionCompleteContentTerminal - ToolExecutionCompleteContentText = rpc.ToolExecutionCompleteContentText - ToolExecutionCompleteContentType = rpc.ToolExecutionCompleteContentType - ToolExecutionCompleteData = rpc.ToolExecutionCompleteData - ToolExecutionCompleteError = rpc.ToolExecutionCompleteError - ToolExecutionCompleteResult = rpc.ToolExecutionCompleteResult - ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData - ToolExecutionProgressData = rpc.ToolExecutionProgressData - ToolExecutionStartData = rpc.ToolExecutionStartData - ToolUserRequestedData = rpc.ToolUserRequestedData - UserInputCompletedData = rpc.UserInputCompletedData - UserInputRequestedData = rpc.UserInputRequestedData - UserMessageAgentMode = rpc.UserMessageAgentMode - UserMessageAttachment = rpc.UserMessageAttachment - UserMessageAttachmentBlob = rpc.UserMessageAttachmentBlob - UserMessageAttachmentDirectory = rpc.UserMessageAttachmentDirectory - UserMessageAttachmentFile = rpc.UserMessageAttachmentFile - UserMessageAttachmentFileLineRange = rpc.UserMessageAttachmentFileLineRange - UserMessageAttachmentGithubReference = rpc.UserMessageAttachmentGithubReference - UserMessageAttachmentGithubReferenceType = rpc.UserMessageAttachmentGithubReferenceType - UserMessageAttachmentSelection = rpc.UserMessageAttachmentSelection - UserMessageAttachmentSelectionDetails = rpc.UserMessageAttachmentSelectionDetails - UserMessageAttachmentSelectionDetailsEnd = rpc.UserMessageAttachmentSelectionDetailsEnd - UserMessageAttachmentSelectionDetailsStart = rpc.UserMessageAttachmentSelectionDetailsStart - UserMessageAttachmentType = rpc.UserMessageAttachmentType - UserMessageData = rpc.UserMessageData - UserToolSessionApproval = rpc.UserToolSessionApproval - UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands - UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool - UserToolSessionApprovalExtensionManagement = rpc.UserToolSessionApprovalExtensionManagement - UserToolSessionApprovalExtensionPermissionAccess = rpc.UserToolSessionApprovalExtensionPermissionAccess - UserToolSessionApprovalMcp = rpc.UserToolSessionApprovalMcp - UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory - UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead - UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite - WorkingDirectoryContext = rpc.WorkingDirectoryContext - WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType - WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation + AbortData = rpc.AbortData + AbortReason = rpc.AbortReason + AssistantIntentData = rpc.AssistantIntentData + AssistantMessageData = rpc.AssistantMessageData + AssistantMessageDeltaData = rpc.AssistantMessageDeltaData + AssistantMessageStartData = rpc.AssistantMessageStartData + AssistantMessageToolRequest = rpc.AssistantMessageToolRequest + AssistantMessageToolRequestType = rpc.AssistantMessageToolRequestType + AssistantReasoningData = rpc.AssistantReasoningData + AssistantReasoningDeltaData = rpc.AssistantReasoningDeltaData + AssistantStreamingDeltaData = rpc.AssistantStreamingDeltaData + AssistantTurnEndData = rpc.AssistantTurnEndData + AssistantTurnStartData = rpc.AssistantTurnStartData + AssistantUsageAPIEndpoint = rpc.AssistantUsageAPIEndpoint + AssistantUsageCopilotUsageTokenDetail = rpc.AssistantUsageCopilotUsageTokenDetail + AssistantUsageData = rpc.AssistantUsageData + Attachment = rpc.Attachment + AttachmentType = rpc.AttachmentType + AutoModeSwitchCompletedData = rpc.AutoModeSwitchCompletedData + AutoModeSwitchRequestedData = rpc.AutoModeSwitchRequestedData + AutoModeSwitchResponse = rpc.AutoModeSwitchResponse + CanvasOpenedAvailability = rpc.CanvasOpenedAvailability + CanvasRegistryChangedCanvas = rpc.CanvasRegistryChangedCanvas + CanvasRegistryChangedCanvasAction = rpc.CanvasRegistryChangedCanvasAction + CapabilitiesChangedData = rpc.CapabilitiesChangedData + CapabilitiesChangedUI = rpc.CapabilitiesChangedUI + CommandCompletedData = rpc.CommandCompletedData + CommandExecuteData = rpc.CommandExecuteData + CommandQueuedData = rpc.CommandQueuedData + CommandsChangedCommand = rpc.CommandsChangedCommand + CommandsChangedData = rpc.CommandsChangedData + CompactionCompleteCompactionTokensUsed = rpc.CompactionCompleteCompactionTokensUsed + CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail = rpc.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail + CustomAgentsUpdatedAgent = rpc.CustomAgentsUpdatedAgent + CustomNotificationPayload = rpc.CustomNotificationPayload + ElicitationCompletedAction = rpc.ElicitationCompletedAction + ElicitationCompletedBooleanContent = rpc.ElicitationCompletedBooleanContent + ElicitationCompletedContent = rpc.ElicitationCompletedContent + ElicitationCompletedData = rpc.ElicitationCompletedData + ElicitationCompletedNumberContent = rpc.ElicitationCompletedNumberContent + ElicitationCompletedStringArrayContent = rpc.ElicitationCompletedStringArrayContent + ElicitationCompletedStringContent = rpc.ElicitationCompletedStringContent + ElicitationRequestedData = rpc.ElicitationRequestedData + ElicitationRequestedMode = rpc.ElicitationRequestedMode + ElicitationRequestedSchema = rpc.ElicitationRequestedSchema + ElicitationRequestedSchemaType = rpc.ElicitationRequestedSchemaType + EmbeddedBlobResourceContents = rpc.EmbeddedBlobResourceContents + EmbeddedTextResourceContents = rpc.EmbeddedTextResourceContents + ExitPlanModeAction = rpc.ExitPlanModeAction + ExitPlanModeCompletedData = rpc.ExitPlanModeCompletedData + ExitPlanModeRequestedData = rpc.ExitPlanModeRequestedData + ExtensionsLoadedExtension = rpc.ExtensionsLoadedExtension + ExtensionsLoadedExtensionSource = rpc.ExtensionsLoadedExtensionSource + ExtensionsLoadedExtensionStatus = rpc.ExtensionsLoadedExtensionStatus + ExternalToolCompletedData = rpc.ExternalToolCompletedData + ExternalToolRequestedData = rpc.ExternalToolRequestedData + HandoffRepository = rpc.HandoffRepository + HandoffSourceType = rpc.HandoffSourceType + HookEndData = rpc.HookEndData + HookEndError = rpc.HookEndError + HookStartData = rpc.HookStartData + McpAppToolCallCompleteData = rpc.McpAppToolCallCompleteData + McpAppToolCallCompleteError = rpc.McpAppToolCallCompleteError + McpAppToolCallCompleteToolMeta = rpc.McpAppToolCallCompleteToolMeta + McpAppToolCallCompleteToolMetaUI = rpc.McpAppToolCallCompleteToolMetaUI + McpOauthCompletedData = rpc.McpOauthCompletedData + McpOauthRequiredData = rpc.McpOauthRequiredData + McpOauthRequiredStaticClientConfig = rpc.McpOauthRequiredStaticClientConfig + McpOauthRequiredStaticClientConfigGrantType = rpc.McpOauthRequiredStaticClientConfigGrantType + McpServersLoadedServer = rpc.McpServersLoadedServer + McpServerSource = rpc.McpServerSource + McpServerStatus = rpc.McpServerStatus + McpServerTransport = rpc.McpServerTransport + ModelCallFailureData = rpc.ModelCallFailureData + ModelCallFailureSource = rpc.ModelCallFailureSource + PendingMessagesModifiedData = rpc.PendingMessagesModifiedData + PermissionApproved = rpc.PermissionApproved + PermissionApprovedForLocation = rpc.PermissionApprovedForLocation + PermissionApprovedForSession = rpc.PermissionApprovedForSession + PermissionCancelled = rpc.PermissionCancelled + PermissionCompletedData = rpc.PermissionCompletedData + PermissionDeniedByContentExclusionPolicy = rpc.PermissionDeniedByContentExclusionPolicy + PermissionDeniedByPermissionRequestHook = rpc.PermissionDeniedByPermissionRequestHook + PermissionDeniedByRules = rpc.PermissionDeniedByRules + PermissionDeniedInteractivelyByUser = rpc.PermissionDeniedInteractivelyByUser + PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser = rpc.PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser + PermissionPromptRequest = rpc.PermissionPromptRequest + PermissionPromptRequestCommands = rpc.PermissionPromptRequestCommands + PermissionPromptRequestCustomTool = rpc.PermissionPromptRequestCustomTool + PermissionPromptRequestExtensionManagement = rpc.PermissionPromptRequestExtensionManagement + PermissionPromptRequestExtensionPermissionAccess = rpc.PermissionPromptRequestExtensionPermissionAccess + PermissionPromptRequestHook = rpc.PermissionPromptRequestHook + PermissionPromptRequestKind = rpc.PermissionPromptRequestKind + PermissionPromptRequestMcp = rpc.PermissionPromptRequestMcp + PermissionPromptRequestMemory = rpc.PermissionPromptRequestMemory + PermissionPromptRequestPath = rpc.PermissionPromptRequestPath + PermissionPromptRequestPathAccessKind = rpc.PermissionPromptRequestPathAccessKind + PermissionPromptRequestRead = rpc.PermissionPromptRequestRead + PermissionPromptRequestURL = rpc.PermissionPromptRequestURL + PermissionPromptRequestWrite = rpc.PermissionPromptRequestWrite + PermissionRequest = rpc.PermissionRequest + PermissionRequestCommand = rpc.PermissionRequestCommand + PermissionRequestCustomTool = rpc.PermissionRequestCustomTool + PermissionRequestedData = rpc.PermissionRequestedData + PermissionRequestExtensionManagement = rpc.PermissionRequestExtensionManagement + PermissionRequestExtensionPermissionAccess = rpc.PermissionRequestExtensionPermissionAccess + PermissionRequestHook = rpc.PermissionRequestHook + PermissionRequestKind = rpc.PermissionRequestKind + PermissionRequestMcp = rpc.PermissionRequestMcp + PermissionRequestMemory = rpc.PermissionRequestMemory + PermissionRequestMemoryAction = rpc.PermissionRequestMemoryAction + PermissionRequestMemoryDirection = rpc.PermissionRequestMemoryDirection + PermissionRequestRead = rpc.PermissionRequestRead + PermissionRequestShell = rpc.PermissionRequestShell + PermissionRequestShellCommand = rpc.PermissionRequestShellCommand + PermissionRequestShellPossibleURL = rpc.PermissionRequestShellPossibleURL + PermissionRequestURL = rpc.PermissionRequestURL + PermissionRequestWrite = rpc.PermissionRequestWrite + PermissionResult = rpc.PermissionResult + PermissionResultKind = rpc.PermissionResultKind + PermissionRule = rpc.PermissionRule + PlanChangedOperation = rpc.PlanChangedOperation + PossibleURL = rpc.PossibleURL + RawPermissionPromptRequest = rpc.RawPermissionPromptRequest + RawPermissionRequest = rpc.RawPermissionRequest + RawPermissionResult = rpc.RawPermissionResult + RawSessionEventData = rpc.RawSessionEventData + RawSystemNotification = rpc.RawSystemNotification + RawToolExecutionCompleteContent = rpc.RawToolExecutionCompleteContent + RawUserMessageAttachment = rpc.RawUserMessageAttachment + ReasoningSummary = rpc.ReasoningSummary + SamplingCompletedData = rpc.SamplingCompletedData + SamplingRequestedData = rpc.SamplingRequestedData + SessionBackgroundTasksChangedData = rpc.SessionBackgroundTasksChangedData + SessionCanvasOpenedData = rpc.SessionCanvasOpenedData + SessionCanvasRegistryChangedData = rpc.SessionCanvasRegistryChangedData + SessionCompactionCompleteData = rpc.SessionCompactionCompleteData + SessionCompactionStartData = rpc.SessionCompactionStartData + SessionContextChangedData = rpc.SessionContextChangedData + SessionCustomAgentsUpdatedData = rpc.SessionCustomAgentsUpdatedData + SessionCustomNotificationData = rpc.SessionCustomNotificationData + SessionErrorData = rpc.SessionErrorData + SessionEvent = rpc.SessionEvent + SessionEventData = rpc.SessionEventData + SessionEventType = rpc.SessionEventType + SessionExtensionsLoadedData = rpc.SessionExtensionsLoadedData + SessionHandoffData = rpc.SessionHandoffData + SessionIdleData = rpc.SessionIdleData + SessionInfoData = rpc.SessionInfoData + SessionMcpServersLoadedData = rpc.SessionMcpServersLoadedData + SessionMcpServerStatusChangedData = rpc.SessionMcpServerStatusChangedData + SessionMode = rpc.SessionMode + SessionModeChangedData = rpc.SessionModeChangedData + SessionModelChangeData = rpc.SessionModelChangeData + SessionModelChangeDataContextTier = rpc.SessionModelChangeDataContextTier + SessionPlanChangedData = rpc.SessionPlanChangedData + SessionRemoteSteerableChangedData = rpc.SessionRemoteSteerableChangedData + SessionResumeData = rpc.SessionResumeData + SessionScheduleCancelledData = rpc.SessionScheduleCancelledData + SessionScheduleCreatedData = rpc.SessionScheduleCreatedData + SessionShutdownData = rpc.SessionShutdownData + SessionSkillsLoadedData = rpc.SessionSkillsLoadedData + SessionSnapshotRewindData = rpc.SessionSnapshotRewindData + SessionStartData = rpc.SessionStartData + SessionTaskCompleteData = rpc.SessionTaskCompleteData + SessionTitleChangedData = rpc.SessionTitleChangedData + SessionToolsUpdatedData = rpc.SessionToolsUpdatedData + SessionTruncationData = rpc.SessionTruncationData + SessionUsageInfoData = rpc.SessionUsageInfoData + SessionWarningData = rpc.SessionWarningData + SessionWorkspaceFileChangedData = rpc.SessionWorkspaceFileChangedData + ShutdownCodeChanges = rpc.ShutdownCodeChanges + ShutdownModelMetric = rpc.ShutdownModelMetric + ShutdownModelMetricRequests = rpc.ShutdownModelMetricRequests + ShutdownModelMetricTokenDetail = rpc.ShutdownModelMetricTokenDetail + ShutdownModelMetricUsage = rpc.ShutdownModelMetricUsage + ShutdownTokenDetail = rpc.ShutdownTokenDetail + ShutdownType = rpc.ShutdownType + SkillInvokedData = rpc.SkillInvokedData + SkillInvokedTrigger = rpc.SkillInvokedTrigger + SkillsLoadedSkill = rpc.SkillsLoadedSkill + SkillSource = rpc.SkillSource + SubagentCompletedData = rpc.SubagentCompletedData + SubagentDeselectedData = rpc.SubagentDeselectedData + SubagentFailedData = rpc.SubagentFailedData + SubagentSelectedData = rpc.SubagentSelectedData + SubagentStartedData = rpc.SubagentStartedData + SystemMessageData = rpc.SystemMessageData + SystemMessageMetadata = rpc.SystemMessageMetadata + SystemMessageRole = rpc.SystemMessageRole + SystemNotification = rpc.SystemNotification + SystemNotificationAgentCompleted = rpc.SystemNotificationAgentCompleted + SystemNotificationAgentCompletedStatus = rpc.SystemNotificationAgentCompletedStatus + SystemNotificationAgentIdle = rpc.SystemNotificationAgentIdle + SystemNotificationData = rpc.SystemNotificationData + SystemNotificationInstructionDiscovered = rpc.SystemNotificationInstructionDiscovered + SystemNotificationNewInboxMessage = rpc.SystemNotificationNewInboxMessage + SystemNotificationShellCompleted = rpc.SystemNotificationShellCompleted + SystemNotificationShellDetachedCompleted = rpc.SystemNotificationShellDetachedCompleted + SystemNotificationType = rpc.SystemNotificationType + ToolExecutionCompleteContent = rpc.ToolExecutionCompleteContent + ToolExecutionCompleteContentAudio = rpc.ToolExecutionCompleteContentAudio + ToolExecutionCompleteContentImage = rpc.ToolExecutionCompleteContentImage + ToolExecutionCompleteContentResource = rpc.ToolExecutionCompleteContentResource + ToolExecutionCompleteContentResourceDetails = rpc.ToolExecutionCompleteContentResourceDetails + ToolExecutionCompleteContentResourceLink = rpc.ToolExecutionCompleteContentResourceLink + ToolExecutionCompleteContentResourceLinkIcon = rpc.ToolExecutionCompleteContentResourceLinkIcon + ToolExecutionCompleteContentResourceLinkIconTheme = rpc.ToolExecutionCompleteContentResourceLinkIconTheme + ToolExecutionCompleteContentTerminal = rpc.ToolExecutionCompleteContentTerminal + ToolExecutionCompleteContentText = rpc.ToolExecutionCompleteContentText + ToolExecutionCompleteContentType = rpc.ToolExecutionCompleteContentType + ToolExecutionCompleteData = rpc.ToolExecutionCompleteData + ToolExecutionCompleteError = rpc.ToolExecutionCompleteError + ToolExecutionCompleteResult = rpc.ToolExecutionCompleteResult + ToolExecutionCompleteToolDescription = rpc.ToolExecutionCompleteToolDescription + ToolExecutionCompleteToolDescriptionMeta = rpc.ToolExecutionCompleteToolDescriptionMeta + ToolExecutionCompleteToolDescriptionMetaUI = rpc.ToolExecutionCompleteToolDescriptionMetaUI + ToolExecutionCompleteToolDescriptionMetaUIVisibility = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibility + ToolExecutionCompleteUIResource = rpc.ToolExecutionCompleteUIResource + ToolExecutionCompleteUIResourceMeta = rpc.ToolExecutionCompleteUIResourceMeta + ToolExecutionCompleteUIResourceMetaUI = rpc.ToolExecutionCompleteUIResourceMetaUI + ToolExecutionCompleteUIResourceMetaUICsp = rpc.ToolExecutionCompleteUIResourceMetaUICsp + ToolExecutionCompleteUIResourceMetaUIPermissions = rpc.ToolExecutionCompleteUIResourceMetaUIPermissions + ToolExecutionCompleteUIResourceMetaUIPermissionsCamera = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsCamera + ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite + ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation + ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone = rpc.ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone + ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData + ToolExecutionProgressData = rpc.ToolExecutionProgressData + ToolExecutionStartData = rpc.ToolExecutionStartData + ToolUserRequestedData = rpc.ToolUserRequestedData + UserInputCompletedData = rpc.UserInputCompletedData + UserInputRequestedData = rpc.UserInputRequestedData + UserMessageAgentMode = rpc.UserMessageAgentMode + UserMessageAttachment = rpc.UserMessageAttachment + UserMessageAttachmentBlob = rpc.UserMessageAttachmentBlob + UserMessageAttachmentDirectory = rpc.UserMessageAttachmentDirectory + UserMessageAttachmentFile = rpc.UserMessageAttachmentFile + UserMessageAttachmentFileLineRange = rpc.UserMessageAttachmentFileLineRange + UserMessageAttachmentGithubReference = rpc.UserMessageAttachmentGithubReference + UserMessageAttachmentGithubReferenceType = rpc.UserMessageAttachmentGithubReferenceType + UserMessageAttachmentSelection = rpc.UserMessageAttachmentSelection + UserMessageAttachmentSelectionDetails = rpc.UserMessageAttachmentSelectionDetails + UserMessageAttachmentSelectionDetailsEnd = rpc.UserMessageAttachmentSelectionDetailsEnd + UserMessageAttachmentSelectionDetailsStart = rpc.UserMessageAttachmentSelectionDetailsStart + UserMessageAttachmentType = rpc.UserMessageAttachmentType + UserMessageData = rpc.UserMessageData + UserToolSessionApproval = rpc.UserToolSessionApproval + UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands + UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool + UserToolSessionApprovalExtensionManagement = rpc.UserToolSessionApprovalExtensionManagement + UserToolSessionApprovalExtensionPermissionAccess = rpc.UserToolSessionApprovalExtensionPermissionAccess + UserToolSessionApprovalMcp = rpc.UserToolSessionApprovalMcp + UserToolSessionApprovalMemory = rpc.UserToolSessionApprovalMemory + UserToolSessionApprovalRead = rpc.UserToolSessionApprovalRead + UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite + WorkingDirectoryContext = rpc.WorkingDirectoryContext + WorkingDirectoryContextHostType = rpc.WorkingDirectoryContextHostType + WorkspaceFileChangedOperation = rpc.WorkspaceFileChangedOperation ) // Session-event constants are generated in the rpc package and re-exported here for source compatibility. @@ -265,6 +287,8 @@ const ( AutoModeSwitchResponseNo = rpc.AutoModeSwitchResponseNo AutoModeSwitchResponseYes = rpc.AutoModeSwitchResponseYes AutoModeSwitchResponseYesAlways = rpc.AutoModeSwitchResponseYesAlways + CanvasOpenedAvailabilityReady = rpc.CanvasOpenedAvailabilityReady + CanvasOpenedAvailabilityStale = rpc.CanvasOpenedAvailabilityStale ElicitationCompletedActionAccept = rpc.ElicitationCompletedActionAccept ElicitationCompletedActionCancel = rpc.ElicitationCompletedActionCancel ElicitationCompletedActionDecline = rpc.ElicitationCompletedActionDecline @@ -294,6 +318,10 @@ const ( McpServerStatusNeedsAuth = rpc.McpServerStatusNeedsAuth McpServerStatusNotConfigured = rpc.McpServerStatusNotConfigured McpServerStatusPending = rpc.McpServerStatusPending + McpServerTransportHTTP = rpc.McpServerTransportHTTP + McpServerTransportMemory = rpc.McpServerTransportMemory + McpServerTransportSse = rpc.McpServerTransportSse + McpServerTransportStdio = rpc.McpServerTransportStdio ModelCallFailureSourceMcpSampling = rpc.ModelCallFailureSourceMcpSampling ModelCallFailureSourceSubagent = rpc.ModelCallFailureSourceSubagent ModelCallFailureSourceTopLevel = rpc.ModelCallFailureSourceTopLevel @@ -366,6 +394,7 @@ const ( SessionEventTypeExternalToolRequested = rpc.SessionEventTypeExternalToolRequested SessionEventTypeHookEnd = rpc.SessionEventTypeHookEnd SessionEventTypeHookStart = rpc.SessionEventTypeHookStart + SessionEventTypeMcpAppToolCallComplete = rpc.SessionEventTypeMcpAppToolCallComplete SessionEventTypeMcpOauthCompleted = rpc.SessionEventTypeMcpOauthCompleted SessionEventTypeMcpOauthRequired = rpc.SessionEventTypeMcpOauthRequired SessionEventTypeModelCallFailure = rpc.SessionEventTypeModelCallFailure @@ -375,6 +404,8 @@ const ( SessionEventTypeSamplingCompleted = rpc.SessionEventTypeSamplingCompleted SessionEventTypeSamplingRequested = rpc.SessionEventTypeSamplingRequested SessionEventTypeSessionBackgroundTasksChanged = rpc.SessionEventTypeSessionBackgroundTasksChanged + SessionEventTypeSessionCanvasOpened = rpc.SessionEventTypeSessionCanvasOpened + SessionEventTypeSessionCanvasRegistryChanged = rpc.SessionEventTypeSessionCanvasRegistryChanged SessionEventTypeSessionCompactionComplete = rpc.SessionEventTypeSessionCompactionComplete SessionEventTypeSessionCompactionStart = rpc.SessionEventTypeSessionCompactionStart SessionEventTypeSessionContextChanged = rpc.SessionEventTypeSessionContextChanged @@ -423,9 +454,14 @@ const ( SessionEventTypeUserMessage = rpc.SessionEventTypeUserMessage SessionModeAutopilot = rpc.SessionModeAutopilot SessionModeInteractive = rpc.SessionModeInteractive + SessionModelChangeDataContextTierDefault = rpc.SessionModelChangeDataContextTierDefault + SessionModelChangeDataContextTierLongContext = rpc.SessionModelChangeDataContextTierLongContext SessionModePlan = rpc.SessionModePlan ShutdownTypeError = rpc.ShutdownTypeError ShutdownTypeRoutine = rpc.ShutdownTypeRoutine + SkillInvokedTriggerAgentInvoked = rpc.SkillInvokedTriggerAgentInvoked + SkillInvokedTriggerContextLoad = rpc.SkillInvokedTriggerContextLoad + SkillInvokedTriggerUserInvoked = rpc.SkillInvokedTriggerUserInvoked SkillSourceBuiltin = rpc.SkillSourceBuiltin SkillSourceCustom = rpc.SkillSourceCustom SkillSourceInherited = rpc.SkillSourceInherited @@ -451,6 +487,8 @@ const ( ToolExecutionCompleteContentTypeResourceLink = rpc.ToolExecutionCompleteContentTypeResourceLink ToolExecutionCompleteContentTypeTerminal = rpc.ToolExecutionCompleteContentTypeTerminal ToolExecutionCompleteContentTypeText = rpc.ToolExecutionCompleteContentTypeText + ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityApp + ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel = rpc.ToolExecutionCompleteToolDescriptionMetaUIVisibilityModel UserMessageAgentModeAutopilot = rpc.UserMessageAgentModeAutopilot UserMessageAgentModeInteractive = rpc.UserMessageAgentModeInteractive UserMessageAgentModePlan = rpc.UserMessageAgentModePlan diff --git a/java/.lastmerge b/java/.lastmerge index 88ed2a952..473fef56d 100644 --- a/java/.lastmerge +++ b/java/.lastmerge @@ -1 +1 @@ -f6c1adf8329ad4206e5ed2e8d12fb8082bc841a2 +f4d22d70016c377881d86e4c77f8a3f93746ffae diff --git a/java/CHANGELOG.md b/java/CHANGELOG.md deleted file mode 100644 index 8e174dd21..000000000 --- a/java/CHANGELOG.md +++ /dev/null @@ -1,535 +0,0 @@ -# Changelog - -All notable changes to the GitHub Copilot SDK for Java will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - -> Note: This file is automatically modified by scripts and coding agents. Do not change it manually. - -## [Unreleased] - -> **Reference implementation sync:** [`github/copilot-sdk@e20f5be`](https://github.com/github/copilot-sdk/commit/e20f5bef125860accb30c60d1b35109371a77f16) - -## [1.0.0-beta-java.4] - 2026-05-16 - -> **Reference implementation sync:** [`github/copilot-sdk@e20f5be`](https://github.com/github/copilot-sdk/commit/e20f5bef125860accb30c60d1b35109371a77f16) -## [1.0.0-beta-java.3] - 2026-05-11 - -> **Reference implementation sync:** [`github/copilot-sdk@4a0437b`](https://github.com/github/copilot-sdk/commit/4a0437bb03a0b60a1867f14ae8e3faf053afa5aa) -## [1.0.0-beta-java.2] - 2026-05-08 - -> **Reference implementation sync:** [`github/copilot-sdk@066a69c`](https://github.com/github/copilot-sdk/commit/066a69c1e849adf1bd98564ab1b52316ec471182) -## [1.0.0-beta-java.1] - 2026-05-05 - -> **Reference implementation sync:** [`github/copilot-sdk@c063458`](https://github.com/github/copilot-sdk/commit/c063458ecc3d606766f04cf203b11b08de672cc8) -## [0.3.0-java.2] - 2026-04-26 - -> **Reference implementation sync:** [`github/copilot-sdk@dd2dcbc`](https://github.com/github/copilot-sdk/commit/dd2dcbc439256acfb9feb2cff07c0b9c820091b8) -## [0.3.0-java-preview.1] - 2026-04-21 - -> **Reference implementation sync:** [`github/copilot-sdk@922959f`](https://github.com/github/copilot-sdk/commit/922959f4a7b83509c3620d4881733c6c5677f00c) -## [0.3.0-java-preview.0] - 2026-04-21 - -> **Reference implementation sync:** [`github/copilot-sdk@c3fa6cb`](https://github.com/github/copilot-sdk/commit/c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1) -## [0.2.2-java.1] - 2026-04-07 - -> **Reference implementation sync:** [`github/copilot-sdk@c3fa6cb`](https://github.com/github/copilot-sdk/commit/c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1) -### Added - -- Slash commands — register `/command` handlers invoked from the CLI TUI via `SessionConfig.setCommands()` (reference implementation: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757)) -- `CommandDefinition`, `CommandContext`, `CommandHandler`, `CommandWireDefinition` — types for defining and handling slash commands -- `CommandExecuteEvent` — event dispatched when a registered slash command is executed -- Elicitation (UI dialogs) — incoming handler via `SessionConfig.setOnElicitationRequest()` and outgoing convenience methods via `session.getUi()` (reference implementation: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757)) -- `ElicitationContext`, `ElicitationHandler`, `ElicitationParams`, `ElicitationResult`, `ElicitationResultAction`, `ElicitationSchema`, `InputOptions` — types for elicitation -- `ElicitationRequestedEvent` — event dispatched when an elicitation request is received -- `SessionUiApi` — convenience API on `session.getUi()` for `confirm()`, `select()`, `input()`, and `elicitation()` calls -- `SessionCapabilities` and `SessionUiCapabilities` — session capability reporting populated from create/resume response -- `CapabilitiesChangedEvent` — event dispatched when session capabilities are updated -- `CopilotClient.getSessionMetadata(String)` — O(1) session lookup by ID -- `GetSessionMetadataResponse` — response type for `getSessionMetadata` - -### Fixed - -- Permission events already resolved by a pre-hook now short-circuit before invoking the client-side handler -- `SessionUiApi` Javadoc now uses valid Java null-check syntax instead of `?.` -- README updated to say "GitHub Copilot CLI 1.0.17" instead of "GitHub Copilot 1.0.17" - -## [0.2.1-java.1] - 2026-04-02 - -> **Reference implementation sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15) -## [0.2.1-java.0] - 2026-03-26 - -> **Reference implementation sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15) -### Added - -- `UnknownSessionEvent` — forward-compatible placeholder for event types not yet known to the SDK; unknown events are now dispatched to handlers instead of being silently dropped (reference implementation: [`d82fd62`](https://github.com/github/copilot-sdk/commit/d82fd62)) -- `PermissionRequestResultKind.NO_RESULT` — new constant that signals the handler intentionally abstains from answering a permission request, leaving it unanswered for another client (reference implementation: [`df59a0e`](https://github.com/github/copilot-sdk/commit/df59a0e)) -- `ToolDefinition.skipPermission` field and `ToolDefinition.createSkipPermission()` factory — marks a tool to skip the permission prompt (reference implementation: [`10c4d02`](https://github.com/github/copilot-sdk/commit/10c4d02)) -- `SystemMessageMode.CUSTOMIZE` — new enum value for fine-grained system prompt customization (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) -- `SectionOverrideAction` enum — specifies the operation on a system prompt section (replace, remove, append, prepend, transform) (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) -- `SectionOverride` class — describes how one section of the system prompt should be modified, with optional transform callback (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) -- `SystemPromptSections` constants — well-known section identifier strings for use with CUSTOMIZE mode (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) -- `SystemMessageConfig.setSections(Map)` — section-level overrides for CUSTOMIZE mode (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) -- `systemMessage.transform` RPC handler — the SDK now registers a handler that invokes transform callbacks registered in the session config (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) -- `CopilotSession.setModel(String, String)` — new overload that accepts an optional reasoning effort level (reference implementation: [`ea90f07`](https://github.com/github/copilot-sdk/commit/ea90f07)) -- `CopilotSession.log(String, String, Boolean, String)` — new overload with an optional `url` parameter (minor addition) -- `BlobAttachment` class — inline base64-encoded binary attachment for messages (e.g., images) (reference implementation: [`698b259`](https://github.com/github/copilot-sdk/commit/698b259)) -- `MessageAttachment` sealed interface — type-safe base for all attachment types (`Attachment`, `BlobAttachment`), with Jackson polymorphic serialization support -- `TelemetryConfig` class — OpenTelemetry configuration for the CLI server; set on `CopilotClientOptions.setTelemetry()` (reference implementation: [`f2d21a0`](https://github.com/github/copilot-sdk/commit/f2d21a0)) -- `CopilotClientOptions.setTelemetry(TelemetryConfig)` — enables OpenTelemetry instrumentation in the CLI server (reference implementation: [`f2d21a0`](https://github.com/github/copilot-sdk/commit/f2d21a0)) - -### Changed - -- `Attachment` record now implements `MessageAttachment` sealed interface -- `BlobAttachment` class now implements `MessageAttachment` sealed interface and is `final` -- `MessageOptions.setAttachments(List)` — parameter type changed from `List` to `List` to support both `Attachment` and `BlobAttachment` in the same list with full compile-time safety -- `SendMessageRequest.setAttachments(List)` — matching change for the internal request type - -### Deprecated - -- `CopilotClientOptions.setAutoRestart(boolean)` — this option has no effect and will be removed in a future release - -## [0.1.32-java.0] - 2026-03-17 - -> **Reference implementation sync:** [`github/copilot-sdk@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83) -## [1.0.11] - 2026-03-12 - -> **Reference implementation sync:** [`github/copilot-sdk@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83) -### Added - -- `CopilotClientOptions.setOnListModels(Supplier>>)` — custom handler for `listModels()` used in BYOK mode to return models from a custom provider instead of querying the CLI (reference implementation: [`e478657`](https://github.com/github/copilot-sdk/commit/e478657)) -- `SessionConfig.setAgent(String)` — pre-selects a custom agent by name when creating a session (reference implementation: [`7766b1a`](https://github.com/github/copilot-sdk/commit/7766b1a)) -- `ResumeSessionConfig.setAgent(String)` — pre-selects a custom agent by name when resuming a session (reference implementation: [`7766b1a`](https://github.com/github/copilot-sdk/commit/7766b1a)) -- `SessionConfig.setOnEvent(Consumer)` — registers an event handler before the `session.create` RPC is issued, ensuring no early events are missed (reference implementation: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) -- `ResumeSessionConfig.setOnEvent(Consumer)` — registers an event handler before the `session.resume` RPC is issued (reference implementation: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) -- New broadcast session event types (protocol v3): `ExternalToolRequestedEvent` (`external_tool.requested`), `ExternalToolCompletedEvent` (`external_tool.completed`), `PermissionRequestedEvent` (`permission.requested`), `PermissionCompletedEvent` (`permission.completed`), `CommandQueuedEvent` (`command.queued`), `CommandCompletedEvent` (`command.completed`), `ExitPlanModeRequestedEvent` (`exit_plan_mode.requested`), `ExitPlanModeCompletedEvent` (`exit_plan_mode.completed`), `SystemNotificationEvent` (`system.notification`) (reference implementation: [`1653812`](https://github.com/github/copilot-sdk/commit/1653812), [`396e8b3`](https://github.com/github/copilot-sdk/commit/396e8b3)) -- `CopilotSession.log(String)` and `CopilotSession.log(String, String, Boolean)` — log a message to the session timeline (reference implementation: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) - -### Changed - -- **Protocol version bumped to v3.** The SDK now supports CLI servers running v2 or v3 (backward-compatible range). Sessions are now registered in the client's session map *before* the `session.create`/`session.resume` RPC is issued, ensuring broadcast events emitted immediately on session start are never dropped (reference implementation: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7), [`1653812`](https://github.com/github/copilot-sdk/commit/1653812)) -- In protocol v3, tool calls and permission requests that have a registered handler are now handled automatically via `ExternalToolRequestedEvent` and `PermissionRequestedEvent` broadcast events; results are sent back via `session.tools.handlePendingToolCall` and `session.permissions.handlePendingPermissionRequest` RPC calls (reference implementation: [`1653812`](https://github.com/github/copilot-sdk/commit/1653812)) - -## [1.0.10] - 2026-03-03 - -> **Reference implementation sync:** [`github/copilot-sdk@dcd86c1`](https://github.com/github/copilot-sdk/commit/dcd86c189501ce1b46b787ca60d90f3f315f3079) -### Added - -- `CopilotSession.setModel(String)` — changes the model for an existing session mid-conversation; the new model takes effect for the next message, and conversation history is preserved (reference implementation: [`bd98e3a`](https://github.com/github/copilot-sdk/commit/bd98e3a)) -- `ToolDefinition.createOverride(String, String, Map, ToolHandler)` — creates a tool definition that overrides a built-in CLI tool with the same name (reference implementation: [`f843c80`](https://github.com/github/copilot-sdk/commit/f843c80)) -- `ToolDefinition` record now includes `overridesBuiltInTool` field; when `true`, signals to the CLI that the custom tool intentionally replaces a built-in (reference implementation: [`f843c80`](https://github.com/github/copilot-sdk/commit/f843c80)) -- `CopilotSession.listAgents()` — lists custom agents available for selection (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) -- `CopilotSession.getCurrentAgent()` — gets the currently selected custom agent (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) -- `CopilotSession.selectAgent(String)` — selects a custom agent for the session (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) -- `CopilotSession.deselectAgent()` — deselects the current custom agent (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) -- `CopilotSession.compact()` — triggers immediate session context compaction (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) -- `AgentInfo` — new JSON type representing a custom agent with `name`, `displayName`, and `description` (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) -- New event types: `SessionTaskCompleteEvent` (`session.task_complete`), `AssistantStreamingDeltaEvent` (`assistant.streaming_delta`), `SubagentDeselectedEvent` (`subagent.deselected`) (reference implementation: various commits) -- `AssistantTurnStartEvent` data now includes `interactionId` field -- `AssistantMessageEvent` data now includes `interactionId` field -- `ToolExecutionCompleteEvent` data now includes `model` and `interactionId` fields -- `SkillInvokedEvent` data now includes `pluginName` and `pluginVersion` fields -- `AssistantUsageEvent` data now includes `copilotUsage` field with `CopilotUsage` and `TokenDetails` nested types -- E2E tests for custom tool permission approval and denial flows (reference implementation: [`388f2f3`](https://github.com/github/copilot-sdk/commit/388f2f3)) - -### Changed - -- **Breaking:** `createSession(SessionConfig)` now requires a non-null `onPermissionRequest` handler; throws `IllegalArgumentException` if not provided (reference implementation: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) -- **Breaking:** `resumeSession(String, ResumeSessionConfig)` now requires a non-null `onPermissionRequest` handler; throws `IllegalArgumentException` if not provided (reference implementation: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) -- **Breaking:** The no-arg `createSession()` and `resumeSession(String)` overloads were removed (reference implementation: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) -- `AssistantMessageDeltaEvent` data: `totalResponseSizeBytes` field moved to new `AssistantStreamingDeltaEvent` (reference implementation: various) - -### Fixed - -- Permission checks now also apply to SDK-registered custom tools, invoking the `onPermissionRequest` handler with `kind="custom-tool"` before executing tools (reference implementation: [`388f2f3`](https://github.com/github/copilot-sdk/commit/388f2f3)) - -## [1.0.9] - 2026-02-16 - -> **Reference implementation sync:** [`github/copilot-sdk@e40d57c`](https://github.com/github/copilot-sdk/commit/e40d57c86e18b495722adbf42045288c03924342) -### Added - -#### Cookbook with Practical Recipes - -Added a comprehensive cookbook with 5 practical recipes demonstrating common SDK usage patterns. All examples are JBang-compatible and can be run directly without a full Maven project setup. - -**Recipes:** -- **Error Handling** - Connection failures, timeouts, cleanup patterns, tool errors -- **Multiple Sessions** - Parallel conversations, custom session IDs, lifecycle management -- **Managing Local Files** - AI-powered file organization with grouping strategies -- **PR Visualization** - Interactive CLI tool for analyzing PR age distribution via GitHub MCP Server -- **Persisting Sessions** - Save and resume conversations across restarts - -**Location:** `src/site/markdown/cookbook/` - -**Usage:** -```bash -jbang BasicErrorHandling.java -jbang MultipleSessions.java -jbang PRVisualization.java github/copilot-sdk -``` - -Each recipe includes JBang prerequisites, usage instructions, and best practices. - -#### Session Context and Filtering - -Added session context tracking and filtering capabilities to help manage multiple Copilot sessions across different repositories and working directories. - -**New Classes:** -- `SessionContext` - Represents working directory context (cwd, gitRoot, repository, branch) with fluent setters -- `SessionListFilter` - Filter sessions by context fields (extends SessionContext) -- `SessionContextChangedEvent` - Event fired when working directory context changes between turns - -**Updated APIs:** -- `SessionMetadata.getContext()` - Returns optional context information for persisted sessions -- `CopilotClient.listSessions(SessionListFilter)` - New overload to filter sessions by context criteria - -**Example:** -```java -// List sessions for a specific repository -var filter = new SessionListFilter() - .setRepository("owner/repo") - .setBranch("main"); -var sessions = client.listSessions(filter).get(); - -// Access context information -for (var session : sessions) { - var ctx = session.getContext(); - if (ctx != null) { - System.out.println("CWD: " + ctx.getCwd()); - System.out.println("Repo: " + ctx.getRepository()); - } -} - -// Listen for context changes -session.on(SessionContextChangedEvent.class, event -> { - SessionContext newContext = event.getData(); - System.out.println("Working directory changed to: " + newContext.getCwd()); -}); -``` - -**Requirements:** -- GitHub Copilot CLI 0.0.409 or later - -## [1.0.8] - 2026-02-08 - -> **Reference implementation sync:** [`github/copilot-sdk@05e3c46`](https://github.com/github/copilot-sdk/commit/05e3c46c8c23130c9c064dc43d00ec78f7a75eab) - -### Added - -#### ResumeSessionConfig Parity with SessionConfig -Added missing options to `ResumeSessionConfig` for parity with `SessionConfig` when resuming sessions. You can now change the model, system message, tool filters, and other settings when resuming: - -- `model` - Change the AI model when resuming -- `systemMessage` - Override or extend the system prompt -- `availableTools` - Restrict which tools are available -- `excludedTools` - Disable specific tools -- `configDir` - Override configuration directory -- `infiniteSessions` - Configure infinite session behavior - -**Example:** -```java -var config = new ResumeSessionConfig() - .setModel("claude-sonnet-4") - .setReasoningEffort("high") - .setSystemMessage(new SystemMessageConfig() - .setMode(SystemMessageMode.APPEND) - .setContent("Focus on security.")); - -var session = client.resumeSession(sessionId, config).get(); -``` - -#### EventErrorHandler for Custom Error Handling -Added `EventErrorHandler` interface for custom handling of exceptions thrown by event handlers. Set via `session.setEventErrorHandler()` to receive the event and exception when a handler fails. - -```java -session.setEventErrorHandler((event, exception) -> { - logger.error("Handler failed for event: " + event.getType(), exception); -}); -``` - -#### EventErrorPolicy for Dispatch Control -Added `EventErrorPolicy` enum to control whether event dispatch continues or stops when a handler throws an exception. Errors are always logged at `WARNING` level. The default policy is `PROPAGATE_AND_LOG_ERRORS` which stops dispatch on the first error. Set `SUPPRESS_AND_LOG_ERRORS` to continue dispatching despite errors: - -```java -session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); -``` - -The `EventErrorHandler` is always invoked regardless of the policy. - -#### Type-Safe Event Handlers -Promoted type-safe `on(Class, Consumer)` event handlers as the primary API. Handlers now receive strongly-typed events instead of raw `AbstractSessionEvent`. - -```java -session.on(AssistantMessageEvent.class, msg -> { - System.out.println(msg.getData().getContent()); -}); -``` - -#### SpotBugs Static Analysis -Integrated SpotBugs for static code analysis with exclusion filters for `events` and `json` packages. - -### Changed - -- **Copilot CLI**: Minimum version updated to **0.0.405** -- **CopilotClient**: Made `final` to prevent Finalizer attacks (security hardening) -- **JBang Example**: Refactored `jbang-example.java` with streamlined session creation and usage metrics display -- **Code Style**: Use `var` for local variable type inference throughout the codebase - -### Fixed - -- **SpotBugs OS_OPEN_STREAM**: Wrap `BufferedReader` in try-with-resources to prevent resource leaks -- **SpotBugs REC_CATCH_EXCEPTION**: Narrow exception catch in `JsonRpcClient.handleMessage()` -- **SpotBugs DM_DEFAULT_ENCODING**: Add explicit UTF-8 charset to `InputStreamReader` -- **SpotBugs EI_EXPOSE_REP**: Add defensive copies to collection getters in events and JSON packages - -## [1.0.7] - 2026-02-05 - -### Added - -#### Session Lifecycle Hooks -Extended the hooks system with three new hook types for session lifecycle control: -- **`onSessionStart`** - Called when a session starts (new or resumed) -- **`onSessionEnd`** - Called when a session ends -- **`onUserPromptSubmitted`** - Called when the user submits a prompt - -New types: -- `SessionStartHandler`, `SessionStartHookInput`, `SessionStartHookOutput` -- `SessionEndHandler`, `SessionEndHookInput`, `SessionEndHookOutput` -- `UserPromptSubmittedHandler`, `UserPromptSubmittedHookInput`, `UserPromptSubmittedHookOutput` - -#### Session Lifecycle Events (Client-Level) -Added client-level lifecycle event subscriptions: -- `client.onLifecycle(handler)` - Subscribe to all session lifecycle events -- `client.onLifecycle(eventType, handler)` - Subscribe to specific event types -- `SessionLifecycleEventTypes.CREATED`, `DELETED`, `UPDATED`, `FOREGROUND`, `BACKGROUND` - -New types: `SessionLifecycleEvent`, `SessionLifecycleEventMetadata`, `SessionLifecycleHandler` - -#### Foreground Session Control (TUI+Server Mode) -For servers running with `--ui-server`: -- `client.getForegroundSessionId()` - Get the session displayed in TUI -- `client.setForegroundSessionId(sessionId)` - Switch TUI display to a session - -New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` - -#### New Event Types -- **`SessionShutdownEvent`** - Emitted when session is shutting down, includes reason and exit code -- **`SkillInvokedEvent`** - Emitted when a skill is invoked, includes skill name and context - -#### Extended Event Data -- `AssistantMessageEvent.Data` - Added `id`, `isLastReply`, `thinkingContent` fields -- `AssistantUsageEvent.Data` - Added `outputReasoningTokens` field -- `SessionCompactionCompleteEvent.Data` - Added `success`, `messagesRemoved`, `tokensRemoved` fields -- `SessionErrorEvent.Data` - Extended with additional error context - -#### Documentation -- New **[hooks.md](src/site/markdown/hooks.md)** - Comprehensive guide covering all 5 session hooks with examples for security gates, logging, result enrichment, and lifecycle management -- Expanded **[documentation.md](src/site/markdown/documentation.md)** with all 33 event types, `getMessages()`, `abort()`, and custom timeout examples -- Enhanced **[advanced.md](src/site/markdown/advanced.md)** with session hooks, lifecycle events, and foreground session control -- Added **[.github/copilot-instructions.md](.github/copilot-instructions.md)** for AI assistants - -#### Testing -- `SessionEventParserTest` - 850+ lines of unit tests for JSON event deserialization -- `SessionEventsE2ETest` - End-to-end tests for session event lifecycle -- `ErrorHandlingTest` - Tests for error handling scenarios -- Enhanced `E2ETestContext` with snapshot validation and expected prompt logging -- Added logging configuration (`logging.properties`) - -#### Build & CI -- JaCoCo 0.8.14 for test coverage reporting -- Coverage reports generated at `target/site/jacoco-coverage/` -- New test report action at `.github/actions/test-report/` -- JaCoCo coverage summary in workflow summary -- Coverage report artifact upload - -### Changed - -- **Copilot CLI**: Minimum version updated from 0.0.400 to **0.0.404** -- Refactored `ProcessInfo` and `Connection` to use records -- Extended `SessionHooks` to support 5 hook types (was 2) -- Renamed test methods to match snapshot naming conventions with Javadoc - -### Fixed - -- Improved timeout exception handling with detailed logging -- Test infrastructure improvements for proxy resilience - -## [1.0.6] - 2026-02-02 - -### Added - -- Auth options for BYOK configuration (`authType`, `apiKey`, `organizationId`, `endpoint`) -- Reasoning effort configuration (`reasoningEffort` in session config) -- User input handler for freeform user prompts (`UserInputHandler`, `UserInputRequest`, `UserInputResponse`) -- Pre-tool use and post-tool use hooks (`PreToolUseHandler`, `PostToolUseHandler`) -- VSCode launch and debug configurations -- Logging configuration for test debugging - -### Changed - -- Enhanced permission request handling with graceful error recovery -- Updated test harness integration to clone from reference implementation SDK -- Improved logging for session events and user input requests - -### Fixed - -- Non-null answer enforcement in user input responses for CLI compatibility -- Permission handler error handling improvements - -## [1.0.5] - 2026-01-29 - -### Added - -- Skills configuration: `skillDirectories` and `disabledSkills` in `SessionConfig` -- Skill events handling (`SkillInvokedEvent`) -- Javadoc verification step in build workflow -- Deploy-site job for automatic documentation deployment after releases - -### Changed - -- Merged reference implementation SDK changes (commit 87ff5510) -- Added agentic-merge-reference-impl Claude skill for tracking reference implementation changes - -### Fixed - -- Resume session handling to keep first client alive -- Build workflow updated to use `test-compile` instead of `compile` -- NPM dependency installation in CI workflow -- Enhanced error handling in permission request processing -- Checkstyle and Maven Resources Plugin version updates -- Test harness CLI installation to match reference implementation version - -## [1.0.4] - 2026-01-27 - -### Added - -- Advanced usage documentation with comprehensive examples -- Getting started guide with Maven and JBang instructions -- Package-info.java files for `com.github.copilot.sdk`, `events`, and `json` packages -- `@since` annotations on all public classes -- Versioned documentation with version selector on GitHub Pages -- Maven resources plugin for site markdown filtering - -### Changed - -- Refactored tool argument handling for improved type safety -- Optimized event listener registration in examples -- Enhanced site navigation with documentation links -- Merged reference implementation SDK changes from commit f902b76 - -### Fixed - -- BufferedReader replaced with BufferedInputStream for accurate JSON-RPC byte reading -- Timeout thread now uses daemon thread to prevent JVM exit blocking -- XML root element corrected from `` to `` in site.xml -- Badge titles in README for consistency - -## [1.0.3] - 2026-01-26 - -### Added - -- MCP Servers documentation and integration examples -- Infinite sessions documentation section -- Versioned documentation template with version selector -- Guidelines for porting reference implementation SDK changes to Java -- Configuration for automatically generated release notes - -### Changed - -- Renamed and retitled GitHub Actions workflows for clarity -- Improved gh-pages initialization and remote setup - -### Fixed - -- Documentation navigation to include MCP Servers section -- GitHub Pages deployment workflow to use correct branch -- Enhanced version handling in documentation build steps -- Rollback mechanism added for release failures - -## [1.0.2] - 2026-01-25 - -### Added - -- Infinite sessions support with `InfiniteSessionConfig` and workspace persistence -- GitHub Actions workflow for GitHub Pages deployment -- Daily schedule trigger for SDK E2E tests -- Checkstyle configuration and Maven integration - -### Changed - -- Updated GitHub Actions to latest action versions -- Enhanced Maven site deployment with documentation versioning -- Simplified GitHub release title naming convention - -### Fixed - -- Documentation links in site.xml and README for consistency -- Maven build step to include `clean` for fresh builds -- Image handling in README and site generation - -## [1.0.1] - 2026-01-22 - -### Added - -- Metadata APIs implementation -- Tool execution progress event (`ToolExecutionProgressEvent`) -- SDK protocol version 2 support -- Image in README for visual representation -- Detailed sections in README with usage examples -- Badges for build status, Maven Central, Java version, and license - -### Changed - -- Enhanced version handling in Maven release workflow -- Updated SCM connection URLs to use HTTPS - -### Fixed - -- GitHub release command version formatting and title -- Documentation commit messages to include version information -- JBang dependency declaration with correct group ID - -## [1.0.0] - 2026-01-21 - -### Added - -- Initial release of the GitHub Copilot SDK for Java -- Core classes: `CopilotClient`, `CopilotSession`, `JsonRpcClient` -- Session configuration with `SessionConfig` -- Custom tools with `ToolDefinition` and `ToolHandler` -- Event system with 30+ event types extending `AbstractSessionEvent` -- Permission handling with `PermissionHandler` -- BYOK (Bring Your Own Key) support with `ProviderConfig` -- MCP server integration via `McpServerConfig` -- System message customization with `SystemMessageConfig` -- File attachments support -- Streaming responses with delta events -- JBang example for quick testing -- GitHub Actions workflows for testing and Maven Central publishing -- Pre-commit hook for Spotless code formatting -- Comprehensive API documentation - -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.4...HEAD -[1.0.0-beta-java.4]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.3...v1.0.0-beta-java.4 -[1.0.0-beta-java.3]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.2...v1.0.0-beta-java.3 -[1.0.0-beta-java.2]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.1...v1.0.0-beta-java.2 -[1.0.0-beta-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java.2...v1.0.0-beta-java.1 -[0.3.0-java.2]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java.2 -[0.3.0-java-preview.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.1 -[0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 -[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 -[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 -[0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0 -[0.1.32-java.0]: https://github.com/github/copilot-sdk-java/compare/v1.0.11...v0.1.32-java.0 -[1.0.11]: https://github.com/github/copilot-sdk-java/compare/v1.0.10...v1.0.11 -[1.0.10]: https://github.com/github/copilot-sdk-java/compare/v1.0.9...v1.0.10 -[1.0.9]: https://github.com/github/copilot-sdk-java/compare/v1.0.8...v1.0.9 -[1.0.8]: https://github.com/github/copilot-sdk-java/compare/v1.0.7...v1.0.8 -[1.0.7]: https://github.com/github/copilot-sdk-java/compare/v1.0.6...v1.0.7 -[1.0.6]: https://github.com/github/copilot-sdk-java/compare/v1.0.5...v1.0.6 -[1.0.5]: https://github.com/github/copilot-sdk-java/compare/v1.0.4...v1.0.5 -[1.0.4]: https://github.com/github/copilot-sdk-java/compare/v1.0.3...v1.0.4 -[1.0.3]: https://github.com/github/copilot-sdk-java/compare/v1.0.2...v1.0.3 -[1.0.2]: https://github.com/github/copilot-sdk-java/compare/v1.0.1...v1.0.2 -[1.0.1]: https://github.com/github/copilot-sdk-java/compare/1.0.0...v1.0.1 -[1.0.0]: https://github.com/github/copilot-sdk-java/releases/tag/1.0.0 diff --git a/java/pom.xml b/java/pom.xml index 066822acc..de2727111 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -94,7 +94,7 @@ reference-impl-sync workflow and deal with the subsequent PR. --> - ^1.0.49-1 + ^1.0.52-1 @@ -321,7 +321,7 @@ CompactionTest) where snapshot matching can be sensitive to non-deterministic compaction behaviour. Revisit this once this issue is successfully resolved. - https://github.com/github/copilot-sdk/issues/1227 + https://github.com/github/copilot-sdk/issues/1227 --> 2 diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 0a96ab9f1..34bc83a9d 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -37,9 +37,25 @@ function toJavaClassName(typeName: string): string { return typeName.split(/[._]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); } +/** Java reserved keywords and Object method names that cannot be used as record component names. */ +const JAVA_RESERVED_IDENTIFIERS = new Set([ + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", + "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", + "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", + "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", + "volatile", "while", + // Object methods that conflict with record component accessor names + "wait", "notify", "notifyAll", "getClass", "clone", "finalize", "toString", "hashCode", "equals", +]); + function toCamelCase(name: string): string { const pascal = toPascalCase(name); - return pascal.charAt(0).toLowerCase() + pascal.slice(1); + let result = pascal.charAt(0).toLowerCase() + pascal.slice(1); + if (JAVA_RESERVED_IDENTIFIERS.has(result)) { + result = result + "_"; + } + return result; } function toEnumConstant(value: string): string { @@ -102,6 +118,11 @@ interface JavaTypeResult { let currentDefinitions: Record = {}; const pendingStandaloneTypes = new Map(); +// Cross-schema definitions: keyed by schema filename (e.g. "session-events.schema.json"), +// value is the definitions map from that schema. Populated by generateRpcTypes so that +// cross-schema $ref values like "session-events.schema.json#/definitions/Foo" can be resolved. +const crossSchemaDefinitions = new Map>(); + /** * Resolve a $ref in a JSON Schema against the current definitions. * Returns the resolved schema, or the original if no $ref is present. @@ -131,6 +152,28 @@ function schemaTypeToJava( // Resolve $ref first — register standalone types for generation if (schema.$ref) { + // Handle cross-schema $ref (e.g. "session-events.schema.json#/definitions/Foo") + const crossSchemaMatch = schema.$ref.match(/^([^#]+)#\/definitions\/(.+)$/); + if (crossSchemaMatch) { + const [, schemaFile, typeName] = crossSchemaMatch; + const externalDefs = crossSchemaDefinitions.get(schemaFile); + if (externalDefs) { + const resolved = externalDefs[typeName]; + if (resolved) { + // Save and swap currentDefinitions so recursive calls resolve against + // the external schema's definitions. + const savedDefs = currentDefinitions; + currentDefinitions = externalDefs; + const result = schemaTypeToJava(resolved, required, context, propName, nestedTypes); + currentDefinitions = savedDefs; + return result; + } + } + // Fallback: extract just the type name and warn + console.warn(`[codegen] Unresolved cross-schema $ref: ${schema.$ref}`); + return { javaType: typeName, imports }; + } + const name = schema.$ref.replace(/^#\/definitions\//, ""); const resolved = currentDefinitions[name]; if (resolved) { @@ -883,6 +926,19 @@ async function generateRpcTypes(schemaPath: string): Promise { // Set module-level definitions for $ref resolution currentDefinitions = (schema.definitions ?? {}) as Record; pendingStandaloneTypes.clear(); + crossSchemaDefinitions.clear(); + + // Load cross-schema definitions (session-events) so that cross-schema $ref values + // like "session-events.schema.json#/definitions/Foo" can be resolved. + try { + const sessionEventsSchemaPath = await getSessionEventsSchemaPath(); + const sessionEventsContent = await fs.readFile(sessionEventsSchemaPath, "utf-8"); + const sessionEventsSchema = JSON.parse(sessionEventsContent) as JSONSchema7; + crossSchemaDefinitions.set("session-events.schema.json", + (sessionEventsSchema.definitions ?? {}) as Record); + } catch (e) { + console.warn(`[codegen] Could not load session-events schema for cross-ref resolution: ${e}`); + } const packageName = "com.github.copilot.sdk.generated.rpc"; const packageDir = `src/generated/java/com/github/copilot/sdk/generated/rpc`; diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index c1d610b39..436d4f19a 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.49-1", + "@github/copilot": "^1.0.52-1", "json-schema": "^0.4.0", "tsx": "^4.20.6" } @@ -428,28 +428,31 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.49-3", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.49-3.tgz", - "integrity": "sha512-MoaUolkTfeDTeUpKjzLNZLyykdJmhW7xNEzSmM+zz9erZE0St1ACFz3TigJMM05r4L1cPR6j1TY37XfPExqdyg==", + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.52-1.tgz", + "integrity": "sha512-oz6m/dOpTU+FaCWXqYZj5JkJmRT+/RYcrmtGal39V+gOxTA2Nc9wIeLH1SMwMoOXC9Q6DN6keiY0wqWcHirPVg==", "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "detect-libc": "^2.1.2" + }, "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.49-3", - "@github/copilot-darwin-x64": "1.0.49-3", - "@github/copilot-linux-arm64": "1.0.49-3", - "@github/copilot-linux-x64": "1.0.49-3", - "@github/copilot-linuxmusl-arm64": "1.0.49-3", - "@github/copilot-linuxmusl-x64": "1.0.49-3", - "@github/copilot-win32-arm64": "1.0.49-3", - "@github/copilot-win32-x64": "1.0.49-3" + "@github/copilot-darwin-arm64": "1.0.52-1", + "@github/copilot-darwin-x64": "1.0.52-1", + "@github/copilot-linux-arm64": "1.0.52-1", + "@github/copilot-linux-x64": "1.0.52-1", + "@github/copilot-linuxmusl-arm64": "1.0.52-1", + "@github/copilot-linuxmusl-x64": "1.0.52-1", + "@github/copilot-win32-arm64": "1.0.52-1", + "@github/copilot-win32-x64": "1.0.52-1" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.49-3", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.49-3.tgz", - "integrity": "sha512-z6WpgoT+aro2nuA2zGfpxsMPtGSS3ZNACXERjfBxBzEoVjTMJi8kD1tpHFIPPCcLfaLniIi01Q6rvxMmZC6iKw==", + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.52-1.tgz", + "integrity": "sha512-DWXtC/yItZVtkSQhPyRMEkFwa2mcY2rg2cu/uwJ15L9ReiYvlKYEZQDe1TMqkT+U6+k9KjA2L2HQfXVm14/vTw==", "cpu": [ "arm64" ], @@ -463,9 +466,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.49-3", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.49-3.tgz", - "integrity": "sha512-ox9zs0uaFroB5SujopKFMz6/1shs2JsI5eIx4Kb/gugDrwU+Y3VVJJLw+dbEElJjQOCsb33kD9n+MsV1T6dubA==", + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.52-1.tgz", + "integrity": "sha512-NFTJkzzlTALMfbj9CDJ7N09PRPTVFq1+71hk+zoNx1uT/pi954liV6tKSaNAihPIXTMQKfJXGwEdjtvACpc8Vg==", "cpu": [ "x64" ], @@ -479,9 +482,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.49-3", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.49-3.tgz", - "integrity": "sha512-1uZaRtTH5H8HcPWKiN7eWJHsmmaW+tq6Eaxdme95Dfup4G9hemZMDHfdTjPXjZ6xykuoVKqWgC6knlk71JTWxQ==", + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.52-1.tgz", + "integrity": "sha512-CZE29v+RPJClHgVE1rU+RpRWSG8lm48koRZ0taKVopqLRD6NWKjBOwFKYJojk08H8/K+BWr/paM5+R8hEZHxZw==", "cpu": [ "arm64" ], @@ -495,9 +498,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.49-3", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.49-3.tgz", - "integrity": "sha512-OebfGDDFFn+KbiEbSHX8TvXRe77JeH1SBJyzle5QRSD/nBqNGEkNClRMGm8M5/cqyke6TbRP2XmmAQAApJmaQA==", + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.52-1.tgz", + "integrity": "sha512-tJhLQV70TJLq3hPXg7P6pHPfE4vaT2nENIXZsHu6fBkOcsSAxX1APSv6Bkyfsiod8EfFHkcG2+n7VXiVg8WqFw==", "cpu": [ "x64" ], @@ -510,6 +513,79 @@ "copilot-linux-x64": "copilot" } }, + "node_modules/@github/copilot-linuxmusl-arm64": { + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.52-1.tgz", + "integrity": "sha512-u24wHsUumldUEPWX/5z5IEuJvixiQEYF82N04P1g65dvOknq+89dpj+GND4Rh3Vr5u13drgj5AJqkJbWB8N+EQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linuxmusl-arm64": "copilot" + } + }, + "node_modules/@github/copilot-linuxmusl-x64": { + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.52-1.tgz", + "integrity": "sha512-wM22FxcHL8NlnesKKQPPvtk4ojqefN7irU5tQcX+IunpD1izVQl7AOXhZyHoQ21zQnN0De8EapxOUc+WnvlxpA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linuxmusl-x64": "copilot" + } + }, + "node_modules/@github/copilot-win32-arm64": { + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.52-1.tgz", + "integrity": "sha512-ecvfl9N7DPSwpiT2ZNUSXR1ZrSKwpkByOU6VcNphh4RptPZ0iNfyRNLhFCwSfFz+FvB6z2LZi+F7jSzQ3SaT3w==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "copilot-win32-arm64": "copilot.exe" + } + }, + "node_modules/@github/copilot-win32-x64": { + "version": "1.0.52-1", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.52-1.tgz", + "integrity": "sha512-a9Ct7krktP+/pfPdh/K57deYzzmL13e5Tb1pf5E152u4o/5xKzfgroNFUOzotFfFhs1jFhdzKCm3WHNLIvVEHA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "copilot-win32-x64": "copilot.exe" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 8726bacc1..672b42d65 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.49-1", + "@github/copilot": "^1.0.52-1", "json-schema": "^0.4.0", "tsx": "^4.20.6" } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java index 98af7b90a..ab58b24e5 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java @@ -53,7 +53,7 @@ public record AssistantMessageEventData( /** Generation phase for phased-output models (e.g., thinking vs. response phases) */ @JsonProperty("phase") String phase, /** Actual output token count from the API response (completion_tokens), used for accurate token accounting */ - @JsonProperty("outputTokens") Double outputTokens, + @JsonProperty("outputTokens") Long outputTokens, /** CAPI interaction ID for correlating this message with upstream telemetry */ @JsonProperty("interactionId") String interactionId, /** GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantStreamingDeltaEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantStreamingDeltaEvent.java index 70707a56e..31acae7c6 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantStreamingDeltaEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantStreamingDeltaEvent.java @@ -36,7 +36,7 @@ public final class AssistantStreamingDeltaEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record AssistantStreamingDeltaEventData( /** Cumulative total bytes received from the streaming response so far */ - @JsonProperty("totalResponseSizeBytes") Double totalResponseSizeBytes + @JsonProperty("totalResponseSizeBytes") Long totalResponseSizeBytes ) { } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsageTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsageTokenDetail.java index 895f19030..bea7cf162 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsageTokenDetail.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsageTokenDetail.java @@ -22,11 +22,11 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record AssistantUsageCopilotUsageTokenDetail( /** Number of tokens in this billing batch */ - @JsonProperty("batchSize") Double batchSize, + @JsonProperty("batchSize") Long batchSize, /** Cost per batch of tokens */ - @JsonProperty("costPerBatch") Double costPerBatch, + @JsonProperty("costPerBatch") Long costPerBatch, /** Total token count for this entry */ - @JsonProperty("tokenCount") Double tokenCount, + @JsonProperty("tokenCount") Long tokenCount, /** Token category (e.g., "input", "output") */ @JsonProperty("tokenType") String tokenType ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java index d704a862c..7d06ae9a0 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java @@ -39,21 +39,21 @@ public record AssistantUsageEventData( /** Model identifier used for this API call */ @JsonProperty("model") String model, /** Number of input tokens consumed */ - @JsonProperty("inputTokens") Double inputTokens, + @JsonProperty("inputTokens") Long inputTokens, /** Number of output tokens produced */ - @JsonProperty("outputTokens") Double outputTokens, + @JsonProperty("outputTokens") Long outputTokens, /** Number of tokens read from prompt cache */ - @JsonProperty("cacheReadTokens") Double cacheReadTokens, + @JsonProperty("cacheReadTokens") Long cacheReadTokens, /** Number of tokens written to prompt cache */ - @JsonProperty("cacheWriteTokens") Double cacheWriteTokens, + @JsonProperty("cacheWriteTokens") Long cacheWriteTokens, /** Number of output tokens used for reasoning (e.g., chain-of-thought) */ - @JsonProperty("reasoningTokens") Double reasoningTokens, + @JsonProperty("reasoningTokens") Long reasoningTokens, /** Model multiplier cost for billing purposes */ @JsonProperty("cost") Double cost, /** Duration of the API call in milliseconds */ - @JsonProperty("duration") Double duration, + @JsonProperty("duration") Long duration, /** Time to first token in milliseconds. Only available for streaming requests */ - @JsonProperty("ttftMs") Double ttftMs, + @JsonProperty("timeToFirstTokenMs") Long timeToFirstTokenMs, /** Average inter-token latency in milliseconds. Only available for streaming requests */ @JsonProperty("interTokenLatencyMs") Double interTokenLatencyMs, /** What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageQuotaSnapshot.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageQuotaSnapshot.java index 9e5675360..1bfa2c088 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageQuotaSnapshot.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageQuotaSnapshot.java @@ -25,16 +25,16 @@ public record AssistantUsageQuotaSnapshot( /** Whether the user has an unlimited usage entitlement */ @JsonProperty("isUnlimitedEntitlement") Boolean isUnlimitedEntitlement, /** Total requests allowed by the entitlement */ - @JsonProperty("entitlementRequests") Double entitlementRequests, + @JsonProperty("entitlementRequests") Long entitlementRequests, /** Number of requests already consumed */ - @JsonProperty("usedRequests") Double usedRequests, + @JsonProperty("usedRequests") Long usedRequests, /** Whether usage is still permitted after quota exhaustion */ @JsonProperty("usageAllowedWithExhaustedQuota") Boolean usageAllowedWithExhaustedQuota, - /** Number of requests over the entitlement limit */ + /** Number of additional usage requests made this period */ @JsonProperty("overage") Double overage, - /** Whether overage is allowed when quota is exhausted */ + /** Whether additional usage is allowed when quota is exhausted */ @JsonProperty("overageAllowedWithExhaustedQuota") Boolean overageAllowedWithExhaustedQuota, - /** Percentage of quota remaining (0.0 to 1.0) */ + /** Percentage of quota remaining (0 to 100) */ @JsonProperty("remainingPercentage") Double remainingPercentage, /** Date when the quota resets */ @JsonProperty("resetDate") OffsetDateTime resetDate diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java index b1a768adc..4c68eb746 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java @@ -40,7 +40,7 @@ public record AutoModeSwitchRequestedEventData( /** The rate limit error code that triggered this request */ @JsonProperty("errorCode") String errorCode, /** Seconds until the rate limit resets, when known. Lets clients render a humanized reset time alongside the prompt. */ - @JsonProperty("retryAfterSeconds") Double retryAfterSeconds + @JsonProperty("retryAfterSeconds") Long retryAfterSeconds ) { } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsed.java b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsed.java index 440979392..c3469eb7b 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsed.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsed.java @@ -22,17 +22,17 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record CompactionCompleteCompactionTokensUsed( /** Input tokens consumed by the compaction LLM call */ - @JsonProperty("inputTokens") Double inputTokens, + @JsonProperty("inputTokens") Long inputTokens, /** Output tokens produced by the compaction LLM call */ - @JsonProperty("outputTokens") Double outputTokens, + @JsonProperty("outputTokens") Long outputTokens, /** Cached input tokens reused in the compaction LLM call */ - @JsonProperty("cacheReadTokens") Double cacheReadTokens, + @JsonProperty("cacheReadTokens") Long cacheReadTokens, /** Tokens written to prompt cache in the compaction LLM call */ - @JsonProperty("cacheWriteTokens") Double cacheWriteTokens, + @JsonProperty("cacheWriteTokens") Long cacheWriteTokens, /** Per-request cost and usage data from the CAPI copilot_usage response field */ @JsonProperty("copilotUsage") CompactionCompleteCompactionTokensUsedCopilotUsage copilotUsage, /** Duration of the compaction LLM call in milliseconds */ - @JsonProperty("duration") Double duration, + @JsonProperty("duration") Long duration, /** Model identifier used for the compaction LLM call */ @JsonProperty("model") String model ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.java index 3c7fde8ad..5c368251f 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.java @@ -22,11 +22,11 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail( /** Number of tokens in this billing batch */ - @JsonProperty("batchSize") Double batchSize, + @JsonProperty("batchSize") Long batchSize, /** Cost per batch of tokens */ - @JsonProperty("costPerBatch") Double costPerBatch, + @JsonProperty("costPerBatch") Long costPerBatch, /** Total token count for this entry */ - @JsonProperty("tokenCount") Double tokenCount, + @JsonProperty("tokenCount") Long tokenCount, /** Token category (e.g., "input", "output") */ @JsonProperty("tokenType") String tokenType ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java index 33a3f8618..42de2f1f8 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java @@ -46,7 +46,7 @@ public record ModelCallFailureEventData( /** HTTP status code from the failed request */ @JsonProperty("statusCode") Long statusCode, /** Duration of the failed API call in milliseconds */ - @JsonProperty("durationMs") Double durationMs, + @JsonProperty("durationMs") Long durationMs, /** Where the failed model call originated */ @JsonProperty("source") ModelCallFailureSource source, /** Raw provider/runtime error message for restricted telemetry */ diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionCompleteEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionCompleteEvent.java index b6a3775e9..3c1252003 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionCompleteEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionCompleteEvent.java @@ -40,19 +40,21 @@ public record SessionCompactionCompleteEventData( /** Error message if compaction failed */ @JsonProperty("error") String error, /** Total tokens in conversation before compaction */ - @JsonProperty("preCompactionTokens") Double preCompactionTokens, + @JsonProperty("preCompactionTokens") Long preCompactionTokens, /** Total tokens in conversation after compaction */ - @JsonProperty("postCompactionTokens") Double postCompactionTokens, + @JsonProperty("postCompactionTokens") Long postCompactionTokens, /** Number of messages before compaction */ - @JsonProperty("preCompactionMessagesLength") Double preCompactionMessagesLength, + @JsonProperty("preCompactionMessagesLength") Long preCompactionMessagesLength, /** Number of messages removed during compaction */ - @JsonProperty("messagesRemoved") Double messagesRemoved, + @JsonProperty("messagesRemoved") Long messagesRemoved, /** Number of tokens removed during compaction */ - @JsonProperty("tokensRemoved") Double tokensRemoved, + @JsonProperty("tokensRemoved") Long tokensRemoved, + /** User-supplied focus instructions provided to a manual `/compact` invocation. Omitted for automatic compaction and for manual compaction with no focus text. */ + @JsonProperty("customInstructions") String customInstructions, /** LLM-generated summary of the compacted conversation history */ @JsonProperty("summaryContent") String summaryContent, /** Checkpoint snapshot number created for recovery */ - @JsonProperty("checkpointNumber") Double checkpointNumber, + @JsonProperty("checkpointNumber") Long checkpointNumber, /** File path where the checkpoint was stored */ @JsonProperty("checkpointPath") String checkpointPath, /** Token usage breakdown for the compaction LLM call (aligned with assistant.usage format) */ @@ -60,11 +62,11 @@ public record SessionCompactionCompleteEventData( /** GitHub request tracing ID (x-github-request-id header) for the compaction LLM call */ @JsonProperty("requestId") String requestId, /** Token count from system message(s) after compaction */ - @JsonProperty("systemTokens") Double systemTokens, + @JsonProperty("systemTokens") Long systemTokens, /** Token count from non-system messages (user, assistant, tool) after compaction */ - @JsonProperty("conversationTokens") Double conversationTokens, + @JsonProperty("conversationTokens") Long conversationTokens, /** Token count from tool definitions after compaction */ - @JsonProperty("toolDefinitionsTokens") Double toolDefinitionsTokens + @JsonProperty("toolDefinitionsTokens") Long toolDefinitionsTokens ) { } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionStartEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionStartEvent.java index 2ff7ace48..051f83f4c 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionStartEvent.java @@ -36,11 +36,11 @@ public final class SessionCompactionStartEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionCompactionStartEventData( /** Token count from system message(s) at compaction start */ - @JsonProperty("systemTokens") Double systemTokens, + @JsonProperty("systemTokens") Long systemTokens, /** Token count from non-system messages (user, assistant, tool) at compaction start */ - @JsonProperty("conversationTokens") Double conversationTokens, + @JsonProperty("conversationTokens") Long conversationTokens, /** Token count from tool definitions at compaction start */ - @JsonProperty("toolDefinitionsTokens") Double toolDefinitionsTokens + @JsonProperty("toolDefinitionsTokens") Long toolDefinitionsTokens ) { } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java index 93316117b..a5983fc2a 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java @@ -39,7 +39,7 @@ public record SessionResumeEventData( /** ISO 8601 timestamp when the session was resumed */ @JsonProperty("resumeTime") OffsetDateTime resumeTime, /** Total number of persisted events in the session at the time of resume */ - @JsonProperty("eventCount") Double eventCount, + @JsonProperty("eventCount") Long eventCount, /** Model currently selected at resume time */ @JsonProperty("selectedModel") String selectedModel, /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java index 269416994..e9d3c4432 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java @@ -47,9 +47,9 @@ public record SessionShutdownEventData( /** Session-wide per-token-type accumulated token counts */ @JsonProperty("tokenDetails") Map tokenDetails, /** Cumulative time spent in API calls during the session, in milliseconds */ - @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, + @JsonProperty("totalApiDurationMs") Long totalApiDurationMs, /** Unix timestamp (milliseconds) when the session started */ - @JsonProperty("sessionStartTime") Double sessionStartTime, + @JsonProperty("sessionStartTime") Long sessionStartTime, /** Aggregate code change metrics for the session */ @JsonProperty("codeChanges") ShutdownCodeChanges codeChanges, /** Per-model usage breakdown, keyed by model identifier */ @@ -57,13 +57,13 @@ public record SessionShutdownEventData( /** Model that was selected at the time of shutdown */ @JsonProperty("currentModel") String currentModel, /** Total tokens in context window at shutdown */ - @JsonProperty("currentTokens") Double currentTokens, + @JsonProperty("currentTokens") Long currentTokens, /** System message token count at shutdown */ - @JsonProperty("systemTokens") Double systemTokens, + @JsonProperty("systemTokens") Long systemTokens, /** Non-system message token count at shutdown */ - @JsonProperty("conversationTokens") Double conversationTokens, + @JsonProperty("conversationTokens") Long conversationTokens, /** Tool definitions token count at shutdown */ - @JsonProperty("toolDefinitionsTokens") Double toolDefinitionsTokens + @JsonProperty("toolDefinitionsTokens") Long toolDefinitionsTokens ) { } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionSnapshotRewindEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionSnapshotRewindEvent.java index 0b564f291..b121fdff3 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SessionSnapshotRewindEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionSnapshotRewindEvent.java @@ -38,7 +38,7 @@ public record SessionSnapshotRewindEventData( /** Event ID that was rewound to; this event and all after it were removed */ @JsonProperty("upToEventId") String upToEventId, /** Number of events that were removed by the rewind */ - @JsonProperty("eventsRemoved") Double eventsRemoved + @JsonProperty("eventsRemoved") Long eventsRemoved ) { } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java index d3484b3e1..0143908f5 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java @@ -39,7 +39,7 @@ public record SessionStartEventData( /** Unique identifier for the session */ @JsonProperty("sessionId") String sessionId, /** Schema version number for the session event format */ - @JsonProperty("version") Double version, + @JsonProperty("version") Long version, /** Identifier of the software producing the events (e.g., "copilot-agent") */ @JsonProperty("producer") String producer, /** Version string of the Copilot application */ diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionTruncationEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionTruncationEvent.java index b2ffad48f..60e7134e6 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SessionTruncationEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionTruncationEvent.java @@ -36,19 +36,19 @@ public final class SessionTruncationEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionTruncationEventData( /** Maximum token count for the model's context window */ - @JsonProperty("tokenLimit") Double tokenLimit, + @JsonProperty("tokenLimit") Long tokenLimit, /** Total tokens in conversation messages before truncation */ - @JsonProperty("preTruncationTokensInMessages") Double preTruncationTokensInMessages, + @JsonProperty("preTruncationTokensInMessages") Long preTruncationTokensInMessages, /** Number of conversation messages before truncation */ - @JsonProperty("preTruncationMessagesLength") Double preTruncationMessagesLength, + @JsonProperty("preTruncationMessagesLength") Long preTruncationMessagesLength, /** Total tokens in conversation messages after truncation */ - @JsonProperty("postTruncationTokensInMessages") Double postTruncationTokensInMessages, + @JsonProperty("postTruncationTokensInMessages") Long postTruncationTokensInMessages, /** Number of conversation messages after truncation */ - @JsonProperty("postTruncationMessagesLength") Double postTruncationMessagesLength, + @JsonProperty("postTruncationMessagesLength") Long postTruncationMessagesLength, /** Number of tokens removed by truncation */ - @JsonProperty("tokensRemovedDuringTruncation") Double tokensRemovedDuringTruncation, + @JsonProperty("tokensRemovedDuringTruncation") Long tokensRemovedDuringTruncation, /** Number of messages removed by truncation */ - @JsonProperty("messagesRemovedDuringTruncation") Double messagesRemovedDuringTruncation, + @JsonProperty("messagesRemovedDuringTruncation") Long messagesRemovedDuringTruncation, /** Identifier of the component that performed truncation (e.g., "BasicTruncator") */ @JsonProperty("performedBy") String performedBy ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionUsageInfoEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionUsageInfoEvent.java index 7c9c19eaf..84d73703a 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SessionUsageInfoEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionUsageInfoEvent.java @@ -36,17 +36,17 @@ public final class SessionUsageInfoEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionUsageInfoEventData( /** Maximum token count for the model's context window */ - @JsonProperty("tokenLimit") Double tokenLimit, + @JsonProperty("tokenLimit") Long tokenLimit, /** Current number of tokens in the context window */ - @JsonProperty("currentTokens") Double currentTokens, + @JsonProperty("currentTokens") Long currentTokens, /** Current number of messages in the conversation */ - @JsonProperty("messagesLength") Double messagesLength, + @JsonProperty("messagesLength") Long messagesLength, /** Token count from system message(s) */ - @JsonProperty("systemTokens") Double systemTokens, + @JsonProperty("systemTokens") Long systemTokens, /** Token count from non-system messages (user, assistant, tool) */ - @JsonProperty("conversationTokens") Double conversationTokens, + @JsonProperty("conversationTokens") Long conversationTokens, /** Token count from tool definitions */ - @JsonProperty("toolDefinitionsTokens") Double toolDefinitionsTokens, + @JsonProperty("toolDefinitionsTokens") Long toolDefinitionsTokens, /** Whether this is the first usage_info event emitted in this session */ @JsonProperty("isInitial") Boolean isInitial ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownCodeChanges.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownCodeChanges.java index 1afb58b69..9512573bd 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownCodeChanges.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownCodeChanges.java @@ -23,9 +23,9 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record ShutdownCodeChanges( /** Total number of lines added during the session */ - @JsonProperty("linesAdded") Double linesAdded, + @JsonProperty("linesAdded") Long linesAdded, /** Total number of lines removed during the session */ - @JsonProperty("linesRemoved") Double linesRemoved, + @JsonProperty("linesRemoved") Long linesRemoved, /** List of file paths that were modified during the session */ @JsonProperty("filesModified") List filesModified ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricRequests.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricRequests.java index 1872a6603..901d2a3e7 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricRequests.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricRequests.java @@ -22,7 +22,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record ShutdownModelMetricRequests( /** Total number of API requests made to this model */ - @JsonProperty("count") Double count, + @JsonProperty("count") Long count, /** Cumulative cost multiplier for requests to this model */ @JsonProperty("cost") Double cost ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java index 658efe35a..f179898a3 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java @@ -22,6 +22,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record ShutdownModelMetricTokenDetail( /** Accumulated token count for this token type */ - @JsonProperty("tokenCount") Double tokenCount + @JsonProperty("tokenCount") Long tokenCount ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricUsage.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricUsage.java index bd47eaeb5..a2301664b 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricUsage.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricUsage.java @@ -22,14 +22,14 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record ShutdownModelMetricUsage( /** Total input tokens consumed across all requests to this model */ - @JsonProperty("inputTokens") Double inputTokens, + @JsonProperty("inputTokens") Long inputTokens, /** Total output tokens produced across all requests to this model */ - @JsonProperty("outputTokens") Double outputTokens, + @JsonProperty("outputTokens") Long outputTokens, /** Total tokens read from prompt cache across all requests */ - @JsonProperty("cacheReadTokens") Double cacheReadTokens, + @JsonProperty("cacheReadTokens") Long cacheReadTokens, /** Total tokens written to prompt cache across all requests */ - @JsonProperty("cacheWriteTokens") Double cacheWriteTokens, + @JsonProperty("cacheWriteTokens") Long cacheWriteTokens, /** Total reasoning tokens produced across all requests to this model */ - @JsonProperty("reasoningTokens") Double reasoningTokens + @JsonProperty("reasoningTokens") Long reasoningTokens ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java index 6a9b3188a..856e4d2f6 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java @@ -22,6 +22,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record ShutdownTokenDetail( /** Accumulated token count for this token type */ - @JsonProperty("tokenCount") Double tokenCount + @JsonProperty("tokenCount") Long tokenCount ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentCompletedEvent.java index f61235b4a..45bdade9a 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentCompletedEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentCompletedEvent.java @@ -44,11 +44,11 @@ public record SubagentCompletedEventData( /** Model used by the sub-agent */ @JsonProperty("model") String model, /** Total number of tool calls made by the sub-agent */ - @JsonProperty("totalToolCalls") Double totalToolCalls, + @JsonProperty("totalToolCalls") Long totalToolCalls, /** Total tokens (input + output) consumed by the sub-agent */ - @JsonProperty("totalTokens") Double totalTokens, + @JsonProperty("totalTokens") Long totalTokens, /** Wall-clock duration of the sub-agent execution in milliseconds */ - @JsonProperty("durationMs") Double durationMs + @JsonProperty("durationMs") Long durationMs ) { } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentFailedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentFailedEvent.java index 19644eee1..3437e93ba 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentFailedEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentFailedEvent.java @@ -46,11 +46,11 @@ public record SubagentFailedEventData( /** Model used by the sub-agent (if any model calls succeeded before failure) */ @JsonProperty("model") String model, /** Total number of tool calls made before the sub-agent failed */ - @JsonProperty("totalToolCalls") Double totalToolCalls, + @JsonProperty("totalToolCalls") Long totalToolCalls, /** Total tokens (input + output) consumed before the sub-agent failed */ - @JsonProperty("totalTokens") Double totalTokens, + @JsonProperty("totalTokens") Long totalTokens, /** Wall-clock duration of the sub-agent execution in milliseconds */ - @JsonProperty("durationMs") Double durationMs + @JsonProperty("durationMs") Long durationMs ) { } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java index b4ac9b799..8d1c1c5c2 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java @@ -54,6 +54,8 @@ public record ToolExecutionCompleteEventData( @JsonProperty("toolTelemetry") Map toolTelemetry, /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ @JsonProperty("turnId") String turnId, + /** Whether this tool execution ran inside a sandbox container */ + @JsonProperty("sandboxed") Boolean sandboxed, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java index bf30633b0..0e0de5dd2 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java @@ -44,7 +44,7 @@ public record UserMessageEventData( @JsonProperty("attachments") List attachments, /** Normalized document MIME types that were sent natively instead of through tagged_files XML */ @JsonProperty("supportedNativeDocumentMimeTypes") List supportedNativeDocumentMimeTypes, - /** Path-backed native document attachments that stayed on the tagged_files path flow because native upload would exceed the request size limit */ + /** 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 */ @JsonProperty("nativeDocumentPathFallbackPaths") List nativeDocumentPathFallbackPaths, /** Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user) */ @JsonProperty("source") String source, diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AbortReason.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AbortReason.java new file mode 100644 index 000000000..0d0302fa4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AbortReason.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Finite reason code describing why the current turn was aborted + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AbortReason { + /** The {@code user_initiated} variant. */ + USER_INITIATED("user_initiated"), + /** The {@code remote_command} variant. */ + REMOTE_COMMAND("remote_command"), + /** The {@code user_abort} variant. */ + USER_ABORT("user_abort"); + + private final String value; + AbortReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AbortReason fromValue(String value) { + for (AbortReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AbortReason value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountQuotaSnapshot.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountQuotaSnapshot.java index 2bd1ac2f6..a13f01195 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountQuotaSnapshot.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountQuotaSnapshot.java @@ -32,9 +32,9 @@ public record AccountQuotaSnapshot( @JsonProperty("usageAllowedWithExhaustedQuota") Boolean usageAllowedWithExhaustedQuota, /** Percentage of entitlement remaining */ @JsonProperty("remainingPercentage") Double remainingPercentage, - /** Number of overage requests made this period */ + /** Number of additional usage requests made this period */ @JsonProperty("overage") Double overage, - /** Whether overage is allowed when quota is exhausted */ + /** Whether additional usage is allowed when quota is exhausted */ @JsonProperty("overageAllowedWithExhaustedQuota") Boolean overageAllowedWithExhaustedQuota, /** Date when the quota resets (ISO 8601 string) */ @JsonProperty("resetDate") OffsetDateTime resetDate diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java index 66493bce2..df2a24622 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java @@ -10,6 +10,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; import javax.annotation.processing.Generated; /** @@ -28,6 +30,20 @@ public record AgentInfo( /** Description of the agent's purpose */ @JsonProperty("description") String description, /** Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. */ - @JsonProperty("path") String path + @JsonProperty("path") String path, + /** Stable identifier for selection. For most agents this is the same as `name`; for plugin/builtin agents it may differ. Always populated; defaults to `name` when no distinct id was assigned. */ + @JsonProperty("id") String id, + /** Where the agent definition was loaded from */ + @JsonProperty("source") AgentInfoSource source, + /** Whether the agent can be selected directly by the user. Agents marked `false` are subagent-only. */ + @JsonProperty("userInvocable") Boolean userInvocable, + /** Allowed tool names for this agent. Empty array means none; omitted means inherit defaults. */ + @JsonProperty("tools") List tools, + /** Preferred model id for this agent. When omitted, inherits the outer agent's model. */ + @JsonProperty("model") String model, + /** 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. */ + @JsonProperty("skills") List skills ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfoSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfoSource.java new file mode 100644 index 000000000..699df1787 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfoSource.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Where the agent definition was loaded from + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AgentInfoSource { + /** The {@code user} variant. */ + USER("user"), + /** The {@code project} variant. */ + PROJECT("project"), + /** The {@code inherited} variant. */ + INHERITED("inherited"), + /** The {@code remote} variant. */ + REMOTE("remote"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code builtin} variant. */ + BUILTIN("builtin"); + + private final String value; + AgentInfoSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AgentInfoSource fromValue(String value) { + for (AgentInfoSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AgentInfoSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/EventsAgentScope.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/EventsAgentScope.java new file mode 100644 index 000000000..e748df0d3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/EventsAgentScope.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum EventsAgentScope { + /** The {@code primary} variant. */ + PRIMARY("primary"), + /** The {@code all} variant. */ + ALL("all"); + + private final String value; + EventsAgentScope(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static EventsAgentScope fromValue(String value) { + for (EventsAgentScope v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown EventsAgentScope value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/EventsCursorStatus.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/EventsCursorStatus.java new file mode 100644 index 000000000..eb68386ad --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/EventsCursorStatus.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * 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 started from the beginning of the remaining history. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum EventsCursorStatus { + /** The {@code ok} variant. */ + OK("ok"), + /** The {@code expired} variant. */ + EXPIRED("expired"); + + private final String value; + EventsCursorStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static EventsCursorStatus fromValue(String value) { + for (EventsCursorStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown EventsCursorStatus value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstalledPlugin.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstalledPlugin.java new file mode 100644 index 000000000..9f48489da --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstalledPlugin.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `InstalledPlugin` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record InstalledPlugin( + /** Plugin name */ + @JsonProperty("name") String name, + /** Marketplace the plugin came from (empty string for direct repo installs) */ + @JsonProperty("marketplace") String marketplace, + /** Version installed (if available) */ + @JsonProperty("version") String version, + /** Installation timestamp */ + @JsonProperty("installed_at") String installedAt, + /** Whether the plugin is currently enabled */ + @JsonProperty("enabled") Boolean enabled, + /** Path where the plugin is cached locally */ + @JsonProperty("cache_path") String cachePath, + /** Source for direct repo installs (when marketplace is empty) */ + @JsonProperty("source") Object source +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSources.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSources.java index d0a23b337..0cc669e22 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSources.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSources.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; /** @@ -33,9 +34,11 @@ public record InstructionsSources( @JsonProperty("type") InstructionsSourcesType type, /** Where this source lives — used for UI grouping */ @JsonProperty("location") InstructionsSourcesLocation location, - /** Glob pattern from frontmatter — when set, this instruction applies only to matching files */ - @JsonProperty("applyTo") String applyTo, + /** Glob pattern(s) from frontmatter — when set, this instruction applies only to matching files */ + @JsonProperty("applyTo") List applyTo, /** Short description (body after frontmatter) for use in instruction tables */ - @JsonProperty("description") String description + @JsonProperty("description") String description, + /** When true, this source starts disabled and must be toggled on by the user */ + @JsonProperty("defaultDisabled") Boolean defaultDisabled ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesLocation.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesLocation.java index 01b702cfe..943591213 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesLocation.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesLocation.java @@ -21,7 +21,9 @@ public enum InstructionsSourcesLocation { /** The {@code repository} variant. */ REPOSITORY("repository"), /** The {@code working-directory} variant. */ - WORKING_DIRECTORY("working-directory"); + WORKING_DIRECTORY("working-directory"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"); private final String value; InstructionsSourcesLocation(String value) { this.value = value; } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesType.java index 8de131942..b451f40c6 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesType.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesType.java @@ -27,7 +27,9 @@ public enum InstructionsSourcesType { /** The {@code nested-agents} variant. */ NESTED_AGENTS("nested-agents"), /** The {@code child-instructions} variant. */ - CHILD_INSTRUCTIONS("child-instructions"); + CHILD_INSTRUCTIONS("child-instructions"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"); private final String value; InstructionsSourcesType(String value) { this.value = value; } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpExecuteSamplingRequest.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpExecuteSamplingRequest.java new file mode 100644 index 000000000..9dcf307ea --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpExecuteSamplingRequest.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Raw MCP CreateMessageRequest params, as received in the `sampling.requested` event. Treated as opaque at the schema layer; the runtime converts the embedded MCP messages into the OpenAI chat-completion shape internally. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpExecuteSamplingRequest() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpExecuteSamplingResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpExecuteSamplingResult.java new file mode 100644 index 000000000..6524610d0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpExecuteSamplingResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpExecuteSamplingResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpSamplingExecutionAction.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpSamplingExecutionAction.java new file mode 100644 index 000000000..a00f41e82 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpSamplingExecutionAction.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpSamplingExecutionAction { + /** The {@code success} variant. */ + SUCCESS("success"), + /** The {@code failure} variant. */ + FAILURE("failure"), + /** The {@code cancelled} variant. */ + CANCELLED("cancelled"); + + private final String value; + McpSamplingExecutionAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpSamplingExecutionAction fromValue(String value) { + for (McpSamplingExecutionAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpSamplingExecutionAction value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpSetEnvValueModeDetails.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpSetEnvValueModeDetails.java new file mode 100644 index 000000000..d62e10c30 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpSetEnvValueModeDetails.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpSetEnvValueModeDetails { + /** The {@code direct} variant. */ + DIRECT("direct"), + /** The {@code indirect} variant. */ + INDIRECT("indirect"); + + private final String value; + McpSetEnvValueModeDetails(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpSetEnvValueModeDetails fromValue(String value) { + for (McpSetEnvValueModeDetails v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpSetEnvValueModeDetails value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotCurrentMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotCurrentMode.java new file mode 100644 index 000000000..5d55d1c4b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotCurrentMode.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot') + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum MetadataSnapshotCurrentMode { + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code plan} variant. */ + PLAN("plan"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"); + + private final String value; + MetadataSnapshotCurrentMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static MetadataSnapshotCurrentMode fromValue(String value) { + for (MetadataSnapshotCurrentMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown MetadataSnapshotCurrentMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadata.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadata.java new file mode 100644 index 000000000..b97a5f12c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadata.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record MetadataSnapshotRemoteMetadata( + /** The original resource identifier (task ID or PR node ID), preserved across event-replay reconstructions. Falls back to `sessionId` when absent. */ + @JsonProperty("resourceId") String resourceId, + /** The repository the remote session targets. */ + @JsonProperty("repository") MetadataSnapshotRemoteMetadataRepository repository, + /** The pull request number the remote session is associated with, if any. */ + @JsonProperty("pullRequestNumber") Long pullRequestNumber, + /** Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. */ + @JsonProperty("taskType") MetadataSnapshotRemoteMetadataTaskType taskType +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadataRepository.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadataRepository.java new file mode 100644 index 000000000..fb6c62e8a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadataRepository.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The repository the remote session targets. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record MetadataSnapshotRemoteMetadataRepository( + /** The GitHub owner (user or organization) of the target repository. */ + @JsonProperty("owner") String owner, + /** The GitHub repository name (without owner). */ + @JsonProperty("name") String name, + /** The branch the remote session is operating on. */ + @JsonProperty("branch") String branch +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadataTaskType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadataTaskType.java new file mode 100644 index 000000000..387d39234 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/MetadataSnapshotRemoteMetadataTaskType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum MetadataSnapshotRemoteMetadataTaskType { + /** The {@code cca} variant. */ + CCA("cca"), + /** The {@code cli} variant. */ + CLI("cli"); + + private final String value; + MetadataSnapshotRemoteMetadataTaskType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static MetadataSnapshotRemoteMetadataTaskType fromValue(String value) { + for (MetadataSnapshotRemoteMetadataTaskType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown MetadataSnapshotRemoteMetadataTaskType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/OptionsUpdateEnvValueMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/OptionsUpdateEnvValueMode.java new file mode 100644 index 000000000..f75daae61 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/OptionsUpdateEnvValueMode.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum OptionsUpdateEnvValueMode { + /** The {@code direct} variant. */ + DIRECT("direct"), + /** The {@code indirect} variant. */ + INDIRECT("indirect"); + + private final String value; + OptionsUpdateEnvValueMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static OptionsUpdateEnvValueMode fromValue(String value) { + for (OptionsUpdateEnvValueMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown OptionsUpdateEnvValueMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PendingPermissionRequest.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PendingPermissionRequest.java new file mode 100644 index 000000000..f4d84c730 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PendingPermissionRequest.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `PendingPermissionRequest` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PendingPermissionRequest( + /** Unique identifier for the pending permission request */ + @JsonProperty("requestId") String requestId, + /** The user-facing permission prompt details (commands, write, read, mcp, url, memory, custom-tool, path, hook) */ + @JsonProperty("request") Object request +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionLocationType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionLocationType.java new file mode 100644 index 000000000..c3b368cac --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionLocationType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether the location is a git repo or directory + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum PermissionLocationType { + /** The {@code repo} variant. */ + REPO("repo"), + /** The {@code dir} variant. */ + DIR("dir"); + + private final String value; + PermissionLocationType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static PermissionLocationType fromValue(String value) { + for (PermissionLocationType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown PermissionLocationType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionPathsConfig.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionPathsConfig.java new file mode 100644 index 000000000..1ac49881b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionPathsConfig.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * If specified, replaces the session's path-permission policy. The runtime constructs the appropriate PathManager based on these inputs (rooted at the session's working directory). Omit to leave the current path policy unchanged. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PermissionPathsConfig( + /** If true, the runtime allows access to all paths without prompting. Equivalent to constructing an UnrestrictedPathManager. */ + @JsonProperty("unrestricted") Boolean unrestricted, + /** Additional directories to allow tool access to (in addition to the session's working directory). When `unrestricted` is true, these are still pre-populated on the UnrestrictedPathManager so they remain visible via getDirectories() (e.g. for @-mention completion). */ + @JsonProperty("additionalDirectories") List additionalDirectories, + /** Whether to include the system temp directory in the allowed list (defaults to true). Ignored when `unrestricted` is true. */ + @JsonProperty("includeTempDirectory") Boolean includeTempDirectory, + /** Workspace root path (special-cased to be allowed even before the directory exists). Ignored when `unrestricted` is true. */ + @JsonProperty("workspacePath") String workspacePath +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionRule.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionRule.java new file mode 100644 index 000000000..ad8a57d8f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionRule.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `PermissionRule` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PermissionRule( + /** The rule kind, such as Shell or GitHubMCP */ + @JsonProperty("kind") String kind, + /** Argument value matched against the request, or null when the rule kind has no argument (e.g. 'read', 'write', 'memory'). */ + @JsonProperty("argument") String argument +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionRulesSet.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionRulesSet.java new file mode 100644 index 000000000..c29f11ba4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionRulesSet.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PermissionRulesSet( + /** Rules that auto-approve matching requests */ + @JsonProperty("approved") List approved, + /** Rules that auto-deny matching requests */ + @JsonProperty("denied") List denied +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionUrlsConfig.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionUrlsConfig.java new file mode 100644 index 000000000..46ff5501f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionUrlsConfig.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * If specified, replaces the session's URL-permission policy. The runtime constructs a fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy unchanged. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PermissionUrlsConfig( + /** If true, the runtime allows access to all URLs without prompting. Initial allow-list is ignored when this is true. */ + @JsonProperty("unrestricted") Boolean unrestricted, + /** Initial list of allowed URL/domain patterns. Patterns may include path components. Ignored when `unrestricted` is true. */ + @JsonProperty("initialAllowed") List initialAllowed +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicy.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicy.java new file mode 100644 index 000000000..2b31068e0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicy.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Schema for the `PermissionsConfigureAdditionalContentExclusionPolicy` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PermissionsConfigureAdditionalContentExclusionPolicy( + @JsonProperty("rules") List rules, + @JsonProperty("last_updated_at") Object lastUpdatedAt, + /** Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. */ + @JsonProperty("scope") PermissionsConfigureAdditionalContentExclusionPolicyScope scope +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyRule.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyRule.java new file mode 100644 index 000000000..c41050f1b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyRule.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Schema for the `PermissionsConfigureAdditionalContentExclusionPolicyRule` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PermissionsConfigureAdditionalContentExclusionPolicyRule( + @JsonProperty("paths") List paths, + @JsonProperty("ifAnyMatch") List ifAnyMatch, + @JsonProperty("ifNoneMatch") List ifNoneMatch, + /** Schema for the `PermissionsConfigureAdditionalContentExclusionPolicyRuleSource` type. */ + @JsonProperty("source") PermissionsConfigureAdditionalContentExclusionPolicyRuleSource source +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyRuleSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyRuleSource.java new file mode 100644 index 000000000..372e341da --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyRuleSource.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `PermissionsConfigureAdditionalContentExclusionPolicyRuleSource` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PermissionsConfigureAdditionalContentExclusionPolicyRuleSource( + @JsonProperty("name") String name, + @JsonProperty("type") String type +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyScope.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyScope.java new file mode 100644 index 000000000..1c378ec62 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsConfigureAdditionalContentExclusionPolicyScope.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum PermissionsConfigureAdditionalContentExclusionPolicyScope { + /** The {@code repo} variant. */ + REPO("repo"), + /** The {@code all} variant. */ + ALL("all"); + + private final String value; + PermissionsConfigureAdditionalContentExclusionPolicyScope(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static PermissionsConfigureAdditionalContentExclusionPolicyScope fromValue(String value) { + for (PermissionsConfigureAdditionalContentExclusionPolicyScope v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown PermissionsConfigureAdditionalContentExclusionPolicyScope value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsModifyRulesScope.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsModifyRulesScope.java new file mode 100644 index 000000000..53dc33391 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsModifyRulesScope.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether the change applies to ephemeral session-scoped rules (cleared at session end) or to location-scoped rules persisted via the location-permissions config file. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum PermissionsModifyRulesScope { + /** The {@code session} variant. */ + SESSION("session"), + /** The {@code location} variant. */ + LOCATION("location"); + + private final String value; + PermissionsModifyRulesScope(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static PermissionsModifyRulesScope fromValue(String value) { + for (PermissionsModifyRulesScope v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown PermissionsModifyRulesScope value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsSetApproveAllSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsSetApproveAllSource.java new file mode 100644 index 000000000..9e88acc59 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PermissionsSetApproveAllSource.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum PermissionsSetApproveAllSource { + /** The {@code cli_flag} variant. */ + CLI_FLAG("cli_flag"), + /** The {@code slash_command} variant. */ + SLASH_COMMAND("slash_command"), + /** The {@code autopilot_confirmation} variant. */ + AUTOPILOT_CONFIRMATION("autopilot_confirmation"), + /** The {@code rpc} variant. */ + RPC("rpc"); + + private final String value; + PermissionsSetApproveAllSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static PermissionsSetApproveAllSource fromValue(String value) { + for (PermissionsSetApproveAllSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown PermissionsSetApproveAllSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingResult.java index 299d8f358..b0ed087bd 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingResult.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingResult.java @@ -10,10 +10,11 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; import javax.annotation.processing.Generated; /** - * Server liveness response, including the echoed message, current timestamp, and protocol version. + * Server liveness response, including the echoed message, current server timestamp, and protocol version. * * @since 1.0.0 */ @@ -23,8 +24,8 @@ public record PingResult( /** Echoed message (or default greeting) */ @JsonProperty("message") String message, - /** Server timestamp in milliseconds */ - @JsonProperty("timestamp") Long timestamp, + /** ISO 8601 timestamp when the server handled the ping */ + @JsonProperty("timestamp") OffsetDateTime timestamp, /** Server protocol version number */ @JsonProperty("protocolVersion") Long protocolVersion ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/QueuePendingItems.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/QueuePendingItems.java new file mode 100644 index 000000000..ccffaed7e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/QueuePendingItems.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `QueuePendingItems` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record QueuePendingItems( + /** 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 */ + @JsonProperty("displayText") String displayText +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/QueuePendingItemsKind.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/QueuePendingItemsKind.java new file mode 100644 index 000000000..90f9e7f62 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/QueuePendingItemsKind.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether this item is a queued user message or a queued slash command / model change + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum QueuePendingItemsKind { + /** The {@code message} variant. */ + MESSAGE("message"), + /** The {@code command} variant. */ + COMMAND("command"); + + private final String value; + QueuePendingItemsKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static QueuePendingItemsKind fromValue(String value) { + for (QueuePendingItemsKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown QueuePendingItemsKind value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ScheduleEntry.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ScheduleEntry.java new file mode 100644 index 000000000..8038341a8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ScheduleEntry.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Schema for the `ScheduleEntry` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ScheduleEntry( + /** Sequential id assigned by the runtime within the session. Stable across resumes (rebuilt from the event log). */ + @JsonProperty("id") Long id, + /** Interval between scheduled ticks, in milliseconds. */ + @JsonProperty("intervalMs") Long intervalMs, + /** Prompt text that gets enqueued on every tick. */ + @JsonProperty("prompt") String prompt, + /** Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`). */ + @JsonProperty("recurring") Boolean recurring, + /** Display-only label for the prompt as shown in the UI (e.g. `/skill-name` for a skill-invocation schedule). The actual enqueued prompt is `prompt`. */ + @JsonProperty("displayPrompt") String displayPrompt, + /** ISO 8601 timestamp when the next tick is scheduled to fire. */ + @JsonProperty("nextRunAt") OffsetDateTime nextRunAt +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SecretsAddFilterValuesParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SecretsAddFilterValuesParams.java new file mode 100644 index 000000000..e8d1b5542 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SecretsAddFilterValuesParams.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Secret values to add to the redaction filter. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SecretsAddFilterValuesParams( + /** Raw secret values to register for redaction */ + @JsonProperty("values") List values +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SecretsAddFilterValuesResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SecretsAddFilterValuesResult.java new file mode 100644 index 000000000..7762377ff --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SecretsAddFilterValuesResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Confirmation that the secret values were registered. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SecretsAddFilterValuesResult( + /** Whether the values were successfully registered */ + @JsonProperty("ok") Boolean ok +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SendAgentMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SendAgentMode.java new file mode 100644 index 000000000..7dee184c9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SendAgentMode.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * The UI mode the agent was in when this message was sent. Defaults to the session's current mode. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SendAgentMode { + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code plan} variant. */ + PLAN("plan"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"), + /** The {@code shell} variant. */ + SHELL("shell"); + + private final String value; + SendAgentMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SendAgentMode fromValue(String value) { + for (SendAgentMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SendAgentMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SendMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SendMode.java new file mode 100644 index 000000000..c424caabc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SendMode.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SendMode { + /** The {@code enqueue} variant. */ + ENQUEUE("enqueue"), + /** The {@code immediate} variant. */ + IMMEDIATE("immediate"); + + private final String value; + SendMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SendMode fromValue(String value) { + for (SendMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SendMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java index 9de3df51c..436fcc696 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java @@ -30,6 +30,8 @@ public final class ServerRpc { public final ServerToolsApi tools; /** API methods for the {@code account} namespace. */ public final ServerAccountApi account; + /** API methods for the {@code secrets} namespace. */ + public final ServerSecretsApi secrets; /** API methods for the {@code mcp} namespace. */ public final ServerMcpApi mcp; /** API methods for the {@code skills} namespace. */ @@ -49,6 +51,7 @@ public ServerRpc(RpcCaller caller) { this.models = new ServerModelsApi(caller); this.tools = new ServerToolsApi(caller); this.account = new ServerAccountApi(caller); + this.secrets = new ServerSecretsApi(caller); this.mcp = new ServerMcpApi(caller); this.skills = new ServerSkillsApi(caller); this.sessionFs = new ServerSessionFsApi(caller); diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSecretsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSecretsApi.java new file mode 100644 index 000000000..af8f6c3c5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSecretsApi.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.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code secrets} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerSecretsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerSecretsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Secret values to add to the redaction filter. + * @since 1.0.0 + */ + public CompletableFuture addFilterValues(SecretsAddFilterValuesParams params) { + return caller.invoke("secrets.addFilterValues", params, SecretsAddFilterValuesResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionsApi.java index a78a2fbf5..72a13699c 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionsApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionsApi.java @@ -45,4 +45,174 @@ public CompletableFuture connect() { return caller.invoke("sessions.connect", java.util.Map.of(), SessionsConnectResult.class); } + /** + * Optional metadata-load limit and context filter applied to the returned sessions. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("sessions.list", java.util.Map.of(), SessionsListResult.class); + } + + /** + * GitHub task ID to look up. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture findByTaskId(SessionsFindByTaskIdParams params) { + return caller.invoke("sessions.findByTaskId", params, SessionsFindByTaskIdResult.class); + } + + /** + * UUID prefix to resolve to a unique session ID. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture findByPrefix(SessionsFindByPrefixParams params) { + return caller.invoke("sessions.findByPrefix", params, SessionsFindByPrefixResult.class); + } + + /** + * Optional working-directory context used to score session relevance. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getLastForContext(SessionsGetLastForContextParams params) { + return caller.invoke("sessions.getLastForContext", params, SessionsGetLastForContextResult.class); + } + + /** + * Session ID whose event-log file path to compute. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getEventFilePath() { + return caller.invoke("sessions.getEventFilePath", java.util.Map.of(), SessionsGetEventFilePathResult.class); + } + + /** + * Map of sessionId -> on-disk size in bytes for each session's workspace directory. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getSizes() { + return caller.invoke("sessions.getSizes", java.util.Map.of(), SessionsGetSizesResult.class); + } + + /** + * Session IDs to test for live in-use locks. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture checkInUse(SessionsCheckInUseParams params) { + return caller.invoke("sessions.checkInUse", params, SessionsCheckInUseResult.class); + } + + /** + * Session ID to look up the persisted remote-steerable flag for. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getPersistedRemoteSteerable() { + return caller.invoke("sessions.getPersistedRemoteSteerable", java.util.Map.of(), SessionsGetPersistedRemoteSteerableResult.class); + } + + /** + * Session ID to close. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture close() { + return caller.invoke("sessions.close", java.util.Map.of(), Void.class); + } + + /** + * Session IDs to close, deactivate, and delete from disk. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture bulkDelete(SessionsBulkDeleteParams params) { + return caller.invoke("sessions.bulkDelete", params, SessionsBulkDeleteResult.class); + } + + /** + * Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture pruneOld(SessionsPruneOldParams params) { + return caller.invoke("sessions.pruneOld", params, SessionsPruneOldResult.class); + } + + /** + * Session ID whose pending events should be flushed to disk. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture save() { + return caller.invoke("sessions.save", java.util.Map.of(), Void.class); + } + + /** + * Session ID whose in-use lock should be released. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture releaseLock() { + return caller.invoke("sessions.releaseLock", java.util.Map.of(), Void.class); + } + + /** + * Session metadata records to enrich with summary and context information. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture enrichMetadata(SessionsEnrichMetadataParams params) { + return caller.invoke("sessions.enrichMetadata", params, SessionsEnrichMetadataResult.class); + } + + /** + * Active session ID and an optional flag for deferring repo-level hooks until folder trust. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture reloadPluginHooks(SessionsReloadPluginHooksParams params) { + return caller.invoke("sessions.reloadPluginHooks", params, Void.class); + } + + /** + * Active session ID whose deferred repo-level hooks should be loaded. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture loadDeferredRepoHooks() { + return caller.invoke("sessions.loadDeferredRepoHooks", java.util.Map.of(), SessionsLoadDeferredRepoHooksResult.class); + } + + /** + * Manager-wide additional plugins to register; replaces any previously-configured set. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setAdditionalPlugins(SessionsSetAdditionalPluginsParams params) { + return caller.invoke("sessions.setAdditionalPlugins", params, Void.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAbortParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAbortParams.java new file mode 100644 index 000000000..8f7c47f7c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAbortParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Parameters for aborting the current turn + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAbortParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Finite reason code describing why the current turn was aborted */ + @JsonProperty("reason") AbortReason reason +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAbortResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAbortResult.java new file mode 100644 index 000000000..4a8d42c71 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAbortResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result of aborting the current turn + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAbortResult( + /** Whether the abort completed successfully */ + @JsonProperty("success") Boolean success, + /** Error message if the abort failed */ + @JsonProperty("error") String error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthApi.java index ceb245027..5b87bf3b3 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthApi.java @@ -18,6 +18,8 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") public final class SessionAuthApi { + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + private final RpcCaller caller; private final String sessionId; @@ -29,10 +31,27 @@ public final class SessionAuthApi { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture getStatus() { return caller.invoke("session.auth.getStatus", java.util.Map.of("sessionId", this.sessionId), SessionAuthGetStatusResult.class); } + /** + * New auth credentials to install on the session. Omit to leave credentials unchanged. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setCredentials(SessionAuthSetCredentialsParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.auth.setCredentials", _p, SessionAuthSetCredentialsResult.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthSetCredentialsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthSetCredentialsParams.java new file mode 100644 index 000000000..5e5e6b502 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthSetCredentialsParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * New auth credentials to install on the session. Omit to leave credentials unchanged. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAuthSetCredentialsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The new auth credentials to install on the session. When omitted or `undefined`, the call is a no-op and the session's existing credentials are preserved. The runtime stores the value verbatim and uses it for outbound model/API requests; it does NOT re-validate or re-fetch the associated Copilot user response. Several variants carry secret material; treat this method's params as containing secrets at rest and in transit. */ + @JsonProperty("credentials") Object credentials +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthSetCredentialsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthSetCredentialsResult.java new file mode 100644 index 000000000..5aef213a4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthSetCredentialsResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the credential update succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAuthSetCredentialsResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java index df5e6d9f2..7a142ecf9 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java @@ -31,6 +31,8 @@ public final class SessionCommandsApi { /** * Optional filters controlling which command sources to include in the listing. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture list() { @@ -42,6 +44,8 @@ public CompletableFuture list() { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture invoke(SessionCommandsInvokeParams params) { @@ -55,6 +59,8 @@ public CompletableFuture invoke(SessionCommandsInvokeParams params) { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture handlePendingCommand(SessionCommandsHandlePendingCommandParams params) { @@ -64,10 +70,42 @@ public CompletableFuture handlePendin } /** - * Queued command request ID and the result indicating whether the client handled it. + * Slash command name and argument string to execute synchronously. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture execute(SessionCommandsExecuteParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.commands.execute", _p, SessionCommandsExecuteResult.class); + } + + /** + * Slash-prefixed command string to enqueue for FIFO processing. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture enqueue(SessionCommandsEnqueueParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.commands.enqueue", _p, SessionCommandsEnqueueResult.class); + } + + /** + * Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture respondToQueuedCommand(SessionCommandsRespondToQueuedCommandParams params) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsEnqueueParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsEnqueueParams.java new file mode 100644 index 000000000..6ca80d251 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsEnqueueParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Slash-prefixed command string to enqueue for FIFO processing. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +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 +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsEnqueueResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsEnqueueResult.java new file mode 100644 index 000000000..e968e1df8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsEnqueueResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the command was accepted into the local execution queue. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsEnqueueResult( + /** True when the command was accepted into the local execution queue. False when the call targets a session that does not support local command queueing (e.g. remote sessions). */ + @JsonProperty("queued") Boolean queued +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsExecuteParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsExecuteParams.java new file mode 100644 index 000000000..1f1e73acb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsExecuteParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Slash command name and argument string to execute synchronously. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsExecuteParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the slash command to invoke (without the leading '/'). */ + @JsonProperty("commandName") String commandName, + /** Argument string to pass to the command (empty string if none). */ + @JsonProperty("args") String args +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsExecuteResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsExecuteResult.java new file mode 100644 index 000000000..a03b08c66 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsExecuteResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Error message produced while executing the command, if any. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsExecuteResult( + /** Error message produced while executing the command, if any. Omitted when the handler succeeded. */ + @JsonProperty("error") String error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java index b5a2e31f2..8eb5d2dbc 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Queued command request ID and the result indicating whether the client handled it. + * Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). * * @since 1.0.0 */ @@ -23,9 +23,9 @@ public record SessionCommandsRespondToQueuedCommandParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** Request ID from the queued command event */ + /** Request ID from the `command.queued` event the host is responding to. */ @JsonProperty("requestId") String requestId, - /** Result of the queued command execution */ + /** Result of the queued command execution. */ @JsonProperty("result") Object result ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java index eb05c1d9e..3cf5a6de3 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Indicates whether the queued-command response was accepted by the session. + * Indicates whether the queued-command response was matched to a pending request. * * @since 1.0.0 */ @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionCommandsRespondToQueuedCommandResult( - /** Whether the response was accepted (false if the requestId was not found or already resolved) */ + /** Whether a pending queued command with the given request ID was found and resolved. False when the request was already resolved, cancelled, or unknown. */ @JsonProperty("success") Boolean success ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionContext.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionContext.java new file mode 100644 index 000000000..1a8c148bd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionContext.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `SessionContext` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionContext( + /** Most recent working directory for this session */ + @JsonProperty("cwd") String cwd, + /** Git repository root, if the cwd was inside a git repo */ + @JsonProperty("gitRoot") String gitRoot, + /** Repository slug in `owner/name` form, when known */ + @JsonProperty("repository") String repository, + /** Repository host type */ + @JsonProperty("hostType") SessionContextHostType hostType, + /** Active git branch */ + @JsonProperty("branch") String branch +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionContextHostType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionContextHostType.java new file mode 100644 index 000000000..db0945e31 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionContextHostType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Repository host type + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionContextHostType { + /** The {@code github} variant. */ + GITHUB("github"), + /** The {@code ado} variant. */ + ADO("ado"); + + private final String value; + SessionContextHostType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionContextHostType fromValue(String value) { + for (SessionContextHostType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionContextHostType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogApi.java new file mode 100644 index 000000000..58351e99a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogApi.java @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code eventLog} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionEventLogApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionEventLogApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Cursor, batch size, and optional long-poll/filter parameters for reading session events. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture read(SessionEventLogReadParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.eventLog.read", _p, SessionEventLogReadResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture tail() { + return caller.invoke("session.eventLog.tail", java.util.Map.of("sessionId", this.sessionId), SessionEventLogTailResult.class); + } + + /** + * Event type to register consumer interest for, used by runtime gating logic. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture registerInterest(SessionEventLogRegisterInterestParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.eventLog.registerInterest", _p, SessionEventLogRegisterInterestResult.class); + } + + /** + * Opaque handle previously returned by `registerInterest` to release. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture releaseInterest(SessionEventLogReleaseInterestParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.eventLog.releaseInterest", _p, SessionEventLogReleaseInterestResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReadParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReadParams.java new file mode 100644 index 000000000..cad49d452 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReadParams.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Cursor, batch size, and optional long-poll/filter parameters for reading session events. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEventLogReadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque cursor returned by a previous read. Omit on the first call to start from the beginning of the session's persisted history. */ + @JsonProperty("cursor") String cursor, + /** Maximum number of events to return in this batch (1–1000, default 200). */ + @JsonProperty("max") Long max, + /** Milliseconds to wait for new events when the cursor is at the tail of history. 0 (default) returns immediately even if no events are available. Capped at 30000ms. Ephemeral events that arrive during the wait are delivered in this batch but are NOT replayable on a subsequent read (use a non-zero waitMs in your next call to capture future ephemerals as they happen). */ + @JsonProperty("waitMs") Long waitMs, + /** Either '*' to receive all event types, or a non-empty list of event types to receive */ + @JsonProperty("types") Object types, + /** 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. */ + @JsonProperty("agentScope") EventsAgentScope agentScope +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReadResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReadResult.java new file mode 100644 index 000000000..725a76792 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReadResult.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Batch of session events returned by a read, with cursor and continuation metadata. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEventLogReadResult( + /** Events are delivered in two batches per read: persisted events first (in append order), then ephemeral events (in seq order). When `waitMs > 0` and the catch-up batches were empty, post-wait events follow the same two-batch ordering. Persisted and ephemeral events do not interleave within a single read. */ + @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. */ + @JsonProperty("cursor") String cursor, + /** True when the read returned `max` events and more events are available immediately. When false, the next read with a non-zero `waitMs` will block until a new event arrives or the wait expires. */ + @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 started from the beginning of the remaining history. */ + @JsonProperty("cursorStatus") EventsCursorStatus cursorStatus +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogRegisterInterestParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogRegisterInterestParams.java new file mode 100644 index 000000000..3bddc6904 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogRegisterInterestParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Event type to register consumer interest for, used by runtime gating logic. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEventLogRegisterInterestParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The event type the consumer wants the runtime to treat as 'observed' for behavior-switching gating. Some runtime code paths inspect whether any consumer is interested in a specific event type and choose a different implementation accordingly (e.g. `mcp.oauth_required`: when interest is registered the runtime delegates the full interactive OAuth flow to the consumer; when no interest is registered the runtime installs a browserless fallback that silently reuses cached tokens). SDK clients that long-poll events do NOT automatically appear as listeners to these gating checks — they must explicitly call `registerInterest` for each event type they want the runtime to count as having a consumer. Multiple registrations for the same event type from the same or different consumers are tracked independently and must each be released. See: `mcp.oauth_required`, `sampling.requested`, `auto_mode_switch.requested`, `user_input.requested`, `elicitation.requested`, `command.queued`, `exit_plan_mode.requested`. */ + @JsonProperty("eventType") String eventType +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogRegisterInterestResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogRegisterInterestResult.java new file mode 100644 index 000000000..e0a6bb5a5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogRegisterInterestResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Opaque handle representing an event-type interest registration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEventLogRegisterInterestResult( + /** Opaque handle for this registration. Pass to releaseInterest to release. Each call to registerInterest produces a fresh handle, even when the same eventType is registered multiple times. */ + @JsonProperty("handle") String handle +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReleaseInterestParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReleaseInterestParams.java new file mode 100644 index 000000000..c6bd4efb1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReleaseInterestParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Opaque handle previously returned by `registerInterest` to release. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEventLogReleaseInterestParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Handle returned by a previous `registerInterest` call. Idempotent: releasing an unknown or already-released handle is a no-op (returns success). When the last outstanding handle for an event type is released, the runtime reverts to its 'no consumer' code path for that event type. */ + @JsonProperty("handle") String handle +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReleaseInterestResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReleaseInterestResult.java new file mode 100644 index 000000000..0ba6b149a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogReleaseInterestResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEventLogReleaseInterestResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogTailParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogTailParams.java new file mode 100644 index 000000000..270af92dc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogTailParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEventLogTailParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogTailResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogTailResult.java new file mode 100644 index 000000000..a966cd591 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionEventLogTailResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Snapshot of the current tail cursor without returning any events. Use this when a consumer wants to subscribe to live events going forward without first paginating through the entire persisted history (which would happen if `read` were called without a cursor on a long-lived session). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEventLogTailResult( + /** Opaque cursor pointing at the current tail of the session's persisted-events history. Pass back to `read` to receive only events that arrive AFTER this snapshot. When the session has no events, this returns the same sentinel as an unset cursor (i.e. equivalent to omitting the cursor on a first read). */ + @JsonProperty("cursor") String cursor +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryResult.java index df6549a9d..edc1e9c83 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryResult.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryResult.java @@ -29,8 +29,8 @@ public record SessionFsSqliteQueryResult( @JsonProperty("columns") List columns, /** Number of rows affected (for INSERT/UPDATE/DELETE) */ @JsonProperty("rowsAffected") Long rowsAffected, - /** Last inserted row ID (for INSERT) */ - @JsonProperty("lastInsertRowid") Double lastInsertRowid, + /** SQLite last_insert_rowid() value for INSERT. */ + @JsonProperty("lastInsertRowid") Long lastInsertRowid, /** Describes a filesystem error. */ @JsonProperty("error") SessionFsError error ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryAbortManualCompactionParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryAbortManualCompactionParams.java new file mode 100644 index 000000000..c08926c39 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryAbortManualCompactionParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistoryAbortManualCompactionParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryAbortManualCompactionResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryAbortManualCompactionResult.java new file mode 100644 index 000000000..efefbd1a0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryAbortManualCompactionResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether an in-progress manual compaction was aborted. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistoryAbortManualCompactionResult( + /** Whether an in-progress manual compaction was aborted. False when no manual compaction was running, when its abort controller was already aborted, or when the session is remote. */ + @JsonProperty("aborted") Boolean aborted +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryApi.java index 9988a4a88..143250b0c 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryApi.java @@ -30,7 +30,7 @@ public final class SessionHistoryApi { } /** - * Identifies the target session. + * Optional compaction parameters. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -54,4 +54,34 @@ public CompletableFuture truncate(SessionHistoryTr return caller.invoke("session.history.truncate", _p, SessionHistoryTruncateResult.class); } + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture cancelBackgroundCompaction() { + return caller.invoke("session.history.cancelBackgroundCompaction", java.util.Map.of("sessionId", this.sessionId), SessionHistoryCancelBackgroundCompactionResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture abortManualCompaction() { + return caller.invoke("session.history.abortManualCompaction", java.util.Map.of("sessionId", this.sessionId), SessionHistoryAbortManualCompactionResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture summarizeForHandoff() { + return caller.invoke("session.history.summarizeForHandoff", java.util.Map.of("sessionId", this.sessionId), SessionHistorySummarizeForHandoffResult.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCancelBackgroundCompactionParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCancelBackgroundCompactionParams.java new file mode 100644 index 000000000..faba738ed --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCancelBackgroundCompactionParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistoryCancelBackgroundCompactionParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCancelBackgroundCompactionResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCancelBackgroundCompactionResult.java new file mode 100644 index 000000000..22d9b926e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCancelBackgroundCompactionResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether an in-progress background compaction was cancelled. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistoryCancelBackgroundCompactionResult( + /** Whether an in-progress background compaction was cancelled. False when no compaction was running, when the session is remote, or when the underlying processor was unavailable. */ + @JsonProperty("cancelled") Boolean cancelled +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactParams.java index 8737d590f..30a3fd393 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactParams.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactParams.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Identifies the target session. + * Optional compaction parameters. * * @since 1.0.0 */ diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactResult.java index f7a8664b9..e7546b44c 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactResult.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Compaction outcome with the number of tokens and messages removed and the resulting context window breakdown. + * Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. * * @since 1.0.0 */ @@ -27,6 +27,8 @@ public record SessionHistoryCompactResult( @JsonProperty("tokensRemoved") Long tokensRemoved, /** Number of messages removed during compaction */ @JsonProperty("messagesRemoved") Long messagesRemoved, + /** Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). */ + @JsonProperty("summaryContent") String summaryContent, /** Post-compaction context window usage breakdown */ @JsonProperty("contextWindow") HistoryCompactContextWindow contextWindow ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistorySummarizeForHandoffParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistorySummarizeForHandoffParams.java new file mode 100644 index 000000000..946b9232f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistorySummarizeForHandoffParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistorySummarizeForHandoffParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistorySummarizeForHandoffResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistorySummarizeForHandoffResult.java new file mode 100644 index 000000000..d49b225d2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistorySummarizeForHandoffResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Markdown summary of the conversation context (empty when not available). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistorySummarizeForHandoffResult( + /** Markdown summary of the conversation context produced by an LLM. Empty string when there are no messages or when the session does not support local summarization. */ + @JsonProperty("summary") String summary +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstalledPlugin.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstalledPlugin.java new file mode 100644 index 000000000..8ca95b8b3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstalledPlugin.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `SessionInstalledPlugin` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionInstalledPlugin( + /** Plugin name */ + @JsonProperty("name") String name, + /** Marketplace the plugin came from (empty string for direct repo installs) */ + @JsonProperty("marketplace") String marketplace, + /** Installed version, if known */ + @JsonProperty("version") String version, + /** Installation timestamp (ISO-8601) */ + @JsonProperty("installed_at") String installedAt, + /** Whether the plugin is currently enabled */ + @JsonProperty("enabled") Boolean enabled, + /** Path where the plugin is cached locally */ + @JsonProperty("cache_path") String cachePath, + /** Source descriptor for direct repo installs (when marketplace is empty) */ + @JsonProperty("source") Object source +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsApi.java index 23c4cf3b6..5aec59dfd 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsApi.java @@ -29,6 +29,8 @@ public final class SessionInstructionsApi { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture getSources() { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogParams.java index f593c11c2..66e0d4c85 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogParams.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogParams.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Message text, optional severity level, persistence flag, and optional follow-up URL. + * Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. * * @since 1.0.0 */ @@ -27,9 +27,13 @@ public record SessionLogParams( @JsonProperty("message") String message, /** Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". */ @JsonProperty("level") SessionLogLevel level, + /** Domain category for this log entry (e.g., "mcp", "subscription", "policy", "model"). Maps to `infoType`/`warningType`/`errorType` on the emitted event. Defaults to "notification". */ + @JsonProperty("type") String type, /** When true, the message is transient and not persisted to the session event log on disk */ @JsonProperty("ephemeral") Boolean ephemeral, /** Optional URL the user can open in their browser for more details */ - @JsonProperty("url") String url + @JsonProperty("url") String url, + /** Optional actionable tip displayed alongside the message. Only honored on `level: "info"`. */ + @JsonProperty("tip") String tip ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLspApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLspApi.java new file mode 100644 index 000000000..79aa00055 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLspApi.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code lsp} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionLspApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionLspApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Parameters for (re)loading the merged LSP configuration set. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture initialize(SessionLspInitializeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.lsp.initialize", _p, Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLspInitializeParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLspInitializeParams.java new file mode 100644 index 000000000..37c064aa7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLspInitializeParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Parameters for (re)loading the merged LSP configuration set. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionLspInitializeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Working directory used to load project-level LSP configs. Defaults to the session working directory when omitted. */ + @JsonProperty("workingDirectory") String workingDirectory, + /** Git root used as the boundary when traversing for project-level LSP configs (supports monorepos). */ + @JsonProperty("gitRoot") String gitRoot, + /** Force re-initialization even when LSP configs were already loaded for the working directory. */ + @JsonProperty("force") Boolean force +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpApi.java index 93714e068..be2d22d4b 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpApi.java @@ -83,4 +83,59 @@ public CompletableFuture reload() { return caller.invoke("session.mcp.reload", java.util.Map.of("sessionId", this.sessionId), Void.class); } + /** + * Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture executeSampling(SessionMcpExecuteSamplingParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.executeSampling", _p, SessionMcpExecuteSamplingResult.class); + } + + /** + * The requestId previously passed to executeSampling that should be cancelled. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture cancelSamplingExecution(SessionMcpCancelSamplingExecutionParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.cancelSamplingExecution", _p, SessionMcpCancelSamplingExecutionResult.class); + } + + /** + * Mode controlling how MCP server env values are resolved (`direct` or `indirect`). + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setEnvValueMode(SessionMcpSetEnvValueModeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.setEnvValueMode", _p, SessionMcpSetEnvValueModeResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture removeGitHub() { + return caller.invoke("session.mcp.removeGitHub", java.util.Map.of("sessionId", this.sessionId), SessionMcpRemoveGitHubResult.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpCancelSamplingExecutionParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpCancelSamplingExecutionParams.java new file mode 100644 index 000000000..191eeda98 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpCancelSamplingExecutionParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The requestId previously passed to executeSampling that should be cancelled. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpCancelSamplingExecutionParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The requestId previously passed to executeSampling that should be cancelled */ + @JsonProperty("requestId") String requestId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpCancelSamplingExecutionResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpCancelSamplingExecutionResult.java new file mode 100644 index 000000000..35147aed2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpCancelSamplingExecutionResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpCancelSamplingExecutionResult( + /** True if an in-flight execution with the given requestId was found and signalled to cancel. False when no such execution is in flight (already completed, never started, or cancelled by another caller). */ + @JsonProperty("cancelled") Boolean cancelled +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpExecuteSamplingParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpExecuteSamplingParams.java new file mode 100644 index 000000000..19a9adffb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpExecuteSamplingParams.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpExecuteSamplingParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Caller-provided unique identifier for this sampling execution. Use this same ID with cancelSamplingExecution to cancel the in-flight call. Must be unique within the session for the lifetime of the call. */ + @JsonProperty("requestId") String requestId, + /** Name of the MCP server that initiated the sampling request */ + @JsonProperty("serverName") String serverName, + /** The original MCP JSON-RPC request ID (string or number). Used by the runtime to correlate the inference with the originating MCP request for telemetry; this is distinct from `requestId` (which is the schema-level cancellation handle). */ + @JsonProperty("mcpRequestId") Object mcpRequestId, + /** Raw MCP CreateMessageRequest params, as received in the `sampling.requested` event. Treated as opaque at the schema layer; the runtime converts the embedded MCP messages into the OpenAI chat-completion shape internally. */ + @JsonProperty("request") McpExecuteSamplingRequest request +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpExecuteSamplingResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpExecuteSamplingResult.java new file mode 100644 index 000000000..d22143c53 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpExecuteSamplingResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Outcome of an MCP sampling execution: success result, failure error, or cancellation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpExecuteSamplingResult( + /** Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. */ + @JsonProperty("action") McpSamplingExecutionAction action, + /** MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. */ + @JsonProperty("result") McpExecuteSamplingResult result, + /** Error description, present when action='failure'. */ + @JsonProperty("error") String error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpRemoveGitHubParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpRemoveGitHubParams.java new file mode 100644 index 000000000..5c7915d9f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpRemoveGitHubParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpRemoveGitHubParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpRemoveGitHubResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpRemoveGitHubResult.java new file mode 100644 index 000000000..1f13b31c4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpRemoveGitHubResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpRemoveGitHubResult( + /** True when the auto-managed `github` MCP server was removed; false when no removal happened (e.g. user has explicitly configured a `github` server, or the server was not registered). */ + @JsonProperty("removed") Boolean removed +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpSetEnvValueModeParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpSetEnvValueModeParams.java new file mode 100644 index 000000000..01efee333 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpSetEnvValueModeParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Mode controlling how MCP server env values are resolved (`direct` or `indirect`). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpSetEnvValueModeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** How environment-variable values supplied to MCP servers are resolved. "direct" passes literal string values; "indirect" treats values as references (e.g. names of environment variables on the host) that the runtime resolves before launch. Defaults to the runtime's startup mode; clients that intentionally launch MCP servers with literal values (e.g. CLI prompt mode and ACP) set this to "direct". */ + @JsonProperty("mode") McpSetEnvValueModeDetails mode +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpSetEnvValueModeResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpSetEnvValueModeResult.java new file mode 100644 index 000000000..9e69732d2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpSetEnvValueModeResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Env-value mode recorded on the session after the update. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpSetEnvValueModeResult( + /** Mode recorded on the session after the update */ + @JsonProperty("mode") McpSetEnvValueModeDetails mode +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadata.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadata.java new file mode 100644 index 000000000..6069f8ab4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadata.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `SessionMetadata` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadata( + /** Stable session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Session creation time as an ISO 8601 timestamp */ + @JsonProperty("startTime") String startTime, + /** Last-modified time of the session's persisted state, as ISO 8601 */ + @JsonProperty("modifiedTime") String modifiedTime, + /** Short summary of the session, when one has been derived */ + @JsonProperty("summary") String summary, + /** Optional human-friendly name set via /rename */ + @JsonProperty("name") String name, + /** True for remote (GitHub) sessions; false for local */ + @JsonProperty("isRemote") Boolean isRemote, + /** Schema for the `SessionContext` type. */ + @JsonProperty("context") SessionContext context, + /** GitHub task ID, when this local session is bound to one. Only present for local sessions exported to remote control. */ + @JsonProperty("mcTaskId") String mcTaskId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataApi.java new file mode 100644 index 000000000..b0a82347a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataApi.java @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code metadata} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMetadataApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionMetadataApi(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 + */ + public CompletableFuture snapshot() { + return caller.invoke("session.metadata.snapshot", java.util.Map.of("sessionId", this.sessionId), SessionMetadataSnapshotResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture isProcessing() { + return caller.invoke("session.metadata.isProcessing", java.util.Map.of("sessionId", this.sessionId), SessionMetadataIsProcessingResult.class); + } + + /** + * Model identifier and token limits used to compute the context-info breakdown. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture contextInfo(SessionMetadataContextInfoParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.metadata.contextInfo", _p, SessionMetadataContextInfoResult.class); + } + + /** + * Updated working-directory/git context to record on the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture recordContextChange(SessionMetadataRecordContextChangeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.metadata.recordContextChange", _p, Void.class); + } + + /** + * Absolute path to set as the session's new working directory. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setWorkingDirectory(SessionMetadataSetWorkingDirectoryParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.metadata.setWorkingDirectory", _p, SessionMetadataSetWorkingDirectoryResult.class); + } + + /** + * Model identifier to use when re-tokenizing the session's existing messages. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture recomputeContextTokens(SessionMetadataRecomputeContextTokensParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.metadata.recomputeContextTokens", _p, SessionMetadataRecomputeContextTokensResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataContextInfoParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataContextInfoParams.java new file mode 100644 index 000000000..94176d544 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataContextInfoParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Model identifier and token limits used to compute the context-info breakdown. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataContextInfoParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. */ + @JsonProperty("promptTokenLimit") Long promptTokenLimit, + /** Maximum output tokens allowed by the target model. Pass 0 if unknown. */ + @JsonProperty("outputTokenLimit") Long outputTokenLimit, + /** Model identifier used for tokenization. Omit to use the session default. Used both for token counting and to compute display values. */ + @JsonProperty("selectedModel") String selectedModel +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataContextInfoResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataContextInfoResult.java new file mode 100644 index 000000000..fe8e15f78 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataContextInfoResult.java @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token breakdown for the session's current context window, or null if uninitialized. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataContextInfoResult( + /** Token breakdown for the current context window, or null if the session has not yet been initialized (no system prompt or tool metadata cached). */ + @JsonProperty("contextInfo") SessionMetadataContextInfoResultContextInfo contextInfo +) { + + /** Token-usage breakdown for the session's current context window */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionMetadataContextInfoResultContextInfo( + /** The model used for token counting */ + @JsonProperty("modelName") String modelName, + /** Tokens consumed by the system prompt */ + @JsonProperty("systemTokens") Long systemTokens, + /** Tokens consumed by user/assistant/tool messages */ + @JsonProperty("conversationTokens") Long conversationTokens, + /** Tokens consumed by tool definitions sent to the model (excludes deferred tools) */ + @JsonProperty("toolDefinitionsTokens") Long toolDefinitionsTokens, + /** Sum of system, conversation and tool-definition tokens */ + @JsonProperty("totalTokens") Long totalTokens, + /** Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified) */ + @JsonProperty("promptTokenLimit") Long promptTokenLimit, + /** Token count at which background compaction starts (configurable percentage of promptTokenLimit) */ + @JsonProperty("compactionThreshold") Long compactionThreshold, + /** Total context limit for /context display. promptTokenLimit + min(32k or 64k, outputTokenLimit) depending on model. */ + @JsonProperty("limit") Long limit, + /** Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%) */ + @JsonProperty("bufferTokens") Long bufferTokens + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataIsProcessingParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataIsProcessingParams.java new file mode 100644 index 000000000..5e86f186a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataIsProcessingParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataIsProcessingParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataIsProcessingResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataIsProcessingResult.java new file mode 100644 index 000000000..fb5d4e59b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataIsProcessingResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the local session is currently processing a turn or background continuation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataIsProcessingResult( + /** Whether the session is currently processing user/agent messages. False for non-local sessions (which don't run a local agentic loop). Reflects an in-flight turn or background continuation. */ + @JsonProperty("processing") Boolean processing +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecomputeContextTokensParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecomputeContextTokensParams.java new file mode 100644 index 000000000..27824912c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecomputeContextTokensParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Model identifier to use when re-tokenizing the session's existing messages. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataRecomputeContextTokensParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Model identifier used for tokenization. The runtime token-counts both chat-context and system-context messages against this model. */ + @JsonProperty("modelId") String modelId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecomputeContextTokensResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecomputeContextTokensResult.java new file mode 100644 index 000000000..c8d398a4d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecomputeContextTokensResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Re-tokenize the session's existing messages against `modelId` and return the token totals. Useful for hosts that want an initial estimate of context usage on session resume, before the next agent turn fires `session.context_info_changed` events. Returns zeros for an empty session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataRecomputeContextTokensResult( + /** Sum of tokens across chat-context and system-context messages currently held by the session. */ + @JsonProperty("totalTokens") Long totalTokens, + /** Tokens contributed by user/assistant/tool messages (excludes system/developer prompts). */ + @JsonProperty("messagesTokenCount") Long messagesTokenCount, + /** Tokens contributed by system/developer prompt snapshots. */ + @JsonProperty("systemTokenCount") Long systemTokenCount +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecordContextChangeParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecordContextChangeParams.java new file mode 100644 index 000000000..24e1c79ba --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecordContextChangeParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Updated working-directory/git context to record on the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataRecordContextChangeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Updated working directory and git context. Emitted as the new payload of `session.context_changed`. */ + @JsonProperty("context") SessionWorkingDirectoryContext context +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecordContextChangeResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecordContextChangeResult.java new file mode 100644 index 000000000..08d780868 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataRecordContextChangeResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataRecordContextChangeResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSetWorkingDirectoryParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSetWorkingDirectoryParams.java new file mode 100644 index 000000000..436286f65 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSetWorkingDirectoryParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Absolute path to set as the session's new working directory. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataSetWorkingDirectoryParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Absolute path to set as the session's working directory. The runtime updates the session's recorded cwd so subsequent operations (shell tools, file lookups, telemetry) anchor to it. */ + @JsonProperty("workingDirectory") String workingDirectory +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSetWorkingDirectoryResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSetWorkingDirectoryResult.java new file mode 100644 index 000000000..b1c0f5fb9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSetWorkingDirectoryResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataSetWorkingDirectoryResult( + /** Working directory after the update */ + @JsonProperty("workingDirectory") String workingDirectory +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSnapshotParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSnapshotParams.java new file mode 100644 index 000000000..ffbad219e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSnapshotParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataSnapshotParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSnapshotResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSnapshotResult.java new file mode 100644 index 000000000..49002659e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMetadataSnapshotResult.java @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Point-in-time snapshot of slow-changing session identifier and state fields + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMetadataSnapshotResult( + /** The unique identifier of the session */ + @JsonProperty("sessionId") String sessionId, + /** ISO 8601 timestamp of when the session started */ + @JsonProperty("startTime") OffsetDateTime startTime, + /** ISO 8601 timestamp of when the session's persisted state was last modified on disk. For new sessions, equals startTime. For resumed sessions, reflects the previous modification time at construction. */ + @JsonProperty("modifiedTime") OffsetDateTime modifiedTime, + /** Whether this is a remote session (i.e., one whose runtime executes elsewhere and is steered through this process) */ + @JsonProperty("isRemote") Boolean isRemote, + /** True when the session was detected to be in use by another process at construction time. Local consumers may surface a confirmation prompt before fully attaching. Always false for new sessions. */ + @JsonProperty("alreadyInUse") Boolean alreadyInUse, + /** Absolute path to the session's workspace directory on disk, or null if the session has no associated workspace */ + @JsonProperty("workspacePath") String workspacePath, + /** User-provided name supplied at session construction (via `--name`), if any. Immutable after construction. */ + @JsonProperty("initialName") String initialName, + /** Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. */ + @JsonProperty("remoteMetadata") MetadataSnapshotRemoteMetadata remoteMetadata, + /** Short human-readable summary of the session, if known. Omitted when no summary has been generated. */ + @JsonProperty("summary") String summary, + /** Absolute path to the session's current working directory */ + @JsonProperty("workingDirectory") String workingDirectory, + /** The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot') */ + @JsonProperty("currentMode") MetadataSnapshotCurrentMode currentMode, + /** Currently selected model identifier, if any */ + @JsonProperty("selectedModel") String selectedModel, + /** Public-facing workspace metadata for this session, or null if the session has no associated workspace. Excludes runtime-internal fields (GitHub IDs, summary count, internal flags). */ + @JsonProperty("workspace") SessionMetadataSnapshotResultWorkspace workspace +) { + + /** Public-facing projection of workspace metadata for SDK / TUI consumers */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionMetadataSnapshotResultWorkspace( + /** Workspace identifier (1:1 with sessionId) */ + @JsonProperty("id") String id, + /** Current working directory at session start */ + @JsonProperty("cwd") String cwd, + /** Resolved git root for cwd, if any */ + @JsonProperty("git_root") String gitRoot, + /** Repository identifier in 'owner/repo' or 'org/project/repo' format, if any */ + @JsonProperty("repository") String repository, + /** Repository host type, if known */ + @JsonProperty("host_type") WorkspaceSummaryHostType hostType, + /** Branch checked out at session start, if any */ + @JsonProperty("branch") String branch, + /** Display name for the session, if set */ + @JsonProperty("name") String name, + /** ISO 8601 timestamp when the workspace was created */ + @JsonProperty("created_at") OffsetDateTime createdAt, + /** ISO 8601 timestamp when the workspace was last updated */ + @JsonProperty("updated_at") OffsetDateTime updatedAt + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeApi.java index 9e67580dc..b28cd5d37 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeApi.java @@ -31,6 +31,8 @@ public final class SessionModeApi { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture get() { @@ -42,6 +44,8 @@ public CompletableFuture get() { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture set(SessionModeSetParams params) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelApi.java index 55b3b18c9..b2c112b66 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelApi.java @@ -31,6 +31,8 @@ public final class SessionModelApi { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture getCurrent() { @@ -42,6 +44,8 @@ public CompletableFuture getCurrent() { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture switchTo(SessionModelSwitchToParams params) { @@ -50,4 +54,19 @@ public CompletableFuture switchTo(SessionModelSwitch return caller.invoke("session.model.switchTo", _p, SessionModelSwitchToResult.class); } + /** + * Reasoning effort level to apply to the currently selected model. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setReasoningEffort(SessionModelSetReasoningEffortParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.model.setReasoningEffort", _p, SessionModelSetReasoningEffortResult.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentResult.java index 4a5a60525..5afd22911 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentResult.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * The currently selected model for the session. + * The currently selected model and reasoning effort for the session. * * @since 1.0.0 */ @@ -22,6 +22,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record SessionModelGetCurrentResult( /** Currently active model identifier */ - @JsonProperty("modelId") String modelId + @JsonProperty("modelId") String modelId, + /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ + @JsonProperty("reasoningEffort") String reasoningEffort ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSetReasoningEffortParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSetReasoningEffortParams.java new file mode 100644 index 000000000..da6bb1870 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSetReasoningEffortParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Reasoning effort level to apply to the currently selected model. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelSetReasoningEffortParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Reasoning effort level to apply to the currently selected model. The host is responsible for validating the value against the model's supported levels before calling. */ + @JsonProperty("reasoningEffort") String reasoningEffort +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSetReasoningEffortResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSetReasoningEffortResult.java new file mode 100644 index 000000000..0ec6e239e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSetReasoningEffortResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Update the session's reasoning effort without changing the selected model. Use `switchTo` instead when you also need to change the model. The runtime stores the effort on the session and applies it to subsequent turns. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelSetReasoningEffortResult( + /** Reasoning effort level recorded on the session after the update */ + @JsonProperty("reasoningEffort") String reasoningEffort +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameApi.java index 371dc15ab..73b6c6bc2 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameApi.java @@ -31,6 +31,8 @@ public final class SessionNameApi { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture get() { @@ -42,6 +44,8 @@ public CompletableFuture get() { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture set(SessionNameSetParams params) { @@ -50,4 +54,19 @@ public CompletableFuture set(SessionNameSetParams params) { return caller.invoke("session.name.set", _p, Void.class); } + /** + * Auto-generated session summary to apply as the session's name when no user-set name exists. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setAuto(SessionNameSetAutoParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.name.setAuto", _p, SessionNameSetAutoResult.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetAutoParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetAutoParams.java new file mode 100644 index 000000000..4b6db1bde --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetAutoParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Auto-generated session summary to apply as the session's name when no user-set name exists. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionNameSetAutoParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Auto-generated session summary. Empty/whitespace-only values are ignored; values are trimmed before persisting. */ + @JsonProperty("summary") String summary +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetAutoResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetAutoResult.java new file mode 100644 index 000000000..134694a8c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetAutoResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the auto-generated summary was applied as the session's name. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionNameSetAutoResult( + /** Whether the auto-generated summary was persisted. False if the session already has a user-set name, the summary normalized to empty, or the session does not have a workspace. */ + @JsonProperty("applied") Boolean applied +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsApi.java new file mode 100644 index 000000000..8624c57e3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsApi.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code options} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionOptionsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionOptionsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Patch of mutable session options to apply to the running session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture update(SessionOptionsUpdateParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.options.update", _p, SessionOptionsUpdateResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsUpdateParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsUpdateParams.java new file mode 100644 index 000000000..fa768dd7b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsUpdateParams.java @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Patch of mutable session options to apply to the running session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionOptionsUpdateParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The model ID to use for assistant turns. */ + @JsonProperty("model") String model, + /** Reasoning effort for the selected model (model-defined enum). */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Identifier of the client driving the session. */ + @JsonProperty("clientName") String clientName, + /** Identifier sent to LSP-style integrations. */ + @JsonProperty("lspClientName") String lspClientName, + /** Stable integration identifier used for analytics and rate-limit attribution. */ + @JsonProperty("integrationId") String integrationId, + /** Map of feature-flag IDs to their boolean enabled state. */ + @JsonProperty("featureFlags") Map featureFlags, + /** Whether experimental capabilities are enabled. */ + @JsonProperty("isExperimentalMode") Boolean isExperimentalMode, + /** Custom model-provider configuration (BYOK). Opaque shape; see `ProviderConfig` in the runtime. */ + @JsonProperty("provider") Object provider, + /** Absolute working-directory path for shell tools. */ + @JsonProperty("workingDirectory") String workingDirectory, + /** Allowlist of tool names available to this session. */ + @JsonProperty("availableTools") List availableTools, + /** Denylist of tool names for this session. */ + @JsonProperty("excludedTools") List excludedTools, + /** Whether shell-script safety heuristics are enabled. */ + @JsonProperty("enableScriptSafety") Boolean enableScriptSafety, + /** Shell init profile (`None` or `NonInteractive`). */ + @JsonProperty("shellInitProfile") String shellInitProfile, + /** Per-shell process flags (e.g., `pwsh` arguments). */ + @JsonProperty("shellProcessFlags") List shellProcessFlags, + /** Sandbox configuration shape; opaque to SDK consumers. See `SandboxConfig` in the runtime. */ + @JsonProperty("sandboxConfig") Object sandboxConfig, + /** Whether interactive shell sessions are logged. */ + @JsonProperty("logInteractiveShells") Boolean logInteractiveShells, + /** How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). */ + @JsonProperty("envValueMode") OptionsUpdateEnvValueMode envValueMode, + /** Additional directories to search for skills. */ + @JsonProperty("skillDirectories") List skillDirectories, + /** Skill IDs that should be excluded from this session. */ + @JsonProperty("disabledSkills") List disabledSkills, + /** Whether to discover custom instructions on demand after successful file views (AGENTS.md / CLAUDE.md / .github/copilot-instructions.md surfacing). Combined with `skipCustomInstructions` and the runtime-side `ON_DEMAND_INSTRUCTIONS` feature flag. */ + @JsonProperty("enableOnDemandInstructionDiscovery") Boolean enableOnDemandInstructionDiscovery, + /** Full set of installed plugins for the session. Replaces the existing list; the runtime invalidates the skills cache only when the list materially changes. */ + @JsonProperty("installedPlugins") List installedPlugins, + /** Whether to default custom agents to local-only execution. */ + @JsonProperty("customAgentsLocalOnly") Boolean customAgentsLocalOnly, + /** Whether to skip loading custom instruction sources. */ + @JsonProperty("skipCustomInstructions") Boolean skipCustomInstructions, + /** Instruction source IDs to exclude from the system prompt. */ + @JsonProperty("disabledInstructionSources") List disabledInstructionSources, + /** Whether to include the `Co-authored-by` trailer in commit messages. */ + @JsonProperty("coauthorEnabled") Boolean coauthorEnabled, + /** Optional path for trajectory output. */ + @JsonProperty("trajectoryFile") String trajectoryFile, + /** Whether to stream model responses. */ + @JsonProperty("enableStreaming") Boolean enableStreaming, + /** Override URL for the Copilot API endpoint. */ + @JsonProperty("copilotUrl") String copilotUrl, + /** Whether to disable the `ask_user` tool (encourages autonomous behavior). */ + @JsonProperty("askUserDisabled") Boolean askUserDisabled, + /** Whether to allow auto-mode continuation across turns. */ + @JsonProperty("continueOnAutoMode") Boolean continueOnAutoMode, + /** Whether the session is running in an interactive UI. */ + @JsonProperty("runningInInteractiveMode") Boolean runningInInteractiveMode, + /** Whether to surface reasoning-summary events from the model. */ + @JsonProperty("enableReasoningSummaries") Boolean enableReasoningSummaries, + /** Runtime context discriminator (e.g., `cli`, `actions`). */ + @JsonProperty("agentContext") String agentContext, + /** Override directory for the session-events log. When unset, the runtime's default events log directory is used. */ + @JsonProperty("eventsLogDirectory") String eventsLogDirectory, + /** Additional content-exclusion policies to merge into the session's policy set. Opaque shape; see `ContentExclusionApiResponse` in the runtime. */ + @JsonProperty("additionalContentExclusionPolicies") List additionalContentExclusionPolicies, + /** Whether to expose the `manage_schedule` tool to the agent. The runtime always owns the per-session schedule registry; this flag only controls tool exposure (typically gated to staff users). */ + @JsonProperty("manageScheduleEnabled") Boolean manageScheduleEnabled +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsUpdateResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsUpdateResult.java new file mode 100644 index 000000000..a20c64ab8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionOptionsUpdateResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the session options patch was applied successfully. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionOptionsUpdateResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsApi.java index 6a32d2d19..082268d89 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsApi.java @@ -23,10 +23,38 @@ public final class SessionPermissionsApi { private final RpcCaller caller; private final String sessionId; + /** API methods for the {@code permissions.paths} sub-namespace. */ + public final SessionPermissionsPathsApi paths; + /** API methods for the {@code permissions.locations} sub-namespace. */ + public final SessionPermissionsLocationsApi locations; + /** API methods for the {@code permissions.folderTrust} sub-namespace. */ + public final SessionPermissionsFolderTrustApi folderTrust; + /** API methods for the {@code permissions.urls} sub-namespace. */ + public final SessionPermissionsUrlsApi urls; + /** @param caller the RPC transport function */ SessionPermissionsApi(RpcCaller caller, String sessionId) { this.caller = caller; this.sessionId = sessionId; + this.paths = new SessionPermissionsPathsApi(caller, sessionId); + this.locations = new SessionPermissionsLocationsApi(caller, sessionId); + this.folderTrust = new SessionPermissionsFolderTrustApi(caller, sessionId); + this.urls = new SessionPermissionsUrlsApi(caller, sessionId); + } + + /** + * Patch of permission policy fields to apply (omit a field to leave it unchanged). + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture configure(SessionPermissionsConfigureParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.configure", _p, SessionPermissionsConfigureResult.class); } /** @@ -34,6 +62,8 @@ public final class SessionPermissionsApi { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture handlePendingPermissionRequest(SessionPermissionsHandlePendingPermissionRequestParams params) { @@ -43,10 +73,22 @@ public CompletableFuture } /** - * Whether to auto-approve all tool permission requests for the rest of the session. + * No parameters; returns currently-pending permission requests for the session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture pendingRequests() { + return caller.invoke("session.permissions.pendingRequests", java.util.Map.of("sessionId", this.sessionId), SessionPermissionsPendingRequestsResult.class); + } + + /** + * Allow-all toggle for tool permission requests, with an optional telemetry source. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture setApproveAll(SessionPermissionsSetApproveAllParams params) { @@ -55,12 +97,59 @@ public CompletableFuture setApproveAll(Se return caller.invoke("session.permissions.setApproveAll", _p, SessionPermissionsSetApproveAllResult.class); } + /** + * Scope and add/remove instructions for modifying session- or location-scoped permission rules. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture modifyRules(SessionPermissionsModifyRulesParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.modifyRules", _p, SessionPermissionsModifyRulesResult.class); + } + + /** + * Toggles whether permission prompts should be bridged into session events for this client. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setRequired(SessionPermissionsSetRequiredParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.setRequired", _p, SessionPermissionsSetRequiredResult.class); + } + /** * No parameters; clears all session-scoped tool permission approvals. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture resetSessionApprovals() { return caller.invoke("session.permissions.resetSessionApprovals", java.util.Map.of("sessionId", this.sessionId), SessionPermissionsResetSessionApprovalsResult.class); } + /** + * Notification payload describing the permission prompt that the client just rendered. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture notifyPromptShown(SessionPermissionsNotifyPromptShownParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.notifyPromptShown", _p, SessionPermissionsNotifyPromptShownResult.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsConfigureParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsConfigureParams.java new file mode 100644 index 000000000..494495fc5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsConfigureParams.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Patch of permission policy fields to apply (omit a field to leave it unchanged). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsConfigureParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** If specified, sets whether tool permission requests are auto-approved without prompting. Omit to leave the current value unchanged. */ + @JsonProperty("approveAllToolPermissionRequests") Boolean approveAllToolPermissionRequests, + /** If specified, sets whether path/URL read permission requests are auto-approved. Omit to leave the current value unchanged. */ + @JsonProperty("approveAllReadPermissionRequests") Boolean approveAllReadPermissionRequests, + /** If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. */ + @JsonProperty("rules") PermissionRulesSet rules, + /** If specified, replaces the session's path-permission policy. The runtime constructs the appropriate PathManager based on these inputs (rooted at the session's working directory). Omit to leave the current path policy unchanged. */ + @JsonProperty("paths") PermissionPathsConfig paths, + /** If specified, replaces the session's URL-permission policy. The runtime constructs a fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy unchanged. */ + @JsonProperty("urls") PermissionUrlsConfig urls, + /** If specified, replaces the host-supplied GitHub Content Exclusion policies on the session (combined with natively-discovered policies when evaluating tool/file access). Omit to leave the current policies unchanged. */ + @JsonProperty("additionalContentExclusionPolicies") List additionalContentExclusionPolicies +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsConfigureResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsConfigureResult.java new file mode 100644 index 000000000..7f9ce4150 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsConfigureResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsConfigureResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustAddTrustedParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustAddTrustedParams.java new file mode 100644 index 000000000..184087f22 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustAddTrustedParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Folder path to add to trusted folders. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsFolderTrustAddTrustedParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Folder path to mark as trusted */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustAddTrustedResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustAddTrustedResult.java new file mode 100644 index 000000000..dbe09efc5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustAddTrustedResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsFolderTrustAddTrustedResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustApi.java new file mode 100644 index 000000000..5337a40f1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustApi.java @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code permissions.folderTrust} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionPermissionsFolderTrustApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionPermissionsFolderTrustApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Folder path to check for trust. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture isTrusted(SessionPermissionsFolderTrustIsTrustedParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.folderTrust.isTrusted", _p, SessionPermissionsFolderTrustIsTrustedResult.class); + } + + /** + * Folder path to add to trusted folders. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture addTrusted(SessionPermissionsFolderTrustAddTrustedParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.folderTrust.addTrusted", _p, SessionPermissionsFolderTrustAddTrustedResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustIsTrustedParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustIsTrustedParams.java new file mode 100644 index 000000000..bacbe5649 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustIsTrustedParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Folder path to check for trust. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsFolderTrustIsTrustedParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Folder path to check */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustIsTrustedResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustIsTrustedResult.java new file mode 100644 index 000000000..3f2e26a0d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsFolderTrustIsTrustedResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Folder trust check result. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsFolderTrustIsTrustedResult( + /** Whether the folder is trusted */ + @JsonProperty("trusted") Boolean trusted +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestParams.java index 7991f3248..f8f10a8d1 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestParams.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestParams.java @@ -25,7 +25,7 @@ public record SessionPermissionsHandlePendingPermissionRequestParams( @JsonProperty("sessionId") String sessionId, /** Request ID of the pending permission request */ @JsonProperty("requestId") String requestId, - /** Decision to apply to a pending permission request. */ + /** The client's response to the pending permission prompt */ @JsonProperty("result") Object result ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsAddToolApprovalParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsAddToolApprovalParams.java new file mode 100644 index 000000000..42429498d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsAddToolApprovalParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Location-scoped tool approval to persist. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsLocationsAddToolApprovalParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Location key (git root or cwd) to persist the approval to */ + @JsonProperty("locationKey") String locationKey, + /** Tool approval to persist and apply */ + @JsonProperty("approval") Object approval +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsAddToolApprovalResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsAddToolApprovalResult.java new file mode 100644 index 000000000..fae259c23 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsAddToolApprovalResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsLocationsAddToolApprovalResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApi.java new file mode 100644 index 000000000..388bd49df --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApi.java @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code permissions.locations} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionPermissionsLocationsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionPermissionsLocationsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Working directory to resolve into a location-permissions key. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture resolve(SessionPermissionsLocationsResolveParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.locations.resolve", _p, SessionPermissionsLocationsResolveResult.class); + } + + /** + * Working directory to load persisted location permissions for. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture apply(SessionPermissionsLocationsApplyParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.locations.apply", _p, SessionPermissionsLocationsApplyResult.class); + } + + /** + * Location-scoped tool approval to persist. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture addToolApproval(SessionPermissionsLocationsAddToolApprovalParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.locations.addToolApproval", _p, SessionPermissionsLocationsAddToolApprovalResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApplyParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApplyParams.java new file mode 100644 index 000000000..7af5ef28f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApplyParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Working directory to load persisted location permissions for. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsLocationsApplyParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Working directory whose persisted location permissions should be applied */ + @JsonProperty("workingDirectory") String workingDirectory +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApplyResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApplyResult.java new file mode 100644 index 000000000..b2514d8af --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsApplyResult.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Summary of persisted location permissions applied to the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsLocationsApplyResult( + /** Location key used in the location-permissions store */ + @JsonProperty("locationKey") String locationKey, + /** Whether the location is a git repo or directory */ + @JsonProperty("locationType") PermissionLocationType locationType, + /** Whether a different location was applied since the previous apply call */ + @JsonProperty("changed") Boolean changed, + /** Number of location-scoped rules added to the live permission service */ + @JsonProperty("appliedRuleCount") Long appliedRuleCount, + /** Number of persisted allowed directories added to the live path manager */ + @JsonProperty("appliedDirectoryCount") Long appliedDirectoryCount, + /** Location-scoped rules applied to the live permission service */ + @JsonProperty("appliedRules") List appliedRules +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsResolveParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsResolveParams.java new file mode 100644 index 000000000..d6ba06fba --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsResolveParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Working directory to resolve into a location-permissions key. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsLocationsResolveParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Working directory whose permission location should be resolved */ + @JsonProperty("workingDirectory") String workingDirectory +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsResolveResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsResolveResult.java new file mode 100644 index 000000000..3254bb2c6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsLocationsResolveResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Resolved location-permissions key and type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsLocationsResolveResult( + /** Location key used in the location-permissions store */ + @JsonProperty("locationKey") String locationKey, + /** Whether the location is a git repo or directory */ + @JsonProperty("locationType") PermissionLocationType locationType +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsModifyRulesParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsModifyRulesParams.java new file mode 100644 index 000000000..ca91f611e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsModifyRulesParams.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Scope and add/remove instructions for modifying session- or location-scoped permission rules. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsModifyRulesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Whether the change applies to ephemeral session-scoped rules (cleared at session end) or to location-scoped rules persisted via the location-permissions config file. */ + @JsonProperty("scope") PermissionsModifyRulesScope scope, + /** Rules to add to the scope. Applied before `remove`/`removeAll`. */ + @JsonProperty("add") List add, + /** Specific rules to remove from the scope. Ignored when `removeAll` is true. */ + @JsonProperty("remove") List remove, + /** When true, removes every rule currently in the scope (after any `add` is applied). Useful for clearing the location scope wholesale. */ + @JsonProperty("removeAll") Boolean removeAll +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsModifyRulesResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsModifyRulesResult.java new file mode 100644 index 000000000..b4ed7786f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsModifyRulesResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsModifyRulesResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsNotifyPromptShownParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsNotifyPromptShownParams.java new file mode 100644 index 000000000..50129680f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsNotifyPromptShownParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Notification payload describing the permission prompt that the client just rendered. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsNotifyPromptShownParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Human-readable description of the prompt the user is being asked to approve. Used by the runtime to fire the registered `permission_prompt` notification hook (e.g. terminal bell, desktop notification). */ + @JsonProperty("message") String message +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsNotifyPromptShownResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsNotifyPromptShownResult.java new file mode 100644 index 000000000..6f2a368f4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsNotifyPromptShownResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsNotifyPromptShownResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsAddParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsAddParams.java new file mode 100644 index 000000000..5730b6b5e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsAddParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Directory path to add to the session's allowed directories. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsAddParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Directory to add to the allow-list. The runtime resolves and validates the path before adding. */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsAddResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsAddResult.java new file mode 100644 index 000000000..efb8fd5c4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsAddResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsAddResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsApi.java new file mode 100644 index 000000000..38ad70c67 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsApi.java @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code permissions.paths} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionPermissionsPathsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionPermissionsPathsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * No parameters; returns the session's allow-listed directories. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.permissions.paths.list", java.util.Map.of("sessionId", this.sessionId), SessionPermissionsPathsListResult.class); + } + + /** + * Directory path to add to the session's allowed directories. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture add(SessionPermissionsPathsAddParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.paths.add", _p, SessionPermissionsPathsAddResult.class); + } + + /** + * Directory path to set as the session's new primary working directory. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture updatePrimary(SessionPermissionsPathsUpdatePrimaryParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.paths.updatePrimary", _p, SessionPermissionsPathsUpdatePrimaryResult.class); + } + + /** + * Path to evaluate against the session's allowed directories. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture isPathWithinAllowedDirectories(SessionPermissionsPathsIsPathWithinAllowedDirectoriesParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.paths.isPathWithinAllowedDirectories", _p, SessionPermissionsPathsIsPathWithinAllowedDirectoriesResult.class); + } + + /** + * Path to evaluate against the session's workspace (primary) directory. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture isPathWithinWorkspace(SessionPermissionsPathsIsPathWithinWorkspaceParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.paths.isPathWithinWorkspace", _p, SessionPermissionsPathsIsPathWithinWorkspaceResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinAllowedDirectoriesParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinAllowedDirectoriesParams.java new file mode 100644 index 000000000..71bd09a61 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinAllowedDirectoriesParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Path to evaluate against the session's allowed directories. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsIsPathWithinAllowedDirectoriesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path to check against the session's allowed directories */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinAllowedDirectoriesResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinAllowedDirectoriesResult.java new file mode 100644 index 000000000..6aadee068 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinAllowedDirectoriesResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the supplied path is within the session's allowed directories. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsIsPathWithinAllowedDirectoriesResult( + /** Whether the path is within the session's allowed directories */ + @JsonProperty("allowed") Boolean allowed +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinWorkspaceParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinWorkspaceParams.java new file mode 100644 index 000000000..699bf4cc8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinWorkspaceParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Path to evaluate against the session's workspace (primary) directory. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsIsPathWithinWorkspaceParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path to check against the session workspace directory */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinWorkspaceResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinWorkspaceResult.java new file mode 100644 index 000000000..3d64be687 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsIsPathWithinWorkspaceResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the supplied path is within the session's workspace directory. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsIsPathWithinWorkspaceResult( + /** Whether the path is within the session workspace directory */ + @JsonProperty("allowed") Boolean allowed +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsListParams.java new file mode 100644 index 000000000..23fad022d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * No parameters; returns the session's allow-listed directories. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsListResult.java new file mode 100644 index 000000000..ab56c4a29 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsListResult.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Snapshot of the session's allow-listed directories and primary working directory. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsListResult( + /** All directories currently allowed for tool access on this session. */ + @JsonProperty("directories") List directories, + /** The primary working directory for this session. */ + @JsonProperty("primary") String primary +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsUpdatePrimaryParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsUpdatePrimaryParams.java new file mode 100644 index 000000000..626ef5752 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsUpdatePrimaryParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Directory path to set as the session's new primary working directory. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsUpdatePrimaryParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Directory to set as the new primary working directory for the session's permission policy. */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsUpdatePrimaryResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsUpdatePrimaryResult.java new file mode 100644 index 000000000..34b8c403e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPathsUpdatePrimaryResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPathsUpdatePrimaryResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPendingRequestsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPendingRequestsParams.java new file mode 100644 index 000000000..c7ec7f946 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPendingRequestsParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * No parameters; returns currently-pending permission requests for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPendingRequestsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPendingRequestsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPendingRequestsResult.java new file mode 100644 index 000000000..b2b3c4ec1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsPendingRequestsResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * List of pending permission requests reconstructed from event history. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsPendingRequestsResult( + /** Pending permission prompts reconstructed from the session's event history. Equivalent to the set of `permission.requested` events that have not yet been followed by a matching `permission.completed` event. Used by clients (e.g. the CLI) to hydrate UI for prompts that were emitted before the client attached to the session. */ + @JsonProperty("items") List items +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllParams.java index ac599a2df..c61bdcca0 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllParams.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllParams.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Whether to auto-approve all tool permission requests for the rest of the session. + * Allow-all toggle for tool permission requests, with an optional telemetry source. * * @since 1.0.0 */ @@ -24,6 +24,8 @@ public record SessionPermissionsSetApproveAllParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Whether to auto-approve all tool permission requests */ - @JsonProperty("enabled") Boolean enabled + @JsonProperty("enabled") Boolean enabled, + /** Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. */ + @JsonProperty("source") PermissionsSetApproveAllSource source ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetRequiredParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetRequiredParams.java new file mode 100644 index 000000000..e862dd76f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetRequiredParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Toggles whether permission prompts should be bridged into session events for this client. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsSetRequiredParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Whether the client wants `permission.requested` events bridged from the session-owned permission service. CLI clients that render prompt UI set this to `true` for as long as their listener is mounted; headless callers leave it unset (the default is `false`). */ + @JsonProperty("required") Boolean required +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetRequiredResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetRequiredResult.java new file mode 100644 index 000000000..56f90afbc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetRequiredResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsSetRequiredResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsApi.java new file mode 100644 index 000000000..1f46c66dc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsApi.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code permissions.urls} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionPermissionsUrlsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionPermissionsUrlsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Whether the URL-permission policy should run in unrestricted mode. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setUnrestrictedMode(SessionPermissionsUrlsSetUnrestrictedModeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.urls.setUnrestrictedMode", _p, SessionPermissionsUrlsSetUnrestrictedModeResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsSetUnrestrictedModeParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsSetUnrestrictedModeParams.java new file mode 100644 index 000000000..ed3c0bc6b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsSetUnrestrictedModeParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Whether the URL-permission policy should run in unrestricted mode. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsUrlsSetUnrestrictedModeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Whether to allow access to all URLs without prompting. Toggles the runtime's URL-permission policy in place. */ + @JsonProperty("enabled") Boolean enabled +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsSetUnrestrictedModeResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsSetUnrestrictedModeResult.java new file mode 100644 index 000000000..41199db33 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsUrlsSetUnrestrictedModeResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsUrlsSetUnrestrictedModeResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanApi.java index 8f4bc6192..165621138 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanApi.java @@ -31,6 +31,8 @@ public final class SessionPlanApi { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture read() { @@ -42,6 +44,8 @@ public CompletableFuture read() { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture update(SessionPlanUpdateParams params) { @@ -52,6 +56,8 @@ public CompletableFuture update(SessionPlanUpdateParams params) { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture delete() { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueApi.java new file mode 100644 index 000000000..19533541d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueApi.java @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code queue} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionQueueApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionQueueApi(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 + */ + public CompletableFuture pendingItems() { + return caller.invoke("session.queue.pendingItems", java.util.Map.of("sessionId", this.sessionId), SessionQueuePendingItemsResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture removeMostRecent() { + return caller.invoke("session.queue.removeMostRecent", java.util.Map.of("sessionId", this.sessionId), SessionQueueRemoveMostRecentResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture clear() { + return caller.invoke("session.queue.clear", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueClearParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueClearParams.java new file mode 100644 index 000000000..1ab26019e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueClearParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQueueClearParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueuePendingItemsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueuePendingItemsParams.java new file mode 100644 index 000000000..6bc51f807 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueuePendingItemsParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQueuePendingItemsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueuePendingItemsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueuePendingItemsResult.java new file mode 100644 index 000000000..9f839fdc1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueuePendingItemsResult.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Snapshot of the session's pending queued items and immediate-steering messages. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQueuePendingItemsResult( + /** Pending queued items in submission order. Includes user messages, queued slash commands, and queued model changes; omits internal system items. */ + @JsonProperty("items") List items, + /** Display text for messages currently in the immediate steering queue (interjections sent during a running turn). */ + @JsonProperty("steeringMessages") List steeringMessages +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueRemoveMostRecentParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueRemoveMostRecentParams.java new file mode 100644 index 000000000..1ca36be68 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueRemoveMostRecentParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQueueRemoveMostRecentParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueRemoveMostRecentResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueRemoveMostRecentResult.java new file mode 100644 index 000000000..a92c93d5f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionQueueRemoveMostRecentResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether a user-facing pending item was removed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionQueueRemoveMostRecentResult( + /** True if a user-facing pending item was removed (LIFO across both queues); false when no removable items remained. */ + @JsonProperty("removed") Boolean removed +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java index ba3c91dd0..f5f0ec3cc 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java @@ -54,4 +54,19 @@ public CompletableFuture disable() { return caller.invoke("session.remote.disable", java.util.Map.of("sessionId", this.sessionId), Void.class); } + /** + * New remote-steerability state to persist as a `session.remote_steerable_changed` event. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture notifySteerableChanged(SessionRemoteNotifySteerableChangedParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.remote.notifySteerableChanged", _p, Void.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteNotifySteerableChangedParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteNotifySteerableChangedParams.java new file mode 100644 index 000000000..8851fad7a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteNotifySteerableChangedParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * New remote-steerability state to persist as a `session.remote_steerable_changed` event. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteNotifySteerableChangedParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Whether the session now supports remote steering via GitHub. The runtime persists this as a `session.remote_steerable_changed` event so resume/replay sees the up-to-date capability. */ + @JsonProperty("remoteSteerable") Boolean remoteSteerable +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteNotifySteerableChangedResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteNotifySteerableChangedResult.java new file mode 100644 index 000000000..857f37362 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteNotifySteerableChangedResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Persist a steerability change as a `session.remote_steerable_changed` event. Used by the host (CLI / SDK consumer) when it has just finished enabling or disabling steering on a remote exporter that the runtime does not directly own. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteNotifySteerableChangedResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java index ffd890a76..a2639eb0a 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java @@ -54,24 +54,38 @@ public final class SessionRpc { public final SessionMcpApi mcp; /** API methods for the {@code plugins} namespace. */ public final SessionPluginsApi plugins; + /** API methods for the {@code options} namespace. */ + public final SessionOptionsApi options; + /** API methods for the {@code lsp} namespace. */ + public final SessionLspApi lsp; /** API methods for the {@code extensions} namespace. */ public final SessionExtensionsApi extensions; /** API methods for the {@code tools} namespace. */ public final SessionToolsApi tools; /** API methods for the {@code commands} namespace. */ public final SessionCommandsApi commands; + /** API methods for the {@code telemetry} namespace. */ + public final SessionTelemetryApi telemetry; /** API methods for the {@code ui} namespace. */ public final SessionUiApi ui; /** API methods for the {@code permissions} namespace. */ public final SessionPermissionsApi permissions; + /** API methods for the {@code metadata} namespace. */ + public final SessionMetadataApi metadata; /** API methods for the {@code shell} namespace. */ public final SessionShellApi shell; /** API methods for the {@code history} namespace. */ public final SessionHistoryApi history; + /** API methods for the {@code queue} namespace. */ + public final SessionQueueApi queue; + /** API methods for the {@code eventLog} namespace. */ + public final SessionEventLogApi eventLog; /** API methods for the {@code usage} namespace. */ public final SessionUsageApi usage; /** API methods for the {@code remote} namespace. */ public final SessionRemoteApi remote; + /** API methods for the {@code schedule} namespace. */ + public final SessionScheduleApi schedule; /** * Creates a new session RPC client. @@ -95,19 +109,28 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.skills = new SessionSkillsApi(caller, sessionId); this.mcp = new SessionMcpApi(caller, sessionId); this.plugins = new SessionPluginsApi(caller, sessionId); + this.options = new SessionOptionsApi(caller, sessionId); + this.lsp = new SessionLspApi(caller, sessionId); this.extensions = new SessionExtensionsApi(caller, sessionId); this.tools = new SessionToolsApi(caller, sessionId); this.commands = new SessionCommandsApi(caller, sessionId); + this.telemetry = new SessionTelemetryApi(caller, sessionId); this.ui = new SessionUiApi(caller, sessionId); this.permissions = new SessionPermissionsApi(caller, sessionId); + this.metadata = new SessionMetadataApi(caller, sessionId); this.shell = new SessionShellApi(caller, sessionId); this.history = new SessionHistoryApi(caller, sessionId); + this.queue = new SessionQueueApi(caller, sessionId); + this.eventLog = new SessionEventLogApi(caller, sessionId); this.usage = new SessionUsageApi(caller, sessionId); this.remote = new SessionRemoteApi(caller, sessionId); + this.schedule = new SessionScheduleApi(caller, sessionId); } /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture suspend() { @@ -115,10 +138,57 @@ public CompletableFuture suspend() { } /** - * Message text, optional severity level, persistence flag, and optional follow-up URL. + * Parameters for sending a user message to the session + *

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

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

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture shutdown(SessionShutdownParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.shutdown", _p, Void.class); + } + + /** + * Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture log(SessionLogParams params) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleApi.java new file mode 100644 index 000000000..a0714233b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleApi.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code schedule} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionScheduleApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionScheduleApi(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 + */ + public CompletableFuture list() { + return caller.invoke("session.schedule.list", java.util.Map.of("sessionId", this.sessionId), SessionScheduleListResult.class); + } + + /** + * Identifier of the scheduled prompt to remove. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture stop(SessionScheduleStopParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.schedule.stop", _p, SessionScheduleStopResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleListParams.java new file mode 100644 index 000000000..0b32d10d8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionScheduleListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleListResult.java new file mode 100644 index 000000000..80202f88d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Snapshot of the currently active recurring prompts for this session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionScheduleListResult( + /** Active scheduled prompts, ordered by id. */ + @JsonProperty("entries") List entries +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleStopParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleStopParams.java new file mode 100644 index 000000000..f82cca373 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleStopParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of the scheduled prompt to remove. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionScheduleStopParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Id of the scheduled prompt to remove. */ + @JsonProperty("id") Long id +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleStopResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleStopResult.java new file mode 100644 index 000000000..803706803 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionScheduleStopResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Remove a scheduled prompt by id. The result entry is omitted if the id was unknown. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionScheduleStopResult( + /** The removed entry, or omitted if no entry matched. */ + @JsonProperty("entry") ScheduleEntry entry +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSendParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSendParams.java new file mode 100644 index 000000000..caebce526 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSendParams.java @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Parameters for sending a user message to the session + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSendParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The user message text */ + @JsonProperty("prompt") String prompt, + /** If provided, this is shown in the timeline instead of `prompt` */ + @JsonProperty("displayPrompt") String displayPrompt, + /** Optional attachments (files, directories, selections, blobs, GitHub references) to include with the message */ + @JsonProperty("attachments") List attachments, + /** How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. */ + @JsonProperty("mode") SendMode mode, + /** If true, adds the message to the front of the queue instead of the end */ + @JsonProperty("prepend") Boolean prepend, + /** If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. */ + @JsonProperty("billable") Boolean billable, + /** If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange */ + @JsonProperty("requiredTool") String requiredTool, + /** Optional provenance tag copied to the resulting user.message event. Supported values are `system`, `command-*`, and `schedule-*`. */ + @JsonProperty("source") Object source, + /** The UI mode the agent was in when this message was sent. Defaults to the session's current mode. */ + @JsonProperty("agentMode") SendAgentMode agentMode, + /** Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. */ + @JsonProperty("requestHeaders") Map requestHeaders, + /** W3C Trace Context traceparent header for distributed tracing of this agent turn */ + @JsonProperty("traceparent") String traceparent, + /** W3C Trace Context tracestate header for distributed tracing */ + @JsonProperty("tracestate") String tracestate, + /** If true, await completion of the agentic loop for this message before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageId`; the caller can rely on the agent having processed the message before the call resolves. */ + @JsonProperty("wait") Boolean wait_ +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSendResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSendResult.java new file mode 100644 index 000000000..d93c58ce1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSendResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result of sending a user message + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSendResult( + /** Unique identifier assigned to the message */ + @JsonProperty("messageId") String messageId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellApi.java index 4a8e6a86c..6ed7c8b6b 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellApi.java @@ -34,6 +34,8 @@ public final class SessionShellApi { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture exec(SessionShellExecParams params) { @@ -47,6 +49,8 @@ public CompletableFuture exec(SessionShellExecParams par *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture kill(SessionShellKillParams params) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShutdownParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShutdownParams.java new file mode 100644 index 000000000..e7bf266e8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShutdownParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Parameters for shutting down the session + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionShutdownParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Why the session is being shut down. Defaults to "routine" when omitted. */ + @JsonProperty("type") ShutdownType type, + /** Optional human-readable reason. Typically the message of the error that triggered shutdown when type is 'error'. */ + @JsonProperty("reason") String reason +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java index a96f410f0..82cea6a8f 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java @@ -39,6 +39,16 @@ public CompletableFuture list() { return caller.invoke("session.skills.list", java.util.Map.of("sessionId", this.sessionId), SessionSkillsListResult.class); } + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getInvoked() { + return caller.invoke("session.skills.getInvoked", java.util.Map.of("sessionId", this.sessionId), SessionSkillsGetInvokedResult.class); + } + /** * Name of the skill to enable for the session. *

@@ -79,4 +89,14 @@ public CompletableFuture reload() { return caller.invoke("session.skills.reload", java.util.Map.of("sessionId", this.sessionId), SessionSkillsReloadResult.class); } + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture ensureLoaded() { + return caller.invoke("session.skills.ensureLoaded", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnsureLoadedParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnsureLoadedParams.java new file mode 100644 index 000000000..8b7ab0e62 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnsureLoadedParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsEnsureLoadedParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsGetInvokedParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsGetInvokedParams.java new file mode 100644 index 000000000..8ae29d5e7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsGetInvokedParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsGetInvokedParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsGetInvokedResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsGetInvokedResult.java new file mode 100644 index 000000000..f20c657c0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsGetInvokedResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Skills invoked during this session, ordered by invocation time (most recent last). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsGetInvokedResult( + /** Skills invoked during this session, ordered by invocation time (most recent last) */ + @JsonProperty("skills") List skills +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java index 577d91ee3..5fa5ca90f 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java @@ -54,6 +54,51 @@ public CompletableFuture list() { return caller.invoke("session.tasks.list", java.util.Map.of("sessionId", this.sessionId), SessionTasksListResult.class); } + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture refresh() { + return caller.invoke("session.tasks.refresh", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture waitForPending() { + return caller.invoke("session.tasks.waitForPending", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + + /** + * Identifier of the background task to fetch progress for. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getProgress(SessionTasksGetProgressParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.getProgress", _p, SessionTasksGetProgressResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getCurrentPromotable() { + return caller.invoke("session.tasks.getCurrentPromotable", java.util.Map.of("sessionId", this.sessionId), SessionTasksGetCurrentPromotableResult.class); + } + /** * Identifier of the task to promote to background mode. *

@@ -69,6 +114,16 @@ public CompletableFuture promoteToBackgro return caller.invoke("session.tasks.promoteToBackground", _p, SessionTasksPromoteToBackgroundResult.class); } + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture promoteCurrentToBackground() { + return caller.invoke("session.tasks.promoteCurrentToBackground", java.util.Map.of("sessionId", this.sessionId), SessionTasksPromoteCurrentToBackgroundResult.class); + } + /** * Identifier of the background task to cancel. *

diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetCurrentPromotableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetCurrentPromotableParams.java new file mode 100644 index 000000000..e183c7bc4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetCurrentPromotableParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksGetCurrentPromotableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetCurrentPromotableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetCurrentPromotableResult.java new file mode 100644 index 000000000..65eee3220 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetCurrentPromotableResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The first sync-waiting task that can currently be promoted to background mode. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksGetCurrentPromotableResult( + /** The first sync-waiting task (agent first, then shell) that can currently be promoted to background mode. Omitted if no such task exists. The returned task is guaranteed to have executionMode='sync' and canPromoteToBackground=true at the time of the call. */ + @JsonProperty("task") Object task +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetProgressParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetProgressParams.java new file mode 100644 index 000000000..bcdf9a8d8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetProgressParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of the background task to fetch progress for. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksGetProgressParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier (agent ID or shell ID) */ + @JsonProperty("id") String id +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetProgressResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetProgressResult.java new file mode 100644 index 000000000..d2a4c7eb5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksGetProgressResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Progress information for the task, or null when no task with that ID is tracked. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksGetProgressResult( + /** Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. */ + @JsonProperty("progress") Object progress +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteCurrentToBackgroundParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteCurrentToBackgroundParams.java new file mode 100644 index 000000000..31c7fa94a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteCurrentToBackgroundParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksPromoteCurrentToBackgroundParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteCurrentToBackgroundResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteCurrentToBackgroundResult.java new file mode 100644 index 000000000..590082505 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteCurrentToBackgroundResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The promoted task as it now exists in background mode, omitted if no promotable task was waiting. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksPromoteCurrentToBackgroundResult( + /** The promoted task as it now exists in background mode, omitted if no promotable task was waiting. Atomic operation: avoids the race window of getCurrentPromotable + promoteToBackground. */ + @JsonProperty("task") Object task +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRefreshParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRefreshParams.java new file mode 100644 index 000000000..3560835af --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRefreshParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRefreshParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRefreshResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRefreshResult.java new file mode 100644 index 000000000..c38636976 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRefreshResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRefreshResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksWaitForPendingParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksWaitForPendingParams.java new file mode 100644 index 000000000..6dc935822 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksWaitForPendingParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksWaitForPendingParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksWaitForPendingResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksWaitForPendingResult.java new file mode 100644 index 000000000..5cbce9cd5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksWaitForPendingResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksWaitForPendingResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTelemetryApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTelemetryApi.java new file mode 100644 index 000000000..5cad4e771 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTelemetryApi.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code telemetry} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionTelemetryApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionTelemetryApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Feature override key/value pairs to attach to subsequent telemetry events from this session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture setFeatureOverrides(SessionTelemetrySetFeatureOverridesParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.telemetry.setFeatureOverrides", _p, Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTelemetrySetFeatureOverridesParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTelemetrySetFeatureOverridesParams.java new file mode 100644 index 000000000..aa0d4e3c3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTelemetrySetFeatureOverridesParams.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Feature override key/value pairs to attach to subsequent telemetry events from this session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTelemetrySetFeatureOverridesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Override key/value pairs to attach to subsequent telemetry events from this session. Replaces any previously-set overrides. */ + @JsonProperty("features") Map features +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsApi.java index 323fdfe51..ded588627 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsApi.java @@ -34,6 +34,8 @@ public final class SessionToolsApi { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture handlePendingToolCall(SessionToolsHandlePendingToolCallParams params) { @@ -42,4 +44,14 @@ public CompletableFuture handlePendingT return caller.invoke("session.tools.handlePendingToolCall", _p, SessionToolsHandlePendingToolCallResult.class); } + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture initializeAndValidate() { + return caller.invoke("session.tools.initializeAndValidate", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsInitializeAndValidateParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsInitializeAndValidateParams.java new file mode 100644 index 000000000..d3ec2aeea --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsInitializeAndValidateParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionToolsInitializeAndValidateParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsInitializeAndValidateResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsInitializeAndValidateResult.java new file mode 100644 index 000000000..d25c0e5a8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsInitializeAndValidateResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Resolve, build, and validate the runtime tool list for this session. Subagent sessions and consumer flows that need an initialized tool set before `send` invoke this. Default base-class implementation is a no-op for sessions that don't support tool validation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionToolsInitializeAndValidateResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiApi.java index ef37d580d..3ae829588 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiApi.java @@ -34,6 +34,8 @@ public final class SessionUiApi { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture elicitation(SessionUiElicitationParams params) { @@ -47,6 +49,8 @@ public CompletableFuture elicitation(SessionUiElicit *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture handlePendingElicitation(SessionUiHandlePendingElicitationParams params) { @@ -55,4 +59,89 @@ public CompletableFuture handlePendingE return caller.invoke("session.ui.handlePendingElicitation", _p, SessionUiHandlePendingElicitationResult.class); } + /** + * Request ID of a pending `user_input.requested` event and the user's response. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture handlePendingUserInput(SessionUiHandlePendingUserInputParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handlePendingUserInput", _p, SessionUiHandlePendingUserInputResult.class); + } + + /** + * Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture handlePendingSampling(SessionUiHandlePendingSamplingParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handlePendingSampling", _p, SessionUiHandlePendingSamplingResult.class); + } + + /** + * Request ID of a pending `auto_mode_switch.requested` event and the user's response. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture handlePendingAutoModeSwitch(SessionUiHandlePendingAutoModeSwitchParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handlePendingAutoModeSwitch", _p, SessionUiHandlePendingAutoModeSwitchResult.class); + } + + /** + * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture handlePendingExitPlanMode(SessionUiHandlePendingExitPlanModeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handlePendingExitPlanMode", _p, SessionUiHandlePendingExitPlanModeResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture registerDirectAutoModeSwitchHandler() { + return caller.invoke("session.ui.registerDirectAutoModeSwitchHandler", java.util.Map.of("sessionId", this.sessionId), SessionUiRegisterDirectAutoModeSwitchHandlerResult.class); + } + + /** + * Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture unregisterDirectAutoModeSwitchHandler(SessionUiUnregisterDirectAutoModeSwitchHandlerParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.unregisterDirectAutoModeSwitchHandler", _p, SessionUiUnregisterDirectAutoModeSwitchHandlerResult.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingAutoModeSwitchParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingAutoModeSwitchParams.java new file mode 100644 index 000000000..ab37b178c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingAutoModeSwitchParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request ID of a pending `auto_mode_switch.requested` event and the user's response. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingAutoModeSwitchParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the auto_mode_switch.requested event */ + @JsonProperty("requestId") String requestId, + /** User's choice for auto-mode switching: yes (allow this turn), yes_always (allow + persist as setting), or no (decline). */ + @JsonProperty("response") UIAutoModeSwitchResponse response +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingAutoModeSwitchResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingAutoModeSwitchResult.java new file mode 100644 index 000000000..0df5ca571 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingAutoModeSwitchResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending UI request was resolved by this call. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingAutoModeSwitchResult( + /** True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingExitPlanModeParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingExitPlanModeParams.java new file mode 100644 index 000000000..a83d7d038 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingExitPlanModeParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingExitPlanModeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the exit_plan_mode.requested event */ + @JsonProperty("requestId") String requestId, + /** Schema for the `UIExitPlanModeResponse` type. */ + @JsonProperty("response") UIExitPlanModeResponse response +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingExitPlanModeResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingExitPlanModeResult.java new file mode 100644 index 000000000..c3c183b51 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingExitPlanModeResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending UI request was resolved by this call. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingExitPlanModeResult( + /** True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingSamplingParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingSamplingParams.java new file mode 100644 index 000000000..6c4304fd5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingSamplingParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingSamplingParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the sampling.requested event */ + @JsonProperty("requestId") String requestId, + /** Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. */ + @JsonProperty("response") UIHandlePendingSamplingResponse response +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingSamplingResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingSamplingResult.java new file mode 100644 index 000000000..2fa7e09e1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingSamplingResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending UI request was resolved by this call. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingSamplingResult( + /** True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingUserInputParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingUserInputParams.java new file mode 100644 index 000000000..3e1624bf2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingUserInputParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request ID of a pending `user_input.requested` event and the user's response. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingUserInputParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the user_input.requested event */ + @JsonProperty("requestId") String requestId, + /** Schema for the `UIUserInputResponse` type. */ + @JsonProperty("response") UIUserInputResponse response +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingUserInputResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingUserInputResult.java new file mode 100644 index 000000000..fb240b3c5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingUserInputResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending UI request was resolved by this call. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingUserInputResult( + /** True if the request was still pending and was resolved by this call. False if the request ID was unknown, already resolved by another client (e.g. GitHub), expired, or otherwise no longer pending. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiRegisterDirectAutoModeSwitchHandlerParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiRegisterDirectAutoModeSwitchHandlerParams.java new file mode 100644 index 000000000..42c8ba2a5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiRegisterDirectAutoModeSwitchHandlerParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiRegisterDirectAutoModeSwitchHandlerParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiRegisterDirectAutoModeSwitchHandlerResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiRegisterDirectAutoModeSwitchHandlerResult.java new file mode 100644 index 000000000..5aedb596b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiRegisterDirectAutoModeSwitchHandlerResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiRegisterDirectAutoModeSwitchHandlerResult( + /** Opaque handle representing the registration. Pass this same handle to `unregisterDirectAutoModeSwitchHandler` when the in-process handler is no longer active. Multiple registrations are reference-counted; the server bridge will only dispatch auto-mode-switch requests when no handles are active. */ + @JsonProperty("handle") String handle +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiUnregisterDirectAutoModeSwitchHandlerParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiUnregisterDirectAutoModeSwitchHandlerParams.java new file mode 100644 index 000000000..c22ad46e7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiUnregisterDirectAutoModeSwitchHandlerParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiUnregisterDirectAutoModeSwitchHandlerParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Handle previously returned by `registerDirectAutoModeSwitchHandler` */ + @JsonProperty("handle") String handle +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiUnregisterDirectAutoModeSwitchHandlerResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiUnregisterDirectAutoModeSwitchHandlerResult.java new file mode 100644 index 000000000..6d36ad751 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiUnregisterDirectAutoModeSwitchHandlerResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the handle was active and the registration count was decremented. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiUnregisterDirectAutoModeSwitchHandlerResult( + /** True if the handle was active and decremented the counter; false if the handle was unknown. */ + @JsonProperty("unregistered") Boolean unregistered +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java index ee7bf42cd..21feae576 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.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.time.OffsetDateTime; import java.util.Map; import javax.annotation.processing.Generated; @@ -27,13 +28,13 @@ public record SessionUsageGetMetricsResult( /** Raw count of user-initiated API requests */ @JsonProperty("totalUserRequests") Long totalUserRequests, /** Session-wide accumulated nano-AI units cost */ - @JsonProperty("totalNanoAiu") Long totalNanoAiu, + @JsonProperty("totalNanoAiu") Double totalNanoAiu, /** Session-wide per-token-type accumulated token counts */ @JsonProperty("tokenDetails") Map tokenDetails, /** Total time spent in model API calls (milliseconds) */ - @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, - /** Session start timestamp (epoch milliseconds) */ - @JsonProperty("sessionStartTime") Long sessionStartTime, + @JsonProperty("totalApiDurationMs") Long totalApiDurationMs, + /** ISO 8601 timestamp when the session started */ + @JsonProperty("sessionStartTime") OffsetDateTime sessionStartTime, /** Aggregated code change metrics */ @JsonProperty("codeChanges") UsageMetricsCodeChanges codeChanges, /** Per-model token and request metrics, keyed by model identifier */ diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkingDirectoryContext.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkingDirectoryContext.java new file mode 100644 index 000000000..4c17aa5ea --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkingDirectoryContext.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkingDirectoryContext( + /** Current working directory path */ + @JsonProperty("cwd") String cwd, + /** Root directory of the git repository, resolved via git rev-parse */ + @JsonProperty("gitRoot") String gitRoot, + /** Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) */ + @JsonProperty("repository") String repository, + /** Hosting platform type of the repository */ + @JsonProperty("hostType") SessionWorkingDirectoryContextHostType hostType, + /** Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com") */ + @JsonProperty("repositoryHost") String repositoryHost, + /** Current git branch name */ + @JsonProperty("branch") String branch, + /** Head commit of the current git branch */ + @JsonProperty("headCommit") String headCommit, + /** Merge-base commit SHA (fork point from the remote default branch) */ + @JsonProperty("baseCommit") String baseCommit +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkingDirectoryContextHostType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkingDirectoryContextHostType.java new file mode 100644 index 000000000..0c5a0cffe --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkingDirectoryContextHostType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Hosting platform type of the repository + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionWorkingDirectoryContextHostType { + /** The {@code github} variant. */ + GITHUB("github"), + /** The {@code ado} variant. */ + ADO("ado"); + + private final String value; + SessionWorkingDirectoryContextHostType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionWorkingDirectoryContextHostType fromValue(String value) { + for (SessionWorkingDirectoryContextHostType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionWorkingDirectoryContextHostType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesApi.java index a88463737..de51e4a90 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesApi.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesApi.java @@ -31,6 +31,8 @@ public final class SessionWorkspacesApi { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture getWorkspace() { @@ -39,6 +41,8 @@ public CompletableFuture getWorkspace() { /** * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture listFiles() { @@ -50,6 +54,8 @@ public CompletableFuture listFiles() { *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture readFile(SessionWorkspacesReadFileParams params) { @@ -63,6 +69,8 @@ public CompletableFuture readFile(SessionWorksp *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ public CompletableFuture createFile(SessionWorkspacesCreateFileParams params) { @@ -71,4 +79,44 @@ public CompletableFuture createFile(SessionWorkspacesCreateFileParams para return caller.invoke("session.workspaces.createFile", _p, Void.class); } + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture listCheckpoints() { + return caller.invoke("session.workspaces.listCheckpoints", java.util.Map.of("sessionId", this.sessionId), SessionWorkspacesListCheckpointsResult.class); + } + + /** + * Checkpoint number to read. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture readCheckpoint(SessionWorkspacesReadCheckpointParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.workspaces.readCheckpoint", _p, SessionWorkspacesReadCheckpointResult.class); + } + + /** + * Pasted content to save as a UTF-8 file in the session workspace. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture saveLargePaste(SessionWorkspacesSaveLargePasteParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.workspaces.saveLargePaste", _p, SessionWorkspacesSaveLargePasteResult.class); + } + } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java index 3772d5f93..ded4778b4 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java @@ -11,11 +11,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.OffsetDateTime; -import java.util.UUID; import javax.annotation.processing.Generated; /** - * Current workspace metadata for the session, or null when not available. + * Current workspace metadata for the session, including its absolute filesystem path when available. * * @since 1.0.0 */ @@ -24,17 +23,20 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record SessionWorkspacesGetWorkspaceResult( /** Current workspace metadata, or null if not available */ - @JsonProperty("workspace") SessionWorkspacesGetWorkspaceResultWorkspace workspace + @JsonProperty("workspace") SessionWorkspacesGetWorkspaceResultWorkspace workspace, + /** Absolute filesystem path to the workspace directory. Omitted when the session has no workspace (e.g. remote sessions). */ + @JsonProperty("path") String path ) { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionWorkspacesGetWorkspaceResultWorkspace( - @JsonProperty("id") UUID id, + @JsonProperty("id") String id, @JsonProperty("cwd") String cwd, @JsonProperty("git_root") String gitRoot, @JsonProperty("repository") String repository, - @JsonProperty("host_type") SessionWorkspacesGetWorkspaceResultWorkspaceHostType hostType, + /** Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. */ + @JsonProperty("host_type") WorkspacesWorkspaceDetailsHostType hostType, @JsonProperty("branch") String branch, @JsonProperty("name") String name, @JsonProperty("user_named") Boolean userNamed, @@ -47,24 +49,5 @@ public record SessionWorkspacesGetWorkspaceResultWorkspace( @JsonProperty("mc_last_event_id") String mcLastEventId, @JsonProperty("chronicle_sync_dismissed") Boolean chronicleSyncDismissed ) { - - public enum SessionWorkspacesGetWorkspaceResultWorkspaceHostType { - /** The {@code github} variant. */ - GITHUB("github"), - /** The {@code ado} variant. */ - ADO("ado"); - - private final String value; - SessionWorkspacesGetWorkspaceResultWorkspaceHostType(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static SessionWorkspacesGetWorkspaceResultWorkspaceHostType fromValue(String value) { - for (SessionWorkspacesGetWorkspaceResultWorkspaceHostType v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown SessionWorkspacesGetWorkspaceResultWorkspaceHostType value: " + value); - } - } } } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListCheckpointsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListCheckpointsParams.java new file mode 100644 index 000000000..3f3c1ac1e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListCheckpointsParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesListCheckpointsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListCheckpointsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListCheckpointsResult.java new file mode 100644 index 000000000..c25be02d4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListCheckpointsResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Workspace checkpoints in chronological order; empty when the workspace is not enabled. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesListCheckpointsResult( + /** Workspace checkpoints in chronological order. Empty when workspace is not enabled. */ + @JsonProperty("checkpoints") List checkpoints +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadCheckpointParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadCheckpointParams.java new file mode 100644 index 000000000..851a01bd4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadCheckpointParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Checkpoint number to read. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesReadCheckpointParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Checkpoint number to read */ + @JsonProperty("number") Long number +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadCheckpointResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadCheckpointResult.java new file mode 100644 index 000000000..4f9e18709 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadCheckpointResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesReadCheckpointResult( + /** Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing */ + @JsonProperty("content") String content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesSaveLargePasteParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesSaveLargePasteParams.java new file mode 100644 index 000000000..466f5b7ba --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesSaveLargePasteParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Pasted content to save as a UTF-8 file in the session workspace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesSaveLargePasteParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Pasted content to save as a UTF-8 file */ + @JsonProperty("content") String content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesSaveLargePasteResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesSaveLargePasteResult.java new file mode 100644 index 000000000..84e5719d7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesSaveLargePasteResult.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Descriptor for the saved paste file, or null when the workspace is unavailable. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesSaveLargePasteResult( + /** Saved-paste descriptor, or null when the workspace is unavailable (e.g. CCA runtime, non-infinite sessions, remote sessions) */ + @JsonProperty("saved") SessionWorkspacesSaveLargePasteResultSaved saved +) { + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionWorkspacesSaveLargePasteResultSaved( + /** Absolute filesystem path to the saved paste file */ + @JsonProperty("filePath") String filePath, + /** Filename within the workspace files directory */ + @JsonProperty("filename") String filename, + /** Size of the saved file in bytes */ + @JsonProperty("sizeBytes") Long sizeBytes + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsBulkDeleteParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsBulkDeleteParams.java new file mode 100644 index 000000000..986103825 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsBulkDeleteParams.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session IDs to close, deactivate, and delete from disk. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsBulkDeleteParams( + /** Session IDs to close, deactivate, and delete from disk */ + @JsonProperty("sessionIds") List sessionIds +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsBulkDeleteResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsBulkDeleteResult.java new file mode 100644 index 000000000..893dd8210 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsBulkDeleteResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Map of sessionId -> bytes freed by removing the session's workspace directory. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsBulkDeleteResult( + /** Map of sessionId -> bytes freed by removing the session's workspace directory. Sessions whose deletion failed are omitted from this map (failures are logged on the server but not surfaced per-id; check the map for absent IDs to detect them). */ + @JsonProperty("freedBytes") Map freedBytes +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCheckInUseParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCheckInUseParams.java new file mode 100644 index 000000000..be8b2e385 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCheckInUseParams.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session IDs to test for live in-use locks. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsCheckInUseParams( + /** Session IDs to test for live in-use locks */ + @JsonProperty("sessionIds") List sessionIds +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCheckInUseResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCheckInUseResult.java new file mode 100644 index 000000000..c22e818cc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCheckInUseResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session IDs from the input set that are currently in use by another process. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsCheckInUseResult( + /** Session IDs from the input set that are currently held by another running process via an alive lock file */ + @JsonProperty("inUse") List inUse +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCloseParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCloseParams.java new file mode 100644 index 000000000..c29d385be --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCloseParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session ID to close. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsCloseParams( + /** Session ID to close */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCloseResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCloseResult.java new file mode 100644 index 000000000..fc8c5ee66 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsCloseResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Closes a session: emits shutdown, flushes pending events to disk, releases the in-use lock, disposes the active session. Idempotent: succeeds even if the session is not currently active. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsCloseResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsEnrichMetadataParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsEnrichMetadataParams.java new file mode 100644 index 000000000..ef3330a97 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsEnrichMetadataParams.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session metadata records to enrich with summary and context information. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsEnrichMetadataParams( + /** Session metadata records to enrich. Records that already have summary and context are returned unchanged. */ + @JsonProperty("sessions") List sessions +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsEnrichMetadataResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsEnrichMetadataResult.java new file mode 100644 index 000000000..fee15a2ed --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsEnrichMetadataResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * The same metadata records, with summary and context fields backfilled where available. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsEnrichMetadataResult( + /** Same records, with summary and context backfilled */ + @JsonProperty("sessions") List sessions +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByPrefixParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByPrefixParams.java new file mode 100644 index 000000000..628d93cb8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByPrefixParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * UUID prefix to resolve to a unique session ID. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsFindByPrefixParams( + /** UUID prefix (>=7 hex chars, <36 chars). Returns the unique session ID, or undefined when there is no match or the prefix matches multiple sessions. */ + @JsonProperty("prefix") String prefix +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByPrefixResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByPrefixResult.java new file mode 100644 index 000000000..94b3a106c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByPrefixResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session ID matching the prefix, omitted when no unique match exists. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsFindByPrefixResult( + /** Omitted when no unique session matches the prefix (no match or ambiguous) */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByTaskIdParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByTaskIdParams.java new file mode 100644 index 000000000..23e1aaaed --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByTaskIdParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * GitHub task ID to look up. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsFindByTaskIdParams( + /** GitHub task ID to look up */ + @JsonProperty("taskId") String taskId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByTaskIdResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByTaskIdResult.java new file mode 100644 index 000000000..eb5b9bfd4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsFindByTaskIdResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * ID of the local session bound to the given GitHub task, or omitted when none. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsFindByTaskIdResult( + /** Omitted when no local session is bound to that GitHub task */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetEventFilePathParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetEventFilePathParams.java new file mode 100644 index 000000000..a23a5434c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetEventFilePathParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session ID whose event-log file path to compute. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsGetEventFilePathParams( + /** Session ID whose event-log file path to compute */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetEventFilePathResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetEventFilePathResult.java new file mode 100644 index 000000000..261a0d452 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetEventFilePathResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Absolute path to the session's events.jsonl file on disk. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsGetEventFilePathResult( + /** Absolute path to the session's events.jsonl file */ + @JsonProperty("filePath") String filePath +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetLastForContextParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetLastForContextParams.java new file mode 100644 index 000000000..0b06f161f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetLastForContextParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional working-directory context used to score session relevance. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsGetLastForContextParams( + /** Optional working-directory context used to score session relevance. When omitted the most-recently-modified session wins. */ + @JsonProperty("context") SessionContext context +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetLastForContextResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetLastForContextResult.java new file mode 100644 index 000000000..c31e8d781 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetLastForContextResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Most-relevant session ID for the supplied context, or omitted when no sessions exist. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsGetLastForContextResult( + /** Most-relevant session ID for the supplied context, or omitted when no sessions exist */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetPersistedRemoteSteerableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetPersistedRemoteSteerableParams.java new file mode 100644 index 000000000..1fd9e476f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetPersistedRemoteSteerableParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session ID to look up the persisted remote-steerable flag for. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsGetPersistedRemoteSteerableParams( + /** Session ID to look up the persisted remote-steerable flag for */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetPersistedRemoteSteerableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetPersistedRemoteSteerableResult.java new file mode 100644 index 000000000..c4de7b09b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetPersistedRemoteSteerableResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The session's persisted remote-steerable flag, or omitted when no value has been persisted. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsGetPersistedRemoteSteerableResult( + /** The session's persisted remote-steerable flag if recorded; omitted when no value has been persisted */ + @JsonProperty("remoteSteerable") Boolean remoteSteerable +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetSizesResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetSizesResult.java new file mode 100644 index 000000000..98ec269e4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsGetSizesResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Map of sessionId -> on-disk size in bytes for each session's workspace directory. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsGetSizesResult( + /** Map of sessionId -> on-disk size in bytes for the session's workspace directory */ + @JsonProperty("sizes") Map sizes +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsListResult.java new file mode 100644 index 000000000..99060bb18 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Persisted sessions matching the filter, ordered most-recently-modified first. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsListResult( + /** Sessions ordered most-recently-modified first */ + @JsonProperty("sessions") List sessions +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsLoadDeferredRepoHooksParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsLoadDeferredRepoHooksParams.java new file mode 100644 index 000000000..67590914e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsLoadDeferredRepoHooksParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Active session ID whose deferred repo-level hooks should be loaded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsLoadDeferredRepoHooksParams( + /** Active session ID whose deferred repo-level hooks should be loaded */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsLoadDeferredRepoHooksResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsLoadDeferredRepoHooksResult.java new file mode 100644 index 000000000..6f2299a49 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsLoadDeferredRepoHooksResult.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Queued repo-level startup prompts and the total hook command count after loading. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsLoadDeferredRepoHooksResult( + /** Repo-level startup prompts queued from repo hook configs. Empty on resume, when no repo configs were pending, or when disableAllHooks is set. */ + @JsonProperty("startupPrompts") List startupPrompts, + /** Total hook command count (user + plugin + repo) loaded for the session by this call. Captured atomically with startupPrompts so callers don't need to read a separate counter. */ + @JsonProperty("hookCount") Long hookCount +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsPruneOldParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsPruneOldParams.java new file mode 100644 index 000000000..e36c033e3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsPruneOldParams.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsPruneOldParams( + /** Delete sessions whose modifiedTime is at least this many days old */ + @JsonProperty("olderThanDays") Long olderThanDays, + /** When true, only report what would be deleted without performing any deletion */ + @JsonProperty("dryRun") Boolean dryRun, + /** When true, named sessions (set via /rename) are also eligible for pruning */ + @JsonProperty("includeNamed") Boolean includeNamed, + /** Session IDs that should never be considered for pruning */ + @JsonProperty("excludeSessionIds") List excludeSessionIds +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsPruneOldResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsPruneOldResult.java new file mode 100644 index 000000000..6bcf5cecd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsPruneOldResult.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Outcome of the prune operation: deleted IDs, dry-run candidates, skipped IDs, total bytes freed, and the dry-run flag. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsPruneOldResult( + /** Session IDs that were deleted (always empty in dry-run mode) */ + @JsonProperty("deleted") List deleted, + /** Session IDs that would be deleted in dry-run mode (always empty otherwise) */ + @JsonProperty("candidates") List candidates, + /** Session IDs that were skipped (e.g., named sessions) */ + @JsonProperty("skipped") List skipped, + /** Total bytes freed (actual when not dry-run, projected when dry-run) */ + @JsonProperty("freedBytes") Long freedBytes, + /** True when no deletions were actually performed */ + @JsonProperty("dryRun") Boolean dryRun +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReleaseLockParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReleaseLockParams.java new file mode 100644 index 000000000..81c968251 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReleaseLockParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session ID whose in-use lock should be released. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsReleaseLockParams( + /** Session ID whose in-use lock should be released */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReleaseLockResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReleaseLockResult.java new file mode 100644 index 000000000..e3c660dc6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReleaseLockResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Release the in-use lock held by this process for the given session. No-op when this process does not currently hold a lock for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsReleaseLockResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReloadPluginHooksParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReloadPluginHooksParams.java new file mode 100644 index 000000000..a9537382a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReloadPluginHooksParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Active session ID and an optional flag for deferring repo-level hooks until folder trust. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsReloadPluginHooksParams( + /** Active session ID to reload hooks for */ + @JsonProperty("sessionId") String sessionId, + /** When true, skip repo-level hooks. Use before folder trust is confirmed; loadDeferredRepoHooks loads them post-trust. */ + @JsonProperty("deferRepoHooks") Boolean deferRepoHooks +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReloadPluginHooksResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReloadPluginHooksResult.java new file mode 100644 index 000000000..0920dd86e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsReloadPluginHooksResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Reload all hooks (user, plugin, optionally repo) and apply them to the active session. Call after installing or removing plugins so their hooks take effect immediately. No-op when no active session matches the given sessionId. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsReloadPluginHooksResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSaveParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSaveParams.java new file mode 100644 index 000000000..ae3b42b02 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSaveParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session ID whose pending events should be flushed to disk. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsSaveParams( + /** Session ID whose pending events should be flushed to disk */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSaveResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSaveResult.java new file mode 100644 index 000000000..74ae1a466 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSaveResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Flush a session's pending events to disk. No-op when no writer exists for the session (e.g., already closed). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsSaveResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSetAdditionalPluginsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSetAdditionalPluginsParams.java new file mode 100644 index 000000000..34ca74de9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSetAdditionalPluginsParams.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Manager-wide additional plugins to register; replaces any previously-configured set. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsSetAdditionalPluginsParams( + /** Manager-wide additional plugins to register. Replaces any previously-configured set. Pass an empty array to clear. */ + @JsonProperty("plugins") List plugins +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSetAdditionalPluginsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSetAdditionalPluginsResult.java new file mode 100644 index 000000000..e6f2a5b4c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsSetAdditionalPluginsResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Replace the manager-wide additional plugins. New session creations and subsequent hook reloads see the new set; already-running sessions keep their existing hook installation until the next reload. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsSetAdditionalPluginsResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ShutdownType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ShutdownType.java new file mode 100644 index 000000000..5dac44cfb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ShutdownType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Why the session is being shut down. Defaults to "routine" when omitted. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ShutdownType { + /** The {@code routine} variant. */ + ROUTINE("routine"), + /** The {@code error} variant. */ + ERROR("error"); + + private final String value; + ShutdownType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ShutdownType fromValue(String value) { + for (ShutdownType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ShutdownType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Skill.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Skill.java index cd896add8..92f94d661 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Skill.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Skill.java @@ -32,6 +32,8 @@ public record Skill( /** Whether the skill is currently enabled */ @JsonProperty("enabled") Boolean enabled, /** Absolute path to the skill file */ - @JsonProperty("path") String path + @JsonProperty("path") String path, + /** Name of the plugin that provides the skill, when source is 'plugin' */ + @JsonProperty("pluginName") String pluginName ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsInvokedSkill.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsInvokedSkill.java new file mode 100644 index 000000000..7dde08942 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsInvokedSkill.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.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Schema for the `SkillsInvokedSkill` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillsInvokedSkill( + /** Unique identifier for the skill */ + @JsonProperty("name") String name, + /** Path to the SKILL.md file */ + @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, + /** Turn number when the skill was invoked */ + @JsonProperty("invokedAtTurn") Long invokedAtTurn +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIAutoModeSwitchResponse.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIAutoModeSwitchResponse.java new file mode 100644 index 000000000..170c378a4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIAutoModeSwitchResponse.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * User's choice for auto-mode switching: yes (allow this turn), yes_always (allow + persist as setting), or no (decline). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum UIAutoModeSwitchResponse { + /** The {@code yes} variant. */ + YES("yes"), + /** The {@code yes_always} variant. */ + YES_ALWAYS("yes_always"), + /** The {@code no} variant. */ + NO("no"); + + private final String value; + UIAutoModeSwitchResponse(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static UIAutoModeSwitchResponse fromValue(String value) { + for (UIAutoModeSwitchResponse v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown UIAutoModeSwitchResponse value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIExitPlanModeAction.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIExitPlanModeAction.java new file mode 100644 index 000000000..7ea38d6fd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIExitPlanModeAction.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * The action the user selected. Defaults to 'autopilot' when autoApproveEdits is true, otherwise 'interactive'. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum UIExitPlanModeAction { + /** The {@code exit_only} variant. */ + EXIT_ONLY("exit_only"), + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"), + /** The {@code autopilot_fleet} variant. */ + AUTOPILOT_FLEET("autopilot_fleet"); + + private final String value; + UIExitPlanModeAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static UIExitPlanModeAction fromValue(String value) { + for (UIExitPlanModeAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown UIExitPlanModeAction value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIExitPlanModeResponse.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIExitPlanModeResponse.java new file mode 100644 index 000000000..55cd430a9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIExitPlanModeResponse.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `UIExitPlanModeResponse` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIExitPlanModeResponse( + /** Whether the plan was approved. */ + @JsonProperty("approved") Boolean approved, + /** The action the user selected. Defaults to 'autopilot' when autoApproveEdits is true, otherwise 'interactive'. */ + @JsonProperty("selectedAction") UIExitPlanModeAction selectedAction, + /** Whether subsequent edits should be auto-approved without confirmation. */ + @JsonProperty("autoApproveEdits") Boolean autoApproveEdits, + /** Feedback from the user when they declined the plan or requested changes. */ + @JsonProperty("feedback") String feedback +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIHandlePendingSamplingResponse.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIHandlePendingSamplingResponse.java new file mode 100644 index 000000000..f00084315 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIHandlePendingSamplingResponse.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIHandlePendingSamplingResponse() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIUserInputResponse.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIUserInputResponse.java new file mode 100644 index 000000000..ec8b6e842 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIUserInputResponse.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `UIUserInputResponse` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIUserInputResponse( + /** The user's answer text */ + @JsonProperty("answer") String answer, + /** True if the user typed a freeform response, false if they selected a presented choice. Used by telemetry to differentiate between free text input and choice selection. */ + @JsonProperty("wasFreeform") Boolean wasFreeform +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsCodeChanges.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsCodeChanges.java index 442c88da2..b425ca3d5 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsCodeChanges.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsCodeChanges.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; /** @@ -26,6 +27,8 @@ public record UsageMetricsCodeChanges( /** Total lines of code removed */ @JsonProperty("linesRemoved") Long linesRemoved, /** Number of distinct files modified */ - @JsonProperty("filesModifiedCount") Long filesModifiedCount + @JsonProperty("filesModifiedCount") Long filesModifiedCount, + /** Distinct file paths modified during the session */ + @JsonProperty("filesModified") List filesModified ) { } diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java index 15a133323..f86aab354 100644 --- a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java @@ -27,7 +27,7 @@ public record UsageMetricsModelMetric( /** Token usage metrics for this model */ @JsonProperty("usage") UsageMetricsModelMetricUsage usage, /** Accumulated nano-AI units cost for this model */ - @JsonProperty("totalNanoAiu") Long totalNanoAiu, + @JsonProperty("totalNanoAiu") Double totalNanoAiu, /** Token count details per type */ @JsonProperty("tokenDetails") Map tokenDetails ) { diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspaceSummaryHostType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspaceSummaryHostType.java new file mode 100644 index 000000000..58cc24ea5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspaceSummaryHostType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Repository host type, if known + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkspaceSummaryHostType { + /** The {@code github} variant. */ + GITHUB("github"), + /** The {@code ado} variant. */ + ADO("ado"); + + private final String value; + WorkspaceSummaryHostType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkspaceSummaryHostType fromValue(String value) { + for (WorkspaceSummaryHostType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkspaceSummaryHostType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspacesCheckpoints.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspacesCheckpoints.java new file mode 100644 index 000000000..81c49f6fa --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspacesCheckpoints.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `WorkspacesCheckpoints` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkspacesCheckpoints( + /** Checkpoint number assigned by the workspace manager */ + @JsonProperty("number") Long number, + /** Human-readable checkpoint title */ + @JsonProperty("title") String title, + /** Filename of the checkpoint within the workspace checkpoints directory */ + @JsonProperty("filename") String filename +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspacesWorkspaceDetailsHostType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspacesWorkspaceDetailsHostType.java new file mode 100644 index 000000000..0872a87a4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/WorkspacesWorkspaceDetailsHostType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkspacesWorkspaceDetailsHostType { + /** The {@code github} variant. */ + GITHUB("github"), + /** The {@code ado} variant. */ + ADO("ado"); + + private final String value; + WorkspacesWorkspaceDetailsHostType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkspacesWorkspaceDetailsHostType fromValue(String value) { + for (WorkspacesWorkspaceDetailsHostType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkspacesWorkspaceDetailsHostType value: " + value); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotSession.java b/java/src/main/java/com/github/copilot/sdk/CopilotSession.java index 5fb8733a2..4b75fbc7e 100644 --- a/java/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/java/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -81,6 +81,7 @@ import com.github.copilot.sdk.json.PermissionRequestResult; import com.github.copilot.sdk.json.PermissionRequestResultKind; import com.github.copilot.sdk.json.PostToolUseHookInput; +import com.github.copilot.sdk.json.PreMcpToolCallHookInput; import com.github.copilot.sdk.json.PreToolUseHookInput; import com.github.copilot.sdk.json.SendMessageRequest; import com.github.copilot.sdk.json.SendMessageResponse; @@ -1547,6 +1548,13 @@ CompletableFuture handleHooksInvoke(String hookType, JsonNode input) { .thenApply(output -> (Object) output); } break; + case "preMcpToolCall" : + if (hooks.getOnPreMcpToolCall() != null) { + PreMcpToolCallHookInput mcpInput = MAPPER.treeToValue(input, PreMcpToolCallHookInput.class); + return hooks.getOnPreMcpToolCall().handle(mcpInput, invocation) + .thenApply(output -> (Object) output); + } + break; case "postToolUse" : if (hooks.getOnPostToolUse() != null) { PostToolUseHookInput postInput = MAPPER.treeToValue(input, PostToolUseHookInput.class); @@ -1774,7 +1782,8 @@ public CompletableFuture log(String message, String level, Boolean ephemer rpcLevel = SessionLogLevel.INFO; } } - return getRpc().log(new SessionLogParams(sessionId, message, rpcLevel, ephemeral, url)).thenApply(r -> null); + return getRpc().log(new SessionLogParams(sessionId, message, rpcLevel, null, ephemeral, url, null)) + .thenApply(r -> null); } /** diff --git a/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java b/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java index e86499b2f..3dec2b352 100644 --- a/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java +++ b/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java @@ -20,8 +20,8 @@ public record PingResponse( /** The echo message from the server. */ @JsonProperty("message") String message, - /** The server timestamp in milliseconds since epoch. */ - @JsonProperty("timestamp") long timestamp, + /** The server timestamp as an ISO 8601 string. */ + @JsonProperty("timestamp") String timestamp, /** * The SDK protocol version supported by the server. The SDK validates that this * version matches the expected version to ensure compatibility. diff --git a/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHandler.java b/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHandler.java new file mode 100644 index 000000000..4ace4d8d8 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHandler.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for pre-MCP-tool-call hooks. + *

+ * This hook is called before an MCP tool call is dispatched to an MCP server, + * allowing you to: + *

    + *
  • Inspect the tool call arguments and server name
  • + *
  • Set, replace, or remove MCP request metadata ({@code _meta})
  • + *
+ * + * @since 1.0.8 + */ +@FunctionalInterface +public interface PreMcpToolCallHandler { + + /** + * Handles a pre-MCP-tool-call hook invocation. + * + * @param input + * the hook input containing server name, tool name, and arguments + * @param invocation + * context information about the invocation + * @return a future that resolves with the hook output, or {@code null} to + * preserve existing metadata (no-op) + */ + CompletableFuture handle(PreMcpToolCallHookInput input, HookInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHookInput.java b/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHookInput.java new file mode 100644 index 000000000..17e32c02f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHookInput.java @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Input for a pre-MCP-tool-call hook. + *

+ * This hook fires before an MCP tool call is dispatched to an MCP server, + * allowing you to inspect or modify the request metadata. + * + * @since 1.0.8 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class PreMcpToolCallHookInput { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("timestamp") + private long timestamp; + + @JsonProperty("cwd") + private String cwd; + + @JsonProperty("serverName") + private String serverName; + + @JsonProperty("toolName") + private String toolName; + + @JsonProperty("arguments") + private JsonNode arguments; + + @JsonProperty("toolCallId") + private String toolCallId; + + @JsonProperty("_meta") + private Map meta; + + /** + * Gets the runtime session ID of the session that triggered the hook. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the runtime session ID of the session that triggered the hook. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public PreMcpToolCallHookInput setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the timestamp of the hook invocation. + * + * @return the timestamp in milliseconds + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Sets the timestamp of the hook invocation. + * + * @param timestamp + * the timestamp in milliseconds + * @return this instance for method chaining + */ + public PreMcpToolCallHookInput setTimestamp(long timestamp) { + this.timestamp = timestamp; + return this; + } + + /** + * Gets the current working directory. + * + * @return the working directory path + */ + public String getCwd() { + return cwd; + } + + /** + * Sets the current working directory. + * + * @param cwd + * the working directory path + * @return this instance for method chaining + */ + public PreMcpToolCallHookInput setCwd(String cwd) { + this.cwd = cwd; + return this; + } + + /** + * Gets the name of the MCP server being called. + * + * @return the server name + */ + public String getServerName() { + return serverName; + } + + /** + * Sets the name of the MCP server being called. + * + * @param serverName + * the server name + * @return this instance for method chaining + */ + public PreMcpToolCallHookInput setServerName(String serverName) { + this.serverName = serverName; + return this; + } + + /** + * Gets the name of the MCP tool being called. + * + * @return the tool name + */ + public String getToolName() { + return toolName; + } + + /** + * Sets the name of the MCP tool being called. + * + * @param toolName + * the tool name + * @return this instance for method chaining + */ + public PreMcpToolCallHookInput setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + /** + * Gets the arguments for the MCP tool call. + * + * @return the arguments as a JSON node, or {@code null} + */ + public JsonNode getArguments() { + return arguments; + } + + /** + * Sets the arguments for the MCP tool call. + * + * @param arguments + * the arguments as a JSON node + * @return this instance for method chaining + */ + public PreMcpToolCallHookInput setArguments(JsonNode arguments) { + this.arguments = arguments; + return this; + } + + /** + * Gets the tool call ID, if available. + * + * @return the tool call ID, or {@code null} + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * Sets the tool call ID. + * + * @param toolCallId + * the tool call ID + * @return this instance for method chaining + */ + public PreMcpToolCallHookInput setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + return this; + } + + /** + * Gets the MCP request metadata, if present. + * + * @return the metadata map, or {@code null} + */ + public Map getMeta() { + return meta; + } + + /** + * Sets the MCP request metadata. + * + * @param meta + * the metadata map + * @return this instance for method chaining + */ + public PreMcpToolCallHookInput setMeta(Map meta) { + this.meta = meta; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHookOutput.java b/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHookOutput.java new file mode 100644 index 000000000..a05111326 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PreMcpToolCallHookOutput.java @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Output for a pre-MCP-tool-call hook. + *

+ * The {@link #metaToUse} property controls outgoing MCP request metadata: + *

    + *
  • Return {@code null} from the hook handler: preserve existing + * {@code _meta} (no-op).
  • + *
  • Return a {@code PreMcpToolCallHookOutput} with {@code metaToUse} left as + * {@code null}: remove {@code _meta} from the request.
  • + *
  • Return a {@code PreMcpToolCallHookOutput} with {@code metaToUse} set to a + * JSON object: replace {@code _meta} with that object.
  • + *
+ * + * @since 1.0.8 + */ +@JsonInclude(JsonInclude.Include.ALWAYS) +public class PreMcpToolCallHookOutput { + + @JsonProperty("metaToUse") + private JsonNode metaToUse; + + /** + * Gets the metadata to use for the outgoing MCP request. + * + * @return the metadata JSON node, or {@code null} to remove metadata + */ + public JsonNode getMetaToUse() { + return metaToUse; + } + + /** + * Sets the metadata to use for the outgoing MCP request. + * + * @param metaToUse + * the metadata JSON node, or {@code null} to remove metadata + * @return this instance for method chaining + */ + public PreMcpToolCallHookOutput setMetaToUse(JsonNode metaToUse) { + this.metaToUse = metaToUse; + return this; + } + + /** + * Creates a hook output that sets the given metadata on the MCP request. + * + * @param metaToUse + * the metadata JSON node to use + * @return the hook output + */ + public static PreMcpToolCallHookOutput withMeta(JsonNode metaToUse) { + return new PreMcpToolCallHookOutput().setMetaToUse(metaToUse); + } + + /** + * Creates a hook output that removes metadata from the MCP request. + * + * @return the hook output with {@code null} metaToUse + */ + public static PreMcpToolCallHookOutput removeMeta() { + return new PreMcpToolCallHookOutput(); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionHooks.java b/java/src/main/java/com/github/copilot/sdk/json/SessionHooks.java index 8e22c3ee8..301d64cb5 100644 --- a/java/src/main/java/com/github/copilot/sdk/json/SessionHooks.java +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionHooks.java @@ -38,6 +38,7 @@ public class SessionHooks { private PreToolUseHandler onPreToolUse; + private PreMcpToolCallHandler onPreMcpToolCall; private PostToolUseHandler onPostToolUse; private UserPromptSubmittedHandler onUserPromptSubmitted; private SessionStartHandler onSessionStart; @@ -64,6 +65,30 @@ public SessionHooks setOnPreToolUse(PreToolUseHandler onPreToolUse) { return this; } + /** + * Gets the pre-MCP-tool-call handler. + * + * @return the handler, or {@code null} if not set + * @since 1.0.8 + */ + public PreMcpToolCallHandler getOnPreMcpToolCall() { + return onPreMcpToolCall; + } + + /** + * Sets the handler called before an MCP tool call is dispatched to an MCP + * server. + * + * @param onPreMcpToolCall + * the handler + * @return this instance for method chaining + * @since 1.0.8 + */ + public SessionHooks setOnPreMcpToolCall(PreMcpToolCallHandler onPreMcpToolCall) { + this.onPreMcpToolCall = onPreMcpToolCall; + return this; + } + /** * Gets the post-tool-use handler. * @@ -160,7 +185,7 @@ public SessionHooks setOnSessionEnd(SessionEndHandler onSessionEnd) { * @return {@code true} if at least one hook handler is set */ public boolean hasHooks() { - return onPreToolUse != null || onPostToolUse != null || onUserPromptSubmitted != null || onSessionStart != null - || onSessionEnd != null; + return onPreToolUse != null || onPreMcpToolCall != null || onPostToolUse != null + || onUserPromptSubmitted != null || onSessionStart != null || onSessionEnd != null; } } diff --git a/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 14ed8ca89..137ad360b 100644 --- a/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -92,7 +92,7 @@ void testStartAndConnectUsingStdio() throws Exception { PingResponse pong = client.ping("test message").get(); assertEquals("pong: test message", pong.message()); - assertTrue(pong.timestamp() >= 0); + assertNotNull(pong.timestamp()); client.stop().get(); assertEquals(ConnectionState.DISCONNECTED, client.getState()); diff --git a/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java index 9680148ff..f75eaa689 100644 --- a/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java +++ b/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -121,6 +121,13 @@ public Path getWorkDir() { return workDir; } + /** + * Gets the repository root for locating shared test assets. + */ + public Path getRepoRoot() { + return repoRoot; + } + /** * Gets the proxy URL. */ diff --git a/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java b/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java index a7d81646b..3b8f8a00b 100644 --- a/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java +++ b/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -17,6 +18,7 @@ import org.junit.jupiter.api.Test; import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.rpc.McpServerStatus; import com.github.copilot.sdk.json.CustomAgentConfig; import com.github.copilot.sdk.json.DefaultAgentConfig; import com.github.copilot.sdk.json.McpServerConfig; @@ -51,9 +53,33 @@ static void teardown() throws Exception { } } - // Helper method to create an MCP stdio server configuration - private McpStdioServerConfig createLocalMcpServer(String command, List args) { - return new McpStdioServerConfig().setCommand(command).setArgs(args).setTools(List.of("*")); + private Map createTestMcpServers(String... serverNames) { + Map servers = new HashMap<>(); + for (String serverName : serverNames) { + servers.put(serverName, createTestMcpServer()); + } + return servers; + } + + private McpStdioServerConfig createTestMcpServer() { + Path harnessDir = ctx.getRepoRoot().resolve("test").resolve("harness"); + return new McpStdioServerConfig().setCommand("node") + .setArgs(List.of(harnessDir.resolve("test-mcp-server.mjs").toString())) + .setWorkingDirectory(harnessDir.toString()).setTools(List.of("*")); + } + + private void waitForMcpServerStatus(CopilotSession session, String serverName, McpServerStatus expectedStatus) + throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60); + while (System.nanoTime() < deadline) { + var result = session.getRpc().mcp.list().get(5, TimeUnit.SECONDS); + if (result.servers() != null && result.servers().stream() + .anyMatch(server -> serverName.equals(server.name()) && expectedStatus == server.status())) { + return; + } + Thread.sleep(200); + } + fail(serverName + " did not reach " + expectedStatus); } // ============ MCP Server Tests ============ @@ -68,8 +94,7 @@ private McpStdioServerConfig createLocalMcpServer(String command, List a void testShouldAcceptMcpServerConfigurationOnSessionCreate() throws Exception { ctx.configureForTest("mcp_and_agents", "should_accept_mcp_server_configuration_on_session_create"); - var mcpServers = new HashMap(); - mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); + var mcpServers = createTestMcpServers("test-server"); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession( @@ -77,6 +102,7 @@ void testShouldAcceptMcpServerConfigurationOnSessionCreate() throws Exception { .get(); assertNotNull(session.getSessionId()); + waitForMcpServerStatus(session, "test-server", McpServerStatus.CONNECTED); // Simple interaction to verify session works AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, @@ -108,20 +134,13 @@ void testShouldAcceptMcpServerConfigurationOnSessionResume() throws Exception { session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); // Resume with MCP servers - var mcpServers = new HashMap(); - mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); + var mcpServers = createTestMcpServers("test-server"); CopilotSession session2 = client.resumeSession(sessionId, new ResumeSessionConfig() .setMcpServers(mcpServers).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertEquals(sessionId, session2.getSessionId()); - - AssistantMessageEvent response = session2.sendAndWait(new MessageOptions().setPrompt("What is 3+3?")) - .get(60, TimeUnit.SECONDS); - - assertNotNull(response); - assertTrue(response.getData().content().contains("6"), - "Response should contain 6: " + response.getData().content()); + waitForMcpServerStatus(session2, "test-server", McpServerStatus.CONNECTED); session2.close(); } @@ -139,9 +158,36 @@ void testShouldHandleMultipleMcpServers() throws Exception { // count ctx.configureForTest("mcp_and_agents", "should_accept_mcp_server_configuration_on_session_create"); + var mcpServers = createTestMcpServers("server1", "server2"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setMcpServers(mcpServers).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + assertNotNull(session.getSessionId()); + waitForMcpServerStatus(session, "server1", McpServerStatus.CONNECTED); + waitForMcpServerStatus(session, "server2", McpServerStatus.CONNECTED); + session.close(); + } + } + + // ============ Custom Agent Tests ============ + + /** + * Verifies that MCP server configuration is accepted without args. + * + * @see Snapshot: + * mcp_and_agents/should_accept_mcp_server_configuration_on_session_create + */ + @Test + void testAcceptMcpServerConfigWithoutArgs() throws Exception { + // Reuse existing snapshot - this test validates that args can be omitted + ctx.configureForTest("mcp_and_agents", "should_accept_mcp_server_configuration_on_session_create"); + var mcpServers = new HashMap(); - mcpServers.put("server1", createLocalMcpServer("echo", List.of("server1"))); - mcpServers.put("server2", createLocalMcpServer("echo", List.of("server2"))); + // Create MCP server config without specifying args + mcpServers.put("test-server", new McpStdioServerConfig().setCommand("echo").setTools(List.of("*"))); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession( @@ -149,6 +195,14 @@ void testShouldHandleMultipleMcpServers() throws Exception { .get(); assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("4"), + "Response should contain 4: " + response.getData().content()); + session.close(); } } @@ -261,8 +315,7 @@ void testShouldAcceptCustomAgentWithMcpServers() throws Exception { // Use combined snapshot since this uses both MCP servers and custom agents ctx.configureForTest("mcp_and_agents", "should_accept_both_mcp_servers_and_custom_agents"); - var agentMcpServers = new HashMap(); - agentMcpServers.put("agent-server", createLocalMcpServer("echo", List.of("agent-mcp"))); + var agentMcpServers = createTestMcpServers("agent-server"); List customAgents = List.of(new CustomAgentConfig().setName("mcp-agent") .setDisplayName("MCP Agent").setDescription("An agent with its own MCP servers") @@ -315,8 +368,7 @@ void testShouldAcceptMultipleCustomAgents() throws Exception { void testShouldAcceptBothMcpServersAndCustomAgents() throws Exception { ctx.configureForTest("mcp_and_agents", "should_accept_both_mcp_servers_and_custom_agents"); - var mcpServers = new HashMap(); - mcpServers.put("shared-server", createLocalMcpServer("echo", List.of("shared"))); + var mcpServers = createTestMcpServers("shared-server"); List customAgents = List.of(new CustomAgentConfig().setName("combined-agent") .setDisplayName("Combined Agent").setDescription("An agent using shared MCP servers") @@ -327,6 +379,7 @@ void testShouldAcceptBothMcpServersAndCustomAgents() throws Exception { .setCustomAgents(customAgents).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); assertNotNull(session.getSessionId()); + waitForMcpServerStatus(session, "shared-server", McpServerStatus.CONNECTED); AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 7+7?")).get(60, TimeUnit.SECONDS); diff --git a/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java b/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java index 041d8181c..6cc8eaa30 100644 --- a/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java +++ b/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java @@ -418,7 +418,7 @@ void testShouldShortCircuitPermissionHandlerWhenSetApproveAllEnabled() throws Ex // Set approve-all so the runtime short-circuits var setResult = session.getRpc().permissions .setApproveAll(new com.github.copilot.sdk.generated.rpc.SessionPermissionsSetApproveAllParams( - session.getSessionId(), true)) + session.getSessionId(), true, null)) .get(10, TimeUnit.SECONDS); assertTrue(setResult.success(), "setApproveAll should succeed"); diff --git a/java/src/test/java/com/github/copilot/sdk/PreMcpToolCallHookTest.java b/java/src/test/java/com/github/copilot/sdk/PreMcpToolCallHookTest.java new file mode 100644 index 000000000..37db10c17 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/PreMcpToolCallHookTest.java @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.McpServerConfig; +import com.github.copilot.sdk.json.McpStdioServerConfig; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PreMcpToolCallHookInput; +import com.github.copilot.sdk.json.PreMcpToolCallHookOutput; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionHooks; + +/** + * Tests for preMcpToolCall hook functionality. + * + *

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

+ */ +public class PreMcpToolCallHookTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that preMcpToolCall hook can set metadata on the MCP request. + * + * @see Snapshot: pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook + */ + @Disabled("Requires snapshot: pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook") + @Test + void testShouldSetMetaViaPreMcpToolCallHook() throws Exception { + ctx.configureForTest("pre_mcp_tool_call_hook", "should_set_meta_via_premcptoolcall_hook"); + + var hookInputs = new java.util.ArrayList(); + + var mcpServers = new HashMap(); + mcpServers.put("meta-echo", new McpStdioServerConfig().setCommand("npx").setArgs(List.of("-y", "mcp-meta-echo")) + .setTools(List.of("*")).setWorkingDirectory(ctx.getWorkDir().toString())); + + var hooks = new SessionHooks().setOnPreMcpToolCall((input, invocation) -> { + hookInputs.add(input); + JsonNode metaNode = MAPPER.valueToTree(Map.of("injected", "by-hook", "source", "test")); + return CompletableFuture.completedFuture(PreMcpToolCallHookOutput.withMeta(metaNode)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers).setHooks(hooks) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt( + "Use the meta-echo/echo_meta tool with value 'test-set'. Reply with just the raw tool result.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertFalse(hookInputs.isEmpty(), "Should have received preMcpToolCall hook calls"); + + // Verify hook input fields + PreMcpToolCallHookInput hookInput = hookInputs.get(0); + assertEquals("meta-echo", hookInput.getServerName()); + assertNotNull(hookInput.getToolName()); + assertNotNull(hookInput.getCwd()); + assertTrue(hookInput.getTimestamp() > 0); + + // Verify the response contains the injected metadata + String content = response.getData().content(); + assertTrue(content.contains("by-hook"), "Response should contain injected metadata: " + content); + + session.close(); + } + } + + /** + * Verifies that preMcpToolCall hook can replace existing metadata. + * + * @see Snapshot: + * pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook + */ + @Disabled("Requires snapshot: pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook") + @Test + void testShouldReplaceMetaViaPreMcpToolCallHook() throws Exception { + ctx.configureForTest("pre_mcp_tool_call_hook", "should_replace_meta_via_premcptoolcall_hook"); + + var mcpServers = new HashMap(); + mcpServers.put("meta-echo", new McpStdioServerConfig().setCommand("npx").setArgs(List.of("-y", "mcp-meta-echo")) + .setTools(List.of("*")).setWorkingDirectory(ctx.getWorkDir().toString())); + + var hooks = new SessionHooks().setOnPreMcpToolCall((input, invocation) -> { + JsonNode metaNode = MAPPER.valueToTree(Map.of("replaced", "true", "original", "gone")); + return CompletableFuture.completedFuture(PreMcpToolCallHookOutput.withMeta(metaNode)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers).setHooks(hooks) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt( + "Use the meta-echo/echo_meta tool with value 'test-replace'. Reply with just the raw tool result.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + + // Verify the response contains the replaced metadata + String content = response.getData().content(); + assertTrue(content.contains("replaced"), "Response should contain replaced metadata: " + content); + + session.close(); + } + } + + /** + * Verifies that preMcpToolCall hook can remove metadata from the MCP request. + * + * @see Snapshot: + * pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook + */ + @Disabled("Requires snapshot: pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook") + @Test + void testShouldRemoveMetaViaPreMcpToolCallHook() throws Exception { + ctx.configureForTest("pre_mcp_tool_call_hook", "should_remove_meta_via_premcptoolcall_hook"); + + var mcpServers = new HashMap(); + mcpServers.put("meta-echo", new McpStdioServerConfig().setCommand("npx").setArgs(List.of("-y", "mcp-meta-echo")) + .setTools(List.of("*")).setWorkingDirectory(ctx.getWorkDir().toString())); + + var hooks = new SessionHooks().setOnPreMcpToolCall((input, invocation) -> { + // Return output with null metaToUse to remove metadata + return CompletableFuture.completedFuture(PreMcpToolCallHookOutput.removeMeta()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers).setHooks(hooks) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt( + "Use the meta-echo/echo_meta tool with value 'test-remove'. Reply with just the raw tool result.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java b/java/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java index 08edfa5fa..0abec58f9 100644 --- a/java/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java +++ b/java/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java @@ -792,10 +792,10 @@ void testParseSessionShutdownEvent() throws Exception { var shutdownEvent = (SessionShutdownEvent) event; assertEquals(ShutdownType.ROUTINE, shutdownEvent.getData().shutdownType()); - assertEquals(5.0, shutdownEvent.getData().totalPremiumRequests()); + assertEquals(Double.valueOf(5.0), shutdownEvent.getData().totalPremiumRequests()); assertEquals("gpt-4", shutdownEvent.getData().currentModel()); assertNotNull(shutdownEvent.getData().codeChanges()); - assertEquals(10.0, shutdownEvent.getData().codeChanges().linesAdded()); + assertEquals((Long) 10L, shutdownEvent.getData().codeChanges().linesAdded()); } @Test @@ -1054,7 +1054,7 @@ void testSessionStartEventAllFields() throws Exception { assertNotNull(event); var data = event.getData(); assertEquals("sess-full", data.sessionId()); - assertEquals(2.0, data.version()); + assertEquals((Long) 2L, data.version()); assertEquals("copilot-cli", data.producer()); assertEquals("1.2.3", data.copilotVersion()); assertNotNull(data.startTime()); @@ -1077,7 +1077,7 @@ void testSessionResumeEventAllFields() throws Exception { assertNotNull(event); var data = event.getData(); assertNotNull(data.resumeTime()); - assertEquals(42.0, data.eventCount()); + assertEquals((Long) 42L, data.eventCount()); } @Test @@ -1178,13 +1178,13 @@ void testSessionTruncationEventAllFields() throws Exception { var event = (SessionTruncationEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals(128000.0, data.tokenLimit()); - assertEquals(150000.0, data.preTruncationTokensInMessages()); - assertEquals(100.0, data.preTruncationMessagesLength()); - assertEquals(120000.0, data.postTruncationTokensInMessages()); - assertEquals(80.0, data.postTruncationMessagesLength()); - assertEquals(30000.0, data.tokensRemovedDuringTruncation()); - assertEquals(20.0, data.messagesRemovedDuringTruncation()); + assertEquals((Long) 128000L, data.tokenLimit()); + assertEquals((Long) 150000L, data.preTruncationTokensInMessages()); + assertEquals((Long) 100L, data.preTruncationMessagesLength()); + assertEquals((Long) 120000L, data.postTruncationTokensInMessages()); + assertEquals((Long) 80L, data.postTruncationMessagesLength()); + assertEquals((Long) 30000L, data.tokensRemovedDuringTruncation()); + assertEquals((Long) 20L, data.messagesRemovedDuringTruncation()); assertEquals("system", data.performedBy()); } @@ -1204,9 +1204,9 @@ void testSessionUsageInfoEventAllFields() throws Exception { var event = (SessionUsageInfoEvent) parseJson(json); assertNotNull(event); var data = event.getData(); - assertEquals(128000.0, data.tokenLimit()); - assertEquals(50000.0, data.currentTokens()); - assertEquals(25.0, data.messagesLength()); + assertEquals((Long) 128000L, data.tokenLimit()); + assertEquals((Long) 50000L, data.currentTokens()); + assertEquals((Long) 25L, data.messagesLength()); } @Test @@ -1240,21 +1240,21 @@ void testSessionCompactionCompleteEventAllFields() throws Exception { var data = event.getData(); assertTrue(data.success()); assertNull(data.error()); - assertEquals(150000.0, data.preCompactionTokens()); - assertEquals(60000.0, data.postCompactionTokens()); - assertEquals(100.0, data.preCompactionMessagesLength()); - assertEquals(50.0, data.messagesRemoved()); - assertEquals(90000.0, data.tokensRemoved()); + assertEquals((Long) 150000L, data.preCompactionTokens()); + assertEquals((Long) 60000L, data.postCompactionTokens()); + assertEquals((Long) 100L, data.preCompactionMessagesLength()); + assertEquals((Long) 50L, data.messagesRemoved()); + assertEquals((Long) 90000L, data.tokensRemoved()); assertEquals("Compacted conversation", data.summaryContent()); - assertEquals(3.0, data.checkpointNumber()); + assertEquals((Long) 3L, data.checkpointNumber()); assertEquals("/checkpoints/3", data.checkpointPath()); assertEquals("req-compact-1", data.requestId()); var tokens = data.compactionTokensUsed(); assertNotNull(tokens); - assertEquals(1000.0, tokens.inputTokens()); - assertEquals(500.0, tokens.outputTokens()); - assertEquals(200.0, tokens.cacheReadTokens()); + assertEquals((Long) 1000L, tokens.inputTokens()); + assertEquals((Long) 500L, tokens.outputTokens()); + assertEquals((Long) 200L, tokens.cacheReadTokens()); } @Test @@ -1288,16 +1288,16 @@ void testSessionShutdownEventAllFields() throws Exception { var data = event.getData(); assertEquals(ShutdownType.ERROR, data.shutdownType()); assertEquals("OOM", data.errorReason()); - assertEquals(10.0, data.totalPremiumRequests()); - assertEquals(5000.5, data.totalApiDurationMs()); - assertEquals(1700000000000.0, data.sessionStartTime()); + assertEquals(Double.valueOf(10.0), data.totalPremiumRequests()); + assertEquals((Long) 5000L, data.totalApiDurationMs()); + assertEquals((Long) 1700000000000L, data.sessionStartTime()); assertEquals("gpt-4-turbo", data.currentModel()); assertNotNull(data.modelMetrics()); var changes = data.codeChanges(); assertNotNull(changes); - assertEquals(50.0, changes.linesAdded()); - assertEquals(20.0, changes.linesRemoved()); + assertEquals((Long) 50L, changes.linesAdded()); + assertEquals((Long) 20L, changes.linesRemoved()); assertNotNull(changes.filesModified()); assertEquals(3, changes.filesModified().size()); assertEquals("a.java", changes.filesModified().get(0)); @@ -1391,7 +1391,7 @@ void testAssistantStreamingDeltaEventAllFields() throws Exception { var event = (AssistantStreamingDeltaEvent) parseJson(json); assertNotNull(event); assertEquals("assistant.streaming_delta", event.getType()); - assertEquals(4096.0, event.getData().totalResponseSizeBytes()); + assertEquals((Long) 4096L, event.getData().totalResponseSizeBytes()); } @Test @@ -1482,12 +1482,12 @@ void testAssistantUsageEventAllFields() throws Exception { assertNotNull(event); var data = event.getData(); assertEquals("gpt-4-turbo", data.model()); - assertEquals(500.0, data.inputTokens()); - assertEquals(200.0, data.outputTokens()); - assertEquals(50.0, data.cacheReadTokens()); - assertEquals(150.0, data.cacheWriteTokens()); + assertEquals((Long) 500L, data.inputTokens()); + assertEquals((Long) 200L, data.outputTokens()); + assertEquals((Long) 50L, data.cacheReadTokens()); + assertEquals((Long) 150L, data.cacheWriteTokens()); assertEquals(0.05, data.cost()); - assertEquals(1234.5, data.duration()); + assertEquals((Long) 1234L, data.duration()); assertEquals("user", data.initiator()); assertEquals("api-1", data.apiCallId()); assertEquals("prov-1", data.providerCallId()); @@ -1497,11 +1497,11 @@ void testAssistantUsageEventAllFields() throws Exception { // Verify copilotUsage assertNotNull(data.copilotUsage()); - assertEquals(1234567.0, data.copilotUsage().totalNanoAiu()); + assertEquals(Double.valueOf(1234567.0), data.copilotUsage().totalNanoAiu()); assertNotNull(data.copilotUsage().tokenDetails()); assertEquals(2, data.copilotUsage().tokenDetails().size()); assertEquals("input", data.copilotUsage().tokenDetails().get(0).tokenType()); - assertEquals(500.0, data.copilotUsage().tokenDetails().get(0).tokenCount()); + assertEquals((Long) 500L, data.copilotUsage().tokenDetails().get(0).tokenCount()); assertEquals("output", data.copilotUsage().tokenDetails().get(1).tokenType()); } @@ -1522,10 +1522,8 @@ void testAssistantUsageEventWithNullQuotaSnapshots() throws Exception { assertNotNull(event); var data = event.getData(); assertEquals("gpt-4-turbo", data.model()); - assertEquals(500.0, data.inputTokens()); - assertEquals(200.0, data.outputTokens()); - // quotaSnapshots is null when absent in JSON (generated class uses nullable - // fields) + assertEquals((Long) 500L, data.inputTokens()); + assertEquals((Long) 200L, data.outputTokens()); assertNull(data.quotaSnapshots()); } @@ -2147,7 +2145,7 @@ void testParseJsonNodeSessionShutdownWithCodeChanges() throws Exception { var event = (SessionShutdownEvent) parseJson(json); assertNotNull(event); assertEquals(ShutdownType.ROUTINE, event.getData().shutdownType()); - assertEquals(100.0, event.getData().codeChanges().linesAdded()); + assertEquals((Long) 100L, event.getData().codeChanges().linesAdded()); assertEquals(1, event.getData().codeChanges().filesModified().size()); } diff --git a/java/src/test/java/com/github/copilot/sdk/TestUtil.java b/java/src/test/java/com/github/copilot/sdk/TestUtil.java index d9462af87..af4474590 100644 --- a/java/src/test/java/com/github/copilot/sdk/TestUtil.java +++ b/java/src/test/java/com/github/copilot/sdk/TestUtil.java @@ -36,9 +36,9 @@ public static String tempPath(String filename) { *

* Resolution order: *

    - *
  1. Search the system PATH using {@code where.exe} (Windows) or {@code which} - * (Linux/macOS).
  2. - *
  3. Fall back to the {@code COPILOT_CLI_PATH} environment variable.
  4. + *
  5. Use the {@code COPILOT_CLI_PATH} environment variable when set.
  6. + *
  7. Otherwise search the system PATH using {@code where.exe} (Windows) or + * {@code which} (Linux/macOS).
  8. *
  9. Walk parent directories looking for * {@code nodejs/node_modules/@github/copilot/index.js}.
  10. *
@@ -55,16 +55,16 @@ public static String tempPath(String filename) { * {@code null} if none was found */ static String findCliPath() { - String copilotInPath = findCopilotInPath(); - if (copilotInPath != null) { - return copilotInPath; - } - String envPath = System.getenv("COPILOT_CLI_PATH"); if (envPath != null && !envPath.isEmpty()) { return envPath; } + String copilotInPath = findCopilotInPath(); + if (copilotInPath != null) { + return copilotInPath; + } + Path current = Paths.get(System.getProperty("user.dir")); while (current != null) { Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/index.js"); diff --git a/java/src/test/java/com/github/copilot/sdk/generated/GeneratedEventTypesCoverageTest.java b/java/src/test/java/com/github/copilot/sdk/generated/GeneratedEventTypesCoverageTest.java index 6cb608e19..7cbaf9a0c 100644 --- a/java/src/test/java/com/github/copilot/sdk/generated/GeneratedEventTypesCoverageTest.java +++ b/java/src/test/java/com/github/copilot/sdk/generated/GeneratedEventTypesCoverageTest.java @@ -45,7 +45,7 @@ void testParseAssistantStreamingDeltaEvent() throws Exception { assertInstanceOf(AssistantStreamingDeltaEvent.class, event); assertEquals("assistant.streaming_delta", event.getType()); var typed = (AssistantStreamingDeltaEvent) event; - assertEquals(1024.0, typed.getData().totalResponseSizeBytes()); + assertEquals((Long) 1024L, typed.getData().totalResponseSizeBytes()); } // ── CapabilitiesChangedEvent ─────────────────────────────────────────── diff --git a/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java b/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java index f32c95c5c..e0f66bd59 100644 --- a/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java +++ b/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -633,7 +633,7 @@ void sessionRpc_log_merges_sessionId() { var stub = new StubCaller(); var session = new SessionRpc(stub, "sess-log"); - var logParams = new SessionLogParams(null, "Hello from test", null, null, null); + var logParams = new SessionLogParams(null, "Hello from test", null, null, null, null, null); session.log(logParams); assertEquals(1, stub.calls.size()); diff --git a/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java index e6ae7e7d9..6601b4829 100644 --- a/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.*; +import java.time.OffsetDateTime; import java.util.List; import java.util.Map; import java.util.UUID; @@ -32,9 +33,9 @@ void pingParams_record() { @Test void pingResult_record() { - var result = new PingResult("pong", 1234L, 2L); + var result = new PingResult("pong", null, 2L); assertEquals("pong", result.message()); - assertEquals(1234L, result.timestamp()); + assertNull(result.timestamp()); assertEquals(2L, result.protocolVersion()); } @@ -246,7 +247,7 @@ void sessionHistoryTruncateParams_record() { @Test void sessionLogParams_record() { - var params = new SessionLogParams("sess-24", "test message", SessionLogLevel.INFO, false, null); + var params = new SessionLogParams("sess-24", "test message", SessionLogLevel.INFO, null, false, null, null); assertEquals("sess-24", params.sessionId()); assertEquals("test message", params.message()); assertEquals(SessionLogLevel.INFO, params.level()); @@ -471,9 +472,10 @@ void sessionWorkspaceReadFileParams_record() { @Test void pingResult_fields() { - var result = new PingResult("pong", 9999L, 1L); + var ts = OffsetDateTime.now(); + var result = new PingResult("pong", ts, 1L); assertEquals("pong", result.message()); - assertEquals(9999L, result.timestamp()); + assertEquals(ts, result.timestamp()); assertEquals(1L, result.protocolVersion()); } @@ -484,7 +486,8 @@ void sessionAgentDeselectResult_empty() { @Test void sessionAgentListResult_with_items() { - var item = new AgentInfo("name1", "Name One", "Desc 1", "/path/to/agent1"); + var item = new AgentInfo("name1", "Name One", "Desc 1", "/path/to/agent1", 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()); @@ -495,7 +498,8 @@ void sessionAgentListResult_with_items() { @Test void sessionAgentGetCurrentResult_nested() { - var agent = new AgentInfo("agent-1", "Agent One", "Does things", null); + var agent = new AgentInfo("agent-1", "Agent One", "Does things", 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()); @@ -511,7 +515,7 @@ void sessionAgentGetCurrentResult_null_agent() { @Test void sessionAgentReloadResult_with_items() { - var item = new AgentInfo("a", "A", "Desc", "/path/to/a"); + var item = new AgentInfo("a", "A", "Desc", "/path/to/a", 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()); @@ -519,7 +523,8 @@ void sessionAgentReloadResult_with_items() { @Test void sessionAgentSelectResult_nested() { - var agent = new AgentInfo("selected", "Selected", "The selected agent", "/path/to/selected"); + var agent = new AgentInfo("selected", "Selected", "The selected agent", "/path/to/selected", null, null, null, + null, null, null, null); var result = new SessionAgentSelectResult(agent); assertEquals("selected", result.agent().name()); } @@ -638,7 +643,7 @@ void sessionFsStatResult_record() { @Test void sessionHistoryCompactResult_nested() { var ctx = new HistoryCompactContextWindow(100000L, 5000L, 20L, 1000L, 3000L, 500L); - var result = new SessionHistoryCompactResult(true, 2000L, 5L, ctx); + var result = new SessionHistoryCompactResult(true, 2000L, 5L, null, ctx); assertTrue(result.success()); assertEquals(2000L, result.tokensRemoved()); assertEquals(5L, result.messagesRemoved()); @@ -715,7 +720,7 @@ void sessionModeSetResult_enum() { @Test void sessionModelGetCurrentResult_record() { - var result = new SessionModelGetCurrentResult("claude-sonnet-4.5"); + var result = new SessionModelGetCurrentResult("claude-sonnet-4.5", null); assertEquals("claude-sonnet-4.5", result.modelId()); } @@ -786,7 +791,7 @@ void sessionSkillsEnableResult_empty() { @Test void sessionSkillsListResult_nested() { - var item = new Skill("deploy", "Deploy the app", SkillSource.PROJECT, true, true, "/skills/deploy.md"); + var item = new Skill("deploy", "Deploy the app", SkillSource.PROJECT, true, true, "/skills/deploy.md", null); var result = new SessionSkillsListResult(List.of(item)); assertEquals(1, result.skills().size()); assertEquals("deploy", result.skills().get(0).name()); @@ -832,9 +837,9 @@ void sessionUiHandlePendingElicitationResult_record() { @Test void sessionUsageGetMetricsResult_nested() { - var changes = new UsageMetricsCodeChanges(100L, 50L, 5L); - var result = new SessionUsageGetMetricsResult(0.5, 10L, null, null, 2000.0, 1700000000000L, changes, null, - "gpt-5", 1000L, 500L); + var changes = new UsageMetricsCodeChanges(100L, 50L, 5L, null); + var result = new SessionUsageGetMetricsResult(0.5, 10L, null, null, 2000L, null, changes, null, "gpt-5", 1000L, + 500L); assertEquals(0.5, result.totalPremiumRequestCost()); assertEquals(10L, result.totalUserRequests()); assertNotNull(result.codeChanges()); diff --git a/nodejs/README.md b/nodejs/README.md index 06d88c752..1cb6e7836 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -131,10 +131,6 @@ Resume an existing session. Returns the session with `workspacePath` populated i Ping the server to check connectivity. -##### `getState(): ConnectionState` - -Get current connection state. - ##### `listSessions(filter?: SessionListFilter): Promise` List all available sessions. Optionally filter by working directory context. @@ -573,8 +569,8 @@ The SDK auto-injects environment context, tool instructions, and security guardr Use `mode: "customize"` to selectively override individual sections of the prompt while preserving the rest: ```typescript -import { SYSTEM_PROMPT_SECTIONS } from "@github/copilot-sdk"; -import type { SectionOverride, SystemPromptSection } from "@github/copilot-sdk"; +import { SYSTEM_MESSAGE_SECTIONS } from "@github/copilot-sdk"; +import type { SectionOverride, SystemMessageSection } from "@github/copilot-sdk"; const session = await client.createSession({ model: "gpt-5", @@ -597,7 +593,7 @@ const session = await client.createSession({ }); ``` -Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`. Use the `SYSTEM_PROMPT_SECTIONS` constant for descriptions of each section. +Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `runtime_instructions`, `last_instructions`. Use the `SYSTEM_MESSAGE_SECTIONS` constant for descriptions of each section. Each section override supports four actions: diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index e7ea0d419..f93e4e02c 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.52-1", + "@github/copilot": "^1.0.53-2", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -663,9 +663,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.52-1.tgz", - "integrity": "sha512-oz6m/dOpTU+FaCWXqYZj5JkJmRT+/RYcrmtGal39V+gOxTA2Nc9wIeLH1SMwMoOXC9Q6DN6keiY0wqWcHirPVg==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.53-2.tgz", + "integrity": "sha512-SkISXco8PFyuOreaPIiBiyQHdXnw51wLmSvzW7yrdD02dH9qRBCcrxPXFS05iLrv3hLCnhhECKJUv1afTPtUBg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -674,20 +674,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.52-1", - "@github/copilot-darwin-x64": "1.0.52-1", - "@github/copilot-linux-arm64": "1.0.52-1", - "@github/copilot-linux-x64": "1.0.52-1", - "@github/copilot-linuxmusl-arm64": "1.0.52-1", - "@github/copilot-linuxmusl-x64": "1.0.52-1", - "@github/copilot-win32-arm64": "1.0.52-1", - "@github/copilot-win32-x64": "1.0.52-1" + "@github/copilot-darwin-arm64": "1.0.53-2", + "@github/copilot-darwin-x64": "1.0.53-2", + "@github/copilot-linux-arm64": "1.0.53-2", + "@github/copilot-linux-x64": "1.0.53-2", + "@github/copilot-linuxmusl-arm64": "1.0.53-2", + "@github/copilot-linuxmusl-x64": "1.0.53-2", + "@github/copilot-win32-arm64": "1.0.53-2", + "@github/copilot-win32-x64": "1.0.53-2" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.52-1.tgz", - "integrity": "sha512-DWXtC/yItZVtkSQhPyRMEkFwa2mcY2rg2cu/uwJ15L9ReiYvlKYEZQDe1TMqkT+U6+k9KjA2L2HQfXVm14/vTw==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.53-2.tgz", + "integrity": "sha512-Ws+YLk9Gyix2IaqzFSuZe00fhX5IGAgNXyVNzkO1MvtnFSj9vGTAFslF94cf3VkpaI8VNf+O3MRGzaQohCpv4A==", "cpu": [ "arm64" ], @@ -701,9 +701,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.52-1.tgz", - "integrity": "sha512-NFTJkzzlTALMfbj9CDJ7N09PRPTVFq1+71hk+zoNx1uT/pi954liV6tKSaNAihPIXTMQKfJXGwEdjtvACpc8Vg==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.53-2.tgz", + "integrity": "sha512-3yHajiuz5UBsdpOlaZCLp2diveXJcIbXbjdjmovPIUrY/2h4yUbQSBEkFuxzV5CAuehQE9S7+NaZYMhUXRIl9Q==", "cpu": [ "x64" ], @@ -717,9 +717,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.52-1.tgz", - "integrity": "sha512-CZE29v+RPJClHgVE1rU+RpRWSG8lm48koRZ0taKVopqLRD6NWKjBOwFKYJojk08H8/K+BWr/paM5+R8hEZHxZw==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.53-2.tgz", + "integrity": "sha512-VgpKr1c7vw8lDqNPOIHYj7Qj6FJY2j3dTh6xaBcItUDLD7y45Pp36JJXrPiVjya7Upx4ThxR/kj9KSRxx4s5pg==", "cpu": [ "arm64" ], @@ -733,9 +733,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.52-1.tgz", - "integrity": "sha512-tJhLQV70TJLq3hPXg7P6pHPfE4vaT2nENIXZsHu6fBkOcsSAxX1APSv6Bkyfsiod8EfFHkcG2+n7VXiVg8WqFw==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.53-2.tgz", + "integrity": "sha512-1mZUCQVeS2y5douEq8tCIEBr1XP8L0UM7fo4MmJHlLT+6ykZz1pyJPtnpO8OO4GGRvenOFd/XM0k2a+KpxYqtw==", "cpu": [ "x64" ], @@ -749,9 +749,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.52-1.tgz", - "integrity": "sha512-u24wHsUumldUEPWX/5z5IEuJvixiQEYF82N04P1g65dvOknq+89dpj+GND4Rh3Vr5u13drgj5AJqkJbWB8N+EQ==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.53-2.tgz", + "integrity": "sha512-gf2wgQu8DuUZSz0Fdq2f13TvWOi6+xkkyQQM3mPtt841UV0K8eUV9MBSRvkv1zbd0RG9MgbPaBLE8TRpWezikg==", "cpu": [ "arm64" ], @@ -765,9 +765,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.52-1.tgz", - "integrity": "sha512-wM22FxcHL8NlnesKKQPPvtk4ojqefN7irU5tQcX+IunpD1izVQl7AOXhZyHoQ21zQnN0De8EapxOUc+WnvlxpA==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.53-2.tgz", + "integrity": "sha512-Zk/o40BOmuNUC6eLZnPRGE45dzdmrPbjusyGOdLKXFzlImHHW2SwYoFchnubzpz81Hzwur3/vCqYtGWjTSa8LA==", "cpu": [ "x64" ], @@ -781,9 +781,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.52-1.tgz", - "integrity": "sha512-ecvfl9N7DPSwpiT2ZNUSXR1ZrSKwpkByOU6VcNphh4RptPZ0iNfyRNLhFCwSfFz+FvB6z2LZi+F7jSzQ3SaT3w==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.53-2.tgz", + "integrity": "sha512-BBMC0QIOn+f61VGfyZbEiFupWJToZwftv9FhJ7xOh1utg3lwmBfjmaG9BKXdnaFqlOjP8mUKbgAkyBJHGZYNOA==", "cpu": [ "arm64" ], @@ -797,9 +797,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.52-1.tgz", - "integrity": "sha512-a9Ct7krktP+/pfPdh/K57deYzzmL13e5Tb1pf5E152u4o/5xKzfgroNFUOzotFfFhs1jFhdzKCm3WHNLIvVEHA==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.53-2.tgz", + "integrity": "sha512-ObuGJ/AGnhTl3kOPIjY9xj3BfucjpQNytmIPQGwgMDDBisSvtfdQ5WVbZlKG736VtQ1epZ1RmzS28bKTudBD/Q==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 611e3f7d3..7aa9076e7 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.52-1", + "@github/copilot": "^1.0.53-2", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index b50ed3477..3881c7288 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.52-1", + "@github/copilot": "^1.0.53-2", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/canvas.ts b/nodejs/src/canvas.ts new file mode 100644 index 000000000..738dfc851 --- /dev/null +++ b/nodejs/src/canvas.ts @@ -0,0 +1,286 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Extension-owned canvases declared via + * `joinSession({ canvases: [createCanvas({...})] })`. + * + * The runtime sends provider callbacks directly as `canvas.open`, + * `canvas.close`, and `canvas.action.invoke` JSON-RPC requests. The SDK + * routes those requests by `canvasId` to the in-process handlers bound by + * `createCanvas`. Re-opening with an existing `instanceId` is how the host + * focuses an existing panel; reload is a renderer-only concern. + */ + +/** JSON Schema object used for canvas inputs. */ +export type CanvasJsonSchema = Record; + +/** + * A single agent-callable action contributed by a canvas. The metadata + * (`name`, `description`, `inputSchema`) is serialized over the wire on + * `session.create` / `session.resume`; the `handler` closure is stripped + * before the declaration is sent and dispatched in-process by the SDK. + * + * Names MUST NOT start with `canvas.` — that prefix is reserved for + * lifecycle verbs. + */ +export interface CanvasAction { + /** Action identifier, unique within the canvas. */ + name: string; + /** Description shown to the model when picking an action. */ + description?: string; + /** Optional JSON Schema for the action's `input` payload. */ + inputSchema?: CanvasJsonSchema; + /** Required per-action dispatch handler. */ + handler: (ctx: CanvasActionContext) => Promise | unknown; +} + +/** + * Declarative metadata for a single canvas, serialized over the wire on + * `session.create` / `session.resume`. + */ +export interface CanvasDeclaration { + /** Canvas id, unique within the declaring connection. */ + id: string; + /** Human-readable label shown in discovery and host UI chrome. */ + displayName: string; + /** Short, single-sentence description shown to the agent in canvas catalogs. */ + description: string; + /** Optional JSON Schema for the `input` payload accepted by `canvas.open`. */ + inputSchema?: CanvasJsonSchema; + /** Agent-invocable actions exposed via `invoke_canvas_action`. */ + actions?: Omit[]; +} + +/** Response returned from `open`. */ +export interface CanvasOpenResponse { + /** URL the host should render. Optional for native canvases. */ + url?: string; + /** Provider-supplied title shown in host chrome. */ + title?: string; + /** Provider-supplied status text shown in host chrome. */ + status?: string; +} + +/** Host capabilities passed to canvas callbacks. */ +export interface CanvasHostContext { + capabilities?: { + canvases?: boolean; + }; +} + +/** Context handed to a canvas's `open` handler. */ +export interface CanvasOpenContext { + /** Session that requested the canvas. */ + sessionId: string; + /** Extension id that owns the canvas. */ + extensionId: string; + /** Canvas id (matches the declaring `CanvasDeclaration.id`). */ + canvasId: string; + /** Stable instance id supplied by the runtime. */ + instanceId: string; + /** Validated `input` payload, shaped by `CanvasDeclaration.inputSchema`. */ + input: unknown; + /** Host capabilities supplied by the runtime. */ + host?: CanvasHostContext; +} + +/** Context handed to a canvas action handler. */ +export interface CanvasActionContext { + /** Session that invoked the action. */ + sessionId: string; + /** Extension id that owns the canvas. */ + extensionId: string; + /** Canvas id targeted by the action. */ + canvasId: string; + /** Instance id targeted by the action. */ + instanceId: string; + /** Action name from `CanvasAction.name`. */ + actionName: string; + /** Validated `input` payload, shaped by the action's `inputSchema`. */ + input: unknown; + /** Host capabilities supplied by the runtime. */ + host?: CanvasHostContext; +} + +/** Context handed to a canvas's `onClose` handler. */ +export interface CanvasLifecycleContext { + /** Session owning the canvas instance. */ + sessionId: string; + /** Extension id that owns the canvas. */ + extensionId: string; + /** Canvas id (matches the declaring `CanvasDeclaration.id`). */ + canvasId: string; + /** Instance id this lifecycle event applies to. */ + instanceId: string; + /** Host capabilities supplied by the runtime. */ + host?: CanvasHostContext; +} + +/** Structured error returned from canvas handlers. */ +export class CanvasError extends Error { + constructor( + public readonly code: string, + message: string + ) { + super(message); + this.name = "CanvasError"; + } + + /** Default error when an action is declared but no `handler` is wired. */ + static noHandler(): CanvasError { + return new CanvasError( + "canvas_action_no_handler", + "No handler implemented for this canvas action" + ); + } +} + +/** + * Options accepted by {@link createCanvas}. Combines the declarative + * {@link CanvasDeclaration} fields with the in-process handler closures. + */ +export interface CanvasOptions { + /** @see CanvasDeclaration.id */ + id: string; + /** @see CanvasDeclaration.displayName */ + displayName: string; + /** @see CanvasDeclaration.description */ + description: string; + /** @see CanvasDeclaration.inputSchema */ + inputSchema?: CanvasJsonSchema; + /** + * Agent-invocable actions exposed via `invoke_canvas_action`. Each action + * carries its own required `handler`; the action's wire metadata + * (`name`, `description`, `inputSchema`) is what reaches the runtime. + */ + actions?: CanvasAction[]; + + /** Required. Open a new canvas instance. */ + open: (ctx: CanvasOpenContext) => Promise | CanvasOpenResponse; + + /** + * Optional. Notified when a canvas instance is closed by the user, the + * agent, or the host. Fire-and-forget: the return value is ignored and + * errors are logged but not surfaced to the runtime. + */ + onClose?: (ctx: CanvasLifecycleContext) => Promise | void; +} + +/** A registered canvas: declarative metadata + in-process handler closures. + * + * Node intentionally uses a per-canvas factory pattern (mirroring + * {@link https://github.com/github/copilot-sdk | `DefineTool`}'s co-location + * ergonomics) where other SDKs (Rust, Python, Go, .NET) expose a single + * `CanvasHandler` per session that switches on `canvasId`. Both shapes target + * the same JSON-RPC wire protocol; the divergence is API ergonomics only. + */ +export class Canvas { + readonly declaration: CanvasDeclaration; + readonly open: NonNullable; + readonly onClose?: CanvasOptions["onClose"]; + /** @internal */ + readonly actionHandlers: Map; + + /** @internal */ + constructor(options: CanvasOptions) { + const actionHandlers = new Map(); + const wireActions: Omit[] | undefined = options.actions?.map( + ({ handler, ...wire }) => { + actionHandlers.set(wire.name, handler); + return wire; + } + ); + + this.declaration = { + id: options.id, + displayName: options.displayName, + description: options.description, + inputSchema: options.inputSchema, + actions: wireActions, + }; + this.open = options.open; + this.onClose = options.onClose; + this.actionHandlers = actionHandlers; + } +} + +/** Create a canvas declaration with bound in-process handlers. + * + * Node intentionally uses this per-canvas factory pattern (mirroring + * `DefineTool`'s co-location ergonomics) where other SDKs (Rust, Python, Go, + * .NET) expose a single `CanvasHandler` per session that switches on + * `canvasId`. Both shapes target the same JSON-RPC wire protocol. + */ +export function createCanvas(options: CanvasOptions): Canvas { + return new Canvas(options); +} + +/** @internal */ +export interface CanvasProviderRequestParams { + sessionId: string; + extensionId: string; + canvasId: string; + instanceId: string; + input?: unknown; + host?: CanvasHostContext; +} + +/** @internal */ +export interface CanvasActionInvokeParams extends CanvasProviderRequestParams { + actionName: string; +} + +/** + * Dispatch a direct `canvas.*` provider request to the matching {@link Canvas} + * handler. + * + * @internal + */ +export async function dispatchCanvasProviderRequest( + canvas: Canvas, + actionName: "canvas.open" | "canvas.close" | string, + params: CanvasActionInvokeParams | CanvasProviderRequestParams +): Promise { + switch (actionName) { + case "canvas.open": { + const result = await canvas.open({ + sessionId: params.sessionId, + extensionId: params.extensionId, + canvasId: params.canvasId, + instanceId: params.instanceId, + input: params.input, + host: params.host, + }); + return result ?? {}; + } + case "canvas.close": { + if (canvas.onClose) { + await canvas.onClose({ + sessionId: params.sessionId, + extensionId: params.extensionId, + canvasId: params.canvasId, + instanceId: params.instanceId, + host: params.host, + }); + } + return undefined; + } + default: { + const perAction = canvas.actionHandlers.get(actionName); + if (!perAction) { + throw CanvasError.noHandler(); + } + return perAction({ + sessionId: params.sessionId, + extensionId: params.extensionId, + canvasId: params.canvasId, + instanceId: params.instanceId, + actionName, + input: params.input, + host: params.host, + }); + } + } +} diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index f4b558024..3e8a4cfa3 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -31,14 +31,19 @@ import { createInternalServerRpc, registerClientSessionApiHandlers, } from "./generated/rpc.js"; +import type { OpenCanvasInstance } from "./generated/rpc.js"; +import { + type CanvasActionInvokeParams, + type CanvasProviderRequestParams, + dispatchCanvasProviderRequest, +} from "./canvas.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; -import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js"; +import { CopilotSession } from "./session.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { getTraceContext } from "./telemetry.js"; import type { AutoModeSwitchRequest, AutoModeSwitchResponse, - ConnectionState, CopilotClientOptions, CustomAgentConfig, ExitPlanModeRequest, @@ -52,6 +57,7 @@ import type { ResumeSessionConfig, SectionTransformFn, SessionConfig, + SessionCapabilities, SessionEvent, SessionFsConfig, SessionLifecycleEvent, @@ -62,9 +68,6 @@ import type { SystemMessageCustomizeConfig, TelemetryConfig, Tool, - ToolCallRequestPayload, - ToolCallResponsePayload, - ToolResultObject, TraceContextProvider, TypedSessionLifecycleHandler, } from "./types.js"; @@ -74,7 +77,7 @@ import { defaultJoinSessionPermissionHandler } from "./types.js"; * Minimum protocol version this SDK can communicate with. * Servers reporting a version below this are rejected. */ -const MIN_PROTOCOL_VERSION = 2; +const MIN_PROTOCOL_VERSION = 3; /** * Check if value is a Zod schema (has toJSONSchema method) @@ -131,6 +134,32 @@ function toWireCustomAgents(agents: CustomAgentConfig[] | undefined): unknown[] }); } +function isCanvasProviderRequestParams(params: unknown): params is CanvasProviderRequestParams { + if (!params || typeof params !== "object") { + return false; + } + + const request = params as { + sessionId?: unknown; + extensionId?: unknown; + canvasId?: unknown; + instanceId?: unknown; + }; + return ( + typeof request.sessionId === "string" && + typeof request.extensionId === "string" && + typeof request.canvasId === "string" && + typeof request.instanceId === "string" + ); +} + +function isCanvasActionInvokeParams(params: unknown): params is CanvasActionInvokeParams { + return ( + isCanvasProviderRequestParams(params) && + typeof (params as { actionName?: unknown }).actionName === "string" + ); +} + /** * Extract transform callbacks from a system message config and prepare the wire payload. * Function-valued actions are replaced with `{ action: "transform" }` for serialization, @@ -251,7 +280,7 @@ export class CopilotClient { private socket: Socket | null = null; private runtimePort: number | null = null; private actualHost: string = "localhost"; - private state: ConnectionState = "disconnected"; + private state: "disconnected" | "connecting" | "connected" | "error" = "disconnected"; private sessions: Map = new Map(); private stderrBuffer: string = ""; // Captures CLI stderr for error messages /** Resolved connection mode chosen in the constructor. */ @@ -807,6 +836,7 @@ export class CopilotClient { this.onGetTraceContext ); session.registerTools(config.tools); + session.registerCanvases(config.canvases); session.registerCommands(config.commands); session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { @@ -853,6 +883,10 @@ export class CopilotClient { overridesBuiltInTool: tool.overridesBuiltInTool, skipPermission: tool.skipPermission, })), + canvases: config.canvases?.map((canvas) => canvas.declaration), + requestCanvasRenderer: config.requestCanvasRenderer, + requestExtensions: config.requestExtensions, + extensionInfo: config.extensionInfo, commands: config.commands?.map((cmd) => ({ name: cmd.name, description: cmd.description, @@ -863,7 +897,7 @@ export class CopilotClient { provider: config.provider, enableSessionTelemetry: config.enableSessionTelemetry, modelCapabilities: config.modelCapabilities, - requestPermission: true, + requestPermission: !!config.onPermissionRequest, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, requestExitPlanMode: !!config.onExitPlanModeRequest, @@ -891,7 +925,7 @@ export class CopilotClient { const { workspacePath, capabilities } = response as { sessionId: string; workspacePath?: string; - capabilities?: { ui?: { elicitation?: boolean } }; + capabilities?: SessionCapabilities; }; session["_workspacePath"] = workspacePath; session.setCapabilities(capabilities); @@ -941,6 +975,7 @@ export class CopilotClient { this.onGetTraceContext ); session.registerTools(config.tools); + session.registerCanvases(config.canvases); session.registerCommands(config.commands); session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { @@ -991,6 +1026,10 @@ export class CopilotClient { overridesBuiltInTool: tool.overridesBuiltInTool, skipPermission: tool.skipPermission, })), + canvases: config.canvases?.map((canvas) => canvas.declaration), + requestCanvasRenderer: config.requestCanvasRenderer, + requestExtensions: config.requestExtensions, + extensionInfo: config.extensionInfo, commands: config.commands?.map((cmd) => ({ name: cmd.name, description: cmd.description, @@ -1022,15 +1061,18 @@ export class CopilotClient { continuePendingWork: config.continuePendingWork, gitHubToken: config.gitHubToken, remoteSession: config.remoteSession, + openCanvases: config.openCanvases, }); - const { workspacePath, capabilities } = response as { + const { workspacePath, capabilities, openCanvases } = response as { sessionId: string; workspacePath?: string; - capabilities?: { ui?: { elicitation?: boolean } }; + capabilities?: SessionCapabilities; + openCanvases?: OpenCanvasInstance[]; }; session["_workspacePath"] = workspacePath; session.setCapabilities(capabilities); + session.setOpenCanvases(openCanvases ?? []); } catch (e) { this.sessions.delete(sessionId); throw e; @@ -1039,22 +1081,6 @@ export class CopilotClient { return session; } - /** - * Gets the current connection state of the client. - * - * @returns The current connection state: "disconnected", "connecting", "connected", or "error" - * - * @example - * ```typescript - * if (client.getState() === "connected") { - * const session = await client.createSession({ onPermissionRequest: approveAll }); - * } - * ``` - */ - getState(): ConnectionState { - return this.state; - } - /** * Sends a ping request to the server to verify connectivity. * @@ -1856,25 +1882,6 @@ export class CopilotClient { this.handleSessionLifecycleNotification(notification); }); - // Protocol v3 servers send tool calls and permission requests as broadcast events - // (external_tool.requested / permission.requested) handled in CopilotSession._dispatchEvent. - // Protocol v2 servers use the older tool.call / permission.request RPC model instead. - // We always register v2 adapters because handlers are set up before version negotiation; - // a v3 server will simply never send these requests. - this.connection.onRequest( - "tool.call", - async (params: ToolCallRequestPayload): Promise => - await this.handleToolCallRequestV2(params) - ); - - this.connection.onRequest( - "permission.request", - async (params: { - sessionId: string; - permissionRequest: unknown; - }): Promise<{ result: unknown }> => await this.handlePermissionRequestV2(params) - ); - this.connection.onRequest( "userInput.request", async (params: { @@ -1919,6 +1926,17 @@ export class CopilotClient { await this.handleSystemMessageTransform(params) ); + this.connection.onRequest("canvas.open", async (params: CanvasProviderRequestParams) => + this.handleCanvasProviderRequest("canvas.open", params) + ); + this.connection.onRequest("canvas.close", async (params: CanvasProviderRequestParams) => + this.handleCanvasProviderRequest("canvas.close", params) + ); + this.connection.onRequest( + "canvas.action.invoke", + async (params: CanvasActionInvokeParams) => this.handleCanvasActionInvokeRequest(params) + ); + // Register client session API handlers. const sessions = this.sessions; registerClientSessionApiHandlers(this.connection, (sessionId) => { @@ -2123,80 +2141,12 @@ export class CopilotClient { return await session._handleSystemMessageTransform(params.sections); } - // ======================================================================== - // Protocol v2 backward-compatibility adapters - // ======================================================================== - - /** - * Handles a v2-style tool.call RPC request from the server. - * Looks up the session and tool handler, executes it, and returns the result - * in the v2 response format. - */ - private async handleToolCallRequestV2( - params: ToolCallRequestPayload - ): Promise { - if ( - !params || - typeof params.sessionId !== "string" || - typeof params.toolCallId !== "string" || - typeof params.toolName !== "string" - ) { - throw new Error("Invalid tool call payload"); - } - - const session = this.sessions.get(params.sessionId); - if (!session) { - throw new Error(`Unknown session ${params.sessionId}`); - } - - const handler = session.getToolHandler(params.toolName); - if (!handler) { - return { - result: { - textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`, - resultType: "failure", - error: `tool '${params.toolName}' not supported`, - toolTelemetry: {}, - }, - }; - } - - try { - const traceparent = (params as { traceparent?: string }).traceparent; - const tracestate = (params as { tracestate?: string }).tracestate; - const invocation = { - sessionId: params.sessionId, - toolCallId: params.toolCallId, - toolName: params.toolName, - arguments: params.arguments, - traceparent, - tracestate, - }; - const result = await handler(params.arguments, invocation); - return { result: this.normalizeToolResultV2(result) }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - result: { - textResultForLlm: - "Invoking this tool produced an error. Detailed information is not available.", - resultType: "failure", - error: message, - toolTelemetry: {}, - }, - }; - } - } - - /** - * Handles a v2-style permission.request RPC request from the server. - */ - private async handlePermissionRequestV2(params: { - sessionId: string; - permissionRequest: unknown; - }): Promise<{ result: unknown }> { - if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) { - throw new Error("Invalid permission request payload"); + private async handleCanvasProviderRequest( + actionName: string, + params: unknown + ): Promise { + if (!isCanvasProviderRequestParams(params)) { + throw new Error("Invalid canvas provider request payload"); } const session = this.sessions.get(params.sessionId); @@ -2204,50 +2154,19 @@ export class CopilotClient { throw new Error(`Session not found: ${params.sessionId}`); } - try { - const result = await session._handlePermissionRequestV2(params.permissionRequest); - return { result }; - } catch (error) { - if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) { - throw error; - } - return { - result: { - kind: "user-not-available", - }, - }; + const canvas = session.getCanvas(params.canvasId); + if (!canvas) { + throw new Error(`No canvas registered with id "${params.canvasId}"`); } - } - private normalizeToolResultV2(result: unknown): ToolResultObject { - if (result === undefined || result === null) { - return { - textResultForLlm: "Tool returned no result", - resultType: "failure", - error: "tool returned no result", - toolTelemetry: {}, - }; - } + return dispatchCanvasProviderRequest(canvas, actionName, params); + } - if (this.isToolResultObject(result)) { - return result; + private async handleCanvasActionInvokeRequest(params: unknown): Promise { + if (!isCanvasActionInvokeParams(params)) { + throw new Error("Invalid canvas provider request payload"); } - const textResult = typeof result === "string" ? result : JSON.stringify(result); - return { - textResultForLlm: textResult, - resultType: "success", - toolTelemetry: {}, - }; - } - - private isToolResultObject(value: unknown): value is ToolResultObject { - return ( - typeof value === "object" && - value !== null && - "textResultForLlm" in value && - typeof (value as ToolResultObject).textResultForLlm === "string" && - "resultType" in value - ); + return this.handleCanvasProviderRequest(params.actionName, params); } } diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index 617052546..95346dec4 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -6,14 +6,32 @@ import { CopilotClient } from "./client.js"; import type { CopilotSession } from "./session.js"; import { defaultJoinSessionPermissionHandler, + type ExtensionInfo, type PermissionHandler, type ResumeSessionConfig, } from "./types.js"; +export { + Canvas, + CanvasError, + createCanvas, + type CanvasAction, + type CanvasActionContext, + type CanvasDeclaration, + type CanvasHostContext, + type CanvasJsonSchema, + type CanvasLifecycleContext, + type CanvasOpenContext, + type CanvasOpenResponse, + type CanvasOptions, +} from "./canvas.js"; + export type JoinSessionConfig = Omit & { onPermissionRequest?: PermissionHandler; }; +export type { ExtensionInfo }; + /** * Joins the current foreground session. * diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 86c19d6f8..f7e379637 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -64,6 +64,18 @@ export type AuthInfoType = | "token" /** Authentication from a Copilot API token. */ | "copilot-api-token"; +/** + * Runtime-controlled routing state for an open canvas instance. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasInstanceAvailability". + */ +/** @experimental */ +export type CanvasInstanceAvailability = + /** The owning provider is currently connected and routing calls will be dispatched normally. */ + | "ready" + /** The owning provider is not currently connected. Routing calls fail with canvas_provider_unavailable until the agent re-issues open_canvas (which rehydrates via a fresh canvas.open) or the provider reconnects. */ + | "stale"; /** * Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command * @@ -120,7 +132,7 @@ export type ContentFilterMode = /** Remove characters that can hide directives. */ | "hidden_characters"; /** - * Server transport type: stdio, http, sse, or memory + * Server transport type: stdio, http, sse (deprecated), or memory * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "DiscoveredMcpServerType". @@ -130,7 +142,7 @@ export type DiscoveredMcpServerType = | "stdio" /** Server communicates over streamable HTTP. */ | "http" - /** Server communicates over Server-Sent Events. */ + /** Server communicates over Server-Sent Events (deprecated). */ | "sse" /** Server is backed by an in-memory runtime implementation. */ | "memory"; @@ -325,6 +337,114 @@ export type SessionLogLevel = | "warning" /** Error message describing a failure. */ | "error"; +/** + * UI theme preference per SEP-1865 + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsHostContextDetailsTheme". + */ +/** @experimental */ +export type McpAppsHostContextDetailsTheme = + /** Light UI theme */ + | "light" + /** Dark UI theme */ + | "dark"; +/** + * Current display mode (SEP-1865) + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsHostContextDetailsDisplayMode". + */ +/** @experimental */ +export type McpAppsHostContextDetailsDisplayMode = + /** Rendered inline within the host conversation surface */ + | "inline" + /** Rendered as a fullscreen overlay */ + | "fullscreen" + /** Rendered as a picture-in-picture floating panel */ + | "pip"; +/** + * Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsHostContextDetailsAvailableDisplayMode". + */ +/** @experimental */ +export type McpAppsHostContextDetailsAvailableDisplayMode = + /** Rendered inline within the host conversation surface */ + | "inline" + /** Rendered as a fullscreen overlay */ + | "fullscreen" + /** Rendered as a picture-in-picture floating panel */ + | "pip"; +/** + * Platform type for responsive design + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsHostContextDetailsPlatform". + */ +/** @experimental */ +export type McpAppsHostContextDetailsPlatform = + /** Host runs in a web browser */ + | "web" + /** Host runs as a desktop application */ + | "desktop" + /** Host runs on a mobile device */ + | "mobile"; +/** + * UI theme preference per SEP-1865 + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsSetHostContextDetailsTheme". + */ +/** @experimental */ +export type McpAppsSetHostContextDetailsTheme = + /** Light UI theme */ + | "light" + /** Dark UI theme */ + | "dark"; +/** + * Current display mode (SEP-1865) + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsSetHostContextDetailsDisplayMode". + */ +/** @experimental */ +export type McpAppsSetHostContextDetailsDisplayMode = + /** Rendered inline within the host conversation surface */ + | "inline" + /** Rendered as a fullscreen overlay */ + | "fullscreen" + /** Rendered as a picture-in-picture floating panel */ + | "pip"; +/** + * Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsSetHostContextDetailsAvailableDisplayMode". + */ +/** @experimental */ +export type McpAppsSetHostContextDetailsAvailableDisplayMode = + /** Rendered inline within the host conversation surface */ + | "inline" + /** Rendered as a fullscreen overlay */ + | "fullscreen" + /** Rendered as a picture-in-picture floating panel */ + | "pip"; +/** + * Platform type for responsive design + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsSetHostContextDetailsPlatform". + */ +/** @experimental */ +export type McpAppsSetHostContextDetailsPlatform = + /** Host runs in a web browser */ + | "web" + /** Host runs as a desktop application */ + | "desktop" + /** Host runs on a mobile device */ + | "mobile"; /** * MCP server configuration (stdio process or remote HTTP/SSE) * @@ -1186,13 +1306,11 @@ export interface AgentInfo { model?: string; /** * MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. + * + * @experimental */ mcpServers?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * Skill names preloaded into this agent's context. Omitted means none. @@ -1588,6 +1706,220 @@ export interface GhCliAuthInfo { token: string; copilotUser?: CopilotUserResponse; } +/** + * Canvas action that the agent or host can invoke. To discover the input schema for a particular action, call the list_canvas_capabilities tool. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasAction". + */ +/** @experimental */ +export interface CanvasAction { + /** + * Action name exposed by the canvas provider + */ + name: string; + /** + * Description of the action + */ + description?: string; + inputSchema?: CanvasJsonSchema; +} +/** + * JSON Schema for canvas open input + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasJsonSchema". + */ +/** @experimental */ +export interface CanvasJsonSchema { + [k: string]: unknown | undefined; +} +/** + * Canvas close parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasCloseRequest". + */ +/** @experimental */ +export interface CanvasCloseRequest { + /** + * Open canvas instance identifier + */ + instanceId: string; +} +/** + * Canvas action invocation parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasInvokeActionRequest". + */ +/** @experimental */ +export interface CanvasInvokeActionRequest { + /** + * Open canvas instance identifier + */ + instanceId: string; + /** + * Action name to invoke + */ + actionName: string; + /** + * Action input + */ + input?: { + [k: string]: unknown | undefined; + }; +} +/** + * Canvas action invocation result. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasInvokeActionResult". + */ +/** @experimental */ +export interface CanvasInvokeActionResult { + /** + * Provider-supplied action result + */ + result?: { + [k: string]: unknown | undefined; + }; +} +/** + * Declared canvases available in this session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasList". + */ +/** @experimental */ +export interface CanvasList { + /** + * Declared canvases available in this session + */ + canvases: DiscoveredCanvas[]; +} +/** + * Canvas available in the current session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredCanvas". + */ +/** @experimental */ +export interface DiscoveredCanvas { + /** + * Human-readable canvas name + */ + displayName: string; + /** + * Short, single-sentence description shown to the agent in canvas catalogs. + */ + description: string; + inputSchema?: CanvasJsonSchema; + /** + * Actions the agent or host may invoke on an open instance + */ + actions?: CanvasAction[]; + /** + * Owning provider identifier + */ + extensionId: string; + /** + * Owning extension display name, when available + */ + extensionName?: string; + /** + * Provider-local canvas identifier + */ + canvasId: string; +} +/** + * Live open-canvas snapshot. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasListOpenResult". + */ +/** @experimental */ +export interface CanvasListOpenResult { + /** + * Currently open canvas instances + */ + openCanvases: OpenCanvasInstance[]; +} +/** + * Open canvas instance snapshot. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "OpenCanvasInstance". + */ +/** @experimental */ +export interface OpenCanvasInstance { + /** + * Stable caller-supplied canvas instance identifier + */ + instanceId: string; + /** + * Owning provider identifier + */ + extensionId: string; + /** + * Owning extension display name, when available + */ + extensionName?: string; + /** + * Provider-local canvas identifier + */ + canvasId: string; + /** + * Rendered title + */ + title?: string; + /** + * Provider-supplied status text + */ + status?: string; + /** + * URL for web-rendered canvases + */ + url?: string; + /** + * Input supplied when the instance was opened + */ + input?: { + [k: string]: unknown | undefined; + }; + /** + * Whether this snapshot came from an idempotent reopen + */ + reopen: boolean; + availability: CanvasInstanceAvailability; +} +/** + * Canvas open parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CanvasOpenRequest". + */ +/** @experimental */ +export interface CanvasOpenRequest { + /** + * Owning provider identifier. Optional when the canvasId is unique across providers; required to disambiguate when multiple providers register the same canvasId. + */ + extensionId?: string; + /** + * Provider-local canvas identifier + */ + canvasId: string; + /** + * Caller-supplied stable instance identifier + */ + instanceId: string; + /** + * Canvas open input + */ + input?: { + [k: string]: unknown | undefined; + }; +} /** * Slash commands available in the session, after applying any include/exclude filters. * @@ -2146,11 +2478,7 @@ export interface ExternalToolTextResultForLlm { * Optional tool-specific telemetry */ toolTelemetry?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * Base64-encoded binary results returned to the model @@ -2183,6 +2511,12 @@ export interface ExternalToolTextResultForLlmBinaryResultsForLlm { * Human-readable description of the binary data */ description?: string; + /** + * Optional metadata from the producing tool. + */ + metadata?: { + [k: string]: unknown | undefined; + }; } /** * Plain text content block @@ -2774,6 +3108,274 @@ export interface LspInitializeRequest { */ force?: boolean; } +/** + * MCP server, tool name, and arguments to invoke from an MCP App view. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsCallToolRequest". + */ +/** @experimental */ +export interface McpAppsCallToolRequest { + /** + * MCP server hosting the tool + */ + serverName: string; + /** + * MCP tool name + */ + toolName: string; + /** + * Tool arguments + */ + arguments?: { + [k: string]: unknown | undefined; + }; + /** + * **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + */ + originServerName: string; +} +/** + * Capability negotiation snapshot + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsDiagnoseCapability". + */ +/** @experimental */ +export interface McpAppsDiagnoseCapability { + /** + * Whether the session has the `mcp-apps` capability + */ + sessionHasMcpApps: boolean; + /** + * Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on + */ + featureFlagEnabled: boolean; + /** + * Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers + */ + advertised: boolean; +} +/** + * MCP server to diagnose MCP Apps wiring for. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsDiagnoseRequest". + */ +/** @experimental */ +export interface McpAppsDiagnoseRequest { + /** + * MCP server to probe + */ + serverName: string; +} +/** + * Diagnostic snapshot of MCP Apps wiring for the named server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsDiagnoseResult". + */ +/** @experimental */ +export interface McpAppsDiagnoseResult { + capability: McpAppsDiagnoseCapability; + server: McpAppsDiagnoseServer; +} +/** + * What the server returned for this session + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsDiagnoseServer". + */ +/** @experimental */ +export interface McpAppsDiagnoseServer { + /** + * Whether the named server is currently connected + */ + connected: boolean; + /** + * Total tools returned by the server's tools/list + */ + toolCount: number; + /** + * Tools whose `_meta.ui` is populated (resourceUri and/or visibility set) + */ + toolsWithUiMeta: number; + /** + * Up to 5 tool names with `_meta.ui` for quick inspection + */ + sampleToolNames: string[]; +} +/** + * Current host context advertised to MCP App guests. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsHostContext". + */ +/** @experimental */ +export interface McpAppsHostContext { + context: McpAppsHostContextDetails; +} +/** + * Current host context + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsHostContextDetails". + */ +/** @experimental */ +export interface McpAppsHostContextDetails { + theme?: McpAppsHostContextDetailsTheme; + /** + * BCP-47 locale, e.g. 'en-US' + */ + locale?: string; + /** + * IANA timezone, e.g. 'America/New_York' + */ + timeZone?: string; + displayMode?: McpAppsHostContextDetailsDisplayMode; + /** + * Display modes the host supports + */ + availableDisplayModes?: McpAppsHostContextDetailsAvailableDisplayMode[]; + platform?: McpAppsHostContextDetailsPlatform; + /** + * Host application identifier + */ + userAgent?: string; + [k: string]: unknown | undefined; +} +/** + * MCP server to list app-callable tools for. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsListToolsRequest". + */ +/** @experimental */ +export interface McpAppsListToolsRequest { + /** + * MCP server hosting the app + */ + serverName: string; + /** + * **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + */ + originServerName: string; +} +/** + * App-callable tools from the named MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsListToolsResult". + */ +/** @experimental */ +export interface McpAppsListToolsResult { + /** + * App-callable tools from the server + */ + tools: { + [k: string]: unknown | undefined; + }[]; +} +/** + * MCP server and resource URI to fetch. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsReadResourceRequest". + */ +/** @experimental */ +export interface McpAppsReadResourceRequest { + /** + * Name of the MCP server hosting the resource + */ + serverName: string; + /** + * Resource URI (typically ui://...) + */ + uri: string; +} +/** + * Resource contents returned by the MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsReadResourceResult". + */ +/** @experimental */ +export interface McpAppsReadResourceResult { + /** + * Resource contents returned by the server + */ + contents: McpAppsResourceContent[]; +} +/** + * Schema for the `McpAppsResourceContent` type. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsResourceContent". + */ +/** @experimental */ +export interface McpAppsResourceContent { + /** + * The resource URI (typically ui://...) + */ + uri: string; + /** + * MIME type of the content + */ + mimeType?: string; + /** + * Text content (e.g. HTML) + */ + text?: string; + /** + * Base64-encoded binary content + */ + blob?: string; + /** + * Resource-level metadata (CSP, permissions, etc.) + */ + _meta?: { + [k: string]: unknown | undefined; + }; +} +/** + * Host context advertised to MCP App guests + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsSetHostContextDetails". + */ +/** @experimental */ +export interface McpAppsSetHostContextDetails { + theme?: McpAppsSetHostContextDetailsTheme; + /** + * BCP-47 locale, e.g. 'en-US' + */ + locale?: string; + /** + * IANA timezone, e.g. 'America/New_York' + */ + timeZone?: string; + displayMode?: McpAppsSetHostContextDetailsDisplayMode; + /** + * Display modes the host supports + */ + availableDisplayModes?: McpAppsSetHostContextDetailsAvailableDisplayMode[]; + platform?: McpAppsSetHostContextDetailsPlatform; + /** + * Host application identifier + */ + userAgent?: string; + [k: string]: unknown | undefined; +} +/** + * Host context to advertise to MCP App guests. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpAppsSetHostContextRequest". + */ +/** @experimental */ +export interface McpAppsSetHostContextRequest { + context: McpAppsSetHostContextDetails; +} /** * The requestId previously passed to executeSampling that should be cancelled. * @@ -3512,21 +4114,50 @@ export interface ModelBilling { */ export interface ModelBillingTokenPrices { /** - * Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + * AI Credits cost per billing batch of input tokens */ inputPrice?: number; /** - * Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + * AI Credits cost per billing batch of output tokens */ outputPrice?: number; /** - * Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + * AI Credits cost per billing batch of cached tokens */ cachePrice?: number; /** * Number of tokens per standard billing batch */ batchSize?: number; + /** + * Maximum context window tokens for the default tier + */ + contextMax?: number; + longContext?: ModelBillingTokenPricesLongContext; +} +/** + * Long context tier pricing (available for models with extended context windows) + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelBillingTokenPricesLongContext". + */ +export interface ModelBillingTokenPricesLongContext { + /** + * AI Credits cost per billing batch of input tokens + */ + inputPrice?: number; + /** + * AI Credits cost per billing batch of output tokens + */ + outputPrice?: number; + /** + * AI Credits cost per billing batch of cached tokens + */ + cachePrice?: number; + /** + * Maximum context window tokens for the long context tier + */ + contextMax?: number; } /** * Override individual model capabilities resolved by the runtime @@ -5580,6 +6211,8 @@ export interface SendRequest { requiredTool?: string; /** * Optional provenance tag copied to the resulting user.message event. Supported values are `system`, `command-*`, and `schedule-*`. + * + * @internal */ source?: { [k: string]: unknown | undefined; @@ -6105,11 +6738,7 @@ export interface SessionFsSqliteQueryResult { * For SELECT: array of row objects. For others: empty array. */ rows: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }[]; /** * Column names from the result set @@ -6869,6 +7498,8 @@ export interface SessionUpdateOptionsParams { isExperimentalMode?: boolean; /** * Custom model-provider configuration (BYOK). Opaque shape; see `ProviderConfig` in the runtime. + * + * @experimental */ provider?: { [k: string]: unknown | undefined; @@ -6899,6 +7530,8 @@ export interface SessionUpdateOptionsParams { shellProcessFlags?: string[]; /** * Sandbox configuration shape; opaque to SDK consumers. See `SandboxConfig` in the runtime. + * + * @experimental */ sandboxConfig?: { [k: string]: unknown | undefined; @@ -6978,10 +7611,10 @@ export interface SessionUpdateOptionsParams { eventsLogDirectory?: string; /** * Additional content-exclusion policies to merge into the session's policy set. Opaque shape; see `ContentExclusionApiResponse` in the runtime. + * + * @experimental */ - additionalContentExclusionPolicies?: { - [k: string]: unknown | undefined; - }[]; + additionalContentExclusionPolicies?: unknown[]; /** * Whether to expose the `manage_schedule` tool to the agent. The runtime always owns the per-session schedule registry; this flag only controls tool exposure (typically gated to staff users). */ @@ -7814,11 +8447,7 @@ export interface Tool { * JSON Schema for the tool's input parameters */ parameters?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * Optional instructions for how to use this tool effectively @@ -8743,6 +9372,16 @@ export interface WorkspacesSaveLargePasteResult { sizeBytes: number; } | null; } +/** + * Standard MCP CallToolResult + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionMcpAppsCallToolResult". + */ +/** @experimental */ +export interface SessionMcpAppsCallToolResult { + [k: string]: unknown | undefined; +} /** * Identifies the target session. * @@ -9153,6 +9792,48 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.auth.setCredentials", { sessionId, ...params }), }, /** @experimental */ + canvas: { + /** + * Lists canvases declared for the session. + * + * @returns Declared canvases available in this session. + */ + list: async (): Promise => + connection.sendRequest("session.canvas.list", { sessionId }), + /** + * Lists currently open canvas instances for the live session. + * + * @returns Live open-canvas snapshot. + */ + listOpen: async (): Promise => + connection.sendRequest("session.canvas.listOpen", { sessionId }), + /** + * Opens or focuses a canvas instance. + * + * @param params Canvas open parameters. + * + * @returns Open canvas instance snapshot. + */ + open: async (params: CanvasOpenRequest): Promise => + connection.sendRequest("session.canvas.open", { sessionId, ...params }), + /** + * Closes an open canvas instance. + * + * @param params Canvas close parameters. + */ + close: async (params: CanvasCloseRequest): Promise => + connection.sendRequest("session.canvas.close", { sessionId, ...params }), + /** + * Invokes an action on an open canvas instance. + * + * @param params Canvas action invocation parameters. + * + * @returns Canvas action invocation result. + */ + invokeAction: async (params: CanvasInvokeActionRequest): Promise => + connection.sendRequest("session.canvas.invokeAction", { sessionId, ...params }), + }, + /** @experimental */ model: { /** * Gets the currently selected model for the session. @@ -9572,6 +10253,59 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin login: async (params: McpOauthLoginRequest): Promise => connection.sendRequest("session.mcp.oauth.login", { sessionId, ...params }), }, + /** @experimental */ + apps: { + /** + * Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. + * + * @param params MCP server and resource URI to fetch. + * + * @returns Resource contents returned by the MCP server. + */ + readResource: async (params: McpAppsReadResourceRequest): Promise => + connection.sendRequest("session.mcp.apps.readResource", { sessionId, ...params }), + /** + * List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. + * + * @param params MCP server to list app-callable tools for. + * + * @returns App-callable tools from the named MCP server. + */ + listTools: async (params: McpAppsListToolsRequest): Promise => + connection.sendRequest("session.mcp.apps.listTools", { sessionId, ...params }), + /** + * Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. + * + * @param params MCP server, tool name, and arguments to invoke from an MCP App view. + * + * @returns Standard MCP CallToolResult + */ + callTool: async (params: McpAppsCallToolRequest): Promise => + connection.sendRequest("session.mcp.apps.callTool", { sessionId, ...params }), + /** + * Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. + * + * @param params Host context to advertise to MCP App guests. + */ + setHostContext: async (params: McpAppsSetHostContextRequest): Promise => + connection.sendRequest("session.mcp.apps.setHostContext", { sessionId, ...params }), + /** + * Read the current host context advertised to MCP App guests. + * + * @returns Current host context advertised to MCP App guests. + */ + getHostContext: async (): Promise => + connection.sendRequest("session.mcp.apps.getHostContext", { sessionId }), + /** + * Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. + * + * @param params MCP server to diagnose MCP Apps wiring for. + * + * @returns Diagnostic snapshot of MCP Apps wiring for the named server. + */ + diagnose: async (params: McpAppsDiagnoseRequest): Promise => + connection.sendRequest("session.mcp.apps.diagnose", { sessionId, ...params }), + }, }, /** @experimental */ plugins: { diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index f00a04bec..506ed6ae2 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -87,7 +87,10 @@ export type SessionEvent = | CustomAgentsUpdatedEvent | McpServersLoadedEvent | McpServerStatusChangedEvent - | ExtensionsLoadedEvent; + | ExtensionsLoadedEvent + | CanvasOpenedEvent + | CanvasRegistryChangedEvent + | McpAppToolCallCompleteEvent; /** * Hosting platform type of the repository (github or ado) */ @@ -243,6 +246,24 @@ export type ToolExecutionCompleteContentResourceLinkIconTheme = * The embedded resource contents, either text or base64-encoded binary */ export type ToolExecutionCompleteContentResourceDetails = EmbeddedTextResourceContents | EmbeddedBlobResourceContents; +/** + * Allowed values for the `ToolExecutionCompleteToolDescriptionMetaUIVisibility` enumeration. + */ +export type ToolExecutionCompleteToolDescriptionMetaUIVisibility = + /** Tool is callable by the model (LLM tool surface) */ + | "model" + /** Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool */ + | "app"; +/** + * 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) + */ +export type SkillInvokedTrigger = + /** Skill invocation requested explicitly by the user, such as via a slash command or UI affordance. */ + | "user-invoked" + /** Skill invocation requested by the agent. */ + | "agent-invoked" + /** Skill content loaded as part of another context, such as a configured custom agent or subagent. */ + | "context-load"; /** * Message role: "system" for system prompts, "developer" for developer-injected instructions */ @@ -451,6 +472,18 @@ export type McpServerStatus = | "disabled" /** The server is not configured for this session. */ | "not_configured"; +/** + * Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server) + */ +export type McpServerTransport = + /** Server communicates over stdio with a local child process. */ + | "stdio" + /** Server communicates over streamable HTTP. */ + | "http" + /** Server communicates over Server-Sent Events (deprecated). */ + | "sse" + /** Server is backed by an in-memory runtime implementation. */ + | "memory"; /** * Discovery source */ @@ -471,6 +504,14 @@ export type ExtensionsLoadedExtensionStatus = | "failed" /** The extension process is starting. */ | "starting"; +/** + * Runtime-controlled routing state for the instance. "ready" when the provider connection is live; "stale" when the provider has gone away and the instance is awaiting rebinding. + */ +export type CanvasOpenedAvailability = + /** Provider connection is live; actions can be invoked. */ + | "ready" + /** Provider has gone away; the instance is awaiting rebinding. */ + | "stale"; /** * Session event "session.start". Session initialization metadata including context and configuration @@ -745,6 +786,10 @@ export interface ErrorData { * GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ providerCallId?: string; + /** + * Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + */ + serviceRequestId?: string; /** * Error stack trace, when available */ @@ -1066,6 +1111,14 @@ export interface ModelChangeData { * Reason the change happened, when not user-initiated. Currently `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path. UI clients can use this to render contextual copy. */ cause?: string; + /** + * Context tier after the model change; null explicitly clears a previously selected tier + */ + contextTier?: /** Default context tier with standard context window size. */ + | "default" + /** Extended context tier with a larger context window. */ + | "long_context" + | null; /** * Newly selected model identifier */ @@ -1464,10 +1517,14 @@ export interface ShutdownData { totalApiDurationMs: number; /** * Session-wide accumulated nano-AI units cost + * + * @experimental */ totalNanoAiu?: number; /** * Total number of premium API requests used during the session + * + * @internal */ totalPremiumRequests?: number; } @@ -1501,6 +1558,8 @@ export interface ShutdownModelMetric { }; /** * Accumulated nano-AI units cost for this model + * + * @experimental */ totalNanoAiu?: number; usage: ShutdownModelMetricUsage; @@ -1511,10 +1570,14 @@ export interface ShutdownModelMetric { export interface ShutdownModelMetricRequests { /** * Cumulative cost multiplier for requests to this model + * + * @experimental */ cost?: number; /** * Total number of API requests made to this model + * + * @experimental */ count?: number; } @@ -1776,6 +1839,10 @@ export interface CompactionCompleteData { * GitHub request tracing ID (x-github-request-id header) for the compaction LLM call */ requestId?: string; + /** + * Copilot service request ID (x-copilot-service-request-id header) for the compaction LLM call + */ + serviceRequestId?: string; /** * Whether compaction completed successfully */ @@ -1809,6 +1876,11 @@ export interface CompactionCompleteCompactionTokensUsed { * Tokens written to prompt cache in the compaction LLM call */ cacheWriteTokens?: number; + /** + * Per-request cost and usage data from the CAPI copilot_usage response field + * + * @internal + */ copilotUsage?: CompactionCompleteCompactionTokensUsedCopilotUsage; /** * Duration of the compaction LLM call in milliseconds @@ -1830,6 +1902,7 @@ export interface CompactionCompleteCompactionTokensUsed { /** * Per-request cost and usage data from the CAPI copilot_usage response field */ +/** @internal */ export interface CompactionCompleteCompactionTokensUsedCopilotUsage { /** * Itemized token usage breakdown @@ -2403,12 +2476,14 @@ export interface AssistantMessageEvent { export interface AssistantMessageData { /** * Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping + * + * @experimental */ - anthropicAdvisorBlocks?: { - [k: string]: unknown | undefined; - }[]; + anthropicAdvisorBlocks?: unknown[]; /** * Anthropic advisor model ID used for this response, for timeline display on replay + * + * @experimental */ anthropicAdvisorModel?: string; /** @@ -2456,6 +2531,10 @@ export interface AssistantMessageData { * GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ requestId?: string; + /** + * Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + */ + serviceRequestId?: string; /** * Tool invocations requested by the assistant in this message */ @@ -2678,9 +2757,16 @@ export interface AssistantUsageData { * Number of tokens written to prompt cache */ cacheWriteTokens?: number; + /** + * Per-request cost and usage data from the CAPI copilot_usage response field + * + * @internal + */ copilotUsage?: AssistantUsageCopilotUsage; /** * Model multiplier cost for billing purposes + * + * @experimental */ cost?: number; /** @@ -2718,6 +2804,8 @@ export interface AssistantUsageData { providerCallId?: string; /** * Per-quota resource usage snapshots, keyed by quota identifier + * + * @internal */ quotaSnapshots?: { [k: string]: AssistantUsageQuotaSnapshot | undefined; @@ -2730,6 +2818,10 @@ export interface AssistantUsageData { * Number of output tokens used for reasoning (e.g., chain-of-thought) */ reasoningTokens?: number; + /** + * Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + */ + serviceRequestId?: string; /** * Time to first token in milliseconds. Only available for streaming requests */ @@ -2738,6 +2830,7 @@ export interface AssistantUsageData { /** * Per-request cost and usage data from the CAPI copilot_usage response field */ +/** @internal */ export interface AssistantUsageCopilotUsage { /** * Itemized token usage breakdown @@ -2772,37 +2865,54 @@ export interface AssistantUsageCopilotUsageTokenDetail { /** * Schema for the `AssistantUsageQuotaSnapshot` type. */ +/** @internal */ export interface AssistantUsageQuotaSnapshot { /** * Total requests allowed by the entitlement + * + * @internal */ entitlementRequests: number; /** * Whether the user has an unlimited usage entitlement + * + * @internal */ isUnlimitedEntitlement: boolean; /** * Number of additional usage requests made this period + * + * @internal */ overage: number; /** * Whether additional usage is allowed when quota is exhausted + * + * @internal */ overageAllowedWithExhaustedQuota: boolean; /** * Percentage of quota remaining (0 to 100) + * + * @internal */ remainingPercentage: number; /** * Date when the quota resets + * + * @internal */ resetDate?: string; /** * Whether usage is still permitted after quota exhaustion + * + * @internal */ usageAllowedWithExhaustedQuota: boolean; /** * Number of requests already consumed + * + * @internal */ usedRequests: number; } @@ -2864,6 +2974,10 @@ export interface ModelCallFailureData { * GitHub request tracing ID (x-github-request-id header) for server-side log correlation */ providerCallId?: string; + /** + * Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + */ + serviceRequestId?: string; source: ModelCallFailureSource; /** * HTTP status code from the failed request @@ -3172,15 +3286,12 @@ export interface ToolExecutionCompleteData { * Unique identifier for the completed tool call */ toolCallId: string; + toolDescription?: ToolExecutionCompleteToolDescription; /** * Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) */ toolTelemetry?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event @@ -3216,6 +3327,7 @@ export interface ToolExecutionCompleteResult { * Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. */ detailedContent?: string; + uiResource?: ToolExecutionCompleteUIResource; } /** * Plain text content block @@ -3384,6 +3496,118 @@ export interface EmbeddedBlobResourceContents { */ uri: string; } +/** + * MCP Apps UI resource content for rendering in a sandboxed iframe + */ +export interface ToolExecutionCompleteUIResource { + _meta?: ToolExecutionCompleteUIResourceMeta; + /** + * Base64-encoded HTML content + */ + blob?: string; + /** + * MIME type of the content + */ + mimeType: string; + /** + * HTML content as a string + */ + text?: string; + /** + * The ui:// URI of the resource + */ + uri: string; +} +/** + * Resource-level UI metadata (CSP, permissions, visual preferences) + */ +export interface ToolExecutionCompleteUIResourceMeta { + ui?: ToolExecutionCompleteUIResourceMetaUI; +} +/** + * Schema for the `ToolExecutionCompleteUIResourceMetaUI` type. + */ +export interface ToolExecutionCompleteUIResourceMetaUI { + csp?: ToolExecutionCompleteUIResourceMetaUICsp; + domain?: string; + permissions?: ToolExecutionCompleteUIResourceMetaUIPermissions; + prefersBorder?: boolean; +} +/** + * Schema for the `ToolExecutionCompleteUIResourceMetaUICsp` type. + */ +export interface ToolExecutionCompleteUIResourceMetaUICsp { + baseUriDomains?: string[]; + connectDomains?: string[]; + frameDomains?: string[]; + resourceDomains?: string[]; +} +/** + * Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissions` type. + */ +export interface ToolExecutionCompleteUIResourceMetaUIPermissions { + camera?: ToolExecutionCompleteUIResourceMetaUIPermissionsCamera; + clipboardWrite?: ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite; + geolocation?: ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation; + microphone?: ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone; +} +/** + * Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsCamera` type. + */ +export interface ToolExecutionCompleteUIResourceMetaUIPermissionsCamera { + [k: string]: unknown | undefined; +} +/** + * Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite` type. + */ +export interface ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite { + [k: string]: unknown | undefined; +} +/** + * Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation` type. + */ +export interface ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation { + [k: string]: unknown | undefined; +} +/** + * Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone` type. + */ +export interface ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone { + [k: string]: unknown | undefined; +} +/** + * Tool definition metadata, present for MCP tools with MCP Apps support + */ +export interface ToolExecutionCompleteToolDescription { + _meta?: ToolExecutionCompleteToolDescriptionMeta; + /** + * Tool description + */ + description?: string; + /** + * Tool name + */ + name: string; +} +/** + * MCP Apps metadata for UI resource association + */ +export interface ToolExecutionCompleteToolDescriptionMeta { + ui?: ToolExecutionCompleteToolDescriptionMetaUI; +} +/** + * Schema for the `ToolExecutionCompleteToolDescriptionMetaUI` type. + */ +export interface ToolExecutionCompleteToolDescriptionMetaUI { + /** + * URI of the UI resource + */ + resourceUri?: string; + /** + * Who can access this tool + */ + visibility?: ToolExecutionCompleteToolDescriptionMetaUIVisibility[]; +} /** * Session event "skill.invoked". Skill invocation details including content, allowed tools, and plugin metadata */ @@ -3446,6 +3670,11 @@ export interface SkillInvokedData { * Version of the plugin this skill originated from, when applicable */ 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), personal-claude (~/.claude/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + */ + source?: string; + trigger?: SkillInvokedTrigger; } /** * Session event "subagent.started". Sub-agent startup details including parent tool call and agent information @@ -3886,11 +4115,7 @@ export interface SystemMessageMetadata { * Template variables used when constructing the prompt */ variables?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; } /** @@ -5136,11 +5361,7 @@ export interface ElicitationRequestedSchema { * Form field definitions, keyed by field name */ properties: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * List of required field names @@ -5859,10 +6080,18 @@ export interface CapabilitiesChangedData { * UI capability changes */ export interface CapabilitiesChangedUI { + /** + * Whether canvas rendering is now supported + */ + canvases?: boolean; /** * Whether elicitation is now supported */ elicitation?: boolean; + /** + * Whether MCP Apps (SEP-1865) UI passthrough is now supported + */ + mcpApps?: boolean; } /** * Session event "exit_plan_mode.requested". Plan approval request with plan content and available user actions @@ -6241,8 +6470,17 @@ export interface McpServersLoadedServer { * Server name (config key) */ name: string; + /** + * Name of the plugin that supplied the effective MCP server config, only when source is plugin + */ + pluginName?: string; + /** + * Version of the plugin that supplied the effective MCP server config, only when source is plugin + */ + pluginVersion?: string; source?: McpServerSource; status: McpServerStatus; + transport?: McpServerTransport; } /** * Session event "session.mcp_server_status_changed". @@ -6278,6 +6516,10 @@ export interface McpServerStatusChangedEvent { * Schema for the `McpServerStatusChangedData` type. */ export interface McpServerStatusChangedData { + /** + * Error message if the server entered a failed state + */ + error?: string; /** * Name of the MCP server whose status changed */ @@ -6338,3 +6580,265 @@ export interface ExtensionsLoadedExtension { source: ExtensionsLoadedExtensionSource; status: ExtensionsLoadedExtensionStatus; } +/** + * Session event "session.canvas.opened". + */ +export interface CanvasOpenedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: CanvasOpenedData; + /** + * 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 "session.canvas.opened". + */ + type: "session.canvas.opened"; +} +/** + * Schema for the `CanvasOpenedData` type. + */ +export interface CanvasOpenedData { + availability: CanvasOpenedAvailability; + /** + * Provider-local canvas identifier + */ + canvasId: string; + /** + * Owning provider identifier + */ + extensionId: string; + /** + * Owning extension display name, when available + */ + extensionName?: string; + /** + * Input supplied when the instance was opened + */ + input?: { + [k: string]: unknown | undefined; + }; + /** + * Stable caller-supplied canvas instance identifier + */ + instanceId: string; + /** + * Whether this notification represents an idempotent reopen + */ + reopen: boolean; + /** + * Provider-supplied status text + */ + status?: string; + /** + * Rendered title + */ + title?: string; + /** + * URL for web-rendered canvases + */ + url?: string; +} +/** + * Session event "session.canvas.registry_changed". + */ +export interface CanvasRegistryChangedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: CanvasRegistryChangedData; + /** + * 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 "session.canvas.registry_changed". + */ + type: "session.canvas.registry_changed"; +} +/** + * Schema for the `CanvasRegistryChangedData` type. + */ +export interface CanvasRegistryChangedData { + /** + * Canvas declarations currently available + */ + canvases: CanvasRegistryChangedCanvas[]; +} +/** + * Schema for the `CanvasRegistryChangedCanvas` type. + */ +export interface CanvasRegistryChangedCanvas { + /** + * Actions the agent or host may invoke + */ + actions?: CanvasRegistryChangedCanvasAction[]; + /** + * Provider-local canvas identifier + */ + canvasId: string; + /** + * Short, single-sentence description shown to the agent in canvas catalogs. + */ + description: string; + /** + * Human-readable canvas name + */ + displayName: string; + /** + * Owning provider identifier + */ + extensionId: string; + /** + * Owning extension display name, when available + */ + extensionName?: string; + /** + * JSON Schema for canvas open input + */ + inputSchema?: { + [k: string]: unknown | undefined; + }; +} +/** + * Schema for the `CanvasRegistryChangedCanvasAction` type. + */ +export interface CanvasRegistryChangedCanvasAction { + /** + * Action description + */ + description?: string; + /** + * JSON Schema for action input + */ + inputSchema?: { + [k: string]: unknown | undefined; + }; + /** + * Action name + */ + name: string; +} +/** + * Session event "mcp_app.tool_call_complete". MCP App view called a tool on a connected MCP server (SEP-1865) + */ +export interface McpAppToolCallCompleteEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpAppToolCallCompleteData; + /** + * 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 "mcp_app.tool_call_complete". + */ + type: "mcp_app.tool_call_complete"; +} +/** + * MCP App view called a tool on a connected MCP server (SEP-1865) + */ +export interface McpAppToolCallCompleteData { + /** + * Arguments passed to the tool by the app view, if any + */ + arguments?: { + [k: string]: unknown | undefined; + }; + /** + * Wall-clock duration of the underlying tools/call in milliseconds + */ + durationMs: number; + error?: McpAppToolCallCompleteError; + /** + * Standard MCP CallToolResult returned by the server. Present whether or not the call set isError. + */ + result?: { + [k: string]: unknown | undefined; + }; + /** + * Name of the MCP server hosting the tool + */ + serverName: string; + /** + * True when the call completed without throwing AND the MCP CallToolResult did not set isError + */ + success: boolean; + toolMeta?: McpAppToolCallCompleteToolMeta; + /** + * MCP tool name that was invoked + */ + toolName: string; +} +/** + * Set when the underlying tools/call threw an error before returning a CallToolResult + */ +export interface McpAppToolCallCompleteError { + /** + * Human-readable error message + */ + message: string; +} +/** + * The tool's `_meta.ui` block at the time of the call, so consumers can decide whether to forward the result to the model without re-listing tools. + */ +export interface McpAppToolCallCompleteToolMeta { + ui?: McpAppToolCallCompleteToolMetaUI; + [k: string]: unknown | undefined; +} +/** + * Schema for the `McpAppToolCallCompleteToolMetaUI` type. + */ +export interface McpAppToolCallCompleteToolMetaUI { + /** + * `ui://` URI declared by the tool's `_meta.ui.resourceUri` + */ + resourceUri?: string; + /** + * Tool visibility per SEP-1865 (typically a subset of `["model","app"]`) + */ + visibility?: string[]; + [k: string]: unknown | undefined; +} diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 6ada0f141..42498c58f 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -11,12 +11,26 @@ export { CopilotClient } from "./client.js"; export { RuntimeConnection } from "./types.js"; export { CopilotSession, type AssistantMessageEvent } from "./session.js"; +export { + Canvas, + CanvasError, + createCanvas, + type CanvasAction, + type CanvasActionContext, + type CanvasDeclaration, + type CanvasHostContext, + type CanvasJsonSchema, + type CanvasLifecycleContext, + type CanvasOpenContext, + type CanvasOpenResponse, + type CanvasOptions, +} from "./canvas.js"; export { defineTool, approveAll, convertMcpCallToolResult, createSessionFsAdapter, - SYSTEM_PROMPT_SECTIONS, + SYSTEM_MESSAGE_SECTIONS, } from "./types.js"; // Re-export the generated session-event types (every *Event interface and // its corresponding *Data payload type, plus supporting unions/aliases) so @@ -40,7 +54,6 @@ export type { AutoModeSwitchHandler, AutoModeSwitchRequest, AutoModeSwitchResponse, - ConnectionState, CopilotClientOptions, StdioRuntimeConnection, TcpRuntimeConnection, @@ -56,6 +69,7 @@ export type { ExitPlanModeHandler, ExitPlanModeRequest, ExitPlanModeResult, + ExtensionInfo, ForegroundSessionInfo, GetAuthStatusResponse, GetStatusResponse, @@ -110,7 +124,7 @@ export type { SystemMessageConfig, SystemMessageCustomizeConfig, SystemMessageReplaceConfig, - SystemPromptSection, + SystemMessageSection, TelemetryConfig, TraceContext, TraceContextProvider, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 4baf35c3e..74823602e 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -11,6 +11,8 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; import { ConnectionError, ResponseError } from "vscode-jsonrpc/node.js"; import { createSessionRpc } from "./generated/rpc.js"; import type { ClientSessionApiHandlers } from "./generated/rpc.js"; +import type { Canvas } from "./canvas.js"; +import type { OpenCanvasInstance } from "./generated/rpc.js"; import { getTraceContext } from "./telemetry.js"; import type { CommandHandler, @@ -28,7 +30,6 @@ import type { MessageOptions, PermissionHandler, PermissionRequest, - PermissionRequestResult, ReasoningEffort, ModelCapabilitiesOverride, SectionTransformFn, @@ -50,10 +51,6 @@ import type { UserInputResponse, } from "./types.js"; -/** @internal */ -export const NO_RESULT_PERMISSION_V2_ERROR = - "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."; - /** * Convert a raw hook input received over the wire into its public-facing shape. * Currently this only deserializes the numeric Unix-ms `timestamp` field on @@ -105,6 +102,7 @@ export class CopilotSession { private typedEventHandlers: Map void>> = new Map(); private toolHandlers: Map = new Map(); + private canvases: Map = new Map(); private commandHandlers: Map = new Map(); private permissionHandler?: PermissionHandler; private userInputHandler?: UserInputHandler; @@ -116,6 +114,7 @@ export class CopilotSession { private _rpc: ReturnType | null = null; private traceContextProvider?: TraceContextProvider; private _capabilities: SessionCapabilities = {}; + private openCanvasInstances: OpenCanvasInstance[] = []; /** @internal Client session API handlers, populated by CopilotClient during create/resume. */ clientSessionApis: ClientSessionApiHandlers = {}; @@ -640,6 +639,33 @@ export class CopilotSession { return this.toolHandlers.get(name); } + /** + * Registers canvas declarations and handlers for this session. + * + * @param canvases - Canvases created via `createCanvas`, or undefined to clear all canvases + * @internal Called by the SDK when creating/resuming a session with `canvases`. + */ + registerCanvases(canvases?: Canvas[]): void { + this.canvases.clear(); + if (!canvases) { + return; + } + for (const canvas of canvases) { + this.canvases.set(canvas.declaration.id, canvas); + } + } + + /** + * Retrieves a registered canvas by id. + * + * @param canvasId - The id of the canvas to retrieve + * @returns The registered Canvas if found, or undefined + * @internal Used by the SDK's direct `canvas.*` dispatcher. + */ + getCanvas(canvasId: string): Canvas | undefined { + return this.canvases.get(canvasId); + } + /** * Registers command handlers for this session. * @@ -750,6 +776,26 @@ export class CopilotSession { this._capabilities = capabilities ?? {}; } + /** + * Snapshot of canvas instances that were already open when the session was + * resumed. Populated from the `session.resume` response; empty for freshly + * created sessions. Returns a defensive copy — mutating the returned array + * has no effect on the session. + */ + get openCanvases(): OpenCanvasInstance[] { + return [...this.openCanvasInstances]; + } + + /** + * Sets the open-canvas snapshot for this session. + * + * @param instances - The `openCanvases` array from the `session.resume` response. + * @internal This method is typically called internally when resuming a session. + */ + setOpenCanvases(instances: OpenCanvasInstance[]): void { + this.openCanvasInstances = [...instances]; + } + private assertElicitation(): void { if (!this._capabilities.ui?.elicitation) { throw new Error( @@ -907,35 +953,6 @@ export class CopilotSession { return { sections: result }; } - /** - * Handles a permission request in the v2 protocol format (synchronous RPC). - * Used as a back-compat adapter when connected to a v2 server. - * - * @param request - The permission request data from the CLI - * @returns A promise that resolves with the permission decision - * @internal This method is for internal use by the SDK. - */ - async _handlePermissionRequestV2(request: unknown): Promise { - if (!this.permissionHandler) { - return { kind: "user-not-available" }; - } - - try { - const result = await this.permissionHandler(request as PermissionRequest, { - sessionId: this.sessionId, - }); - if (result.kind === "no-result") { - throw new Error(NO_RESULT_PERMISSION_V2_ERROR); - } - return result; - } catch (error) { - if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) { - throw error; - } - return { kind: "user-not-available" }; - } - } - /** * Handles a user input request from the Copilot CLI. * diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index fae3418a0..623a4cabd 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -7,10 +7,12 @@ */ // Import and re-export generated session event types +import type { Canvas } from "./canvas.js"; import type { SessionFsProvider } from "./sessionFsProvider.js"; import type { SessionEvent as GeneratedSessionEvent } from "./generated/session-events.js"; import type { CopilotSession } from "./session.js"; import type { RemoteSessionMode } from "./generated/rpc.js"; +import type { OpenCanvasInstance } from "./generated/rpc.js"; export type { RemoteSessionMode } from "./generated/rpc.js"; export type SessionEvent = GeneratedSessionEvent; export type { SessionFsProvider } from "./sessionFsProvider.js"; @@ -543,6 +545,8 @@ export interface SessionCapabilities { ui?: { /** Whether the host supports interactive elicitation dialogs. */ elicitation?: boolean; + /** Whether the host supports canvas rendering. */ + canvases?: boolean; }; } @@ -733,10 +737,10 @@ export interface ToolCallResponsePayload { } /** - * Known system prompt section identifiers for the "customize" mode. + * Known system message section identifiers for the "customize" mode. * Each section corresponds to a distinct part of the system prompt. */ -export type SystemPromptSection = +export type SystemMessageSection = | "identity" | "tone" | "tool_efficiency" @@ -746,10 +750,11 @@ export type SystemPromptSection = | "safety" | "tool_instructions" | "custom_instructions" + | "runtime_instructions" | "last_instructions"; /** Section metadata for documentation and tooling. */ -export const SYSTEM_PROMPT_SECTIONS: Record = { +export const SYSTEM_MESSAGE_SECTIONS: Record = { identity: { description: "Agent identity preamble and mode statement" }, tone: { description: "Response style, conciseness rules, output formatting preferences" }, tool_efficiency: { description: "Tool usage patterns, parallel calling, batching guidelines" }, @@ -759,6 +764,10 @@ export const SYSTEM_PROMPT_SECTIONS: Record>; + sections?: Partial>; /** * Additional content appended after all sections. @@ -1404,6 +1413,16 @@ export interface InfiniteSessionConfig { */ export type ReasoningEffort = "low" | "medium" | "high" | "xhigh"; +/** + * Stable extension identity for session participants that provide canvases. + */ +export interface ExtensionInfo { + /** Extension namespace/source, e.g. "github-app". */ + source: string; + /** Stable provider name within the source namespace. */ + name: string; +} + /** * Shared configuration fields used by both {@link SessionConfig} (for * creating a new session) and {@link ResumeSessionConfig} (for resuming @@ -1457,6 +1476,38 @@ export interface SessionConfigBase { // eslint-disable-next-line @typescript-eslint/no-explicit-any tools?: Tool[]; + /** + * Canvases contributed by this session participant. The declaring + * connection becomes the live provider for `canvas.open|focus|close|reload` + * and `canvas.action.invoke` dispatches targeting each canvas's `id` for + * the lifetime of the connection. Re-declaring the same id on resume + * replaces the prior declaration. + */ + canvases?: Canvas[]; + + /** + * Renderer-side opt-in: when true, the runtime surfaces canvas agent tools + * (`list_canvas_capabilities`, `open_canvas`, `invoke_canvas_action`) to + * the model for this connection. Default off so SDK callers that cannot + * display canvases stay clean. + */ + requestCanvasRenderer?: boolean; + + /** + * Extension surface opt-in: when true, the runtime wires extension + * management tools and per-extension tool dispatch onto the session for + * this connection. Default off so callers that do not expose extensions + * stay clean. + */ + requestExtensions?: boolean; + + /** + * Stable extension identity for canvas providers on this connection. When + * set, the runtime uses `${source}:${name}` as the agent-facing extension + * id instead of a reconnect-specific connection id. + */ + extensionInfo?: ExtensionInfo; + /** * Slash commands registered for this session. * When the CLI has a TUI, each command appears as `/name` for the user to invoke. @@ -1687,6 +1738,12 @@ export interface ResumeSessionConfig extends SessionConfigBase { * @default false */ continuePendingWork?: boolean; + /** + * Snapshot of canvases that were already open when the session was suspended. + * When provided on resume, the runtime can rehydrate canvas state so consumers + * do not need to re-open canvases that were active before the previous shutdown. + */ + openCanvases?: OpenCanvasInstance[]; } /** @@ -1842,11 +1899,6 @@ export type TypedSessionEventHandler = ( */ export type SessionEventHandler = (event: SessionEvent) => void; -/** - * Connection state - */ -export type ConnectionState = "disconnected" | "connecting" | "connected" | "error"; - /** * Working directory context for a session */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 49a2331a0..ff46c75b3 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,6 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, expect, it, onTestFinished, vi } from "vitest"; -import { approveAll, CopilotClient, RuntimeConnection, type ModelInfo } from "../src/index.js"; +import { + approveAll, + CopilotClient, + createCanvas, + RuntimeConnection, + type ModelInfo, +} from "../src/index.js"; import { CopilotSession } from "../src/session.js"; import { defaultJoinSessionPermissionHandler } from "../src/types.js"; @@ -17,18 +23,195 @@ describe("CopilotClient", () => { expect(spy).not.toHaveBeenCalled(); }); - it("throws when a v2 permission handler returns no-result", async () => { + it("forwards canvas declarations and request flags in session.create", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const canvas = createCanvas({ + id: "counter", + displayName: "Counter", + description: "A counter canvas", + actions: [{ name: "increment", description: "Increment the counter" }], + open: () => ({ url: "https://example.test/counter" }), + }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + onPermissionRequest: approveAll, + canvases: [canvas], + requestCanvasRenderer: true, + requestExtensions: true, + extensionInfo: { source: "github-app", name: "counter-provider" }, + }); + + const payload = spy.mock.calls.find(([method]) => method === "session.create")![1] as any; + expect(payload.canvases).toEqual([ + expect.objectContaining({ + id: "counter", + displayName: "Counter", + description: "A counter canvas", + actions: [{ name: "increment", description: "Increment the counter" }], + }), + ]); + expect(payload.requestCanvasRenderer).toBe(true); + expect(payload.requestExtensions).toBe(true); + expect(payload.extensionInfo).toEqual({ + source: "github-app", + name: "counter-provider", + }); + }); + + it("forwards canvas declarations in session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + const canvas = createCanvas({ + id: "counter", + displayName: "Counter", + description: "A counter canvas", + open: () => ({ url: "https://example.test/counter" }), + }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + canvases: [canvas], + requestCanvasRenderer: true, + requestExtensions: true, + extensionInfo: { source: "github-app", name: "counter-provider" }, + }); + + const payload = spy.mock.calls.find(([method]) => method === "session.resume")![1] as any; + expect(payload.canvases).toEqual([expect.objectContaining({ id: "counter" })]); + expect(payload.requestCanvasRenderer).toBe(true); + expect(payload.requestExtensions).toBe(true); + expect(payload.extensionInfo).toEqual({ + source: "github-app", + name: "counter-provider", + }); + expect(payload.openCanvasInstances).toBeUndefined(); + }); + + it("routes direct canvas action requests to registered canvases", async () => { + const canvas = createCanvas({ + id: "counter", + displayName: "Counter", + description: "A counter canvas", + open: ({ instanceId }) => ({ url: `https://example.test/${instanceId}` }), + actions: [ + { + name: "increment", + handler: ({ actionName, input }) => ({ actionName, input }), + }, + ], + }); + const session = new CopilotSession("session-1", {} as any); + session.registerCanvases([canvas]); + const client = new CopilotClient(); + (client as any).sessions.set(session.sessionId, session); + + const result = await (client as any).handleCanvasProviderRequest("increment", { + sessionId: session.sessionId, + extensionId: "project:counter", + canvasId: "counter", + instanceId: "counter-1", + actionName: "increment", + input: { amount: 1 }, + }); + + expect(result).toEqual({ actionName: "increment", input: { amount: 1 } }); + }); + + it("returns canvas_action_no_handler when no per-action handler is registered", async () => { + const canvas = createCanvas({ + id: "counter", + displayName: "Counter", + description: "A counter canvas", + open: () => ({ url: "https://example.test/counter" }), + }); + + const session = new CopilotSession("session-1", {} as any); + session.registerCanvases([canvas]); + const client = new CopilotClient(); + (client as any).sessions.set(session.sessionId, session); + + await expect( + (client as any).handleCanvasProviderRequest("ghost", { + sessionId: session.sessionId, + extensionId: "project:counter", + canvasId: "counter", + instanceId: "counter-1", + actionName: "ghost", + input: undefined, + }) + ).rejects.toMatchObject({ code: "canvas_action_no_handler" }); + }); + + it("rejects malformed direct canvas action payloads", async () => { + const client = new CopilotClient(); + + await expect((client as any).handleCanvasActionInvokeRequest(undefined)).rejects.toThrow( + "Invalid canvas provider request payload" + ); + await expect( + (client as any).handleCanvasActionInvokeRequest({ + sessionId: "session-1", + extensionId: "project:counter", + canvasId: "counter", + instanceId: "counter-1", + }) + ).rejects.toThrow("Invalid canvas provider request payload"); + }); + + it("rejects direct canvas provider payloads without extension ids", async () => { + const open = vi.fn(() => ({ url: "https://example.test/counter" })); + const canvas = createCanvas({ + id: "counter", + displayName: "Counter", + description: "A counter canvas", + open, + }); + const session = new CopilotSession("session-1", {} as any); + session.registerCanvases([canvas]); + const client = new CopilotClient(); + (client as any).sessions.set(session.sessionId, session); + + await expect( + (client as any).handleCanvasProviderRequest("canvas.open", { + sessionId: session.sessionId, + canvasId: "counter", + instanceId: "counter-1", + }) + ).rejects.toThrow("Invalid canvas provider request payload"); + expect(open).not.toHaveBeenCalled(); + }); + + it("throws for unknown direct canvas dispatches", async () => { const session = new CopilotSession("session-1", {} as any); - session.registerPermissionHandler(() => ({ kind: "no-result" })); const client = new CopilotClient(); (client as any).sessions.set(session.sessionId, session); await expect( - (client as any).handlePermissionRequestV2({ + (client as any).handleCanvasProviderRequest("canvas.open", { sessionId: session.sessionId, - permissionRequest: { kind: "write" }, + extensionId: "project:missing", + canvasId: "missing", + instanceId: "missing-1", }) - ).rejects.toThrow(/protocol v2 server/); + ).rejects.toThrow('No canvas registered with id "missing"'); }); it("forwards clientName in session.create request", async () => { @@ -984,7 +1167,7 @@ describe("CopilotClient", () => { await client.start(); onTestFinished(() => client.forceStop()); - expect(client.getState()).toBe("connected"); + expect((client as any).state).toBe("connected"); // Kill the child process to simulate unexpected termination const proc = (client as any).cliProcess as import("node:child_process").ChildProcess; @@ -992,7 +1175,7 @@ describe("CopilotClient", () => { // Wait for the connection.onClose handler to fire await vi.waitFor(() => { - expect(client.getState()).toBe("disconnected"); + expect((client as any).state).toBe("disconnected"); }); }); }); diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index b2021152c..33b7a0636 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -56,14 +56,12 @@ describe("Client", () => { onTestFinishedForceStop(client); await client.start(); - expect(client.getState()).toBe("connected"); const pong = await client.ping("test message"); expect(pong.message).toBe("pong: test message"); expect(Date.parse(pong.timestamp)).not.toBeNaN(); expect(await client.stop()).toHaveLength(0); // No errors on stop - expect(client.getState()).toBe("disconnected"); }); it("should start and connect to server using tcp", async () => { @@ -71,14 +69,12 @@ describe("Client", () => { onTestFinishedForceStop(client); await client.start(); - expect(client.getState()).toBe("connected"); const pong = await client.ping("test message"); expect(pong.message).toBe("pong: test message"); expect(Date.parse(pong.timestamp)).not.toBeNaN(); expect(await client.stop()).toHaveLength(0); // No errors on stop - expect(client.getState()).toBe("disconnected"); }); it.skipIf(process.platform === "darwin")( @@ -101,7 +97,6 @@ describe("Client", () => { await new Promise((resolve) => setTimeout(resolve, 100)); const errors = await client.stop(); - expect(client.getState()).toBe("disconnected"); if (errors.length > 0) { expect(errors[0].message).toContain("Failed to disconnect session"); } @@ -117,7 +112,6 @@ describe("Client", () => { await client.createSession({ onPermissionRequest: approveAll }); await client.forceStop(); - expect(client.getState()).toBe("disconnected"); }); it("should get status with version and protocol info", async () => { diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index 5174c9246..cd67cf672 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -154,10 +154,7 @@ describe("Client options", async () => { } }); - expect(client.getState()).toBe("disconnected"); - const session = await client.createSession({ onPermissionRequest: approveAll }); - expect(client.getState()).toBe("connected"); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); await session.disconnect(); @@ -183,7 +180,6 @@ describe("Client options", async () => { await client.start(); - expect(client.getState()).toBe("connected"); expect((client as unknown as { runtimePort: number }).runtimePort).toBe(port); const response = await client.ping("fixed-port"); diff --git a/nodejs/test/e2e/mcp_and_agents.e2e.test.ts b/nodejs/test/e2e/mcp_and_agents.e2e.test.ts index 93a8df7a4..a593ff988 100644 --- a/nodejs/test/e2e/mcp_and_agents.e2e.test.ts +++ b/nodejs/test/e2e/mcp_and_agents.e2e.test.ts @@ -6,27 +6,62 @@ import { dirname, resolve } from "path"; import { fileURLToPath } from "url"; import { describe, expect, it } from "vitest"; import { z } from "zod"; -import type { CustomAgentConfig, MCPStdioServerConfig, MCPServerConfig } from "../../src/index.js"; +import type { + CopilotSession, + CustomAgentConfig, + MCPStdioServerConfig, + MCPServerConfig, +} from "../../src/index.js"; import { approveAll, defineTool } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const TEST_MCP_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-server.mjs"); +const TEST_HARNESS_DIR = dirname(TEST_MCP_SERVER); + +function createTestMcpServers(...serverNames: string[]): Record { + return Object.fromEntries( + serverNames.map((name) => [ + name, + { + type: "local", + command: "node", + args: [TEST_MCP_SERVER], + workingDirectory: TEST_HARNESS_DIR, + tools: ["*"], + } as MCPStdioServerConfig, + ]) + ); +} + +async function waitForMcpServerStatus( + session: CopilotSession, + serverName: string, + expectedStatus = "connected" +): Promise { + const deadline = Date.now() + 60_000; + let lastStatus = ""; + + while (Date.now() < deadline) { + const result = await session.rpc.mcp.list(); + const server = result.servers.find((s) => s.name === serverName); + if (server?.status === expectedStatus) { + return; + } + lastStatus = server?.status ?? ""; + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + throw new Error(`${serverName} did not reach ${expectedStatus}; last status was ${lastStatus}`); +} describe("MCP Servers and Custom Agents", async () => { const { copilotClient: client, openAiEndpoint } = await createSdkTestContext(); describe("MCP Servers", () => { it("should accept MCP server configuration on session create", async () => { - const mcpServers: Record = { - "test-server": { - type: "local", - command: "echo", - args: ["hello"], - tools: ["*"], - } as MCPStdioServerConfig, - }; + const mcpServers = createTestMcpServers("test-server"); const session = await client.createSession({ onPermissionRequest: approveAll, @@ -34,6 +69,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session.sessionId).toBeDefined(); + await waitForMcpServerStatus(session, "test-server"); // Simple interaction to verify session works const message = await session.sendAndWait({ @@ -48,7 +84,7 @@ describe("MCP Servers and Custom Agents", async () => { const mcpServers: Record = { "test-server": { type: "local", - command: "echo", + command: "git", tools: ["*"], } as MCPStdioServerConfig, }; @@ -60,11 +96,6 @@ describe("MCP Servers and Custom Agents", async () => { expect(session.sessionId).toBeDefined(); - const message = await session.sendAndWait({ - prompt: "What is 2+2?", - }); - expect(message?.data.content).toContain("4"); - await session.disconnect(); }); @@ -75,14 +106,7 @@ describe("MCP Servers and Custom Agents", async () => { await session1.sendAndWait({ prompt: "What is 1+1?" }); // Resume with MCP servers - const mcpServers: Record = { - "test-server": { - type: "local", - command: "echo", - args: ["hello"], - tools: ["*"], - } as MCPStdioServerConfig, - }; + const mcpServers = createTestMcpServers("test-server"); const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, @@ -90,30 +114,13 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session2.sessionId).toBe(sessionId); - - const message = await session2.sendAndWait({ - prompt: "What is 3+3?", - }); - expect(message?.data.content).toContain("6"); + await waitForMcpServerStatus(session2, "test-server"); await session2.disconnect(); }); it("should handle multiple MCP servers", async () => { - const mcpServers: Record = { - server1: { - type: "local", - command: "echo", - args: ["server1"], - tools: ["*"], - } as MCPStdioServerConfig, - server2: { - type: "local", - command: "echo", - args: ["server2"], - tools: ["*"], - } as MCPStdioServerConfig, - }; + const mcpServers = createTestMcpServers("server1", "server2"); const session = await client.createSession({ onPermissionRequest: approveAll, @@ -121,6 +128,8 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session.sessionId).toBeDefined(); + await waitForMcpServerStatus(session, "server1"); + await waitForMcpServerStatus(session, "server2"); await session.disconnect(); }); @@ -132,6 +141,7 @@ describe("MCP Servers and Custom Agents", async () => { args: [TEST_MCP_SERVER], tools: ["*"], env: { TEST_SECRET: "hunter2" }, + workingDirectory: TEST_HARNESS_DIR, } as MCPStdioServerConfig, }; @@ -141,6 +151,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session.sessionId).toBeDefined(); + await waitForMcpServerStatus(session, "env-echo"); const message = await session.sendAndWait({ prompt: "Use the env-echo/get_env tool to read the TEST_SECRET environment variable. Reply with just the value, nothing else.", @@ -239,12 +250,7 @@ describe("MCP Servers and Custom Agents", async () => { description: "An agent with its own MCP servers", prompt: "You are an agent with MCP servers.", mcpServers: { - "agent-server": { - type: "local", - command: "echo", - args: ["agent-mcp"], - tools: ["*"], - } as MCPStdioServerConfig, + ...createTestMcpServers("agent-server"), }, }, ]; @@ -287,14 +293,7 @@ describe("MCP Servers and Custom Agents", async () => { describe("Combined Configuration", () => { it("should accept both MCP servers and custom agents", async () => { - const mcpServers: Record = { - "shared-server": { - type: "local", - command: "echo", - args: ["shared"], - tools: ["*"], - } as MCPStdioServerConfig, - }; + const mcpServers = createTestMcpServers("shared-server"); const customAgents: CustomAgentConfig[] = [ { @@ -312,6 +311,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session.sessionId).toBeDefined(); + await waitForMcpServerStatus(session, "shared-server"); await session.disconnect(); }); diff --git a/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts b/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts index 91e23200c..5d171c778 100644 --- a/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts +++ b/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts @@ -4,11 +4,19 @@ import * as fs from "fs"; import * as path from "path"; +import { fileURLToPath } from "url"; import { describe, expect, it } from "vitest"; import { approveAll, RuntimeConnection } from "../../src/index.js"; -import type { MCPServerConfig } from "../../src/index.js"; +import type { CopilotSession, MCPServerConfig, MCPStdioServerConfig } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +const __filename = fileURLToPath(import.meta.url); +const TEST_MCP_SERVER = path.resolve( + path.dirname(__filename), + "../../../test/harness/test-mcp-server.mjs" +); +const TEST_HARNESS_DIR = path.dirname(TEST_MCP_SERVER); + describe("Session MCP and skills RPC", async () => { // --yolo auto-approves extension permission gates at the CLI level, // preventing breakage from new gates (e.g., extension-permission-access). @@ -34,6 +42,44 @@ describe("Session MCP and skills RPC", async () => { return skillsDir; } + function createTestMcpServers(...serverNames: string[]): Record { + return Object.fromEntries( + serverNames.map((name) => [ + name, + { + type: "stdio", + command: "node", + args: [TEST_MCP_SERVER], + workingDirectory: TEST_HARNESS_DIR, + tools: ["*"], + } as MCPStdioServerConfig, + ]) + ); + } + + async function waitForMcpServerStatus( + session: CopilotSession, + serverName: string, + expectedStatus = "connected" + ): Promise { + const deadline = Date.now() + 60_000; + let lastStatus = ""; + + while (Date.now() < deadline) { + const result = await session.rpc.mcp.list(); + const server = result.servers.find((s) => s.name === serverName); + if (server?.status === expectedStatus) { + return; + } + lastStatus = server?.status ?? ""; + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + throw new Error( + `${serverName} did not reach ${expectedStatus}; last status was ${lastStatus}` + ); + } + async function expectFailure( action: () => Promise, expectedMessage: string @@ -106,20 +152,14 @@ describe("Session MCP and skills RPC", async () => { it("should list mcp servers with configured server", async () => { const serverName = "rpc-list-mcp-server"; - const mcpServers: Record = { - [serverName]: { - type: "stdio", - command: "echo", - args: ["rpc-list-mcp-server"], - tools: ["*"], - }, - }; + const mcpServers = createTestMcpServers(serverName); const session = await client.createSession({ onPermissionRequest: approveAll, mcpServers, }); + await waitForMcpServerStatus(session, serverName); const result = await session.rpc.mcp.list(); const server = result.servers.find((s) => s.name === serverName); expect(server).toBeDefined(); diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index fe6d4b9b2..bb935f829 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, onTestFinished, vi } from "vitest"; import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js"; import { CopilotClient, approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; -import { getFinalAssistantMessage, getNextEventOfType } from "./harness/sdkTestHelper.js"; +import { getFinalAssistantMessage, getNextEventOfType, retry } from "./harness/sdkTestHelper.js"; describe("Sessions", async () => { const { @@ -14,6 +14,18 @@ describe("Sessions", async () => { env, } = await createSdkTestContext(); + async function waitForExchanges(minimumCount = 1) { + await retry( + `capture ${minimumCount} chat completion request(s)`, + async () => { + const exchanges = await openAiEndpoint.getExchanges(); + expect(exchanges.length).toBeGreaterThanOrEqual(minimumCount); + }, + 1_200 + ); + return openAiEndpoint.getExchanges(); + } + it.each([ ["stdio", () => RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH })], ["tcp", () => RuntimeConnection.forTcp({ path: process.env.COPILOT_CLI_PATH })], @@ -233,14 +245,18 @@ describe("Sessions", async () => { availableTools: ["view", "edit"], }); - await session.sendAndWait({ prompt: "What is 1+1?" }); + try { + await session.send({ prompt: "What is 1+1?" }); - // It only tells the model about the specified tools and no others - const traffic = await openAiEndpoint.getExchanges(); - expect(traffic[0].request.tools).toMatchObject([ - { function: { name: "view" } }, - { function: { name: "edit" } }, - ]); + // It only tells the model about the specified tools and no others + const traffic = await waitForExchanges(); + expect(traffic[0].request.tools).toMatchObject([ + { function: { name: "view" } }, + { function: { name: "edit" } }, + ]); + } finally { + await session.disconnect(); + } }); it("should create a session with excludedTools", async () => { @@ -249,16 +265,20 @@ describe("Sessions", async () => { excludedTools: ["view"], }); - await session.sendAndWait({ prompt: "What is 1+1?" }); + try { + await session.send({ prompt: "What is 1+1?" }); - // It has other tools, but not the one we excluded - const traffic = await openAiEndpoint.getExchanges(); - const functionNames = traffic[0].request.tools?.map( - (t) => (t as { function: { name: string } }).function.name - ); - expect(functionNames).toContain("edit"); - expect(functionNames).toContain("grep"); - expect(functionNames).not.toContain("view"); + // It has other tools, but not the one we excluded + const traffic = await waitForExchanges(); + const functionNames = traffic[0].request.tools?.map( + (t) => (t as { function: { name: string } }).function.name + ); + expect(functionNames).toContain("edit"); + expect(functionNames).toContain("grep"); + expect(functionNames).not.toContain("view"); + } finally { + await session.disconnect(); + } }); it("should create a session with defaultAgent excludedTools", async () => { @@ -280,18 +300,19 @@ describe("Sessions", async () => { }, }); - await session.sendAndWait({ prompt: "What is 1+1?" }); - - // The secret_tool should be registered with the runtime but not advertised - // to the default agent's underlying model call. - const traffic = await openAiEndpoint.getExchanges(); - expect(traffic.length).toBeGreaterThan(0); - const functionNames = traffic[0].request.tools?.map( - (t) => (t as { function: { name: string } }).function.name - ); - expect(functionNames).not.toContain("secret_tool"); + try { + await session.send({ prompt: "What is 1+1?" }); - await session.disconnect(); + // The secret_tool should be registered with the runtime but not advertised + // to the default agent's underlying model call. + const traffic = await waitForExchanges(); + const functionNames = traffic[0].request.tools?.map( + (t) => (t as { function: { name: string } }).function.name + ); + expect(functionNames).not.toContain("secret_tool"); + } finally { + await session.disconnect(); + } }); // TODO: This test shows there's a race condition inside client.ts. If createSession is called diff --git a/nodejs/test/e2e/session_config.e2e.test.ts b/nodejs/test/e2e/session_config.e2e.test.ts index b86c3fa51..acb31f058 100644 --- a/nodejs/test/e2e/session_config.e2e.test.ts +++ b/nodejs/test/e2e/session_config.e2e.test.ts @@ -3,10 +3,23 @@ import { writeFile, mkdir } from "fs/promises"; import { join } from "path"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { retry } from "./harness/sdkTestHelper.js"; describe("Session Configuration", async () => { const { copilotClient: client, workDir, openAiEndpoint } = await createSdkTestContext(); + async function waitForExchanges(minimumCount = 1) { + await retry( + `capture ${minimumCount} chat completion request(s)`, + async () => { + const exchanges = await openAiEndpoint.getExchanges(); + expect(exchanges.length).toBeGreaterThanOrEqual(minimumCount); + }, + 1_200 + ); + return openAiEndpoint.getExchanges(); + } + it("should use workingDirectory for tool execution", async () => { const subDir = join(workDir, "subproject"); await mkdir(subDir, { recursive: true }); @@ -428,13 +441,14 @@ describe("Session Configuration", async () => { availableTools: ["view"], }); - await session2.sendAndWait({ prompt: "What is 1+1?" }); - - const exchanges = await openAiEndpoint.getExchanges(); - expect(exchanges.length).toBeGreaterThan(0); - const toolNames = getToolNames(exchanges[exchanges.length - 1]); - expect(toolNames).toEqual(["view"]); + try { + await session2.send({ prompt: "What is 1+1?" }); - await session2.disconnect(); + const exchanges = await waitForExchanges(); + const toolNames = getToolNames(exchanges[exchanges.length - 1]); + expect(toolNames).toEqual(["view"]); + } finally { + await session2.disconnect(); + } }); }); diff --git a/nodejs/test/extension.test.ts b/nodejs/test/extension.test.ts index a522d23d5..1baa83a3a 100644 --- a/nodejs/test/extension.test.ts +++ b/nodejs/test/extension.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { CopilotClient } from "../src/client.js"; import { approveAll } from "../src/index.js"; -import { joinSession } from "../src/extension.js"; +import { createCanvas, joinSession } from "../src/extension.js"; import { defaultJoinSessionPermissionHandler } from "../src/types.js"; describe("joinSession", () => { @@ -46,4 +46,15 @@ describe("joinSession", () => { expect(config.onPermissionRequest).toBe(approveAll); expect(config.suppressResumeEvent).toBe(false); }); + + it("exports the canvas helper from the extension surface", () => { + const canvas = createCanvas({ + id: "counter", + displayName: "Counter", + description: "A counter canvas", + open: () => ({ url: "https://example.test/counter" }), + }); + + expect(canvas.declaration.id).toBe("counter"); + }); }); diff --git a/python/README.md b/python/README.md index 3cee037cc..3a504f966 100644 --- a/python/README.md +++ b/python/README.md @@ -103,7 +103,7 @@ asyncio.run(main()) - ✅ Full JSON-RPC protocol support - ✅ stdio and TCP transports - ✅ Real-time streaming events -- ✅ Session history with `get_messages()` +- ✅ Session history with `get_events()` - ✅ Type hints throughout - ✅ Async/await native - ✅ Async context manager support for automatic resource cleanup @@ -113,7 +113,7 @@ asyncio.run(main()) ### CopilotClient ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient from copilot.session import PermissionHandler async with CopilotClient() as client: @@ -133,40 +133,39 @@ async with CopilotClient() as client: > **Note:** For manual lifecycle management, see [Manual Resource Management](#manual-resource-management) above. ```python -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, RuntimeConnection # Connect to an existing CLI server -client = CopilotClient(ExternalServerConfig(url="localhost:3000")) +client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:3000")) ``` **CopilotClient Constructor:** ```python -CopilotClient( - config=None, # SubprocessConfig | ExternalServerConfig | None - *, - auto_start=True, # auto-start server on first use - on_list_models=None, # custom handler for list_models() -) +CopilotClient() # spawn the bundled runtime with defaults +CopilotClient(connection=..., log_level="debug", github_token=..., ...) ``` -**SubprocessConfig** — spawn a local CLI process: +All options are kw-only parameters: -- `cli_path` (str | None): Path to CLI executable (default: `COPILOT_CLI_PATH` env var, or bundled binary) -- `cli_args` (list[str]): Extra arguments for the CLI executable -- `cwd` (str | None): Working directory for CLI process (default: current dir) -- `use_stdio` (bool): Use stdio transport instead of TCP (default: True) -- `port` (int): Server port for TCP mode (default: 0 for random) -- `log_level` (str): Log level (default: "info") -- `env` (dict | None): Environment variables for the CLI process +- `connection` (RuntimeConnection | None): How to reach the runtime. Use + `RuntimeConnection.for_stdio(...)`, `RuntimeConnection.for_tcp(...)`, or + `RuntimeConnection.for_uri(...)`. Defaults to a stdio connection with the bundled binary. +- `working_directory` (str | None): Working directory for the CLI process (default: current dir). +- `log_level` (str): Log level (default: "info"). +- `env` (dict | None): Environment variables for the CLI process. - `github_token` (str | None): GitHub token for authentication. When provided, takes priority over other auth methods. -- `copilot_home` (str | None): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When `None`, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using `ExternalServerConfig`. +- `base_directory` (str | None): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When `None`, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using a `UriRuntimeConnection`. - `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). - `telemetry` (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `enable_remote_sessions` (bool): Enable remote/cloud session support (default: False). +- `on_list_models` (callable | None): Custom handler for `list_models()`. When provided, the handler is called instead of querying the runtime. -**ExternalServerConfig** — connect to an existing CLI server: +**RuntimeConnection variants:** -- `url` (str): Server URL (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). +- `RuntimeConnection.for_stdio(path=None, args=None)` — spawn a local CLI process and talk over stdio. +- `RuntimeConnection.for_tcp(port=0, connection_token=None, path=None, args=None)` — spawn a local CLI in TCP mode. +- `RuntimeConnection.for_uri(url, connection_token=None)` — connect to an existing CLI server (e.g. `"localhost:8080"`). **`CopilotClient.create_session()`:** @@ -195,12 +194,12 @@ await client.set_foreground_session_id("session-123") # Subscribe to all lifecycle events def on_lifecycle(event): - print(f"{event.type}: {event.sessionId}") + print(f"{event.type}: {event.session_id}") -unsubscribe = client.on(on_lifecycle) +unsubscribe = client.on_lifecycle(on_lifecycle) # Subscribe to specific event type -unsubscribe = client.on("session.foreground", lambda e: print(f"Foreground: {e.sessionId}")) +unsubscribe = client.on_lifecycle("session.foreground", lambda e: print(f"Foreground: {e.session_id}")) # Later, to stop receiving events: unsubscribe() @@ -531,13 +530,13 @@ async with await client.create_session( The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export and automatic W3C Trace Context propagation. ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient -client = CopilotClient(SubprocessConfig( +client = CopilotClient( telemetry={ "otlp_endpoint": "http://localhost:4318", }, -)) +) ``` **TelemetryConfig options:** @@ -575,31 +574,26 @@ session = await client.create_session( Provide your own function to inspect each request and apply custom logic (sync or async): ```python -from copilot.session import PermissionRequestResult -from copilot.generated.session_events import PermissionRequest +from copilot import PermissionRequest, PermissionRequestResult +from copilot.generated.rpc import ( + PermissionDecisionApproveOnce, + PermissionDecisionReject, +) +from copilot.generated.session_events import PermissionRequestShell + def on_permission_request( request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: - # request.kind — what type of operation is being requested: - # "shell" — executing a shell command - # "write" — writing or editing a file - # "read" — reading a file - # "mcp" — calling an MCP tool - # "custom-tool" — calling one of your registered tools - # "url" — fetching a URL - # "memory" — accessing or updating session/workspace memory - # "hook" — invoking a registered hook - # request.tool_call_id — the tool call that triggered this request - # request.tool_name — name of the tool (for custom-tool / mcp) - # request.file_name — file being written (for write) - # request.full_command_text — full shell command (for shell) - - if request.kind.value == "shell": - # Deny shell commands - return PermissionRequestResult(kind="reject") - - return PermissionRequestResult(kind="approve-once") + # ``PermissionRequest`` is a discriminated union — pattern-match on + # the variant class to access the per-kind fields. + match request: + case PermissionRequestShell(full_command_text=cmd): + # Deny shell commands + return PermissionDecisionReject(feedback=f"Shell denied: {cmd}") + case _: + return PermissionDecisionApproveOnce() + session = await client.create_session( on_permission_request=on_permission_request, @@ -615,19 +609,29 @@ async def on_permission_request( ) -> PermissionRequestResult: # Simulate an async approval check (e.g., prompting a user over a network) await asyncio.sleep(0) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() ``` ### Permission Result Kinds -The handler must return a `PermissionRequestResult` with one of the kinds declared by the `PermissionRequestResultKind` type. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. - -| `kind` value | Meaning | -| ---------------------- | ------------------------------------------------------------------------------------------- | -| `"approve-once"` | Allow this single request | -| `"reject"` | Deny the request | -| `"user-not-available"` | Deny the request because no user is available to confirm it (the default) | -| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | +The handler returns a ``PermissionRequestResult``, which is an alias for +``PermissionDecision | PermissionNoResult`` (the generated wire-level +union of every decision variant, plus a small sentinel for v1 servers). +Approval decisions are present-tense — they describe the decision to +apply, not the past-tense outcome reported back on `permission.completed` +session events. + +| Variant | Meaning | +| --------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `PermissionDecisionApproveOnce()` | Allow this single request | +| `PermissionDecisionReject(feedback="…")` | Deny the request (optional feedback string forwarded to the LLM) | +| `PermissionDecisionUserNotAvailable()` | Deny the request because no user is available to confirm it (the default) | +| `PermissionNoResult()` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | + +Several richer variants (``PermissionDecisionApproveForSession``, +``PermissionDecisionApproveForLocation``, ``PermissionDecisionApprovePermanently``, +…) are available for granting longer-lived approvals; see the generated +``copilot.generated.rpc`` module for the full list. ### Resuming Sessions diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index c7a37ea0b..874267c9f 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -4,17 +4,65 @@ JSON-RPC based SDK for programmatic control of GitHub Copilot CLI """ +from .canvas import ( + CanvasAction, + CanvasActionContext, + CanvasDeclaration, + CanvasError, + CanvasHandler, + CanvasHostCapabilities, + CanvasHostContext, + CanvasLifecycleContext, + CanvasOpenContext, + CanvasOpenResponse, + ExtensionInfo, + OpenCanvasInstance, +) from .client import ( + ChildProcessRuntimeConnection, CloudSessionOptions, CloudSessionRepository, CopilotClient, - ExternalServerConfig, + GetAuthStatusResponse, + GetStatusResponse, + LogLevel, + ModelBilling, + ModelCapabilities, ModelCapabilitiesOverride, + ModelInfo, + ModelLimits, ModelLimitsOverride, + ModelPolicy, + ModelSupports, ModelSupportsOverride, + ModelVisionLimits, ModelVisionLimitsOverride, + PingResponse, RemoteSessionMode, - SubprocessConfig, + RuntimeConnection, + SessionBackgroundEvent, + SessionContext, + SessionCreatedEvent, + SessionDeletedEvent, + SessionForegroundEvent, + SessionLifecycleEvent, + SessionLifecycleEventBase, + SessionLifecycleEventMetadata, + SessionLifecycleEventType, + SessionLifecycleHandler, + SessionListFilter, + SessionMetadata, + SessionUpdatedEvent, + StdioRuntimeConnection, + StopError, + TcpRuntimeConnection, + TelemetryConfig, + UriRuntimeConnection, +) +from .generated.session_events import ( + PermissionRequest, + SessionEvent, + SessionEventType, ) from .session import ( AutoModeSwitchHandler, @@ -28,16 +76,50 @@ ElicitationHandler, ElicitationParams, ElicitationResult, + ErrorOccurredHandler, + ErrorOccurredHookInput, + ErrorOccurredHookOutput, ExitPlanModeHandler, ExitPlanModeRequest, ExitPlanModeResult, + InfiniteSessionConfig, InputOptions, + MCPHTTPServerConfig, + MCPServerConfig, + MCPStdioServerConfig, + PermissionHandler, + PermissionNoResult, + PermissionRequestResult, + PostToolUseHandler, + PostToolUseHookInput, + PostToolUseHookOutput, + PreMcpToolCallHandler, + PreMcpToolCallHookInput, + PreMcpToolCallHookOutput, + PreToolUseHandler, + PreToolUseHookInput, + PreToolUseHookOutput, ProviderConfig, SessionCapabilities, + SessionEndHandler, + SessionEndHookInput, + SessionEndHookOutput, + SessionEventHandler, SessionFsCapabilities, SessionFsConfig, + SessionHooks, + SessionStartHandler, + SessionStartHookInput, + SessionStartHookOutput, SessionUiApi, SessionUiCapabilities, + SystemMessageConfig, + UserInputHandler, + UserInputRequest, + UserInputResponse, + UserPromptSubmittedHandler, + UserPromptSubmittedHookInput, + UserPromptSubmittedHookOutput, ) from .session_fs_provider import ( SessionFsFileInfo, @@ -51,6 +133,7 @@ ToolBinaryResult, ToolInvocation, ToolResult, + ToolResultType, convert_mcp_call_tool_result, define_tool, ) @@ -58,46 +141,125 @@ __version__ = "0.1.0" __all__ = [ - "CommandContext", "AutoModeSwitchHandler", "AutoModeSwitchRequest", "AutoModeSwitchResponse", - "CommandDefinition", + "CanvasAction", + "CanvasActionContext", + "CanvasDeclaration", + "CanvasError", + "CanvasHandler", + "CanvasHostCapabilities", + "CanvasHostContext", + "CanvasLifecycleContext", + "CanvasOpenContext", + "CanvasOpenResponse", + "ChildProcessRuntimeConnection", "CloudSessionOptions", "CloudSessionRepository", + "CommandContext", + "CommandDefinition", "CopilotClient", "CopilotSession", "CreateSessionFsHandler", + "ElicitationContext", "ElicitationHandler", "ElicitationParams", - "ElicitationContext", "ElicitationResult", + "ErrorOccurredHandler", + "ErrorOccurredHookInput", + "ErrorOccurredHookOutput", "ExitPlanModeHandler", "ExitPlanModeRequest", "ExitPlanModeResult", - "ExternalServerConfig", + "ExtensionInfo", + "GetAuthStatusResponse", + "GetStatusResponse", + "InfiniteSessionConfig", "InputOptions", + "LogLevel", + "MCPHTTPServerConfig", + "MCPServerConfig", + "MCPStdioServerConfig", + "ModelBilling", + "ModelCapabilities", "ModelCapabilitiesOverride", + "ModelInfo", + "ModelLimits", "ModelLimitsOverride", + "ModelPolicy", + "ModelSupports", "ModelSupportsOverride", + "ModelVisionLimits", "ModelVisionLimitsOverride", + "OpenCanvasInstance", + "PermissionHandler", + "PermissionNoResult", + "PermissionRequest", + "PermissionRequestResult", + "PingResponse", + "PostToolUseHandler", + "PostToolUseHookInput", + "PostToolUseHookOutput", + "PreMcpToolCallHandler", + "PreMcpToolCallHookInput", + "PreMcpToolCallHookOutput", + "PreToolUseHandler", + "PreToolUseHookInput", + "PreToolUseHookOutput", "ProviderConfig", "RemoteSessionMode", + "RuntimeConnection", + "SessionBackgroundEvent", "SessionCapabilities", + "SessionContext", + "SessionCreatedEvent", + "SessionDeletedEvent", + "SessionEndHandler", + "SessionEndHookInput", + "SessionEndHookOutput", + "SessionEvent", + "SessionEventHandler", + "SessionEventType", + "SessionForegroundEvent", "SessionFsCapabilities", "SessionFsConfig", "SessionFsFileInfo", "SessionFsProvider", "SessionFsSqliteProvider", "SessionFsSqliteQueryResult", - "create_session_fs_adapter", + "SessionHooks", + "SessionLifecycleEvent", + "SessionLifecycleEventBase", + "SessionLifecycleEventMetadata", + "SessionLifecycleEventType", + "SessionLifecycleHandler", + "SessionListFilter", + "SessionMetadata", + "SessionStartHandler", + "SessionStartHookInput", + "SessionStartHookOutput", "SessionUiApi", "SessionUiCapabilities", - "SubprocessConfig", + "SessionUpdatedEvent", + "StdioRuntimeConnection", + "StopError", + "SystemMessageConfig", + "TcpRuntimeConnection", + "TelemetryConfig", "Tool", "ToolBinaryResult", "ToolInvocation", "ToolResult", + "ToolResultType", + "UriRuntimeConnection", + "UserInputHandler", + "UserInputRequest", + "UserInputResponse", + "UserPromptSubmittedHandler", + "UserPromptSubmittedHookInput", + "UserPromptSubmittedHookOutput", "convert_mcp_call_tool_result", + "create_session_fs_adapter", "define_tool", ] diff --git a/python/copilot/_jsonrpc.py b/python/copilot/_jsonrpc.py index ecae75b6b..df84a1c5d 100644 --- a/python/copilot/_jsonrpc.py +++ b/python/copilot/_jsonrpc.py @@ -400,9 +400,12 @@ async def _dispatch_request(self, message: dict, handler: RequestHandler): outcome = handler(params) if inspect.isawaitable(outcome): outcome = await outcome - if outcome is not None and not isinstance(outcome, dict): + if outcome is not None and not isinstance( + outcome, dict | list | str | int | float | bool + ): raise ValueError( - f"Request handler must return a dict, got {type(outcome).__name__}" + "Request handler must return a JSON-serializable value, " + f"got {type(outcome).__name__}" ) await self._send_response(message["id"], outcome) except JsonRpcError as exc: @@ -419,7 +422,7 @@ async def _dispatch_request(self, message: dict, handler: RequestHandler): ) await self._send_error_response(message["id"], -32603, str(exc), None) - async def _send_response(self, request_id: str, result: dict | None): + async def _send_response(self, request_id: str, result: Any): response = { "jsonrpc": "2.0", "id": request_id, diff --git a/python/copilot/canvas.py b/python/copilot/canvas.py new file mode 100644 index 000000000..58c5b297d --- /dev/null +++ b/python/copilot/canvas.py @@ -0,0 +1,312 @@ +""" +Canvas declarations, provider callbacks, and host-side canvas RPC types. + +The Copilot CLI runtime sends inbound JSON-RPC requests (``canvas.open``, +``canvas.close``, ``canvas.action.invoke``) to any session that declares +canvases. The SDK forwards every such request to a single user-supplied +:class:`CanvasHandler`; multiplexing across multiple declared canvases is +the implementor's responsibility (e.g. by switching on +:attr:`CanvasOpenContext.canvas_id`). +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Any + +from .generated.rpc import CanvasAction, OpenCanvasInstance + +__all__ = [ + "CanvasAction", + "CanvasActionContext", + "CanvasDeclaration", + "CanvasError", + "CanvasHandler", + "CanvasHostCapabilities", + "CanvasHostContext", + "CanvasLifecycleContext", + "CanvasOpenContext", + "CanvasOpenResponse", + "ExtensionInfo", + "OpenCanvasInstance", +] + + +@dataclass +class ExtensionInfo: + """Stable extension identity for session participants that provide canvases. + + Serializes to ``{"source": ..., "name": ...}`` on the wire. + """ + + source: str + """Extension namespace/source, e.g. ``"github-app"``.""" + + name: str + """Stable provider name within the source namespace.""" + + def to_dict(self) -> dict[str, Any]: + return {"source": self.source, "name": self.name} + + +@dataclass +class CanvasDeclaration: + """Declarative metadata for a single canvas, sent on + ``session.create`` / ``session.resume``. + """ + + id: str + """Canvas identifier, unique within the declaring connection.""" + + display_name: str + """Human-readable name shown in host UI and canvas pickers.""" + + description: str + """Short, single-sentence description shown to the agent in canvas catalogs.""" + + input_schema: dict[str, Any] | None = None + """JSON Schema for the ``input`` payload accepted by ``canvas.open``.""" + + actions: list[CanvasAction] | None = None + """Agent-callable actions this canvas exposes.""" + + def to_dict(self) -> dict[str, Any]: + result: dict[str, Any] = { + "id": self.id, + "displayName": self.display_name, + "description": self.description, + } + if self.input_schema is not None: + result["inputSchema"] = self.input_schema + if self.actions is not None: + result["actions"] = [action.to_dict() for action in self.actions] + return result + + +@dataclass +class CanvasOpenResponse: + """Response returned from :meth:`CanvasHandler.on_open`.""" + + url: str | None = None + """URL the host should render. Optional for canvases with no visual surface.""" + + title: str | None = None + """Provider-supplied title shown in host chrome.""" + + status: str | None = None + """Provider-supplied status text shown in host chrome.""" + + def to_dict(self) -> dict[str, Any]: + result: dict[str, Any] = {} + if self.url is not None: + result["url"] = self.url + if self.title is not None: + result["title"] = self.title + if self.status is not None: + result["status"] = self.status + return result + + +@dataclass +class CanvasHostCapabilities: + """Host capability details passed to canvas provider callbacks.""" + + canvases: bool = False + """Whether the host supports canvas rendering.""" + + @staticmethod + def from_dict(obj: Any) -> CanvasHostCapabilities: + if not isinstance(obj, dict): + return CanvasHostCapabilities() + return CanvasHostCapabilities(canvases=bool(obj.get("canvases", False))) + + +@dataclass +class CanvasHostContext: + """Host capabilities passed to canvas provider callbacks.""" + + capabilities: CanvasHostCapabilities = field(default_factory=CanvasHostCapabilities) + """Host capability details.""" + + @staticmethod + def from_dict(obj: Any) -> CanvasHostContext: + if not isinstance(obj, dict): + return CanvasHostContext() + return CanvasHostContext( + capabilities=CanvasHostCapabilities.from_dict(obj.get("capabilities")) + ) + + +@dataclass +class CanvasOpenContext: + """Context handed to :meth:`CanvasHandler.on_open`.""" + + session_id: str + """Session that requested the canvas.""" + + extension_id: str + """Owning provider identifier.""" + + canvas_id: str + """Canvas id from the declaring :class:`CanvasDeclaration`.""" + + instance_id: str + """Stable instance id supplied by the runtime.""" + + input: Any + """Validated input payload.""" + + host: CanvasHostContext | None = None + """Host capabilities supplied by the runtime.""" + + +@dataclass +class CanvasActionContext: + """Context handed to :meth:`CanvasHandler.on_action`.""" + + session_id: str + """Session that invoked the action.""" + + extension_id: str + """Owning provider identifier.""" + + canvas_id: str + """Canvas id targeted by the action.""" + + instance_id: str + """Instance id targeted by the action.""" + + action_name: str + """Action name from :attr:`CanvasAction.name`.""" + + input: Any + """Validated input payload.""" + + host: CanvasHostContext | None = None + """Host capabilities supplied by the runtime.""" + + +@dataclass +class CanvasLifecycleContext: + """Context handed to a canvas's close lifecycle hook.""" + + session_id: str + """Session owning the canvas instance.""" + + extension_id: str + """Owning provider identifier.""" + + canvas_id: str + """Canvas id from the declaring :class:`CanvasDeclaration`.""" + + instance_id: str + """Instance id this lifecycle event applies to.""" + + host: CanvasHostContext | None = None + """Host capabilities supplied by the runtime.""" + + +class CanvasError(Exception): + """Structured error returned from canvas handlers. + + The serialized envelope is ``{"code": ..., "message": ...}``. The SDK + surfaces this through the JSON-RPC error's ``data`` field while sending + a standard ``-32603`` (internal error) wire code. + """ + + def __init__(self, code: str, message: str) -> None: + self.code = code + self.message = message + super().__init__(f"{code}: {message}") + + def to_envelope(self) -> dict[str, str]: + return {"code": self.code, "message": self.message} + + @classmethod + def no_handler(cls) -> CanvasError: + """Default error returned when a custom action has no handler.""" + return cls( + "canvas_action_no_handler", + "No handler implemented for this canvas action", + ) + + @classmethod + def handler_unset(cls) -> CanvasError: + """Error returned when a canvas RPC arrives but no handler is installed.""" + return cls( + "canvas_handler_unset", + "No CanvasHandler installed on this session; " + "install one via SessionConfig.canvas_handler before creating the session.", + ) + + +class CanvasHandler(ABC): + """Provider-side canvas lifecycle handler. + + A session installs a single :class:`CanvasHandler` via the + ``canvas_handler=`` argument to + :meth:`copilot.CopilotClient.create_session` / + :meth:`copilot.CopilotClient.resume_session`. The handler receives every + inbound ``canvas.open`` / ``canvas.close`` / ``canvas.action.invoke`` + JSON-RPC request the runtime issues for this session and decides — + typically by inspecting :attr:`CanvasOpenContext.canvas_id` — which + application-side canvas should handle the call. + + The SDK does not maintain a per-canvas registry; multiplexing across + declared canvases is the implementor's responsibility. + """ + + @abstractmethod + async def on_open(self, ctx: CanvasOpenContext) -> CanvasOpenResponse: + """Open a new canvas instance. + + May raise :class:`CanvasError` to surface a structured failure to + the host. + """ + + async def on_close(self, ctx: CanvasLifecycleContext) -> None: + """Canvas was closed by the user or agent. Default: no-op.""" + + async def on_action(self, ctx: CanvasActionContext) -> Any: + """Handle a non-lifecycle action declared by the canvas. + + Default raises :meth:`CanvasError.no_handler`. + """ + raise CanvasError.no_handler() + + +# ----- Internal helpers for inbound RPC dispatch (not part of the public API). ----- + + +def _open_context_from_params(params: dict[str, Any]) -> CanvasOpenContext: + return CanvasOpenContext( + session_id=params["sessionId"], + extension_id=params["extensionId"], + canvas_id=params["canvasId"], + instance_id=params["instanceId"], + input=params.get("input"), + host=CanvasHostContext.from_dict(params.get("host")) if params.get("host") else None, + ) + + +def _lifecycle_context_from_params(params: dict[str, Any]) -> CanvasLifecycleContext: + return CanvasLifecycleContext( + session_id=params["sessionId"], + extension_id=params["extensionId"], + canvas_id=params["canvasId"], + instance_id=params["instanceId"], + host=CanvasHostContext.from_dict(params.get("host")) if params.get("host") else None, + ) + + +def _action_context_from_params(params: dict[str, Any]) -> CanvasActionContext: + return CanvasActionContext( + session_id=params["sessionId"], + extension_id=params["extensionId"], + canvas_id=params["canvasId"], + instance_id=params["instanceId"], + action_name=params["actionName"], + input=params.get("input"), + host=CanvasHostContext.from_dict(params.get("host")) if params.get("host") else None, + ) diff --git a/python/copilot/client.py b/python/copilot/client.py index 7af3fb39f..5e795bdde 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -25,28 +25,37 @@ import threading import time import uuid -from collections.abc import Awaitable, Callable -from dataclasses import KW_ONLY, dataclass, field +from collections.abc import Awaitable, Callable, Sequence +from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path from types import TracebackType -from typing import Any, Literal, TypedDict, cast, overload +from typing import Any, ClassVar, Literal, TypedDict, cast, overload from ._diagnostics import log_timing from ._jsonrpc import JsonRpcClient, JsonRpcError, ProcessExitedError from ._sdk_protocol_version import get_sdk_protocol_version -from ._telemetry import get_trace_context, trace_context +from ._telemetry import get_trace_context +from .canvas import ( + CanvasDeclaration, + CanvasError, + CanvasHandler, + ExtensionInfo, + _action_context_from_params, + _lifecycle_context_from_params, + _open_context_from_params, +) from .generated.rpc import ( ClientSessionApiHandlers, - ConnectRequest, + OpenCanvasInstance, RemoteSessionMode, ServerRpc, + _ConnectRequest, _InternalServerRpc, from_datetime, register_client_session_api_handlers, ) from .generated.session_events import ( - PermissionRequest, SessionEvent, session_event_from_dict, ) @@ -71,7 +80,7 @@ _PermissionHandlerFn, ) from .session_fs_provider import SessionFsProvider, create_session_fs_adapter -from .tools import Tool, ToolInvocation, ToolResult +from .tools import Tool logger = logging.getLogger(__name__) @@ -79,7 +88,7 @@ # Connection Types # ============================================================================ -ConnectionState = Literal["disconnected", "connecting", "connected", "error"] +_ConnectionState = Literal["disconnected", "connecting", "connected", "error"] LogLevel = Literal["none", "error", "warning", "info", "debug", "all"] @@ -154,112 +163,150 @@ class TelemetryConfig(TypedDict, total=False): @dataclass -class SubprocessConfig: - """Config for spawning a local Copilot CLI subprocess. +class RuntimeConnection: + """Discriminated config describing how to reach the Copilot runtime. + + Construct via the static factories :meth:`for_stdio`, :meth:`for_tcp`, + or :meth:`for_uri`. Each factory returns the matching subclass; + pattern-match on the subclass (or :func:`isinstance`) to branch on the + transport. Example: - >>> config = SubprocessConfig(github_token="ghp_...") - >>> client = CopilotClient(config) - - >>> # Custom CLI path with TCP transport - >>> config = SubprocessConfig( - ... cli_path="/usr/local/bin/copilot", - ... use_stdio=False, - ... log_level="debug", - ... ) + >>> CopilotClient() # default: stdio with the bundled runtime + >>> CopilotClient(connection=RuntimeConnection.for_uri("localhost:3000")) """ - cli_path: str | None = None - """Path to the Copilot CLI executable. ``None`` uses the bundled binary.""" + @staticmethod + def for_stdio( + *, + path: str | None = None, + args: Sequence[str] = (), + ) -> StdioRuntimeConnection: + """Spawn a runtime child process and communicate over its stdin/stdout. - cli_args: list[str] = field(default_factory=list) - """Extra arguments passed to the CLI executable (inserted before SDK-managed args).""" + This is the default when no :attr:`CopilotClientOptions.connection` + is supplied. - _: KW_ONLY + Args: + path: Path to the runtime executable. When ``None``, uses the + bundled binary. + args: Extra command-line arguments passed to the runtime process. + """ + return StdioRuntimeConnection(path=path, args=tuple(args)) - working_directory: str | None = None - """Working directory for the CLI process. ``None`` uses the current directory.""" + @staticmethod + def for_tcp( + *, + port: int = 0, + connection_token: str | None = None, + path: str | None = None, + args: Sequence[str] = (), + ) -> TcpRuntimeConnection: + """Spawn a runtime child process listening on a TCP socket. - use_stdio: bool = True - """Use stdio transport (``True``, default) or TCP (``False``).""" + Args: + port: TCP port to listen on. ``0`` (the default) auto-allocates + a free port. If the chosen port is already in use, startup + fails. + connection_token: Optional shared secret the SDK sends to the + spawned runtime to authenticate the TCP connection. When + ``None``, a UUID is generated automatically so the loopback + listener is safe by default. + path: Path to the runtime executable. When ``None``, uses the + bundled binary. + args: Extra command-line arguments passed to the runtime process. + """ + return TcpRuntimeConnection( + path=path, + args=tuple(args), + port=port, + connection_token=connection_token, + ) - tcp_connection_token: str | None = None - """Connection token for the headless CLI server (TCP only). + @staticmethod + def for_uri(url: str, *, connection_token: str | None = None) -> UriRuntimeConnection: + """Connect to an already-running runtime at the given URL. - Only meaningful when ``use_stdio=False``. When the SDK spawns the CLI in TCP mode and - this is omitted, a UUID is generated automatically so the loopback listener is safe by - default. Combining this with ``use_stdio=True`` raises :class:`ValueError`. - """ + Args: + url: URL of the runtime to connect to. Accepts ``"port"``, + ``"host:port"``, or a full URL. + connection_token: Optional shared secret to authenticate the + connection. Required when the server was started with a + token; ignored by legacy servers without ``connect`` support. + """ + return UriRuntimeConnection(url=url, connection_token=connection_token) - port: int = 0 - """TCP port for the CLI server (only when ``use_stdio=False``). 0 means random.""" - log_level: LogLevel = "info" - """Log level for the CLI process.""" +@dataclass +class ChildProcessRuntimeConnection(RuntimeConnection): + """Base for :class:`RuntimeConnection` variants that spawn a runtime child process. - env: dict[str, str] | None = None - """Environment variables for the CLI process. ``None`` inherits the current env.""" + Construct via :meth:`RuntimeConnection.stdio` or :meth:`RuntimeConnection.tcp`. + """ - github_token: str | None = None - """GitHub token for authentication. Takes priority over other auth methods.""" + path: str | None = None + """Path to the runtime executable. ``None`` uses the bundled binary.""" - copilot_home: str | None = None - """Base directory for Copilot data (session state, config, etc.). + args: Sequence[str] = () + """Extra command-line arguments passed to the runtime process.""" - Sets the ``COPILOT_HOME`` environment variable on the spawned CLI process. - When ``None``, the CLI defaults to ``~/.copilot``. - This option is only used when the SDK spawns the CLI process. - """ - use_logged_in_user: bool | None = None - """Use the logged-in user for authentication. +@dataclass +class StdioRuntimeConnection(ChildProcessRuntimeConnection): + """Spawns a runtime child process and communicates over its stdin/stdout. - ``None`` (default) resolves to ``True`` unless ``github_token`` is set. + Construct via :meth:`RuntimeConnection.stdio`. """ - telemetry: TelemetryConfig | None = None - """OpenTelemetry configuration. Providing this enables telemetry — no separate flag needed.""" - - session_fs: SessionFsConfig | None = None - """Connection-level session filesystem provider configuration.""" - session_idle_timeout_seconds: int | None = None - """Server-wide session idle timeout in seconds. +@dataclass +class TcpRuntimeConnection(ChildProcessRuntimeConnection): + """Spawns a runtime child process listening on a TCP socket. - Sessions without activity for this duration are automatically cleaned up. - Set to ``None`` or ``0`` to disable (sessions live indefinitely). - This option is only used when the SDK spawns the CLI process. + Construct via :meth:`RuntimeConnection.tcp`. """ - remote: bool = False - """Enable remote session support (Mission Control integration). + port: int = 0 + """TCP port to listen on. ``0`` (the default) auto-allocates a free port.""" - When ``True``, sessions in a GitHub repository working directory are - accessible from GitHub web and mobile. - This option is only used when the SDK spawns the CLI process. - """ + connection_token: str | None = None + """Shared secret the SDK sends to the spawned runtime. ``None`` auto-generates one.""" @dataclass -class ExternalServerConfig: - """Config for connecting to an existing Copilot CLI server over TCP. +class UriRuntimeConnection(RuntimeConnection): + """Connects to an already-running runtime at the specified URL. - Example: - >>> config = ExternalServerConfig(url="localhost:3000") - >>> client = CopilotClient(config) + Construct via :meth:`RuntimeConnection.uri`. """ - url: str - """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``.""" + url: str = "" + """URL of the runtime to connect to. Accepts ``"port"``, ``"host:port"``, or a full URL.""" - _: KW_ONLY + connection_token: str | None = None + """Shared secret to authenticate the connection.""" - tcp_connection_token: str | None = None - """Connection token sent in the ``connect`` handshake. Required when the server was - started with a token; ignored by legacy servers without ``connect`` support.""" +@dataclass +class _CopilotClientOptions: + """Internal configuration carrier used by :class:`CopilotClient`. + + This is not part of the public API: ``CopilotClient`` accepts all of + these options as keyword arguments directly. + """ + + connection: RuntimeConnection | None = None + working_directory: str | None = None + log_level: LogLevel = "info" + env: dict[str, str] | None = None + github_token: str | None = None + base_directory: str | None = None + use_logged_in_user: bool | None = None + telemetry: TelemetryConfig | None = None session_fs: SessionFsConfig | None = None - """Connection-level session filesystem provider configuration.""" + session_idle_timeout_seconds: int | None = None + enable_remote_sessions: bool = False + on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None # ============================================================================ @@ -272,32 +319,32 @@ class PingResponse: """Response from ping""" message: str # Echo message with "pong: " prefix - timestamp: datetime # ISO 8601 timestamp when the ping was processed - protocolVersion: int # Protocol version for SDK compatibility + timestamp: datetime # Timestamp when the ping was processed + protocol_version: int # Protocol version for SDK compatibility @staticmethod def from_dict(obj: Any) -> PingResponse: assert isinstance(obj, dict) message = obj.get("message") timestamp = obj.get("timestamp") - protocolVersion = obj.get("protocolVersion") - if message is None or timestamp is None or protocolVersion is None: + protocol_version = obj.get("protocolVersion") + if message is None or timestamp is None or protocol_version is None: raise ValueError( f"Missing required fields in PingResponse: message={message}, " - f"timestamp={timestamp}, protocolVersion={protocolVersion}" + f"timestamp={timestamp}, protocolVersion={protocol_version}" ) timestamp_value = ( datetime.fromtimestamp(timestamp / 1000, tz=UTC) if isinstance(timestamp, (int, float)) else from_datetime(timestamp) ) - return PingResponse(str(message), timestamp_value, int(protocolVersion)) + return PingResponse(str(message), timestamp_value, int(protocol_version)) def to_dict(self) -> dict: result: dict = {} result["message"] = self.message result["timestamp"] = self.timestamp.isoformat() - result["protocolVersion"] = self.protocolVersion + result["protocolVersion"] = self.protocol_version return result @@ -329,24 +376,24 @@ class GetStatusResponse: """Response from status.get""" version: str # Package version (e.g., "1.0.0") - protocolVersion: int # Protocol version for SDK compatibility + protocol_version: int # Protocol version for SDK compatibility @staticmethod def from_dict(obj: Any) -> GetStatusResponse: assert isinstance(obj, dict) version = obj.get("version") - protocolVersion = obj.get("protocolVersion") - if version is None or protocolVersion is None: + protocol_version = obj.get("protocolVersion") + if version is None or protocol_version is None: raise ValueError( f"Missing required fields in GetStatusResponse: version={version}, " - f"protocolVersion={protocolVersion}" + f"protocolVersion={protocol_version}" ) - return GetStatusResponse(str(version), int(protocolVersion)) + return GetStatusResponse(str(version), int(protocol_version)) def to_dict(self) -> dict: result: dict = {} result["version"] = self.version - result["protocolVersion"] = self.protocolVersion + result["protocolVersion"] = self.protocol_version return result @@ -678,7 +725,7 @@ class SessionContext: """Working directory context for a session""" working_directory: str # Working directory where the session was created - gitRoot: str | None = None # Git repository root (if in a git repo) + git_root: str | None = None # Git repository root (if in a git repo) repository: str | None = None # GitHub repository in "owner/repo" format branch: str | None = None # Current git branch @@ -690,15 +737,15 @@ def from_dict(obj: Any) -> SessionContext: raise ValueError("Missing required field 'cwd' in SessionContext") return SessionContext( working_directory=str(cwd), - gitRoot=obj.get("gitRoot"), + git_root=obj.get("gitRoot"), repository=obj.get("repository"), branch=obj.get("branch"), ) def to_dict(self) -> dict: result: dict = {"cwd": self.working_directory} - if self.gitRoot is not None: - result["gitRoot"] = self.gitRoot + if self.git_root is not None: + result["gitRoot"] = self.git_root if self.repository is not None: result["repository"] = self.repository if self.branch is not None: @@ -711,7 +758,7 @@ class SessionListFilter: """Filter options for listing sessions""" working_directory: str | None = None # Filter by exact working directory match - gitRoot: str | None = None # Filter by git root + git_root: str | None = None # Filter by git root repository: str | None = None # Filter by repository (owner/repo format) branch: str | None = None # Filter by branch @@ -719,8 +766,8 @@ def to_dict(self) -> dict: result: dict = {} if self.working_directory is not None: result["cwd"] = self.working_directory - if self.gitRoot is not None: - result["gitRoot"] = self.gitRoot + if self.git_root is not None: + result["gitRoot"] = self.git_root if self.repository is not None: result["repository"] = self.repository if self.branch is not None: @@ -732,43 +779,43 @@ def to_dict(self) -> dict: class SessionMetadata: """Metadata about a session""" - sessionId: str # Session identifier - startTime: str # ISO 8601 timestamp when session was created - modifiedTime: str # ISO 8601 timestamp when session was last modified - isRemote: bool # Whether the session is remote + session_id: str # Session identifier + start_time: datetime # Timestamp when session was created + modified_time: datetime # Timestamp when session was last modified + is_remote: bool # Whether the session is remote summary: str | None = None # Optional summary of the session context: SessionContext | None = None # Working directory context @staticmethod def from_dict(obj: Any) -> SessionMetadata: assert isinstance(obj, dict) - sessionId = obj.get("sessionId") - startTime = obj.get("startTime") - modifiedTime = obj.get("modifiedTime") - isRemote = obj.get("isRemote") - if sessionId is None or startTime is None or modifiedTime is None or isRemote is None: + session_id = obj.get("sessionId") + start_time = obj.get("startTime") + modified_time = obj.get("modifiedTime") + is_remote = obj.get("isRemote") + if session_id is None or start_time is None or modified_time is None or is_remote is None: raise ValueError( - f"Missing required fields in SessionMetadata: sessionId={sessionId}, " - f"startTime={startTime}, modifiedTime={modifiedTime}, isRemote={isRemote}" + f"Missing required fields in SessionMetadata: sessionId={session_id}, " + f"startTime={start_time}, modifiedTime={modified_time}, isRemote={is_remote}" ) summary = obj.get("summary") context_dict = obj.get("context") context = SessionContext.from_dict(context_dict) if context_dict else None return SessionMetadata( - sessionId=str(sessionId), - startTime=str(startTime), - modifiedTime=str(modifiedTime), - isRemote=bool(isRemote), + session_id=str(session_id), + start_time=_parse_session_timestamp(start_time), + modified_time=_parse_session_timestamp(modified_time), + is_remote=bool(is_remote), summary=summary, context=context, ) def to_dict(self) -> dict: result: dict = {} - result["sessionId"] = self.sessionId - result["startTime"] = self.startTime - result["modifiedTime"] = self.modifiedTime - result["isRemote"] = self.isRemote + result["sessionId"] = self.session_id + result["startTime"] = self.start_time.isoformat() + result["modifiedTime"] = self.modified_time.isoformat() + result["isRemote"] = self.is_remote if self.summary is not None: result["summary"] = self.summary if self.context is not None: @@ -776,6 +823,18 @@ def to_dict(self) -> dict: return result +def _parse_session_timestamp(value: Any) -> datetime: + """Parse a wire-format timestamp into ``datetime``. + + Accepts either an ISO-8601 string (server-sent JSON) or an existing + ``datetime`` (round-tripped from a previous parse). Returns the value + as-is if it's already a ``datetime``. + """ + if isinstance(value, datetime): + return value + return from_datetime(value) + + # ============================================================================ # Session Lifecycle Types (for TUI+server mode) # ============================================================================ @@ -793,50 +852,103 @@ def to_dict(self) -> dict: class SessionLifecycleEventMetadata: """Metadata for session lifecycle events.""" - startTime: str - modifiedTime: str + start_time: datetime + modified_time: datetime summary: str | None = None @staticmethod def from_dict(data: dict) -> SessionLifecycleEventMetadata: return SessionLifecycleEventMetadata( - startTime=data.get("startTime", ""), - modifiedTime=data.get("modifiedTime", ""), + start_time=_parse_session_timestamp(data.get("startTime", "")), + modified_time=_parse_session_timestamp(data.get("modifiedTime", "")), summary=data.get("summary"), ) @dataclass -class SessionLifecycleEvent: - """Session lifecycle event notification.""" +class SessionLifecycleEventBase: + """Base for session lifecycle event variants. + + Construct concrete variants directly (e.g. :class:`SessionCreatedEvent`, + :class:`SessionDeletedEvent`); pattern-match on the variant class to + branch on the event kind. + """ - type: SessionLifecycleEventType - sessionId: str + session_id: str metadata: SessionLifecycleEventMetadata | None = None - @staticmethod - def from_dict(data: dict) -> SessionLifecycleEvent: - metadata = None - if "metadata" in data and data["metadata"]: - metadata = SessionLifecycleEventMetadata.from_dict(data["metadata"]) - return SessionLifecycleEvent( - type=data.get("type", "session.updated"), - sessionId=data.get("sessionId", ""), - metadata=metadata, - ) +@dataclass +class SessionCreatedEvent(SessionLifecycleEventBase): + """Emitted when a session is created.""" -SessionLifecycleHandler = Callable[[SessionLifecycleEvent], None] + type: ClassVar[Literal["session.created"]] = "session.created" -HandlerUnsubcribe = Callable[[], None] -NO_RESULT_PERMISSION_V2_ERROR = ( - "Permission handlers cannot return 'no-result' when connected to a protocol v2 server." +@dataclass +class SessionDeletedEvent(SessionLifecycleEventBase): + """Emitted when a session is deleted.""" + + type: ClassVar[Literal["session.deleted"]] = "session.deleted" + + +@dataclass +class SessionUpdatedEvent(SessionLifecycleEventBase): + """Emitted when a session is updated (summary/title/etc. changed).""" + + type: ClassVar[Literal["session.updated"]] = "session.updated" + + +@dataclass +class SessionForegroundEvent(SessionLifecycleEventBase): + """Emitted when a session moves to the foreground (TUI+server mode).""" + + type: ClassVar[Literal["session.foreground"]] = "session.foreground" + + +@dataclass +class SessionBackgroundEvent(SessionLifecycleEventBase): + """Emitted when a session moves to the background (TUI+server mode).""" + + type: ClassVar[Literal["session.background"]] = "session.background" + + +SessionLifecycleEvent = ( + SessionCreatedEvent + | SessionDeletedEvent + | SessionUpdatedEvent + | SessionForegroundEvent + | SessionBackgroundEvent ) + +def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent: + """Construct the correct :class:`SessionLifecycleEvent` variant from a wire dict.""" + metadata = None + if "metadata" in data and data["metadata"]: + metadata = SessionLifecycleEventMetadata.from_dict(data["metadata"]) + session_id = data.get("sessionId", "") + event_type = data.get("type") + if event_type == "session.created": + return SessionCreatedEvent(session_id=session_id, metadata=metadata) + if event_type == "session.deleted": + return SessionDeletedEvent(session_id=session_id, metadata=metadata) + if event_type == "session.foreground": + return SessionForegroundEvent(session_id=session_id, metadata=metadata) + if event_type == "session.background": + return SessionBackgroundEvent(session_id=session_id, metadata=metadata) + # Default to ``session.updated`` for unknown event types so consumers + # keep working across server upgrades. + return SessionUpdatedEvent(session_id=session_id, metadata=metadata) + + +SessionLifecycleHandler = Callable[[SessionLifecycleEvent], None] + +HandlerUnsubcribe = Callable[[], None] + # Minimum protocol version this SDK can communicate with. # Servers reporting a version below this are rejected. -MIN_PROTOCOL_VERSION = 2 +_MIN_PROTOCOL_VERSION = 3 def _get_bundled_cli_path() -> str | None: @@ -923,99 +1035,160 @@ class CopilotClient: >>> await client.stop() >>> # Or connect to an existing server - >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) + >>> client = CopilotClient( + ... connection=RuntimeConnection.for_uri("localhost:3000"), + ... ) """ def __init__( self, - config: SubprocessConfig | ExternalServerConfig | None = None, *, - auto_start: bool = True, + connection: RuntimeConnection | None = None, + working_directory: str | None = None, + log_level: LogLevel = "info", + env: dict[str, str] | None = None, + github_token: str | None = None, + base_directory: str | None = None, + use_logged_in_user: bool | None = None, + telemetry: TelemetryConfig | None = None, + session_fs: SessionFsConfig | None = None, + session_idle_timeout_seconds: int | None = None, + enable_remote_sessions: bool = False, on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None, ): """ Initialize a new CopilotClient. + All process-management options (``working_directory``, ``log_level``, + ``env``, ``github_token``, …) apply only when the SDK spawns the runtime + (stdio / tcp connections). They are ignored when connecting to an + existing runtime via :meth:`RuntimeConnection.for_uri`. + Args: - config: Connection configuration. Pass a :class:`SubprocessConfig` to - spawn a local CLI process, or an :class:`ExternalServerConfig` to - connect to an existing server. Defaults to ``SubprocessConfig()``. - auto_start: Automatically start the connection on first use - (default: ``True``). - on_list_models: Custom handler for :meth:`list_models`. When provided, - the handler is called instead of querying the CLI server. + connection: How to reach the runtime. Defaults to + :meth:`RuntimeConnection.for_stdio` with the bundled binary. + working_directory: Working directory for the runtime process. + ``None`` uses the current directory. + log_level: Log level for the runtime process. Defaults to ``"info"``. + env: Environment variables for the runtime process. ``None`` inherits + the current env. + github_token: GitHub token for authentication. Takes priority over + other auth methods. + base_directory: Base directory for Copilot data (session state, + config, etc.). Sets the ``COPILOT_HOME`` environment variable on + the spawned runtime. When ``None``, the runtime defaults to + ``~/.copilot``. + use_logged_in_user: Use the logged-in user for authentication. + ``None`` (default) resolves to ``True`` unless ``github_token`` + is set. + telemetry: OpenTelemetry configuration. Providing this enables + telemetry. + session_fs: Connection-level session filesystem provider + configuration. + session_idle_timeout_seconds: Server-wide session idle timeout in + seconds. Sessions without activity for this duration are + automatically cleaned up. Set to ``None`` or ``0`` to disable. + enable_remote_sessions: Enable remote session support (Mission + Control integration). When ``True``, sessions in a GitHub + repository working directory are accessible from GitHub web + and mobile. + on_list_models: Custom handler for :meth:`list_models`. When + provided, the handler is called instead of querying the runtime + server. Example: - >>> # Default — spawns CLI server using stdio + >>> # Default — spawns runtime using stdio with the bundled binary >>> client = CopilotClient() >>> - >>> # Connect to an existing server - >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) + >>> # Connect to an existing runtime + >>> client = CopilotClient( + ... connection=RuntimeConnection.for_uri("localhost:3000"), + ... ) >>> - >>> # Custom CLI path with specific log level + >>> # Custom runtime path with specific log level >>> client = CopilotClient( - ... SubprocessConfig( - ... cli_path="/usr/local/bin/copilot", - ... log_level="debug", - ... ) + ... connection=RuntimeConnection.for_stdio(path="/usr/local/bin/copilot"), + ... log_level="debug", ... ) """ - if config is None: - config = SubprocessConfig() + options = _CopilotClientOptions( + connection=connection, + working_directory=working_directory, + log_level=log_level, + env=env, + github_token=github_token, + base_directory=base_directory, + use_logged_in_user=use_logged_in_user, + telemetry=telemetry, + session_fs=session_fs, + session_idle_timeout_seconds=session_idle_timeout_seconds, + enable_remote_sessions=enable_remote_sessions, + on_list_models=on_list_models, + ) + connection = ( + options.connection if options.connection is not None else RuntimeConnection.for_stdio() + ) - self._config: SubprocessConfig | ExternalServerConfig = config - self._auto_start = auto_start - self._on_list_models = on_list_models + self._options: _CopilotClientOptions = options + self._connection: RuntimeConnection = connection + self._on_list_models = options.on_list_models - # Resolve connection-mode-specific state + # Resolve connection-mode-specific state. self._actual_host: str = "localhost" - self._is_external_server: bool = isinstance(config, ExternalServerConfig) - - if config.tcp_connection_token is not None and len(config.tcp_connection_token) == 0: - raise ValueError("tcp_connection_token must be a non-empty string") - - if isinstance(config, ExternalServerConfig): - self._actual_host, actual_port = self._parse_cli_url(config.url) - self._actual_port: int | None = actual_port - self._effective_connection_token: str | None = config.tcp_connection_token + self._is_external_server: bool = isinstance(connection, UriRuntimeConnection) + + if isinstance(connection, UriRuntimeConnection): + if connection.connection_token is not None and len(connection.connection_token) == 0: + raise ValueError("connection_token must be a non-empty string") + self._actual_host, actual_port = self._parse_cli_url(connection.url) + self._runtime_port: int | None = actual_port + self._effective_connection_token: str | None = connection.connection_token else: - self._actual_port = None - - if config.tcp_connection_token is not None and config.use_stdio: - raise ValueError("tcp_connection_token cannot be used with use_stdio=True") - if config.use_stdio: - self._effective_connection_token = None - elif config.tcp_connection_token is not None: - self._effective_connection_token = config.tcp_connection_token + assert isinstance(connection, ChildProcessRuntimeConnection) + self._runtime_port = None + + if isinstance(connection, TcpRuntimeConnection): + if ( + connection.connection_token is not None + and len(connection.connection_token) == 0 + ): + raise ValueError("connection_token must be a non-empty string") + self._effective_connection_token = ( + connection.connection_token + if connection.connection_token is not None + else str(uuid.uuid4()) + ) else: - self._effective_connection_token = str(uuid.uuid4()) + self._effective_connection_token = None - # Resolve CLI path: explicit > COPILOT_CLI_PATH env var > bundled binary - effective_env = config.env if config.env is not None else os.environ + # Resolve CLI path: explicit > COPILOT_CLI_PATH env var > bundled binary. + effective_env = options.env if options.env is not None else os.environ self._cli_path_source: str | None = "explicit" - if config.cli_path is None: + if connection.path is None: env_cli_path = effective_env.get("COPILOT_CLI_PATH") if env_cli_path: - config.cli_path = env_cli_path + connection.path = env_cli_path self._cli_path_source = "environment" else: bundled_path = _get_bundled_cli_path() if bundled_path: - config.cli_path = bundled_path + connection.path = bundled_path self._cli_path_source = "bundled" else: raise RuntimeError( "Copilot CLI not found. The bundled CLI binary is not available. " - "Ensure you installed a platform-specific wheel, or provide cli_path." + "Ensure you installed a platform-specific wheel, or set " + "RuntimeConnection.for_stdio(path=...) / " + "RuntimeConnection.for_tcp(path=...)." ) # Resolve use_logged_in_user default - if config.use_logged_in_user is None: - config.use_logged_in_user = not bool(config.github_token) + if options.use_logged_in_user is None: + options.use_logged_in_user = not bool(options.github_token) self._process: subprocess.Popen | None = None self._client: JsonRpcClient | None = None - self._state: ConnectionState = "disconnected" + self._state: _ConnectionState = "disconnected" self._sessions: dict[str, CopilotSession] = {} self._sessions_lock = threading.Lock() self._models_cache: list[ModelInfo] | None = None @@ -1027,9 +1200,9 @@ def __init__( self._lifecycle_handlers_lock = threading.Lock() self._rpc: ServerRpc | None = None self._negotiated_protocol_version: int | None = None - if config.session_fs is not None: - _validate_session_fs_config(config.session_fs) - self._session_fs_config = config.session_fs + if options.session_fs is not None: + _validate_session_fs_config(options.session_fs) + self._session_fs_config = options.session_fs @property def rpc(self) -> ServerRpc: @@ -1039,14 +1212,14 @@ def rpc(self) -> ServerRpc: return self._rpc @property - def actual_port(self) -> int | None: - """The actual TCP port the CLI server is listening on, if using TCP transport. + def runtime_port(self) -> int | None: + """TCP port the runtime is listening on, when using TCP transport. Useful for multi-client scenarios where a second client needs to connect - to the same server. Only available after :meth:`start` completes and + to the same runtime. Only available after :meth:`start` completes and only when not using stdio transport. """ - return self._actual_port + return self._runtime_port def _parse_cli_url(self, url: str) -> tuple[str, int]: """ @@ -1128,18 +1301,18 @@ async def start(self) -> None: """ Start the CLI server and establish a connection. - If connecting to an external server (via :class:`ExternalServerConfig`), + If connecting to an already-running runtime (via :meth:`RuntimeConnection.for_uri`), only establishes the connection. Otherwise, spawns the CLI server process and then connects. - This method is called automatically when creating a session if ``auto_start`` - is True (default). + This method is called automatically when creating a session, so most + callers do not need to call it explicitly. Raises: RuntimeError: If the server fails to start or the connection fails. Example: - >>> client = CopilotClient(auto_start=False) + >>> client = CopilotClient() >>> await client.start() >>> # Now ready to create sessions """ @@ -1285,7 +1458,7 @@ async def stop(self) -> None: self._state = "disconnected" if not self._is_external_server: - self._actual_port = None + self._runtime_port = None if errors: raise ExceptionGroup("errors during CopilotClient.stop()", errors) @@ -1340,7 +1513,7 @@ async def force_stop(self) -> None: self._state = "disconnected" if not self._is_external_server: - self._actual_port = None + self._runtime_port = None async def create_session( self, @@ -1375,19 +1548,24 @@ async def create_session( on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, - on_exit_plan_mode: ExitPlanModeHandler | None = None, - on_auto_mode_switch: AutoModeSwitchHandler | None = None, + on_exit_plan_mode_request: ExitPlanModeHandler | None = None, + on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, github_token: str | None = None, remote_session: RemoteSessionMode | None = None, cloud: CloudSessionOptions | None = None, + canvases: list[CanvasDeclaration] | None = None, + request_canvas_renderer: bool | None = None, + request_extensions: bool | None = None, + extension_info: ExtensionInfo | None = None, + canvas_handler: CanvasHandler | None = None, ) -> CopilotSession: """ Create a new conversation session with the Copilot CLI. Sessions maintain conversation state, handle events, and manage tool execution. - If the client is not connected and ``auto_start`` is enabled, this will - automatically start the connection. + If the client is not yet connected, this will automatically start the + connection. Args: on_permission_request: Optional handler for permission requests. When @@ -1452,7 +1630,6 @@ async def create_session( A :class:`CopilotSession` instance for the new session. Raises: - RuntimeError: If the client is not connected and auto_start is disabled. ValueError: If ``on_permission_request`` is provided but not callable. Example: @@ -1470,10 +1647,7 @@ async def create_session( if on_permission_request is not None and not callable(on_permission_request): raise ValueError("on_permission_request must be callable when provided.") if not self._client: - if self._auto_start: - await self.start() - else: - raise RuntimeError("Client not connected. Call start() first.") + await self.start() tool_defs = [] if tools: @@ -1509,8 +1683,8 @@ async def create_session( if excluded_tools is not None: payload["excludedTools"] = excluded_tools - # Always enable permission request callback - payload["requestPermission"] = True + # Enable permission request callback if handler provided + payload["requestPermission"] = bool(on_permission_request) # Enable user input request callback if handler provided if on_user_input_request: @@ -1518,8 +1692,8 @@ async def create_session( # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) - payload["requestExitPlanMode"] = bool(on_exit_plan_mode) - payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch) + payload["requestExitPlanMode"] = bool(on_exit_plan_mode_request) + payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch_request) # Serialize commands (name + description only) into payload if commands: @@ -1623,6 +1797,15 @@ async def create_session( ] payload["infiniteSessions"] = wire_config + if canvases: + payload["canvases"] = [c.to_dict() for c in canvases] + if request_canvas_renderer is not None: + payload["requestCanvasRenderer"] = request_canvas_renderer + if request_extensions is not None: + payload["requestExtensions"] = request_extensions + if extension_info is not None: + payload["extensionInfo"] = extension_info.to_dict() + if not self._client: raise RuntimeError("Client not connected") @@ -1662,10 +1845,12 @@ async def create_session( session._register_user_input_handler(on_user_input_request) if on_elicitation_request: session._register_elicitation_handler(on_elicitation_request) - if on_exit_plan_mode: - session._register_exit_plan_mode_handler(on_exit_plan_mode) - if on_auto_mode_switch: - session._register_auto_mode_switch_handler(on_auto_mode_switch) + if on_exit_plan_mode_request: + session._register_exit_plan_mode_handler(on_exit_plan_mode_request) + if on_auto_mode_switch_request: + session._register_auto_mode_switch_handler(on_auto_mode_switch_request) + if canvas_handler is not None: + session._register_canvas_handler(canvas_handler) if hooks: session._register_hooks(hooks) if transform_callbacks: @@ -1754,12 +1939,18 @@ async def resume_session( on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, - on_exit_plan_mode: ExitPlanModeHandler | None = None, - on_auto_mode_switch: AutoModeSwitchHandler | None = None, + on_exit_plan_mode_request: ExitPlanModeHandler | None = None, + on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, github_token: str | None = None, remote_session: RemoteSessionMode | None = None, continue_pending_work: bool | None = None, + canvases: list[CanvasDeclaration] | None = None, + request_canvas_renderer: bool | None = None, + request_extensions: bool | None = None, + extension_info: ExtensionInfo | None = None, + canvas_handler: CanvasHandler | None = None, + open_canvases: list[OpenCanvasInstance] | None = None, ) -> CopilotSession: """ Resume an existing conversation session by its ID. @@ -1851,10 +2042,7 @@ async def resume_session( if on_permission_request is not None and not callable(on_permission_request): raise ValueError("on_permission_request must be callable when provided.") if not self._client: - if self._auto_start: - await self.start() - else: - raise RuntimeError("Client not connected. Call start() first.") + await self.start() tool_defs = [] if tools: @@ -1904,16 +2092,16 @@ async def resume_session( else True ) - # Always enable permission request callback - payload["requestPermission"] = True + # Enable permission request callback if handler provided + payload["requestPermission"] = bool(on_permission_request) if on_user_input_request: payload["requestUserInput"] = True # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) - payload["requestExitPlanMode"] = bool(on_exit_plan_mode) - payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch) + payload["requestExitPlanMode"] = bool(on_exit_plan_mode_request) + payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch_request) # Serialize commands (name + description only) into payload if commands: @@ -1979,6 +2167,17 @@ async def resume_session( ] payload["infiniteSessions"] = wire_config + if canvases: + payload["canvases"] = [c.to_dict() for c in canvases] + if open_canvases: + payload["openCanvases"] = [inst.to_dict() for inst in open_canvases] + if request_canvas_renderer is not None: + payload["requestCanvasRenderer"] = request_canvas_renderer + if request_extensions is not None: + payload["requestExtensions"] = request_extensions + if extension_info is not None: + payload["extensionInfo"] = extension_info.to_dict() + if not self._client: raise RuntimeError("Client not connected") @@ -2015,10 +2214,12 @@ async def resume_session( session._register_user_input_handler(on_user_input_request) if on_elicitation_request: session._register_elicitation_handler(on_elicitation_request) - if on_exit_plan_mode: - session._register_exit_plan_mode_handler(on_exit_plan_mode) - if on_auto_mode_switch: - session._register_auto_mode_switch_handler(on_auto_mode_switch) + if on_exit_plan_mode_request: + session._register_exit_plan_mode_handler(on_exit_plan_mode_request) + if on_auto_mode_switch_request: + session._register_auto_mode_switch_handler(on_auto_mode_switch_request) + if canvas_handler is not None: + session._register_canvas_handler(canvas_handler) if hooks: session._register_hooks(hooks) if transform_callbacks: @@ -2051,6 +2252,11 @@ async def resume_session( session._workspace_path = response.get("workspacePath") capabilities = response.get("capabilities") session._set_capabilities(capabilities) + open_canvases_raw = response.get("openCanvases") + if isinstance(open_canvases_raw, list): + session._set_open_canvases( + [OpenCanvasInstance.from_dict(inst) for inst in open_canvases_raw] + ) except BaseException as exc: with self._sessions_lock: self._sessions.pop(session_id, None) @@ -2074,20 +2280,6 @@ async def resume_session( ) return session - def get_state(self) -> ConnectionState: - """ - Get the current connection state of the client. - - Returns: - The current connection state: "disconnected", "connecting", - "connected", or "error". - - Example: - >>> if client.get_state() == "connected": - ... session = await client.create_session() - """ - return self._state - async def ping(self, message: str | None = None) -> PingResponse: """ Send a ping request to the server to verify connectivity. @@ -2256,7 +2448,7 @@ async def get_session_metadata(self, session_id: str) -> SessionMetadata | None: Example: >>> metadata = await client.get_session_metadata("session-123") >>> if metadata: - ... print(f"Session started at: {metadata.startTime}") + ... print(f"Session started at: {metadata.start_time}") """ if not self._client: raise RuntimeError("Client not connected") @@ -2376,14 +2568,16 @@ async def set_foreground_session_id(self, session_id: str) -> None: raise RuntimeError(f"Failed to set foreground session: {error}") @overload - def on(self, handler: SessionLifecycleHandler, /) -> HandlerUnsubcribe: ... + def on_lifecycle(self, handler: SessionLifecycleHandler, /) -> HandlerUnsubcribe: + pass @overload - def on( + def on_lifecycle( self, event_type: SessionLifecycleEventType, /, handler: SessionLifecycleHandler - ) -> HandlerUnsubcribe: ... + ) -> HandlerUnsubcribe: + pass - def on( + def on_lifecycle( self, event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler, /, @@ -2396,8 +2590,8 @@ def on( or change foreground/background state (in TUI+server mode). Can be called in two ways: - - on(handler): Subscribe to all lifecycle events - - on(event_type, handler): Subscribe to a specific event type + - on_lifecycle(handler): Subscribe to all lifecycle events + - on_lifecycle(event_type, handler): Subscribe to a specific event type Args: event_type_or_handler: Either a specific event type to listen for, @@ -2409,10 +2603,12 @@ def on( Example: >>> # Subscribe to specific event type - >>> unsubscribe = client.on("session.foreground", lambda e: print(e.sessionId)) + >>> unsubscribe = client.on_lifecycle( + ... "session.foreground", lambda e: print(e.session_id) + ... ) >>> >>> # Subscribe to all events - >>> unsubscribe = client.on(lambda e: print(f"{e.type}: {e.sessionId}")) + >>> unsubscribe = client.on_lifecycle(lambda e: print(f"{e.type}: {e.session_id}")) >>> >>> # Later, to stop receiving events: >>> unsubscribe() @@ -2444,7 +2640,10 @@ def unsubscribe_typed() -> None: return unsubscribe_typed else: - raise ValueError("Invalid arguments: use on(handler) or on(event_type, handler)") + raise ValueError( + "Invalid arguments: use on_lifecycle(handler) " + "or on_lifecycle(event_type, handler)" + ) def _dispatch_lifecycle_event(self, event: SessionLifecycleEvent) -> None: """Dispatch a lifecycle event to all registered handlers.""" @@ -2479,8 +2678,8 @@ async def _verify_protocol_version(self) -> None: server_version: int | None try: - connect_result = await _InternalServerRpc(self._client).connect( - ConnectRequest(token=self._effective_connection_token) + connect_result = await _InternalServerRpc(self._client)._connect( + _ConnectRequest(token=self._effective_connection_token) ) server_version = connect_result.protocol_version except JsonRpcError as err: @@ -2489,22 +2688,22 @@ async def _verify_protocol_version(self) -> None: # is silently dropped — the legacy server can't enforce one. used_fallback_ping = True ping_result = await self.ping() - server_version = ping_result.protocolVersion + server_version = ping_result.protocol_version else: raise if server_version is None: raise RuntimeError( "SDK protocol version mismatch: " - f"SDK supports versions {MIN_PROTOCOL_VERSION}-{max_version}" + f"SDK supports versions {_MIN_PROTOCOL_VERSION}-{max_version}" ", but server does not report a protocol version. " "Please update your server to ensure compatibility." ) - if server_version < MIN_PROTOCOL_VERSION or server_version > max_version: + if server_version < _MIN_PROTOCOL_VERSION or server_version > max_version: raise RuntimeError( "SDK protocol version mismatch: " - f"SDK supports versions {MIN_PROTOCOL_VERSION}-{max_version}" + f"SDK supports versions {_MIN_PROTOCOL_VERSION}-{max_version}" f", but server reports version {server_version}. " "Please update your SDK or server to ensure compatibility." ) @@ -2546,8 +2745,8 @@ def _convert_provider_to_wire_format( wire_provider["modelId"] = provider["model_id"] if "wire_model" in provider: wire_provider["wireModel"] = provider["wire_model"] - if "max_input_tokens" in provider: - wire_provider["maxPromptTokens"] = provider["max_input_tokens"] + if "max_prompt_tokens" in provider: + wire_provider["maxPromptTokens"] = provider["max_prompt_tokens"] if "max_output_tokens" in provider: wire_provider["maxOutputTokens"] = provider["max_output_tokens"] if "azure" in provider: @@ -2606,19 +2805,21 @@ def _convert_default_agent_to_wire_format( return wire async def _start_cli_server(self) -> None: - """ - Start the CLI server process. + """Start the runtime process. - This spawns the CLI server as a subprocess using the configured transport + This spawns the runtime as a subprocess using the configured transport mode (stdio or TCP). Raises: RuntimeError: If the server fails to start or times out. """ - assert isinstance(self._config, SubprocessConfig) - cfg = self._config + assert isinstance(self._connection, ChildProcessRuntimeConnection) + conn = self._connection + opts = self._options + use_stdio = isinstance(conn, StdioRuntimeConnection) + tcp_port = conn.port if isinstance(conn, TcpRuntimeConnection) else 0 - cli_path = cfg.cli_path + cli_path = conn.path assert cli_path is not None # resolved in __init__ # Verify CLI exists @@ -2627,24 +2828,24 @@ async def _start_cli_server(self) -> None: if (cli_path := shutil.which(cli_path)) is None: raise RuntimeError(f"Copilot CLI not found at {original_path}") - # Start with user-provided cli_args, then add SDK-managed args - args = list(cfg.cli_args) + [ + # Start with user-provided args, then add SDK-managed args + args = list(conn.args) + [ "--headless", "--no-auto-update", "--log-level", - cfg.log_level, + opts.log_level, ] # Add auth-related flags - if cfg.github_token: + if opts.github_token: args.extend(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"]) - if not cfg.use_logged_in_user: + if not opts.use_logged_in_user: args.append("--no-auto-login") - if cfg.session_idle_timeout_seconds is not None and cfg.session_idle_timeout_seconds > 0: - args.extend(["--session-idle-timeout", str(cfg.session_idle_timeout_seconds)]) + if opts.session_idle_timeout_seconds is not None and opts.session_idle_timeout_seconds > 0: + args.extend(["--session-idle-timeout", str(opts.session_idle_timeout_seconds)]) - if cfg.remote: + if opts.enable_remote_sessions: args.append("--remote") # If cli_path is a .js file, run it with node @@ -2659,28 +2860,28 @@ async def _start_cli_server(self) -> None: "cli_path": cli_path, "executable": args[0], "cli_path_source": self._cli_path_source, - "use_stdio": cfg.use_stdio, - "port": None if cfg.use_stdio else cfg.port, + "use_stdio": use_stdio, + "port": None if use_stdio else tcp_port, }, ) # Get environment variables - if cfg.env is None: + if opts.env is None: env = dict(os.environ) else: - env = dict(cfg.env) + env = dict(opts.env) # Set auth token in environment if provided - if cfg.github_token: - env["COPILOT_SDK_AUTH_TOKEN"] = cfg.github_token + if opts.github_token: + env["COPILOT_SDK_AUTH_TOKEN"] = opts.github_token if self._effective_connection_token: env["COPILOT_CONNECTION_TOKEN"] = self._effective_connection_token - if cfg.copilot_home: - env["COPILOT_HOME"] = cfg.copilot_home + if opts.base_directory: + env["COPILOT_HOME"] = opts.base_directory # Set OpenTelemetry environment variables if telemetry config is provided - telemetry = cfg.telemetry + telemetry = opts.telemetry if telemetry is not None: env["COPILOT_OTEL_ENABLED"] = "true" if "otlp_endpoint" in telemetry: @@ -2699,11 +2900,11 @@ async def _start_cli_server(self) -> None: # On Windows, hide the console window to avoid distracting users in GUI apps creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 - cwd = cfg.working_directory or os.getcwd() + cwd = opts.working_directory or os.getcwd() # Choose transport mode spawn_start = time.perf_counter() - if cfg.use_stdio: + if use_stdio: args.append("--stdio") # Use regular Popen with pipes (buffering=0 for unbuffered) self._process = subprocess.Popen( @@ -2717,8 +2918,8 @@ async def _start_cli_server(self) -> None: creationflags=creationflags, ) else: - if cfg.port > 0: - args.extend(["--port", str(cfg.port)]) + if tcp_port > 0: + args.extend(["--port", str(tcp_port)]) self._process = subprocess.Popen( args, stdin=subprocess.DEVNULL, @@ -2736,7 +2937,7 @@ async def _start_cli_server(self) -> None: ) # For stdio mode, we're ready immediately - if cfg.use_stdio: + if use_stdio: return # For TCP mode, wait for port announcement @@ -2755,7 +2956,7 @@ async def read_port(): logger.debug("[CLI] %s", line_str.rstrip()) match = re.search(r"listening on port (\d+)", line_str, re.IGNORECASE) if match: - self._actual_port = int(match.group(1)) + self._runtime_port = int(match.group(1)) return try: @@ -2766,14 +2967,13 @@ async def read_port(): logging.DEBUG, "CopilotClient._start_cli_server TCP port wait complete", port_wait_start, - port=self._actual_port, + port=self._runtime_port, ) except TimeoutError: raise RuntimeError("Timeout waiting for CLI server to start") async def _connect_to_server(self) -> None: - """ - Connect to the CLI server via the configured transport. + """Connect to the runtime via the configured transport. Uses either stdio or TCP based on the client configuration. @@ -2781,8 +2981,7 @@ async def _connect_to_server(self) -> None: RuntimeError: If the connection fails. """ setup_start = time.perf_counter() - use_stdio = isinstance(self._config, SubprocessConfig) and self._config.use_stdio - if use_stdio: + if isinstance(self._connection, StdioRuntimeConnection): await self._connect_via_stdio() else: await self._connect_via_tcp() @@ -2824,16 +3023,10 @@ def handle_notification(method: str, params: dict): session._dispatch_event(event) elif method == "session.lifecycle": # Handle session lifecycle events - lifecycle_event = SessionLifecycleEvent.from_dict(params) + lifecycle_event = _session_lifecycle_event_from_dict(params) self._dispatch_lifecycle_event(lifecycle_event) self._client.set_notification_handler(handle_notification) - # Protocol v3 servers send tool calls / permission requests as broadcast events. - # Protocol v2 servers use the older tool.call / permission.request RPC model. - # We always register v2 adapters because handlers are set up before version - # negotiation; a v3 server will simply never send these requests. - self._client.set_request_handler("tool.call", self._handle_tool_call_request_v2) - self._client.set_request_handler("permission.request", self._handle_permission_request_v2) self._client.set_request_handler("userInput.request", self._handle_user_input_request) self._client.set_request_handler( "exitPlanMode.request", self._handle_exit_plan_mode_request @@ -2845,6 +3038,18 @@ def handle_notification(method: str, params: dict): self._client.set_request_handler( "systemMessage.transform", self._handle_system_message_transform ) + self._client.set_request_handler( + "canvas.open", + self._canvas_request_handler(self._handle_canvas_open), + ) + self._client.set_request_handler( + "canvas.close", + self._canvas_request_handler(self._handle_canvas_close), + ) + self._client.set_request_handler( + "canvas.action.invoke", + self._canvas_request_handler(self._handle_canvas_action_invoke), + ) register_client_session_api_handlers(self._client, self._get_client_session_handlers) # Start listening for messages @@ -2860,7 +3065,7 @@ async def _connect_via_tcp(self) -> None: Raises: RuntimeError: If the server port is not available or connection fails. """ - if not self._actual_port: + if not self._runtime_port: raise RuntimeError("Server port not available") # Create a TCP socket connection with timeout @@ -2876,9 +3081,9 @@ async def _connect_via_tcp(self) -> None: tcp_connect_start = time.perf_counter() logger.info( "CopilotClient._connect_via_tcp connecting to CLI server", - extra={"host": self._actual_host, "port": self._actual_port}, + extra={"host": self._actual_host, "port": self._runtime_port}, ) - sock.connect((self._actual_host, self._actual_port)) + sock.connect((self._actual_host, self._runtime_port)) sock.settimeout(None) # Remove timeout after connection log_timing( logger, @@ -2886,11 +3091,11 @@ async def _connect_via_tcp(self) -> None: "CopilotClient._connect_via_tcp TCP connect complete", tcp_connect_start, host=self._actual_host, - port=self._actual_port, + port=self._runtime_port, ) except OSError as e: raise RuntimeError( - f"Failed to connect to CLI server at {self._actual_host}:{self._actual_port}: {e}" + f"Failed to connect to CLI server at {self._actual_host}:{self._runtime_port}: {e}" ) # Create a file-like wrapper for the socket @@ -2949,15 +3154,10 @@ def handle_notification(method: str, params: dict): session._dispatch_event(event) elif method == "session.lifecycle": # Handle session lifecycle events - lifecycle_event = SessionLifecycleEvent.from_dict(params) + lifecycle_event = _session_lifecycle_event_from_dict(params) self._dispatch_lifecycle_event(lifecycle_event) self._client.set_notification_handler(handle_notification) - # Protocol v3 servers send tool calls / permission requests as broadcast events. - # Protocol v2 servers use the older tool.call / permission.request RPC model. - # We always register v2 adapters; a v3 server will simply never send these requests. - self._client.set_request_handler("tool.call", self._handle_tool_call_request_v2) - self._client.set_request_handler("permission.request", self._handle_permission_request_v2) self._client.set_request_handler("userInput.request", self._handle_user_input_request) self._client.set_request_handler( "exitPlanMode.request", self._handle_exit_plan_mode_request @@ -2969,6 +3169,18 @@ def handle_notification(method: str, params: dict): self._client.set_request_handler( "systemMessage.transform", self._handle_system_message_transform ) + self._client.set_request_handler( + "canvas.open", + self._canvas_request_handler(self._handle_canvas_open), + ) + self._client.set_request_handler( + "canvas.close", + self._canvas_request_handler(self._handle_canvas_close), + ) + self._client.set_request_handler( + "canvas.action.invoke", + self._canvas_request_handler(self._handle_canvas_action_invoke), + ) register_client_session_api_handlers(self._client, self._get_client_session_handlers) # Start listening for messages @@ -3099,116 +3311,112 @@ async def _handle_system_message_transform(self, params: dict) -> dict: return await session._handle_system_message_transform(sections) - # ======================================================================== - # Protocol v2 backward-compatibility adapters - # ======================================================================== - - async def _handle_tool_call_request_v2(self, params: dict) -> dict: - """Handle a v2-style tool.call RPC request from the server.""" - session_id = params.get("sessionId") - tool_call_id = params.get("toolCallId") - tool_name = params.get("toolName") - - if not session_id or not tool_call_id or not tool_name: - raise ValueError("invalid tool call payload") - + def _resolve_canvas_handler(self, session_id: str) -> CanvasHandler: + """Look up the canvas handler for ``session_id`` or raise CanvasError.""" with self._sessions_lock: session = self._sessions.get(session_id) - if not session: - raise ValueError(f"unknown session {session_id}") - - handler = session._get_tool_handler(tool_name) - if not handler: - return { - "result": { - "textResultForLlm": ( - f"Tool '{tool_name}' is not supported by this client instance." - ), - "resultType": "failure", - "error": f"tool '{tool_name}' not supported", - "toolTelemetry": {}, - } - } - - arguments = params.get("arguments") - invocation = ToolInvocation( - session_id=session_id, - tool_call_id=tool_call_id, - tool_name=tool_name, - arguments=arguments, - ) - - tp = params.get("traceparent") - ts = params.get("tracestate") + if session is None: + raise CanvasError( + "canvas_handler_unset", + f"No session registered for {session_id}; cannot dispatch canvas RPC.", + ) + handler = session._get_canvas_handler() + if handler is None: + raise CanvasError.handler_unset() + return handler + async def _handle_canvas_open(self, params: dict) -> dict: + """Handle an inbound ``canvas.open`` request from the CLI runtime.""" try: - with trace_context(tp, ts): - handler_start = time.perf_counter() - result = handler(invocation) - if inspect.isawaitable(result): - result = await result - log_timing( - logger, - logging.DEBUG, - "CopilotClient._handle_tool_call_request_v2 tool dispatch", - handler_start, - session_id=session_id, - tool_call_id=tool_call_id, - tool_name=tool_name, - ) - - tool_result: ToolResult = result # type: ignore[assignment] - return { - "result": { - "textResultForLlm": tool_result.text_result_for_llm, - "resultType": tool_result.result_type, - "error": tool_result.error, - "toolTelemetry": tool_result.tool_telemetry or {}, - } - } + session_id = params["sessionId"] + except KeyError as exc: + raise CanvasError( + "canvas_invalid_request", "canvas.open params missing sessionId" + ) from exc + handler = self._resolve_canvas_handler(session_id) + try: + ctx = _open_context_from_params(params) + except KeyError as exc: + raise CanvasError( + "canvas_invalid_request", f"canvas.open params missing field: {exc.args[0]}" + ) from exc + try: + response = await handler.on_open(ctx) + except CanvasError: + raise except Exception as exc: - return { - "result": { - "textResultForLlm": ( - "Invoking this tool produced an error." - " Detailed information is not available." - ), - "resultType": "failure", - "error": str(exc), - "toolTelemetry": {}, - } - } + raise CanvasError( + "canvas_open_handler_failed", + f"canvas.open handler raised: {exc}", + ) from exc + return response.to_dict() + + async def _handle_canvas_close(self, params: dict) -> None: + """Handle an inbound ``canvas.close`` request from the CLI runtime.""" + try: + session_id = params["sessionId"] + except KeyError as exc: + raise CanvasError( + "canvas_invalid_request", "canvas.close params missing sessionId" + ) from exc + handler = self._resolve_canvas_handler(session_id) + try: + ctx = _lifecycle_context_from_params(params) + except KeyError as exc: + raise CanvasError( + "canvas_invalid_request", f"canvas.close params missing field: {exc.args[0]}" + ) from exc + try: + await handler.on_close(ctx) + except CanvasError: + raise + except Exception as exc: + raise CanvasError( + "canvas_close_handler_failed", + f"canvas.close handler raised: {exc}", + ) from exc + return None - async def _handle_permission_request_v2(self, params: dict) -> dict: - """Handle a v2-style permission.request RPC request from the server.""" - session_id = params.get("sessionId") - permission_request = params.get("permissionRequest") + async def _handle_canvas_action_invoke(self, params: dict) -> Any: + """Handle an inbound ``canvas.action.invoke`` request from the CLI runtime.""" + try: + session_id = params["sessionId"] + except KeyError as exc: + raise CanvasError( + "canvas_invalid_request", + "canvas.action.invoke params missing sessionId", + ) from exc + handler = self._resolve_canvas_handler(session_id) + try: + ctx = _action_context_from_params(params) + except KeyError as exc: + raise CanvasError( + "canvas_invalid_request", + f"canvas.action.invoke params missing field: {exc.args[0]}", + ) from exc + try: + return await handler.on_action(ctx) + except CanvasError: + raise + except Exception as exc: + raise CanvasError( + "canvas_action_handler_failed", + f"canvas.action.invoke handler raised: {exc}", + ) from exc - if not session_id or not permission_request: - raise ValueError("invalid permission request payload") + @staticmethod + def _canvas_request_handler( + coro: Callable[[dict], Awaitable[Any]], + ) -> Callable[[dict], Awaitable[Any]]: + """Wrap a canvas RPC coroutine so ``CanvasError`` becomes a JSON-RPC error + with the structured envelope in the error's ``data`` field, matching the + Rust SDK wire shape. + """ - with self._sessions_lock: - session = self._sessions.get(session_id) - if not session: - raise ValueError(f"unknown session {session_id}") + async def wrapper(params: dict) -> Any: + try: + return await coro(params) + except CanvasError as err: + raise JsonRpcError(-32603, err.message, data=err.to_envelope()) from err - try: - perm_request = PermissionRequest.from_dict(permission_request) - result = await session._handle_permission_request(perm_request) - if result.kind == "no-result": - raise ValueError(NO_RESULT_PERMISSION_V2_ERROR) - return {"result": {"kind": result.kind}} - except ValueError as exc: - if str(exc) == NO_RESULT_PERMISSION_V2_ERROR: - raise - return { - "result": { - "kind": "user-not-available", - } - } - except Exception: # pylint: disable=broad-except - return { - "result": { - "kind": "user-not-available", - } - } + return wrapper diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index ce9f98f2a..7069d23c1 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -2,8 +2,9 @@ AUTO-GENERATED FILE - DO NOT EDIT Generated from: api.schema.json """ +from __future__ import annotations -from typing import TYPE_CHECKING +from typing import ClassVar, TYPE_CHECKING from .session_events import AbortReason, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval @@ -268,6 +269,151 @@ class AuthInfoType(Enum): TOKEN = "token" USER = "user" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasAction: + """Canvas action that the agent or host can invoke. To discover the input schema for a + particular action, call the list_canvas_capabilities tool. + """ + name: str + """Action name exposed by the canvas provider""" + + description: str | None = None + """Description of the action""" + + input_schema: Any = None + """JSON Schema for the action input""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasAction': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + description = from_union([from_str, from_none], obj.get("description")) + input_schema = obj.get("inputSchema") + return CanvasAction(name, description, input_schema) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.input_schema is not None: + result["inputSchema"] = self.input_schema + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasCloseRequest: + """Canvas close parameters.""" + + instance_id: str + """Open canvas instance identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasCloseRequest': + assert isinstance(obj, dict) + instance_id = from_str(obj.get("instanceId")) + return CanvasCloseRequest(instance_id) + + def to_dict(self) -> dict: + result: dict = {} + result["instanceId"] = from_str(self.instance_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class CanvasInstanceAvailability(Enum): + """Runtime-controlled routing state for an open canvas instance.""" + + READY = "ready" + STALE = "stale" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasInvokeActionRequest: + """Canvas action invocation parameters.""" + + action_name: str + """Action name to invoke""" + + instance_id: str + """Open canvas instance identifier""" + + input: Any = None + """Action input""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasInvokeActionRequest': + assert isinstance(obj, dict) + action_name = from_str(obj.get("actionName")) + instance_id = from_str(obj.get("instanceId")) + input = obj.get("input") + return CanvasInvokeActionRequest(action_name, instance_id, input) + + def to_dict(self) -> dict: + result: dict = {} + result["actionName"] = from_str(self.action_name) + result["instanceId"] = from_str(self.instance_id) + if self.input is not None: + result["input"] = self.input + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasInvokeActionResult: + """Canvas action invocation result.""" + + result: Any = None + """Provider-supplied action result""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasInvokeActionResult': + assert isinstance(obj, dict) + result = obj.get("result") + return CanvasInvokeActionResult(result) + + def to_dict(self) -> dict: + result: dict = {} + if self.result is not None: + result["result"] = self.result + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasOpenRequest: + """Canvas open parameters.""" + + canvas_id: str + """Provider-local canvas identifier""" + + instance_id: str + """Caller-supplied stable instance identifier""" + + extension_id: str | None = None + """Owning provider identifier. Optional when the canvasId is unique across providers; + required to disambiguate when multiple providers register the same canvasId. + """ + input: Any = None + """Canvas open input""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasOpenRequest': + assert isinstance(obj, dict) + canvas_id = from_str(obj.get("canvasId")) + instance_id = from_str(obj.get("instanceId")) + extension_id = from_union([from_str, from_none], obj.get("extensionId")) + input = obj.get("input") + return CanvasOpenRequest(canvas_id, instance_id, extension_id, input) + + def to_dict(self) -> dict: + result: dict = {} + result["canvasId"] = from_str(self.canvas_id) + result["instanceId"] = from_str(self.instance_id) + if self.extension_id is not None: + result["extensionId"] = from_union([from_str, from_none], self.extension_id) + if self.input is not None: + result["input"] = self.input + return result + # Experimental: this type is part of an experimental API and may change or be removed. class SlashCommandInputCompletion(Enum): """Optional completion hint for the input (e.g. 'directory' for filesystem path completion)""" @@ -384,6 +530,31 @@ def to_dict(self) -> dict: result["includeSkills"] = from_union([from_bool, from_none], self.include_skills) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CommandsRespondToQueuedCommandRequest: + """Queued-command request ID and the result indicating whether the host executed it (and + whether to stop processing further queued commands). + """ + request_id: str + """Request ID from the `command.queued` event the host is responding to.""" + + result: QueuedCommandResult + """Result of the queued command execution.""" + + @staticmethod + def from_dict(obj: Any) -> 'CommandsRespondToQueuedCommandRequest': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = _load_QueuedCommandResult(obj.get("result")) + return CommandsRespondToQueuedCommandRequest(request_id, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = (self.result).to_dict() + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CommandsRespondToQueuedCommandResult: @@ -426,17 +597,17 @@ def to_dict(self) -> dict: # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class ConnectRequest: +class _ConnectRequest: """Optional connection token presented by the SDK client during the handshake.""" token: str | None = None """Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN""" @staticmethod - def from_dict(obj: Any) -> 'ConnectRequest': + def from_dict(obj: Any) -> '_ConnectRequest': assert isinstance(obj, dict) token = from_union([from_str, from_none], obj.get("token")) - return ConnectRequest(token) + return _ConnectRequest(token) def to_dict(self) -> dict: result: dict = {} @@ -446,7 +617,7 @@ def to_dict(self) -> dict: # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class ConnectResult: +class _ConnectResult: """Handshake result reporting the server's protocol version and package version on success.""" ok: bool @@ -459,12 +630,12 @@ class ConnectResult: """Server package version""" @staticmethod - def from_dict(obj: Any) -> 'ConnectResult': + def from_dict(obj: Any) -> '_ConnectResult': assert isinstance(obj, dict) ok = from_bool(obj.get("ok")) protocol_version = from_int(obj.get("protocolVersion")) version = from_str(obj.get("version")) - return ConnectResult(ok, protocol_version, version) + return _ConnectResult(ok, protocol_version, version) def to_dict(self) -> dict: result: dict = {} @@ -743,7 +914,7 @@ def to_dict(self) -> dict: return result class DiscoveredMCPServerType(Enum): - """Server transport type: stdio, http, sse, or memory""" + """Server transport type: stdio, http, sse (deprecated), or memory""" HTTP = "http" MEMORY = "memory" @@ -972,9 +1143,11 @@ class ExternalToolTextResultForLlmBinaryResultsForLlmType(Enum): RESOURCE = "resource" # Experimental: this type is part of an experimental API and may change or be removed. -class ExternalToolTextResultForLlmContentResourceLinkIconTheme(Enum): - """Theme variant this icon is intended for""" +class Theme(Enum): + """Theme variant this icon is intended for + UI theme preference per SEP-1865 + """ DARK = "dark" LIGHT = "light" @@ -1392,6 +1565,220 @@ def to_dict(self) -> dict: result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsDiagnoseCapability: + """Capability negotiation snapshot""" + + advertised: bool + """Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers""" + + feature_flag_enabled: bool + """Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on""" + + session_has_mcp_apps: bool + """Whether the session has the `mcp-apps` capability""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsDiagnoseCapability': + assert isinstance(obj, dict) + advertised = from_bool(obj.get("advertised")) + feature_flag_enabled = from_bool(obj.get("featureFlagEnabled")) + session_has_mcp_apps = from_bool(obj.get("sessionHasMcpApps")) + return MCPAppsDiagnoseCapability(advertised, feature_flag_enabled, session_has_mcp_apps) + + def to_dict(self) -> dict: + result: dict = {} + result["advertised"] = from_bool(self.advertised) + result["featureFlagEnabled"] = from_bool(self.feature_flag_enabled) + result["sessionHasMcpApps"] = from_bool(self.session_has_mcp_apps) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsDiagnoseRequest: + """MCP server to diagnose MCP Apps wiring for.""" + + server_name: str + """MCP server to probe""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsDiagnoseRequest': + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + return MCPAppsDiagnoseRequest(server_name) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsDiagnoseServer: + """What the server returned for this session""" + + connected: bool + """Whether the named server is currently connected""" + + sample_tool_names: list[str] + """Up to 5 tool names with `_meta.ui` for quick inspection""" + + tool_count: float + """Total tools returned by the server's tools/list""" + + tools_with_ui_meta: float + """Tools whose `_meta.ui` is populated (resourceUri and/or visibility set)""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsDiagnoseServer': + assert isinstance(obj, dict) + connected = from_bool(obj.get("connected")) + sample_tool_names = from_list(from_str, obj.get("sampleToolNames")) + tool_count = from_float(obj.get("toolCount")) + tools_with_ui_meta = from_float(obj.get("toolsWithUiMeta")) + return MCPAppsDiagnoseServer(connected, sample_tool_names, tool_count, tools_with_ui_meta) + + def to_dict(self) -> dict: + result: dict = {} + result["connected"] = from_bool(self.connected) + result["sampleToolNames"] = from_list(from_str, self.sample_tool_names) + result["toolCount"] = to_float(self.tool_count) + result["toolsWithUiMeta"] = to_float(self.tools_with_ui_meta) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPAppsDisplayMode(Enum): + """Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. + + Current display mode (SEP-1865) + + Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. + """ + FULLSCREEN = "fullscreen" + INLINE = "inline" + PIP = "pip" + +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPAppsHostContextDetailsPlatform(Enum): + """Platform type for responsive design""" + + DESKTOP = "desktop" + MOBILE = "mobile" + WEB = "web" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsListToolsRequest: + """MCP server to list app-callable tools for.""" + + origin_server_name: str + """**Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the + app from this server only'), the call is rejected when this differs from `serverName`, + and rejected outright when missing. + """ + server_name: str + """MCP server hosting the app""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsListToolsRequest': + assert isinstance(obj, dict) + origin_server_name = from_str(obj.get("originServerName")) + server_name = from_str(obj.get("serverName")) + return MCPAppsListToolsRequest(origin_server_name, server_name) + + def to_dict(self) -> dict: + result: dict = {} + result["originServerName"] = from_str(self.origin_server_name) + result["serverName"] = from_str(self.server_name) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsListToolsResult: + """App-callable tools from the named MCP server.""" + + tools: list[dict[str, Any]] + """App-callable tools from the server""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsListToolsResult': + assert isinstance(obj, dict) + tools = from_list(lambda x: from_dict(lambda x: x, x), obj.get("tools")) + return MCPAppsListToolsResult(tools) + + def to_dict(self) -> dict: + result: dict = {} + result["tools"] = from_list(lambda x: from_dict(lambda x: x, x), self.tools) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsReadResourceRequest: + """MCP server and resource URI to fetch.""" + + server_name: str + """Name of the MCP server hosting the resource""" + + uri: str + """Resource URI (typically ui://...)""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsReadResourceRequest': + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + uri = from_str(obj.get("uri")) + return MCPAppsReadResourceRequest(server_name, uri) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + result["uri"] = from_str(self.uri) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsResourceContent: + """Schema for the `McpAppsResourceContent` type.""" + + uri: str + """The resource URI (typically ui://...)""" + + meta: dict[str, Any] | None = None + """Resource-level metadata (CSP, permissions, etc.)""" + + blob: str | None = None + """Base64-encoded binary content""" + + mime_type: str | None = None + """MIME type of the content""" + + text: str | None = None + """Text content (e.g. HTML)""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsResourceContent': + assert isinstance(obj, dict) + uri = from_str(obj.get("uri")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + blob = from_union([from_str, from_none], obj.get("blob")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + text = from_union([from_str, from_none], obj.get("text")) + return MCPAppsResourceContent(uri, meta, blob, mime_type, text) + + def to_dict(self) -> dict: + result: dict = {} + result["uri"] = from_str(self.uri) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.blob is not None: + result["blob"] = from_union([from_str, from_none], self.blob) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.text is not None: + result["text"] = from_union([from_str, from_none], self.text) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPCancelSamplingExecutionParams: @@ -1996,44 +2383,40 @@ def to_dict(self) -> dict: return result @dataclass -class ModelBillingTokenPrices: - """Token-level pricing information for this model""" +class ModelBillingTokenPricesLongContext: + """Long context tier pricing (available for models with extended context windows)""" - batch_size: int | None = None - """Number of tokens per standard billing batch""" + cache_price: float | None = None + """AI Credits cost per billing batch of cached tokens""" - cache_price: int | None = None - """Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 - AIU = $0.01 USD) - """ - input_price: int | None = None - """Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU - = $0.01 USD) - """ - output_price: int | None = None - """Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 - AIU = $0.01 USD) - """ + context_max: int | None = None + """Maximum context window tokens for the long context tier""" + + input_price: float | None = None + """AI Credits cost per billing batch of input tokens""" + + output_price: float | None = None + """AI Credits cost per billing batch of output tokens""" @staticmethod - def from_dict(obj: Any) -> 'ModelBillingTokenPrices': + def from_dict(obj: Any) -> 'ModelBillingTokenPricesLongContext': assert isinstance(obj, dict) - batch_size = from_union([from_int, from_none], obj.get("batchSize")) - cache_price = from_union([from_int, from_none], obj.get("cachePrice")) - input_price = from_union([from_int, from_none], obj.get("inputPrice")) - output_price = from_union([from_int, from_none], obj.get("outputPrice")) - return ModelBillingTokenPrices(batch_size, cache_price, input_price, output_price) + cache_price = from_union([from_float, from_none], obj.get("cachePrice")) + context_max = from_union([from_int, from_none], obj.get("contextMax")) + input_price = from_union([from_float, from_none], obj.get("inputPrice")) + output_price = from_union([from_float, from_none], obj.get("outputPrice")) + return ModelBillingTokenPricesLongContext(cache_price, context_max, input_price, output_price) def to_dict(self) -> dict: result: dict = {} - if self.batch_size is not None: - result["batchSize"] = from_union([from_int, from_none], self.batch_size) if self.cache_price is not None: - result["cachePrice"] = from_union([from_int, from_none], self.cache_price) + result["cachePrice"] = from_union([to_float, from_none], self.cache_price) + if self.context_max is not None: + result["contextMax"] = from_union([from_int, from_none], self.context_max) if self.input_price is not None: - result["inputPrice"] = from_union([from_int, from_none], self.input_price) + result["inputPrice"] = from_union([to_float, from_none], self.input_price) if self.output_price is not None: - result["outputPrice"] = from_union([from_int, from_none], self.output_price) + result["outputPrice"] = from_union([to_float, from_none], self.output_price) return result @dataclass @@ -2446,6 +2829,30 @@ class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUserKind(Enum) class PermissionDecisionRejectKind(Enum): REJECT = "reject" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionDecisionRequest: + """Pending permission request ID and the decision to apply (approve/reject and scope).""" + + request_id: str + """Request ID of the pending permission request""" + + result: PermissionDecision + """The client's response to the pending permission prompt""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionRequest': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = _load_PermissionDecision(obj.get("result")) + return PermissionDecisionRequest(request_id, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = (self.result).to_dict() + return result + class PermissionDecisionUserNotAvailableKind(Enum): USER_NOT_AVAILABLE = "user-not-available" @@ -3224,7 +3631,7 @@ def to_dict(self) -> dict: class QueuedCommandHandled: """Schema for the `QueuedCommandHandled` type.""" - handled: bool + handled: ClassVar[str] = "true" """The host actually executed the queued command.""" stop_processing_queue: bool | None = None @@ -3235,13 +3642,12 @@ class QueuedCommandHandled: @staticmethod def from_dict(obj: Any) -> 'QueuedCommandHandled': assert isinstance(obj, dict) - handled = from_bool(obj.get("handled")) stop_processing_queue = from_union([from_bool, from_none], obj.get("stopProcessingQueue")) - return QueuedCommandHandled(handled, stop_processing_queue) + return QueuedCommandHandled(stop_processing_queue) def to_dict(self) -> dict: result: dict = {} - result["handled"] = from_bool(self.handled) + result["handled"] = self.handled if self.stop_processing_queue is not None: result["stopProcessingQueue"] = from_union([from_bool, from_none], self.stop_processing_queue) return result @@ -3251,7 +3657,7 @@ def to_dict(self) -> dict: class QueuedCommandNotHandled: """Schema for the `QueuedCommandNotHandled` type.""" - handled: bool + handled: ClassVar[str] = "false" """The host did not execute the queued command. Unblocks the queue without claiming the command was processed (e.g. when the handler threw before completing). """ @@ -3259,12 +3665,11 @@ class QueuedCommandNotHandled: @staticmethod def from_dict(obj: Any) -> 'QueuedCommandNotHandled': assert isinstance(obj, dict) - handled = from_bool(obj.get("handled")) - return QueuedCommandNotHandled(handled) + return QueuedCommandNotHandled() def to_dict(self) -> dict: result: dict = {} - result["handled"] = from_bool(self.handled) + result["handled"] = self.handled return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -4239,8 +4644,33 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionSetCredentialsResult: - """Indicates whether the credential update succeeded.""" +class SessionSetCredentialsParams: + """New auth credentials to install on the session. Omit to leave credentials unchanged.""" + + credentials: AuthInfo | None = None + """The new auth credentials to install on the session. When omitted or `undefined`, the call + is a no-op and the session's existing credentials are preserved. The runtime stores the + value verbatim and uses it for outbound model/API requests; it does NOT re-validate or + re-fetch the associated Copilot user response. Several variants carry secret material; + treat this method's params as containing secrets at rest and in transit. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SessionSetCredentialsParams': + assert isinstance(obj, dict) + credentials = from_union([_load_AuthInfo, from_none], obj.get("credentials")) + return SessionSetCredentialsParams(credentials) + + def to_dict(self) -> dict: + result: dict = {} + if self.credentials is not None: + result["credentials"] = from_union([lambda x: (x).to_dict(), from_none], self.credentials) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionSetCredentialsResult: + """Indicates whether the credential update succeeded.""" success: bool """Whether the operation succeeded""" @@ -5196,6 +5626,25 @@ class TaskInfoType(Enum): AGENT = "agent" SHELL = "shell" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskList: + """Background tasks currently tracked by the session.""" + + tasks: list[TaskInfo] + """Currently tracked tasks""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskList': + assert isinstance(obj, dict) + tasks = from_list(_load_TaskInfo, obj.get("tasks")) + return TaskList(tasks) + + def to_dict(self) -> dict: + result: dict = {} + result["tasks"] = from_list(lambda x: (x).to_dict(), self.tasks) + return result + class TaskShellInfoType(Enum): SHELL = "shell" @@ -5237,6 +5686,29 @@ def to_dict(self) -> dict: result["cancelled"] = from_bool(self.cancelled) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksGetCurrentPromotableResult: + """The first sync-waiting task that can currently be promoted to background mode.""" + + task: TaskInfo | None = None + """The first sync-waiting task (agent first, then shell) that can currently be promoted to + background mode. Omitted if no such task exists. The returned task is guaranteed to have + executionMode='sync' and canPromoteToBackground=true at the time of the call. + """ + + @staticmethod + def from_dict(obj: Any) -> 'TasksGetCurrentPromotableResult': + assert isinstance(obj, dict) + task = from_union([_load_TaskInfo, from_none], obj.get("task")) + return TasksGetCurrentPromotableResult(task) + + def to_dict(self) -> dict: + result: dict = {} + if self.task is not None: + result["task"] = from_union([lambda x: (x).to_dict(), from_none], self.task) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TasksGetProgressRequest: @@ -5256,6 +5728,30 @@ def to_dict(self) -> dict: result["id"] = from_str(self.id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksPromoteCurrentToBackgroundResult: + """The promoted task as it now exists in background mode, omitted if no promotable task was + waiting. + """ + task: TaskInfo | None = None + """The promoted task as it now exists in background mode, omitted if no promotable task was + waiting. Atomic operation: avoids the race window of getCurrentPromotable + + promoteToBackground. + """ + + @staticmethod + def from_dict(obj: Any) -> 'TasksPromoteCurrentToBackgroundResult': + assert isinstance(obj, dict) + task = from_union([_load_TaskInfo, from_none], obj.get("task")) + return TasksPromoteCurrentToBackgroundResult(task) + + def to_dict(self) -> dict: + result: dict = {} + if self.task is not None: + result["task"] = from_union([lambda x: (x).to_dict(), from_none], self.task) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TasksPromoteToBackgroundRequest: @@ -6261,6 +6757,127 @@ def to_dict(self) -> dict: result["statusMessage"] = from_union([from_str, from_none], self.status_message) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredCanvas: + """Canvas available in the current session.""" + + canvas_id: str + """Provider-local canvas identifier""" + + description: str + """Short, single-sentence description shown to the agent in canvas catalogs.""" + + display_name: str + """Human-readable canvas name""" + + extension_id: str + """Owning provider identifier""" + + actions: list[CanvasAction] | None = None + """Actions the agent or host may invoke on an open instance""" + + extension_name: str | None = None + """Owning extension display name, when available""" + + input_schema: Any = None + """JSON Schema for canvas open input""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredCanvas': + assert isinstance(obj, dict) + canvas_id = from_str(obj.get("canvasId")) + description = from_str(obj.get("description")) + display_name = from_str(obj.get("displayName")) + extension_id = from_str(obj.get("extensionId")) + actions = from_union([lambda x: from_list(CanvasAction.from_dict, x), from_none], obj.get("actions")) + extension_name = from_union([from_str, from_none], obj.get("extensionName")) + input_schema = obj.get("inputSchema") + return DiscoveredCanvas(canvas_id, description, display_name, extension_id, actions, extension_name, input_schema) + + def to_dict(self) -> dict: + result: dict = {} + result["canvasId"] = from_str(self.canvas_id) + result["description"] = from_str(self.description) + result["displayName"] = from_str(self.display_name) + result["extensionId"] = from_str(self.extension_id) + if self.actions is not None: + result["actions"] = from_union([lambda x: from_list(lambda x: to_class(CanvasAction, x), x), from_none], self.actions) + if self.extension_name is not None: + result["extensionName"] = from_union([from_str, from_none], self.extension_name) + if self.input_schema is not None: + result["inputSchema"] = self.input_schema + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class OpenCanvasInstance: + """Open canvas instance snapshot.""" + + availability: CanvasInstanceAvailability + """Runtime-controlled routing state for an open canvas instance.""" + + canvas_id: str + """Provider-local canvas identifier""" + + extension_id: str + """Owning provider identifier""" + + instance_id: str + """Stable caller-supplied canvas instance identifier""" + + reopen: bool + """Whether this snapshot came from an idempotent reopen""" + + extension_name: str | None = None + """Owning extension display name, when available""" + + input: Any = None + """Input supplied when the instance was opened""" + + status: str | None = None + """Provider-supplied status text""" + + title: str | None = None + """Rendered title""" + + url: str | None = None + """URL for web-rendered canvases""" + + @staticmethod + def from_dict(obj: Any) -> 'OpenCanvasInstance': + assert isinstance(obj, dict) + availability = CanvasInstanceAvailability(obj.get("availability")) + canvas_id = from_str(obj.get("canvasId")) + extension_id = from_str(obj.get("extensionId")) + instance_id = from_str(obj.get("instanceId")) + reopen = from_bool(obj.get("reopen")) + extension_name = from_union([from_str, from_none], obj.get("extensionName")) + input = obj.get("input") + status = from_union([from_str, from_none], obj.get("status")) + title = from_union([from_str, from_none], obj.get("title")) + url = from_union([from_str, from_none], obj.get("url")) + return OpenCanvasInstance(availability, canvas_id, extension_id, instance_id, reopen, extension_name, input, status, title, url) + + def to_dict(self) -> dict: + result: dict = {} + result["availability"] = to_enum(CanvasInstanceAvailability, self.availability) + result["canvasId"] = from_str(self.canvas_id) + result["extensionId"] = from_str(self.extension_id) + result["instanceId"] = from_str(self.instance_id) + result["reopen"] = from_bool(self.reopen) + if self.extension_name is not None: + result["extensionName"] = from_union([from_str, from_none], self.extension_name) + if self.input is not None: + result["input"] = self.input + if self.status is not None: + result["status"] = from_union([from_str, from_none], self.status) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) + if self.url is not None: + result["url"] = from_union([from_str, from_none], self.url) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandInput: @@ -6312,7 +6929,7 @@ class SendAttachmentDirectory: path: str """Absolute directory path""" - type: SlashCommandInputCompletion + type: ClassVar[str] = "directory" """Attachment type discriminator""" @staticmethod @@ -6320,14 +6937,13 @@ def from_dict(obj: Any) -> 'SendAttachmentDirectory': assert isinstance(obj, dict) display_name = from_str(obj.get("displayName")) path = from_str(obj.get("path")) - type = SlashCommandInputCompletion(obj.get("type")) - return SendAttachmentDirectory(display_name, path, type) + return SendAttachmentDirectory(display_name, path) def to_dict(self) -> dict: result: dict = {} result["displayName"] = from_str(self.display_name) result["path"] = from_str(self.path) - result["type"] = to_enum(SlashCommandInputCompletion, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6548,7 +7164,7 @@ class DiscoveredMCPServer: """Configuration source: user, workspace, plugin, or builtin""" type: DiscoveredMCPServerType | None = None - """Server transport type: stdio, http, sse, or memory""" + """Server transport type: stdio, http, sse (deprecated), or memory""" @staticmethod def from_dict(obj: Any) -> 'DiscoveredMCPServer': @@ -6722,6 +7338,9 @@ class ExternalToolTextResultForLlmBinaryResultsForLlm: description: str | None = None """Human-readable description of the binary data""" + metadata: dict[str, Any] | None = None + """Optional metadata from the producing tool.""" + @staticmethod def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmBinaryResultsForLlm': assert isinstance(obj, dict) @@ -6729,7 +7348,8 @@ def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmBinaryResultsForLlm': mime_type = from_str(obj.get("mimeType")) type = ExternalToolTextResultForLlmBinaryResultsForLlmType(obj.get("type")) description = from_union([from_str, from_none], obj.get("description")) - return ExternalToolTextResultForLlmBinaryResultsForLlm(data, mime_type, type, description) + metadata = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("metadata")) + return ExternalToolTextResultForLlmBinaryResultsForLlm(data, mime_type, type, description, metadata) def to_dict(self) -> dict: result: dict = {} @@ -6738,6 +7358,8 @@ def to_dict(self) -> dict: result["type"] = to_enum(ExternalToolTextResultForLlmBinaryResultsForLlmType, self.type) if self.description is not None: result["description"] = from_union([from_str, from_none], self.description) + if self.metadata is not None: + result["metadata"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.metadata) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6754,7 +7376,7 @@ class ExternalToolTextResultForLlmContentResourceLinkIcon: sizes: list[str] | None = None """Available icon sizes (e.g., ['16x16', '32x32'])""" - theme: ExternalToolTextResultForLlmContentResourceLinkIconTheme | None = None + theme: Theme | None = None """Theme variant this icon is intended for""" @staticmethod @@ -6763,7 +7385,7 @@ def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentResourceLinkIcon' src = from_str(obj.get("src")) mime_type = from_union([from_str, from_none], obj.get("mimeType")) sizes = from_union([lambda x: from_list(from_str, x), from_none], obj.get("sizes")) - theme = from_union([ExternalToolTextResultForLlmContentResourceLinkIconTheme, from_none], obj.get("theme")) + theme = from_union([Theme, from_none], obj.get("theme")) return ExternalToolTextResultForLlmContentResourceLinkIcon(src, mime_type, sizes, theme) def to_dict(self) -> dict: @@ -6774,7 +7396,7 @@ def to_dict(self) -> dict: if self.sizes is not None: result["sizes"] = from_union([lambda x: from_list(from_str, x), from_none], self.sizes) if self.theme is not None: - result["theme"] = from_union([lambda x: to_enum(ExternalToolTextResultForLlmContentResourceLinkIconTheme, x), from_none], self.theme) + result["theme"] = from_union([lambda x: to_enum(Theme, x), from_none], self.theme) return result ExternalToolTextResultForLlmContentResourceDetails = EmbeddedTextResourceContents | EmbeddedBlobResourceContents @@ -6790,7 +7412,7 @@ class ExternalToolTextResultForLlmContentAudio: mime_type: str """MIME type of the audio (e.g., audio/wav, audio/mpeg)""" - type: ExternalToolTextResultForLlmContentAudioType + type: ClassVar[str] = "audio" """Content block type discriminator""" @staticmethod @@ -6798,14 +7420,13 @@ def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentAudio': assert isinstance(obj, dict) data = from_str(obj.get("data")) mime_type = from_str(obj.get("mimeType")) - type = ExternalToolTextResultForLlmContentAudioType(obj.get("type")) - return ExternalToolTextResultForLlmContentAudio(data, mime_type, type) + return ExternalToolTextResultForLlmContentAudio(data, mime_type) def to_dict(self) -> dict: result: dict = {} result["data"] = from_str(self.data) result["mimeType"] = from_str(self.mime_type) - result["type"] = to_enum(ExternalToolTextResultForLlmContentAudioType, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6819,7 +7440,7 @@ class ExternalToolTextResultForLlmContentImage: mime_type: str """MIME type of the image (e.g., image/png, image/jpeg)""" - type: ExternalToolTextResultForLlmContentImageType + type: ClassVar[str] = "image" """Content block type discriminator""" @staticmethod @@ -6827,14 +7448,13 @@ def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentImage': assert isinstance(obj, dict) data = from_str(obj.get("data")) mime_type = from_str(obj.get("mimeType")) - type = ExternalToolTextResultForLlmContentImageType(obj.get("type")) - return ExternalToolTextResultForLlmContentImage(data, mime_type, type) + return ExternalToolTextResultForLlmContentImage(data, mime_type) def to_dict(self) -> dict: result: dict = {} result["data"] = from_str(self.data) result["mimeType"] = from_str(self.mime_type) - result["type"] = to_enum(ExternalToolTextResultForLlmContentImageType, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6845,20 +7465,19 @@ class ExternalToolTextResultForLlmContentResource: resource: ExternalToolTextResultForLlmContentResourceDetails """The embedded resource contents, either text or base64-encoded binary""" - type: ExternalToolTextResultForLlmContentResourceType + type: ClassVar[str] = "resource" """Content block type discriminator""" @staticmethod def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentResource': assert isinstance(obj, dict) resource = (lambda x: from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], x))(obj.get("resource")) - type = ExternalToolTextResultForLlmContentResourceType(obj.get("type")) - return ExternalToolTextResultForLlmContentResource(resource, type) + return ExternalToolTextResultForLlmContentResource(resource) def to_dict(self) -> dict: result: dict = {} result["resource"] = from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], self.resource) - result["type"] = to_enum(ExternalToolTextResultForLlmContentResourceType, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6869,7 +7488,7 @@ class ExternalToolTextResultForLlmContentTerminal: text: str """Terminal/shell output text""" - type: ExternalToolTextResultForLlmContentTerminalType + type: ClassVar[str] = "terminal" """Content block type discriminator""" cwd: str | None = None @@ -6882,15 +7501,14 @@ class ExternalToolTextResultForLlmContentTerminal: def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentTerminal': assert isinstance(obj, dict) text = from_str(obj.get("text")) - type = ExternalToolTextResultForLlmContentTerminalType(obj.get("type")) cwd = from_union([from_str, from_none], obj.get("cwd")) exit_code = from_union([from_int, from_none], obj.get("exitCode")) - return ExternalToolTextResultForLlmContentTerminal(text, type, cwd, exit_code) + return ExternalToolTextResultForLlmContentTerminal(text, cwd, exit_code) def to_dict(self) -> dict: result: dict = {} result["text"] = from_str(self.text) - result["type"] = to_enum(ExternalToolTextResultForLlmContentTerminalType, self.type) + result["type"] = self.type if self.cwd is not None: result["cwd"] = from_union([from_str, from_none], self.cwd) if self.exit_code is not None: @@ -6905,20 +7523,19 @@ class ExternalToolTextResultForLlmContentText: text: str """The text content""" - type: KindEnum + type: ClassVar[str] = "text" """Content block type discriminator""" @staticmethod def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentText': assert isinstance(obj, dict) text = from_str(obj.get("text")) - type = KindEnum(obj.get("type")) - return ExternalToolTextResultForLlmContentText(text, type) + return ExternalToolTextResultForLlmContentText(text) def to_dict(self) -> dict: result: dict = {} result["text"] = from_str(self.text) - result["type"] = to_enum(KindEnum, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6926,7 +7543,7 @@ def to_dict(self) -> dict: class SlashCommandTextResult: """Schema for the `SlashCommandTextResult` type.""" - kind: KindEnum + kind: ClassVar[str] = "text" """Text result discriminator""" text: str @@ -6946,16 +7563,15 @@ class SlashCommandTextResult: @staticmethod def from_dict(obj: Any) -> 'SlashCommandTextResult': assert isinstance(obj, dict) - kind = KindEnum(obj.get("kind")) text = from_str(obj.get("text")) markdown = from_union([from_bool, from_none], obj.get("markdown")) preserve_ansi = from_union([from_bool, from_none], obj.get("preserveAnsi")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandTextResult(kind, text, markdown, preserve_ansi, runtime_settings_changed) + return SlashCommandTextResult(text, markdown, preserve_ansi, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(KindEnum, self.kind) + result["kind"] = self.kind result["text"] = from_str(self.text) if self.markdown is not None: result["markdown"] = from_union([from_bool, from_none], self.markdown) @@ -7292,6 +7908,161 @@ def to_dict(self) -> dict: result["url"] = from_union([from_str, from_none], self.url) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsDiagnoseResult: + """Diagnostic snapshot of MCP Apps wiring for the named server.""" + + capability: MCPAppsDiagnoseCapability + """Capability negotiation snapshot""" + + server: MCPAppsDiagnoseServer + """What the server returned for this session""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsDiagnoseResult': + assert isinstance(obj, dict) + capability = MCPAppsDiagnoseCapability.from_dict(obj.get("capability")) + server = MCPAppsDiagnoseServer.from_dict(obj.get("server")) + return MCPAppsDiagnoseResult(capability, server) + + def to_dict(self) -> dict: + result: dict = {} + result["capability"] = to_class(MCPAppsDiagnoseCapability, self.capability) + result["server"] = to_class(MCPAppsDiagnoseServer, self.server) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsHostContextDetails: + """Current host context""" + + available_display_modes: list[MCPAppsDisplayMode] | None = None + """Display modes the host supports""" + + display_mode: MCPAppsDisplayMode | None = None + """Current display mode (SEP-1865)""" + + locale: str | None = None + """BCP-47 locale, e.g. 'en-US'""" + + platform: MCPAppsHostContextDetailsPlatform | None = None + """Platform type for responsive design""" + + theme: Theme | None = None + """UI theme preference per SEP-1865""" + + time_zone: str | None = None + """IANA timezone, e.g. 'America/New_York'""" + + user_agent: str | None = None + """Host application identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsHostContextDetails': + assert isinstance(obj, dict) + available_display_modes = from_union([lambda x: from_list(MCPAppsDisplayMode, x), from_none], obj.get("availableDisplayModes")) + display_mode = from_union([MCPAppsDisplayMode, from_none], obj.get("displayMode")) + locale = from_union([from_str, from_none], obj.get("locale")) + platform = from_union([MCPAppsHostContextDetailsPlatform, from_none], obj.get("platform")) + theme = from_union([Theme, from_none], obj.get("theme")) + time_zone = from_union([from_str, from_none], obj.get("timeZone")) + user_agent = from_union([from_str, from_none], obj.get("userAgent")) + return MCPAppsHostContextDetails(available_display_modes, display_mode, locale, platform, theme, time_zone, user_agent) + + def to_dict(self) -> dict: + result: dict = {} + if self.available_display_modes is not None: + result["availableDisplayModes"] = from_union([lambda x: from_list(lambda x: to_enum(MCPAppsDisplayMode, x), x), from_none], self.available_display_modes) + if self.display_mode is not None: + result["displayMode"] = from_union([lambda x: to_enum(MCPAppsDisplayMode, x), from_none], self.display_mode) + if self.locale is not None: + result["locale"] = from_union([from_str, from_none], self.locale) + if self.platform is not None: + result["platform"] = from_union([lambda x: to_enum(MCPAppsHostContextDetailsPlatform, x), from_none], self.platform) + if self.theme is not None: + result["theme"] = from_union([lambda x: to_enum(Theme, x), from_none], self.theme) + if self.time_zone is not None: + result["timeZone"] = from_union([from_str, from_none], self.time_zone) + if self.user_agent is not None: + result["userAgent"] = from_union([from_str, from_none], self.user_agent) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsSetHostContextDetails: + """Host context advertised to MCP App guests""" + + available_display_modes: list[MCPAppsDisplayMode] | None = None + """Display modes the host supports""" + + display_mode: MCPAppsDisplayMode | None = None + """Current display mode (SEP-1865)""" + + locale: str | None = None + """BCP-47 locale, e.g. 'en-US'""" + + platform: MCPAppsHostContextDetailsPlatform | None = None + """Platform type for responsive design""" + + theme: Theme | None = None + """UI theme preference per SEP-1865""" + + time_zone: str | None = None + """IANA timezone, e.g. 'America/New_York'""" + + user_agent: str | None = None + """Host application identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsSetHostContextDetails': + assert isinstance(obj, dict) + available_display_modes = from_union([lambda x: from_list(MCPAppsDisplayMode, x), from_none], obj.get("availableDisplayModes")) + display_mode = from_union([MCPAppsDisplayMode, from_none], obj.get("displayMode")) + locale = from_union([from_str, from_none], obj.get("locale")) + platform = from_union([MCPAppsHostContextDetailsPlatform, from_none], obj.get("platform")) + theme = from_union([Theme, from_none], obj.get("theme")) + time_zone = from_union([from_str, from_none], obj.get("timeZone")) + user_agent = from_union([from_str, from_none], obj.get("userAgent")) + return MCPAppsSetHostContextDetails(available_display_modes, display_mode, locale, platform, theme, time_zone, user_agent) + + def to_dict(self) -> dict: + result: dict = {} + if self.available_display_modes is not None: + result["availableDisplayModes"] = from_union([lambda x: from_list(lambda x: to_enum(MCPAppsDisplayMode, x), x), from_none], self.available_display_modes) + if self.display_mode is not None: + result["displayMode"] = from_union([lambda x: to_enum(MCPAppsDisplayMode, x), from_none], self.display_mode) + if self.locale is not None: + result["locale"] = from_union([from_str, from_none], self.locale) + if self.platform is not None: + result["platform"] = from_union([lambda x: to_enum(MCPAppsHostContextDetailsPlatform, x), from_none], self.platform) + if self.theme is not None: + result["theme"] = from_union([lambda x: to_enum(Theme, x), from_none], self.theme) + if self.time_zone is not None: + result["timeZone"] = from_union([from_str, from_none], self.time_zone) + if self.user_agent is not None: + result["userAgent"] = from_union([from_str, from_none], self.user_agent) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsReadResourceResult: + """Resource contents returned by the MCP server.""" + + contents: list[MCPAppsResourceContent] + """Resource contents returned by the server""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsReadResourceResult': + assert isinstance(obj, dict) + contents = from_list(MCPAppsResourceContent.from_dict, obj.get("contents")) + return MCPAppsReadResourceResult(contents) + + def to_dict(self) -> dict: + result: dict = {} + result["contents"] = from_list(lambda x: to_class(MCPAppsResourceContent, x), self.contents) + return result + @dataclass class MCPServerConfig: """MCP server configuration (stdio process or remote HTTP/SSE) @@ -7829,28 +8600,52 @@ def to_dict(self) -> dict: return result @dataclass -class ModelBilling: - """Billing information""" +class ModelBillingTokenPrices: + """Token-level pricing information for this model""" - multiplier: float | None = None - """Billing cost multiplier relative to the base rate""" + batch_size: int | None = None + """Number of tokens per standard billing batch""" - token_prices: ModelBillingTokenPrices | None = None - """Token-level pricing information for this model""" + cache_price: float | None = None + """AI Credits cost per billing batch of cached tokens""" + + context_max: int | None = None + """Maximum context window tokens for the default tier""" + + input_price: float | None = None + """AI Credits cost per billing batch of input tokens""" + + long_context: ModelBillingTokenPricesLongContext | None = None + """Long context tier pricing (available for models with extended context windows)""" + + output_price: float | None = None + """AI Credits cost per billing batch of output tokens""" @staticmethod - def from_dict(obj: Any) -> 'ModelBilling': + def from_dict(obj: Any) -> 'ModelBillingTokenPrices': assert isinstance(obj, dict) - multiplier = from_union([from_float, from_none], obj.get("multiplier")) - token_prices = from_union([ModelBillingTokenPrices.from_dict, from_none], obj.get("tokenPrices")) - return ModelBilling(multiplier, token_prices) + batch_size = from_union([from_int, from_none], obj.get("batchSize")) + cache_price = from_union([from_float, from_none], obj.get("cachePrice")) + context_max = from_union([from_int, from_none], obj.get("contextMax")) + input_price = from_union([from_float, from_none], obj.get("inputPrice")) + long_context = from_union([ModelBillingTokenPricesLongContext.from_dict, from_none], obj.get("longContext")) + output_price = from_union([from_float, from_none], obj.get("outputPrice")) + return ModelBillingTokenPrices(batch_size, cache_price, context_max, input_price, long_context, output_price) def to_dict(self) -> dict: result: dict = {} - if self.multiplier is not None: - result["multiplier"] = from_union([to_float, from_none], self.multiplier) - if self.token_prices is not None: - result["tokenPrices"] = from_union([lambda x: to_class(ModelBillingTokenPrices, x), from_none], self.token_prices) + if self.batch_size is not None: + result["batchSize"] = from_union([from_int, from_none], self.batch_size) + if self.cache_price is not None: + result["cachePrice"] = from_union([to_float, from_none], self.cache_price) + if self.context_max is not None: + result["contextMax"] = from_union([from_int, from_none], self.context_max) + if self.input_price is not None: + result["inputPrice"] = from_union([to_float, from_none], self.input_price) + if self.long_context is not None: + result["longContext"] = from_union([lambda x: to_class(ModelBillingTokenPricesLongContext, x), from_none], self.long_context) + if self.output_price is not None: + result["outputPrice"] = from_union([to_float, from_none], self.output_price) return result @dataclass @@ -7977,97 +8772,99 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForLocationApprovalCommands: - """Schema for the `PermissionDecisionApproveForLocationApprovalCommands` type.""" +class PermissionDecisionApproveForLocation: + """Schema for the `PermissionDecisionApproveForLocation` type.""" - command_identifiers: list[str] - """Command identifiers covered by this approval.""" + approval: PermissionDecisionApproveForLocationApproval + """Approval to persist for this location""" - kind: PermissionDecisionApproveForLocationApprovalCommandsKind - """Approval scoped to specific command identifiers.""" + kind: ClassVar[str] = "approve-for-location" + """Approve and persist for this project location""" + + location_key: str + """Location key (git root or cwd) to persist the approval to""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalCommands': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocation': assert isinstance(obj, dict) - command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) - kind = PermissionDecisionApproveForLocationApprovalCommandsKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalCommands(command_identifiers, kind) + approval = _load_PermissionDecisionApproveForLocationApproval(obj.get("approval")) + location_key = from_str(obj.get("locationKey")) + return PermissionDecisionApproveForLocation(approval, location_key) def to_dict(self) -> dict: result: dict = {} - result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCommandsKind, self.kind) + result["approval"] = (self.approval).to_dict() + result["kind"] = self.kind + result["locationKey"] = from_str(self.location_key) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForSessionApprovalCommands: - """Schema for the `PermissionDecisionApproveForSessionApprovalCommands` type.""" - +class PermissionDecisionApproveForLocationApprovalCommands: + """Schema for the `PermissionDecisionApproveForLocationApprovalCommands` type.""" + command_identifiers: list[str] """Command identifiers covered by this approval.""" - kind: PermissionDecisionApproveForLocationApprovalCommandsKind + kind: ClassVar[str] = "commands" """Approval scoped to specific command identifiers.""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalCommands': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalCommands': assert isinstance(obj, dict) command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) - kind = PermissionDecisionApproveForLocationApprovalCommandsKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalCommands(command_identifiers, kind) + return PermissionDecisionApproveForLocationApprovalCommands(command_identifiers) def to_dict(self) -> dict: result: dict = {} result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCommandsKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionsLocationsAddToolApprovalDetailsCommands: - """Schema for the `PermissionsLocationsAddToolApprovalDetailsCommands` type.""" +class PermissionDecisionApproveForSessionApprovalCommands: + """Schema for the `PermissionDecisionApproveForSessionApprovalCommands` type.""" command_identifiers: list[str] """Command identifiers covered by this approval.""" - kind: PermissionDecisionApproveForLocationApprovalCommandsKind + kind: ClassVar[str] = "commands" """Approval scoped to specific command identifiers.""" @staticmethod - def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsCommands': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalCommands': assert isinstance(obj, dict) command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) - kind = PermissionDecisionApproveForLocationApprovalCommandsKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsCommands(command_identifiers, kind) + return PermissionDecisionApproveForSessionApprovalCommands(command_identifiers) def to_dict(self) -> dict: result: dict = {} result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCommandsKind, self.kind) + result["kind"] = self.kind return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class UserToolSessionApprovalCommands: - """Schema for the `UserToolSessionApprovalCommands` type.""" +class PermissionsLocationsAddToolApprovalDetailsCommands: + """Schema for the `PermissionsLocationsAddToolApprovalDetailsCommands` type.""" command_identifiers: list[str] - """Command identifiers approved by the user""" + """Command identifiers covered by this approval.""" - kind: PermissionDecisionApproveForLocationApprovalCommandsKind - """Command approval kind""" + kind: ClassVar[str] = "commands" + """Approval scoped to specific command identifiers.""" @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalCommands': + def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsCommands': assert isinstance(obj, dict) command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) - kind = PermissionDecisionApproveForLocationApprovalCommandsKind(obj.get("kind")) - return UserToolSessionApprovalCommands(command_identifiers, kind) + return PermissionsLocationsAddToolApprovalDetailsCommands(command_identifiers) def to_dict(self) -> dict: result: dict = {} result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCommandsKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8075,7 +8872,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalCustomTool: """Schema for the `PermissionDecisionApproveForLocationApprovalCustomTool` type.""" - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind + kind: ClassVar[str] = "custom-tool" """Approval covering a custom tool.""" tool_name: str @@ -8084,13 +8881,12 @@ class PermissionDecisionApproveForLocationApprovalCustomTool: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalCustomTool': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalCustomToolKind(obj.get("kind")) tool_name = from_str(obj.get("toolName")) - return PermissionDecisionApproveForLocationApprovalCustomTool(kind, tool_name) + return PermissionDecisionApproveForLocationApprovalCustomTool(tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCustomToolKind, self.kind) + result["kind"] = self.kind result["toolName"] = from_str(self.tool_name) return result @@ -8099,7 +8895,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalCustomTool: """Schema for the `PermissionDecisionApproveForSessionApprovalCustomTool` type.""" - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind + kind: ClassVar[str] = "custom-tool" """Approval covering a custom tool.""" tool_name: str @@ -8108,13 +8904,12 @@ class PermissionDecisionApproveForSessionApprovalCustomTool: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalCustomTool': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalCustomToolKind(obj.get("kind")) tool_name = from_str(obj.get("toolName")) - return PermissionDecisionApproveForSessionApprovalCustomTool(kind, tool_name) + return PermissionDecisionApproveForSessionApprovalCustomTool(tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCustomToolKind, self.kind) + result["kind"] = self.kind result["toolName"] = from_str(self.tool_name) return result @@ -8123,7 +8918,7 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsCustomTool: """Schema for the `PermissionsLocationsAddToolApprovalDetailsCustomTool` type.""" - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind + kind: ClassVar[str] = "custom-tool" """Approval covering a custom tool.""" tool_name: str @@ -8132,36 +8927,12 @@ class PermissionsLocationsAddToolApprovalDetailsCustomTool: @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsCustomTool': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalCustomToolKind(obj.get("kind")) - tool_name = from_str(obj.get("toolName")) - return PermissionsLocationsAddToolApprovalDetailsCustomTool(kind, tool_name) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCustomToolKind, self.kind) - result["toolName"] = from_str(self.tool_name) - return result - -@dataclass -class UserToolSessionApprovalCustomTool: - """Schema for the `UserToolSessionApprovalCustomTool` type.""" - - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind - """Custom tool approval kind""" - - tool_name: str - """Custom tool name""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalCustomTool': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalCustomToolKind(obj.get("kind")) tool_name = from_str(obj.get("toolName")) - return UserToolSessionApprovalCustomTool(kind, tool_name) + return PermissionsLocationsAddToolApprovalDetailsCustomTool(tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCustomToolKind, self.kind) + result["kind"] = self.kind result["toolName"] = from_str(self.tool_name) return result @@ -8170,7 +8941,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalExtensionManagement: """Schema for the `PermissionDecisionApproveForLocationApprovalExtensionManagement` type.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind + kind: ClassVar[str] = "extension-management" """Approval covering extension lifecycle operations such as enable, disable, or reload.""" operation: str | None = None @@ -8181,13 +8952,12 @@ class PermissionDecisionApproveForLocationApprovalExtensionManagement: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalExtensionManagement': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalExtensionManagementKind(obj.get("kind")) operation = from_union([from_str, from_none], obj.get("operation")) - return PermissionDecisionApproveForLocationApprovalExtensionManagement(kind, operation) + return PermissionDecisionApproveForLocationApprovalExtensionManagement(operation) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionManagementKind, self.kind) + result["kind"] = self.kind if self.operation is not None: result["operation"] = from_union([from_str, from_none], self.operation) return result @@ -8197,7 +8967,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalExtensionManagement: """Schema for the `PermissionDecisionApproveForSessionApprovalExtensionManagement` type.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind + kind: ClassVar[str] = "extension-management" """Approval covering extension lifecycle operations such as enable, disable, or reload.""" operation: str | None = None @@ -8208,13 +8978,12 @@ class PermissionDecisionApproveForSessionApprovalExtensionManagement: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalExtensionManagement': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalExtensionManagementKind(obj.get("kind")) operation = from_union([from_str, from_none], obj.get("operation")) - return PermissionDecisionApproveForSessionApprovalExtensionManagement(kind, operation) + return PermissionDecisionApproveForSessionApprovalExtensionManagement(operation) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionManagementKind, self.kind) + result["kind"] = self.kind if self.operation is not None: result["operation"] = from_union([from_str, from_none], self.operation) return result @@ -8224,7 +8993,7 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsExtensionManagement: """Schema for the `PermissionsLocationsAddToolApprovalDetailsExtensionManagement` type.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind + kind: ClassVar[str] = "extension-management" """Approval covering extension lifecycle operations such as enable, disable, or reload.""" operation: str | None = None @@ -8235,13 +9004,12 @@ class PermissionsLocationsAddToolApprovalDetailsExtensionManagement: @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsExtensionManagement': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalExtensionManagementKind(obj.get("kind")) operation = from_union([from_str, from_none], obj.get("operation")) - return PermissionsLocationsAddToolApprovalDetailsExtensionManagement(kind, operation) + return PermissionsLocationsAddToolApprovalDetailsExtensionManagement(operation) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionManagementKind, self.kind) + result["kind"] = self.kind if self.operation is not None: result["operation"] = from_union([from_str, from_none], self.operation) return result @@ -8251,7 +9019,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalMCP: """Schema for the `PermissionDecisionApproveForLocationApprovalMcp` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPKind + kind: ClassVar[str] = "mcp" """Approval covering an MCP tool.""" server_name: str @@ -8263,14 +9031,13 @@ class PermissionDecisionApproveForLocationApprovalMCP: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalMCP': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) tool_name = from_union([from_none, from_str], obj.get("toolName")) - return PermissionDecisionApproveForLocationApprovalMCP(kind, server_name, tool_name) + return PermissionDecisionApproveForLocationApprovalMCP(server_name, tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) result["toolName"] = from_union([from_none, from_str], self.tool_name) return result @@ -8280,7 +9047,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalMCP: """Schema for the `PermissionDecisionApproveForSessionApprovalMcp` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPKind + kind: ClassVar[str] = "mcp" """Approval covering an MCP tool.""" server_name: str @@ -8292,14 +9059,13 @@ class PermissionDecisionApproveForSessionApprovalMCP: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalMCP': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) tool_name = from_union([from_none, from_str], obj.get("toolName")) - return PermissionDecisionApproveForSessionApprovalMCP(kind, server_name, tool_name) + return PermissionDecisionApproveForSessionApprovalMCP(server_name, tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) result["toolName"] = from_union([from_none, from_str], self.tool_name) return result @@ -8309,7 +9075,7 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsMCP: """Schema for the `PermissionsLocationsAddToolApprovalDetailsMcp` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPKind + kind: ClassVar[str] = "mcp" """Approval covering an MCP tool.""" server_name: str @@ -8321,42 +9087,13 @@ class PermissionsLocationsAddToolApprovalDetailsMCP: @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsMCP': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPKind(obj.get("kind")) - server_name = from_str(obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - return PermissionsLocationsAddToolApprovalDetailsMCP(kind, server_name, tool_name) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPKind, self.kind) - result["serverName"] = from_str(self.server_name) - result["toolName"] = from_union([from_none, from_str], self.tool_name) - return result - -@dataclass -class UserToolSessionApprovalMCP: - """Schema for the `UserToolSessionApprovalMcp` type.""" - - kind: PermissionDecisionApproveForLocationApprovalMCPKind - """MCP tool approval kind""" - - server_name: str - """MCP server name""" - - tool_name: str | None = None - """Optional MCP tool name, or null for all tools on the server""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalMCP': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) tool_name = from_union([from_none, from_str], obj.get("toolName")) - return UserToolSessionApprovalMCP(kind, server_name, tool_name) + return PermissionsLocationsAddToolApprovalDetailsMCP(server_name, tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) result["toolName"] = from_union([from_none, from_str], self.tool_name) return result @@ -8366,7 +9103,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalMCPSampling: """Schema for the `PermissionDecisionApproveForLocationApprovalMcpSampling` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPSamplingKind + kind: ClassVar[str] = "mcp-sampling" """Approval covering MCP sampling requests for a server.""" server_name: str @@ -8375,13 +9112,12 @@ class PermissionDecisionApproveForLocationApprovalMCPSampling: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalMCPSampling': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPSamplingKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) - return PermissionDecisionApproveForLocationApprovalMCPSampling(kind, server_name) + return PermissionDecisionApproveForLocationApprovalMCPSampling(server_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPSamplingKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) return result @@ -8390,7 +9126,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalMCPSampling: """Schema for the `PermissionDecisionApproveForSessionApprovalMcpSampling` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPSamplingKind + kind: ClassVar[str] = "mcp-sampling" """Approval covering MCP sampling requests for a server.""" server_name: str @@ -8399,13 +9135,12 @@ class PermissionDecisionApproveForSessionApprovalMCPSampling: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalMCPSampling': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPSamplingKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) - return PermissionDecisionApproveForSessionApprovalMCPSampling(kind, server_name) + return PermissionDecisionApproveForSessionApprovalMCPSampling(server_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPSamplingKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) return result @@ -8414,7 +9149,7 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsMCPSampling: """Schema for the `PermissionsLocationsAddToolApprovalDetailsMcpSampling` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPSamplingKind + kind: ClassVar[str] = "mcp-sampling" """Approval covering MCP sampling requests for a server.""" server_name: str @@ -8423,13 +9158,12 @@ class PermissionsLocationsAddToolApprovalDetailsMCPSampling: @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsMCPSampling': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPSamplingKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) - return PermissionsLocationsAddToolApprovalDetailsMCPSampling(kind, server_name) + return PermissionsLocationsAddToolApprovalDetailsMCPSampling(server_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPSamplingKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) return result @@ -8438,18 +9172,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalMemory: """Schema for the `PermissionDecisionApproveForLocationApprovalMemory` type.""" - kind: PermissionDecisionApproveForLocationApprovalMemoryKind + kind: ClassVar[str] = "memory" """Approval covering writes to long-term memory.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalMemory': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMemoryKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalMemory(kind) + return PermissionDecisionApproveForLocationApprovalMemory() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMemoryKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8457,18 +9190,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalMemory: """Schema for the `PermissionDecisionApproveForSessionApprovalMemory` type.""" - kind: PermissionDecisionApproveForLocationApprovalMemoryKind + kind: ClassVar[str] = "memory" """Approval covering writes to long-term memory.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalMemory': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMemoryKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalMemory(kind) + return PermissionDecisionApproveForSessionApprovalMemory() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMemoryKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8476,36 +9208,17 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsMemory: """Schema for the `PermissionsLocationsAddToolApprovalDetailsMemory` type.""" - kind: PermissionDecisionApproveForLocationApprovalMemoryKind + kind: ClassVar[str] = "memory" """Approval covering writes to long-term memory.""" @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsMemory': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMemoryKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsMemory(kind) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMemoryKind, self.kind) - return result - -@dataclass -class UserToolSessionApprovalMemory: - """Schema for the `UserToolSessionApprovalMemory` type.""" - - kind: PermissionDecisionApproveForLocationApprovalMemoryKind - """Memory approval kind""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalMemory': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMemoryKind(obj.get("kind")) - return UserToolSessionApprovalMemory(kind) + return PermissionsLocationsAddToolApprovalDetailsMemory() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMemoryKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8513,18 +9226,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalRead: """Schema for the `PermissionDecisionApproveForLocationApprovalRead` type.""" - kind: PermissionDecisionApproveForLocationApprovalReadKind + kind: ClassVar[str] = "read" """Approval covering read-only filesystem operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalRead': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalReadKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalRead(kind) + return PermissionDecisionApproveForLocationApprovalRead() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalReadKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8532,18 +9244,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalRead: """Schema for the `PermissionDecisionApproveForSessionApprovalRead` type.""" - kind: PermissionDecisionApproveForLocationApprovalReadKind + kind: ClassVar[str] = "read" """Approval covering read-only filesystem operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalRead': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalReadKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalRead(kind) + return PermissionDecisionApproveForSessionApprovalRead() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalReadKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8551,36 +9262,17 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsRead: """Schema for the `PermissionsLocationsAddToolApprovalDetailsRead` type.""" - kind: PermissionDecisionApproveForLocationApprovalReadKind + kind: ClassVar[str] = "read" """Approval covering read-only filesystem operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsRead': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalReadKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsRead(kind) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalReadKind, self.kind) - return result - -@dataclass -class UserToolSessionApprovalRead: - """Schema for the `UserToolSessionApprovalRead` type.""" - - kind: PermissionDecisionApproveForLocationApprovalReadKind - """Read approval kind""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalRead': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalReadKind(obj.get("kind")) - return UserToolSessionApprovalRead(kind) + return PermissionsLocationsAddToolApprovalDetailsRead() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalReadKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8588,18 +9280,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalWrite: """Schema for the `PermissionDecisionApproveForLocationApprovalWrite` type.""" - kind: PermissionDecisionApproveForLocationApprovalWriteKind + kind: ClassVar[str] = "write" """Approval covering filesystem write operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalWrite': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalWriteKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalWrite(kind) + return PermissionDecisionApproveForLocationApprovalWrite() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalWriteKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8607,18 +9298,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalWrite: """Schema for the `PermissionDecisionApproveForSessionApprovalWrite` type.""" - kind: PermissionDecisionApproveForLocationApprovalWriteKind + kind: ClassVar[str] = "write" """Approval covering filesystem write operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalWrite': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalWriteKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalWrite(kind) + return PermissionDecisionApproveForSessionApprovalWrite() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalWriteKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8626,36 +9316,47 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsWrite: """Schema for the `PermissionsLocationsAddToolApprovalDetailsWrite` type.""" - kind: PermissionDecisionApproveForLocationApprovalWriteKind + kind: ClassVar[str] = "write" """Approval covering filesystem write operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsWrite': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalWriteKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsWrite(kind) + return PermissionsLocationsAddToolApprovalDetailsWrite() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalWriteKind, self.kind) + result["kind"] = self.kind return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class UserToolSessionApprovalWrite: - """Schema for the `UserToolSessionApprovalWrite` type.""" +class PermissionDecisionApproveForSession: + """Schema for the `PermissionDecisionApproveForSession` type.""" - kind: PermissionDecisionApproveForLocationApprovalWriteKind - """Write approval kind""" + kind: ClassVar[str] = "approve-for-session" + """Approve and remember for the rest of the session""" + + approval: PermissionDecisionApproveForSessionApproval | None = None + """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts)""" + + domain: str | None = None + """URL domain to approve for the rest of the session (URL prompts only)""" @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalWrite': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForSession': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalWriteKind(obj.get("kind")) - return UserToolSessionApprovalWrite(kind) + approval = from_union([_load_PermissionDecisionApproveForSessionApproval, from_none], obj.get("approval")) + domain = from_union([from_str, from_none], obj.get("domain")) + return PermissionDecisionApproveForSession(approval, domain) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalWriteKind, self.kind) + result["kind"] = self.kind + if self.approval is not None: + result["approval"] = from_union([lambda x: (x).to_dict(), from_none], self.approval) + if self.domain is not None: + result["domain"] = from_union([from_str, from_none], self.domain) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8663,18 +9364,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveOnce: """Schema for the `PermissionDecisionApproveOnce` type.""" - kind: PermissionDecisionApproveOnceKind + kind: ClassVar[str] = "approve-once" """Approve this single request only""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveOnce': assert isinstance(obj, dict) - kind = PermissionDecisionApproveOnceKind(obj.get("kind")) - return PermissionDecisionApproveOnce(kind) + return PermissionDecisionApproveOnce() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveOnceKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8685,20 +9385,19 @@ class PermissionDecisionApprovePermanently: domain: str """URL domain to approve permanently""" - kind: PermissionDecisionApprovePermanentlyKind + kind: ClassVar[str] = "approve-permanently" """Approve and persist across sessions (URL prompts only)""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApprovePermanently': assert isinstance(obj, dict) domain = from_str(obj.get("domain")) - kind = PermissionDecisionApprovePermanentlyKind(obj.get("kind")) - return PermissionDecisionApprovePermanently(domain, kind) + return PermissionDecisionApprovePermanently(domain) def to_dict(self) -> dict: result: dict = {} result["domain"] = from_str(self.domain) - result["kind"] = to_enum(PermissionDecisionApprovePermanentlyKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8706,18 +9405,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproved: """Schema for the `PermissionDecisionApproved` type.""" - kind: PermissionDecisionApprovedKind + kind: ClassVar[str] = "approved" """The permission request was approved""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproved': assert isinstance(obj, dict) - kind = PermissionDecisionApprovedKind(obj.get("kind")) - return PermissionDecisionApproved(kind) + return PermissionDecisionApproved() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApprovedKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8728,7 +9426,7 @@ class PermissionDecisionApprovedForLocation: approval: UserToolSessionApproval """The approval to persist for this location""" - kind: PermissionDecisionApprovedForLocationKind + kind: ClassVar[str] = "approved-for-location" """Approved and persisted for this project location""" location_key: str @@ -8738,14 +9436,13 @@ class PermissionDecisionApprovedForLocation: def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocation': assert isinstance(obj, dict) approval = UserToolSessionApproval.from_dict(obj.get("approval")) - kind = PermissionDecisionApprovedForLocationKind(obj.get("kind")) location_key = from_str(obj.get("locationKey")) - return PermissionDecisionApprovedForLocation(approval, kind, location_key) + return PermissionDecisionApprovedForLocation(approval, location_key) def to_dict(self) -> dict: result: dict = {} result["approval"] = to_class(UserToolSessionApproval, self.approval) - result["kind"] = to_enum(PermissionDecisionApprovedForLocationKind, self.kind) + result["kind"] = self.kind result["locationKey"] = from_str(self.location_key) return result @@ -8757,20 +9454,19 @@ class PermissionDecisionApprovedForSession: approval: UserToolSessionApproval """The approval to add as a session-scoped rule""" - kind: PermissionDecisionApprovedForSessionKind + kind: ClassVar[str] = "approved-for-session" """Approved and remembered for the rest of the session""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSession': assert isinstance(obj, dict) approval = UserToolSessionApproval.from_dict(obj.get("approval")) - kind = PermissionDecisionApprovedForSessionKind(obj.get("kind")) - return PermissionDecisionApprovedForSession(approval, kind) + return PermissionDecisionApprovedForSession(approval) def to_dict(self) -> dict: result: dict = {} result["approval"] = to_class(UserToolSessionApproval, self.approval) - result["kind"] = to_enum(PermissionDecisionApprovedForSessionKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8778,7 +9474,7 @@ def to_dict(self) -> dict: class PermissionDecisionCancelled: """Schema for the `PermissionDecisionCancelled` type.""" - kind: PermissionDecisionCancelledKind + kind: ClassVar[str] = "cancelled" """The permission request was cancelled before a response was used""" reason: str | None = None @@ -8787,13 +9483,12 @@ class PermissionDecisionCancelled: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionCancelled': assert isinstance(obj, dict) - kind = PermissionDecisionCancelledKind(obj.get("kind")) reason = from_union([from_str, from_none], obj.get("reason")) - return PermissionDecisionCancelled(kind, reason) + return PermissionDecisionCancelled(reason) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionCancelledKind, self.kind) + result["kind"] = self.kind if self.reason is not None: result["reason"] = from_union([from_str, from_none], self.reason) return result @@ -8803,7 +9498,7 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedByContentExclusionPolicy: """Schema for the `PermissionDecisionDeniedByContentExclusionPolicy` type.""" - kind: PermissionDecisionDeniedByContentExclusionPolicyKind + kind: ClassVar[str] = "denied-by-content-exclusion-policy" """Denied by the organization's content exclusion policy""" message: str @@ -8815,14 +9510,13 @@ class PermissionDecisionDeniedByContentExclusionPolicy: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedByContentExclusionPolicy': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedByContentExclusionPolicyKind(obj.get("kind")) message = from_str(obj.get("message")) path = from_str(obj.get("path")) - return PermissionDecisionDeniedByContentExclusionPolicy(kind, message, path) + return PermissionDecisionDeniedByContentExclusionPolicy(message, path) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedByContentExclusionPolicyKind, self.kind) + result["kind"] = self.kind result["message"] = from_str(self.message) result["path"] = from_str(self.path) return result @@ -8832,7 +9526,7 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedByPermissionRequestHook: """Schema for the `PermissionDecisionDeniedByPermissionRequestHook` type.""" - kind: PermissionDecisionDeniedByPermissionRequestHookKind + kind: ClassVar[str] = "denied-by-permission-request-hook" """Denied by a permission request hook registered by an extension or plugin""" interrupt: bool | None = None @@ -8844,14 +9538,13 @@ class PermissionDecisionDeniedByPermissionRequestHook: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedByPermissionRequestHook': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedByPermissionRequestHookKind(obj.get("kind")) interrupt = from_union([from_bool, from_none], obj.get("interrupt")) message = from_union([from_str, from_none], obj.get("message")) - return PermissionDecisionDeniedByPermissionRequestHook(kind, interrupt, message) + return PermissionDecisionDeniedByPermissionRequestHook(interrupt, message) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedByPermissionRequestHookKind, self.kind) + result["kind"] = self.kind if self.interrupt is not None: result["interrupt"] = from_union([from_bool, from_none], self.interrupt) if self.message is not None: @@ -8863,7 +9556,7 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedByRules: """Schema for the `PermissionDecisionDeniedByRules` type.""" - kind: PermissionDecisionDeniedByRulesKind + kind: ClassVar[str] = "denied-by-rules" """Denied because approval rules explicitly blocked it""" rules: list[PermissionRule] @@ -8872,13 +9565,12 @@ class PermissionDecisionDeniedByRules: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedByRules': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedByRulesKind(obj.get("kind")) rules = from_list(PermissionRule.from_dict, obj.get("rules")) - return PermissionDecisionDeniedByRules(kind, rules) + return PermissionDecisionDeniedByRules(rules) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedByRulesKind, self.kind) + result["kind"] = self.kind result["rules"] = from_list(lambda x: to_class(PermissionRule, x), self.rules) return result @@ -8887,7 +9579,7 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedInteractivelyByUser: """Schema for the `PermissionDecisionDeniedInteractivelyByUser` type.""" - kind: PermissionDecisionDeniedInteractivelyByUserKind + kind: ClassVar[str] = "denied-interactively-by-user" """Denied by the user during an interactive prompt""" feedback: str | None = None @@ -8899,14 +9591,13 @@ class PermissionDecisionDeniedInteractivelyByUser: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedInteractivelyByUser': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedInteractivelyByUserKind(obj.get("kind")) feedback = from_union([from_str, from_none], obj.get("feedback")) force_reject = from_union([from_bool, from_none], obj.get("forceReject")) - return PermissionDecisionDeniedInteractivelyByUser(kind, feedback, force_reject) + return PermissionDecisionDeniedInteractivelyByUser(feedback, force_reject) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedInteractivelyByUserKind, self.kind) + result["kind"] = self.kind if self.feedback is not None: result["feedback"] = from_union([from_str, from_none], self.feedback) if self.force_reject is not None: @@ -8918,18 +9609,17 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser: """Schema for the `PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser` type.""" - kind: PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUserKind + kind: ClassVar[str] = "denied-no-approval-rule-and-could-not-request-from-user" """Denied because no approval rule matched and user confirmation was unavailable""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUserKind(obj.get("kind")) - return PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser(kind) + return PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUserKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8937,7 +9627,7 @@ def to_dict(self) -> dict: class PermissionDecisionReject: """Schema for the `PermissionDecisionReject` type.""" - kind: PermissionDecisionRejectKind + kind: ClassVar[str] = "reject" """Reject the request""" feedback: str | None = None @@ -8946,13 +9636,12 @@ class PermissionDecisionReject: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionReject': assert isinstance(obj, dict) - kind = PermissionDecisionRejectKind(obj.get("kind")) feedback = from_union([from_str, from_none], obj.get("feedback")) - return PermissionDecisionReject(kind, feedback) + return PermissionDecisionReject(feedback) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionRejectKind, self.kind) + result["kind"] = self.kind if self.feedback is not None: result["feedback"] = from_union([from_str, from_none], self.feedback) return result @@ -8962,18 +9651,17 @@ def to_dict(self) -> dict: class PermissionDecisionUserNotAvailable: """Schema for the `PermissionDecisionUserNotAvailable` type.""" - kind: PermissionDecisionUserNotAvailableKind + kind: ClassVar[str] = "user-not-available" """No user is available to confirm the request""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionUserNotAvailable': assert isinstance(obj, dict) - kind = PermissionDecisionUserNotAvailableKind(obj.get("kind")) - return PermissionDecisionUserNotAvailable(kind) + return PermissionDecisionUserNotAvailable() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionUserNotAvailableKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -9159,40 +9847,6 @@ def to_dict(self) -> dict: result["kind"] = to_enum(QueuePendingItemsKind, self.kind) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class QueuedCommandResult: - """Result of the queued command execution. - - Schema for the `QueuedCommandHandled` type. - - Schema for the `QueuedCommandNotHandled` type. - """ - handled: bool - """The host actually executed the queued command. - - The host did not execute the queued command. Unblocks the queue without claiming the - command was processed (e.g. when the handler threw before completing). - """ - stop_processing_queue: bool | None = None - """When true, the runtime will not process subsequent queued commands until a new request - comes in. - """ - - @staticmethod - def from_dict(obj: Any) -> 'QueuedCommandResult': - assert isinstance(obj, dict) - handled = from_bool(obj.get("handled")) - stop_processing_queue = from_union([from_bool, from_none], obj.get("stopProcessingQueue")) - return QueuedCommandResult(handled, stop_processing_queue) - - def to_dict(self) -> dict: - result: dict = {} - result["handled"] = from_bool(self.handled) - if self.stop_processing_queue is not None: - result["stopProcessingQueue"] = from_union([from_bool, from_none], self.stop_processing_queue) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class RemoteEnableRequest: @@ -9290,7 +9944,7 @@ class SendAttachmentBlob: mime_type: str """MIME type of the inline data""" - type: SendAttachmentBlobType + type: ClassVar[str] = "blob" """Attachment type discriminator""" display_name: str | None = None @@ -9301,15 +9955,14 @@ def from_dict(obj: Any) -> 'SendAttachmentBlob': assert isinstance(obj, dict) data = from_str(obj.get("data")) mime_type = from_str(obj.get("mimeType")) - type = SendAttachmentBlobType(obj.get("type")) display_name = from_union([from_str, from_none], obj.get("displayName")) - return SendAttachmentBlob(data, mime_type, type, display_name) + return SendAttachmentBlob(data, mime_type, display_name) def to_dict(self) -> dict: result: dict = {} result["data"] = from_str(self.data) result["mimeType"] = from_str(self.mime_type) - result["type"] = to_enum(SendAttachmentBlobType, self.type) + result["type"] = self.type if self.display_name is not None: result["displayName"] = from_union([from_str, from_none], self.display_name) return result @@ -9325,7 +9978,7 @@ class SendAttachmentFile: path: str """Absolute file path""" - type: SendAttachmentFileType + type: ClassVar[str] = "file" """Attachment type discriminator""" line_range: SendAttachmentFileLineRange | None = None @@ -9336,15 +9989,14 @@ def from_dict(obj: Any) -> 'SendAttachmentFile': assert isinstance(obj, dict) display_name = from_str(obj.get("displayName")) path = from_str(obj.get("path")) - type = SendAttachmentFileType(obj.get("type")) line_range = from_union([SendAttachmentFileLineRange.from_dict, from_none], obj.get("lineRange")) - return SendAttachmentFile(display_name, path, type, line_range) + return SendAttachmentFile(display_name, path, line_range) def to_dict(self) -> dict: result: dict = {} result["displayName"] = from_str(self.display_name) result["path"] = from_str(self.path) - result["type"] = to_enum(SendAttachmentFileType, self.type) + result["type"] = self.type if self.line_range is not None: result["lineRange"] = from_union([lambda x: to_class(SendAttachmentFileLineRange, x), from_none], self.line_range) return result @@ -9366,7 +10018,7 @@ class SendAttachmentGithubReference: title: str """Title of the referenced item""" - type: SendAttachmentGithubReferenceType + type: ClassVar[str] = "github_reference" """Attachment type discriminator""" url: str @@ -9379,9 +10031,8 @@ def from_dict(obj: Any) -> 'SendAttachmentGithubReference': reference_type = SendAttachmentGithubReferenceTypeEnum(obj.get("referenceType")) state = from_str(obj.get("state")) title = from_str(obj.get("title")) - type = SendAttachmentGithubReferenceType(obj.get("type")) url = from_str(obj.get("url")) - return SendAttachmentGithubReference(number, reference_type, state, title, type, url) + return SendAttachmentGithubReference(number, reference_type, state, title, url) def to_dict(self) -> dict: result: dict = {} @@ -9389,10 +10040,113 @@ def to_dict(self) -> dict: result["referenceType"] = to_enum(SendAttachmentGithubReferenceTypeEnum, self.reference_type) result["state"] = from_str(self.state) result["title"] = from_str(self.title) - result["type"] = to_enum(SendAttachmentGithubReferenceType, self.type) + result["type"] = self.type result["url"] = from_str(self.url) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SendRequest: + """Parameters for sending a user message to the session""" + + prompt: str + """The user message text""" + + agent_mode: SendAgentMode | None = None + """The UI mode the agent was in when this message was sent. Defaults to the session's + current mode. + """ + attachments: list[SendAttachment] | None = None + """Optional attachments (files, directories, selections, blobs, GitHub references) to + include with the message + """ + billable: bool | None = None + """If false, this message will not trigger a Premium Request Unit charge. User messages + default to billable. + """ + display_prompt: str | None = None + """If provided, this is shown in the timeline instead of `prompt`""" + + mode: SendMode | None = None + """How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` + interjects during an in-progress turn. + """ + prepend: bool | None = None + """If true, adds the message to the front of the queue instead of the end""" + + request_headers: dict[str, str] | None = None + """Custom HTTP headers to include in outbound model requests for this turn. Merged with + session-level provider headers; per-turn headers augment and overwrite session-level + headers with the same key. + """ + required_tool: str | None = None + """If set, the request will fail if the named tool is not available when this message is + among the user messages at the start of the current exchange + """ + # Internal: this field is an internal SDK API and is not part of the public surface. + source: Any = None + """Optional provenance tag copied to the resulting user.message event. Supported values are + `system`, `command-*`, and `schedule-*`. + """ + traceparent: str | None = None + """W3C Trace Context traceparent header for distributed tracing of this agent turn""" + + tracestate: str | None = None + """W3C Trace Context tracestate header for distributed tracing""" + + wait: bool | None = None + """If true, await completion of the agentic loop for this message before returning. Defaults + to false (fire-and-forget). When true, the result still contains the same `messageId`; + the caller can rely on the agent having processed the message before the call resolves. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SendRequest': + assert isinstance(obj, dict) + prompt = from_str(obj.get("prompt")) + agent_mode = from_union([SendAgentMode, from_none], obj.get("agentMode")) + attachments = from_union([lambda x: from_list(_load_SendAttachment, x), from_none], obj.get("attachments")) + billable = from_union([from_bool, from_none], obj.get("billable")) + display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) + mode = from_union([SendMode, from_none], obj.get("mode")) + prepend = from_union([from_bool, from_none], obj.get("prepend")) + request_headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("requestHeaders")) + required_tool = from_union([from_str, from_none], obj.get("requiredTool")) + source = obj.get("source") + traceparent = from_union([from_str, from_none], obj.get("traceparent")) + tracestate = from_union([from_str, from_none], obj.get("tracestate")) + wait = from_union([from_bool, from_none], obj.get("wait")) + return SendRequest(prompt, agent_mode, attachments, billable, display_prompt, mode, prepend, request_headers, required_tool, source, traceparent, tracestate, wait) + + def to_dict(self) -> dict: + result: dict = {} + result["prompt"] = from_str(self.prompt) + if self.agent_mode is not None: + result["agentMode"] = from_union([lambda x: to_enum(SendAgentMode, x), from_none], self.agent_mode) + if self.attachments is not None: + result["attachments"] = from_union([lambda x: from_list(lambda x: (x).to_dict(), x), from_none], self.attachments) + if self.billable is not None: + result["billable"] = from_union([from_bool, from_none], self.billable) + if self.display_prompt is not None: + result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(SendMode, x), from_none], self.mode) + if self.prepend is not None: + result["prepend"] = from_union([from_bool, from_none], self.prepend) + if self.request_headers is not None: + result["requestHeaders"] = from_union([lambda x: from_dict(from_str, x), from_none], self.request_headers) + if self.required_tool is not None: + result["requiredTool"] = from_union([from_str, from_none], self.required_tool) + if self.source is not None: + result["source"] = self.source + if self.traceparent is not None: + result["traceparent"] = from_union([from_str, from_none], self.traceparent) + if self.tracestate is not None: + result["tracestate"] = from_union([from_str, from_none], self.tracestate) + if self.wait is not None: + result["wait"] = from_union([from_bool, from_none], self.wait) + return result + @dataclass class ServerSkillList: """Skills discovered across global and project sources.""" @@ -9731,7 +10485,7 @@ class SlashCommandAgentPromptResult: display_prompt: str """Prompt text to display to the user""" - kind: SlashCommandAgentPromptResultKind + kind: ClassVar[str] = "agent-prompt" """Agent prompt result discriminator""" prompt: str @@ -9749,16 +10503,15 @@ class SlashCommandAgentPromptResult: def from_dict(obj: Any) -> 'SlashCommandAgentPromptResult': assert isinstance(obj, dict) display_prompt = from_str(obj.get("displayPrompt")) - kind = SlashCommandAgentPromptResultKind(obj.get("kind")) prompt = from_str(obj.get("prompt")) mode = from_union([SessionMode, from_none], obj.get("mode")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandAgentPromptResult(display_prompt, kind, prompt, mode, runtime_settings_changed) + return SlashCommandAgentPromptResult(display_prompt, prompt, mode, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} result["displayPrompt"] = from_str(self.display_prompt) - result["kind"] = to_enum(SlashCommandAgentPromptResultKind, self.kind) + result["kind"] = self.kind result["prompt"] = from_str(self.prompt) if self.mode is not None: result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode) @@ -9771,7 +10524,7 @@ def to_dict(self) -> dict: class SlashCommandCompletedResult: """Schema for the `SlashCommandCompletedResult` type.""" - kind: SlashCommandCompletedResultKind + kind: ClassVar[str] = "completed" """Completed result discriminator""" message: str | None = None @@ -9785,14 +10538,13 @@ class SlashCommandCompletedResult: @staticmethod def from_dict(obj: Any) -> 'SlashCommandCompletedResult': assert isinstance(obj, dict) - kind = SlashCommandCompletedResultKind(obj.get("kind")) message = from_union([from_str, from_none], obj.get("message")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandCompletedResult(kind, message, runtime_settings_changed) + return SlashCommandCompletedResult(message, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(SlashCommandCompletedResultKind, self.kind) + result["kind"] = self.kind if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) if self.runtime_settings_changed is not None: @@ -9807,7 +10559,7 @@ class SlashCommandSelectSubcommandResult: command: str """Parent command name that requires subcommand selection""" - kind: SlashCommandSelectSubcommandResultKind + kind: ClassVar[str] = "select-subcommand" """Select subcommand result discriminator""" options: list[SlashCommandSelectSubcommandOption] @@ -9825,16 +10577,15 @@ class SlashCommandSelectSubcommandResult: def from_dict(obj: Any) -> 'SlashCommandSelectSubcommandResult': assert isinstance(obj, dict) command = from_str(obj.get("command")) - kind = SlashCommandSelectSubcommandResultKind(obj.get("kind")) options = from_list(SlashCommandSelectSubcommandOption.from_dict, obj.get("options")) title = from_str(obj.get("title")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandSelectSubcommandResult(command, kind, options, title, runtime_settings_changed) + return SlashCommandSelectSubcommandResult(command, options, title, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} result["command"] = from_str(self.command) - result["kind"] = to_enum(SlashCommandSelectSubcommandResultKind, self.kind) + result["kind"] = self.kind result["options"] = from_list(lambda x: to_class(SlashCommandSelectSubcommandOption, x), self.options) result["title"] = from_str(self.title) if self.runtime_settings_changed is not None: @@ -9895,7 +10646,7 @@ class TaskShellInfo: status: TaskStatus """Current lifecycle status of the task""" - type: TaskShellInfoType + type: ClassVar[str] = "shell" """Task kind""" can_promote_to_background: bool | None = None @@ -9922,13 +10673,12 @@ def from_dict(obj: Any) -> 'TaskShellInfo': id = from_str(obj.get("id")) started_at = from_datetime(obj.get("startedAt")) status = TaskStatus(obj.get("status")) - type = TaskShellInfoType(obj.get("type")) can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) execution_mode = from_union([TaskExecutionMode, from_none], obj.get("executionMode")) log_path = from_union([from_str, from_none], obj.get("logPath")) pid = from_union([from_int, from_none], obj.get("pid")) - return TaskShellInfo(attachment_mode, command, description, id, started_at, status, type, can_promote_to_background, completed_at, execution_mode, log_path, pid) + return TaskShellInfo(attachment_mode, command, description, id, started_at, status, can_promote_to_background, completed_at, execution_mode, log_path, pid) def to_dict(self) -> dict: result: dict = {} @@ -9938,7 +10688,7 @@ def to_dict(self) -> dict: result["id"] = from_str(self.id) result["startedAt"] = self.started_at.isoformat() result["status"] = to_enum(TaskStatus, self.status) - result["type"] = to_enum(TaskShellInfoType, self.type) + result["type"] = self.type if self.can_promote_to_background is not None: result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) if self.completed_at is not None: @@ -9981,6 +10731,67 @@ def to_dict(self) -> dict: result["pid"] = from_union([from_int, from_none], self.pid) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsCallToolRequest: + """MCP server, tool name, and arguments to invoke from an MCP App view.""" + + origin_server_name: str + """**Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the + app from this server only'), the call is rejected when this differs from `serverName`, + and rejected outright when missing. + """ + server_name: str + """MCP server hosting the tool""" + + tool_name: str + """MCP tool name""" + + arguments: dict[str, Any] | None = None + """Tool arguments""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsCallToolRequest': + assert isinstance(obj, dict) + origin_server_name = from_str(obj.get("originServerName")) + server_name = from_str(obj.get("serverName")) + tool_name = from_str(obj.get("toolName")) + arguments = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("arguments")) + return MCPAppsCallToolRequest(origin_server_name, server_name, tool_name, arguments) + + def to_dict(self) -> dict: + result: dict = {} + result["originServerName"] = from_str(self.origin_server_name) + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_str(self.tool_name) + if self.arguments is not None: + result["arguments"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.arguments) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionLocationAddToolApprovalParams: + """Location-scoped tool approval to persist.""" + + approval: PermissionsLocationsAddToolApprovalDetails + """Tool approval to persist and apply""" + + location_key: str + """Location key (git root or cwd) to persist the approval to""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionLocationAddToolApprovalParams': + assert isinstance(obj, dict) + approval = _load_PermissionsLocationsAddToolApprovalDetails(obj.get("approval")) + location_key = from_str(obj.get("locationKey")) + return PermissionLocationAddToolApprovalParams(approval, location_key) + + def to_dict(self) -> dict: + result: dict = {} + result["approval"] = (self.approval).to_dict() + result["locationKey"] = from_str(self.location_key) + return result + @dataclass class ToolList: """Built-in tools available for the requested model, with their parameters and instructions.""" @@ -10477,11 +11288,49 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SlashCommandInfo: - """Schema for the `SlashCommandInfo` type.""" +class CanvasList: + """Declared canvases available in this session.""" - allow_during_agent_execution: bool - """Whether the command may run while an agent turn is active""" + canvases: list[DiscoveredCanvas] + """Declared canvases available in this session""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasList': + assert isinstance(obj, dict) + canvases = from_list(DiscoveredCanvas.from_dict, obj.get("canvases")) + return CanvasList(canvases) + + def to_dict(self) -> dict: + result: dict = {} + result["canvases"] = from_list(lambda x: to_class(DiscoveredCanvas, x), self.canvases) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CanvasListOpenResult: + """Live open-canvas snapshot.""" + + open_canvases: list[OpenCanvasInstance] + """Currently open canvas instances""" + + @staticmethod + def from_dict(obj: Any) -> 'CanvasListOpenResult': + assert isinstance(obj, dict) + open_canvases = from_list(OpenCanvasInstance.from_dict, obj.get("openCanvases")) + return CanvasListOpenResult(open_canvases) + + def to_dict(self) -> dict: + result: dict = {} + result["openCanvases"] = from_list(lambda x: to_class(OpenCanvasInstance, x), self.open_canvases) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SlashCommandInfo: + """Schema for the `SlashCommandInfo` type.""" + + allow_during_agent_execution: bool + """Whether the command may run while an agent turn is active""" description: str """Human-readable command description""" @@ -10711,20 +11560,19 @@ class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess: extension_name: str """Extension name.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind + kind: ClassVar[str] = "extension-permission-access" """Approval covering an extension's request to access a permission-gated capability.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess': assert isinstance(obj, dict) extension_name = from_str(obj.get("extensionName")) - kind = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess(extension_name, kind) + return PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess(extension_name) def to_dict(self) -> dict: result: dict = {} result["extensionName"] = from_str(self.extension_name) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -10736,20 +11584,19 @@ class PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess: extension_name: str """Extension name.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind + kind: ClassVar[str] = "extension-permission-access" """Approval covering an extension's request to access a permission-gated capability.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess': assert isinstance(obj, dict) extension_name = from_str(obj.get("extensionName")) - kind = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess(extension_name, kind) + return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess(extension_name) def to_dict(self) -> dict: result: dict = {} result["extensionName"] = from_str(self.extension_name) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -10760,179 +11607,75 @@ class PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess: extension_name: str """Extension name.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind + kind: ClassVar[str] = "extension-permission-access" """Approval covering an extension's request to access a permission-gated capability.""" @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess': assert isinstance(obj, dict) extension_name = from_str(obj.get("extensionName")) - kind = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess(extension_name, kind) - - def to_dict(self) -> dict: - result: dict = {} - result["extensionName"] = from_str(self.extension_name) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, self.kind) - return result - -@dataclass -class UserToolSessionApprovalExtensionManagement: - """Schema for the `UserToolSessionApprovalExtensionManagement` type.""" - - kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind - """Extension management approval kind""" - - operation: str | None = None - """Optional operation identifier""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalExtensionManagement': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalExtensionManagementKind(obj.get("kind")) - operation = from_union([from_str, from_none], obj.get("operation")) - return UserToolSessionApprovalExtensionManagement(kind, operation) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionManagementKind, self.kind) - if self.operation is not None: - result["operation"] = from_union([from_str, from_none], self.operation) - return result - -@dataclass -class UserToolSessionApprovalExtensionPermissionAccess: - """Schema for the `UserToolSessionApprovalExtensionPermissionAccess` type.""" - - extension_name: str - """Extension name""" - - kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind - """Extension permission access approval kind""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalExtensionPermissionAccess': - assert isinstance(obj, dict) - extension_name = from_str(obj.get("extensionName")) - kind = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(obj.get("kind")) - return UserToolSessionApprovalExtensionPermissionAccess(extension_name, kind) + return PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess(extension_name) def to_dict(self) -> dict: result: dict = {} result["extensionName"] = from_str(self.extension_name) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class ExternalToolTextResultForLlmContent: - """A content block within a tool result, which may be text, terminal output, image, audio, - or a resource - - Plain text content block - - Terminal/shell output content block with optional exit code and working directory - - Image content block with base64-encoded data - - Audio content block with base64-encoded data - - Resource link content block referencing an external resource - - Embedded resource content block with inline text or binary data - """ - type: ExternalToolTextResultForLlmContentType - """Content block type discriminator""" - - text: str | None = None - """The text content - - Terminal/shell output text - """ - cwd: str | None = None - """Working directory where the command was executed""" +class ExternalToolTextResultForLlm: + """Expanded external tool result payload""" - exit_code: int | None = None - """Process exit code, if the command has completed""" + text_result_for_llm: str + """Text result returned to the model""" - data: str | None = None - """Base64-encoded image data + binary_results_for_llm: list[ExternalToolTextResultForLlmBinaryResultsForLlm] | None = None + """Base64-encoded binary results returned to the model""" - Base64-encoded audio data - """ - mime_type: str | None = None - """MIME type of the image (e.g., image/png, image/jpeg) + contents: list[ExternalToolTextResultForLlmContent] | None = None + """Structured content blocks from the tool""" - MIME type of the audio (e.g., audio/wav, audio/mpeg) + error: str | None = None + """Optional error message for failed executions""" - MIME type of the resource content + result_type: str | None = None + """Execution outcome classification. Optional for back-compat; normalized to 'success' (or + 'failure' when error is present) when missing or unrecognized. """ - description: str | None = None - """Human-readable description of the resource""" - - icons: list[ExternalToolTextResultForLlmContentResourceLinkIcon] | None = None - """Icons associated with this resource""" - - name: str | None = None - """Resource name identifier""" - - size: int | None = None - """Size of the resource in bytes""" - - title: str | None = None - """Human-readable display title for the resource""" - - uri: str | None = None - """URI identifying the resource""" + session_log: str | None = None + """Detailed log content for timeline display""" - resource: ExternalToolTextResultForLlmContentResourceDetails | None = None - """The embedded resource contents, either text or base64-encoded binary""" + tool_telemetry: dict[str, Any] | None = None + """Optional tool-specific telemetry""" @staticmethod - def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContent': + def from_dict(obj: Any) -> 'ExternalToolTextResultForLlm': assert isinstance(obj, dict) - type = ExternalToolTextResultForLlmContentType(obj.get("type")) - text = from_union([from_str, from_none], obj.get("text")) - cwd = from_union([from_str, from_none], obj.get("cwd")) - exit_code = from_union([from_int, from_none], obj.get("exitCode")) - data = from_union([from_str, from_none], obj.get("data")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - description = from_union([from_str, from_none], obj.get("description")) - icons = from_union([lambda x: from_list(ExternalToolTextResultForLlmContentResourceLinkIcon.from_dict, x), from_none], obj.get("icons")) - name = from_union([from_str, from_none], obj.get("name")) - size = from_union([from_int, from_none], obj.get("size")) - title = from_union([from_str, from_none], obj.get("title")) - uri = from_union([from_str, from_none], obj.get("uri")) - resource = from_union([(lambda x: from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], x)), from_none], obj.get("resource")) - return ExternalToolTextResultForLlmContent(type, text, cwd, exit_code, data, mime_type, description, icons, name, size, title, uri, resource) + text_result_for_llm = from_str(obj.get("textResultForLlm")) + binary_results_for_llm = from_union([lambda x: from_list(ExternalToolTextResultForLlmBinaryResultsForLlm.from_dict, x), from_none], obj.get("binaryResultsForLlm")) + contents = from_union([lambda x: from_list(_load_ExternalToolTextResultForLlmContent, x), from_none], obj.get("contents")) + error = from_union([from_str, from_none], obj.get("error")) + result_type = from_union([from_str, from_none], obj.get("resultType")) + session_log = from_union([from_str, from_none], obj.get("sessionLog")) + tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry")) + return ExternalToolTextResultForLlm(text_result_for_llm, binary_results_for_llm, contents, error, result_type, session_log, tool_telemetry) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(ExternalToolTextResultForLlmContentType, self.type) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.cwd is not None: - result["cwd"] = from_union([from_str, from_none], self.cwd) - if self.exit_code is not None: - result["exitCode"] = from_union([from_int, from_none], self.exit_code) - if self.data is not None: - result["data"] = from_union([from_str, from_none], self.data) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) - if self.icons is not None: - result["icons"] = from_union([lambda x: from_list(lambda x: to_class(ExternalToolTextResultForLlmContentResourceLinkIcon, x), x), from_none], self.icons) - if self.name is not None: - result["name"] = from_union([from_str, from_none], self.name) - if self.size is not None: - result["size"] = from_union([from_int, from_none], self.size) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) - if self.uri is not None: - result["uri"] = from_union([from_str, from_none], self.uri) - if self.resource is not None: - result["resource"] = from_union([lambda x: from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], x), from_none], self.resource) + result["textResultForLlm"] = from_str(self.text_result_for_llm) + if self.binary_results_for_llm is not None: + result["binaryResultsForLlm"] = from_union([lambda x: from_list(lambda x: to_class(ExternalToolTextResultForLlmBinaryResultsForLlm, x), x), from_none], self.binary_results_for_llm) + if self.contents is not None: + result["contents"] = from_union([lambda x: from_list(lambda x: (x).to_dict(), x), from_none], self.contents) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.result_type is not None: + result["resultType"] = from_union([from_str, from_none], self.result_type) + if self.session_log is not None: + result["sessionLog"] = from_union([from_str, from_none], self.session_log) + if self.tool_telemetry is not None: + result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -10943,7 +11686,7 @@ class ExternalToolTextResultForLlmContentResourceLink: name: str """Resource name identifier""" - type: ExternalToolTextResultForLlmContentResourceLinkType + type: ClassVar[str] = "resource_link" """Content block type discriminator""" uri: str @@ -10968,19 +11711,18 @@ class ExternalToolTextResultForLlmContentResourceLink: def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentResourceLink': assert isinstance(obj, dict) name = from_str(obj.get("name")) - type = ExternalToolTextResultForLlmContentResourceLinkType(obj.get("type")) uri = from_str(obj.get("uri")) description = from_union([from_str, from_none], obj.get("description")) icons = from_union([lambda x: from_list(ExternalToolTextResultForLlmContentResourceLinkIcon.from_dict, x), from_none], obj.get("icons")) mime_type = from_union([from_str, from_none], obj.get("mimeType")) size = from_union([from_int, from_none], obj.get("size")) title = from_union([from_str, from_none], obj.get("title")) - return ExternalToolTextResultForLlmContentResourceLink(name, type, uri, description, icons, mime_type, size, title) + return ExternalToolTextResultForLlmContentResourceLink(name, uri, description, icons, mime_type, size, title) def to_dict(self) -> dict: result: dict = {} result["name"] = from_str(self.name) - result["type"] = to_enum(ExternalToolTextResultForLlmContentResourceLinkType, self.type) + result["type"] = self.type result["uri"] = from_str(self.uri) if self.description is not None: result["description"] = from_union([from_str, from_none], self.description) @@ -11101,6 +11843,44 @@ def to_dict(self) -> dict: result["sources"] = from_list(lambda x: to_class(InstructionsSources, x), self.sources) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsHostContext: + """Current host context advertised to MCP App guests.""" + + context: MCPAppsHostContextDetails + """Current host context""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsHostContext': + assert isinstance(obj, dict) + context = MCPAppsHostContextDetails.from_dict(obj.get("context")) + return MCPAppsHostContext(context) + + def to_dict(self) -> dict: + result: dict = {} + result["context"] = to_class(MCPAppsHostContextDetails, self.context) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPAppsSetHostContextRequest: + """Host context to advertise to MCP App guests.""" + + context: MCPAppsSetHostContextDetails + """Host context advertised to MCP App guests""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPAppsSetHostContextRequest': + assert isinstance(obj, dict) + context = MCPAppsSetHostContextDetails.from_dict(obj.get("context")) + return MCPAppsSetHostContextRequest(context) + + def to_dict(self) -> dict: + result: dict = {} + result["context"] = to_class(MCPAppsSetHostContextDetails, self.context) + return result + @dataclass class MCPConfigAddRequest: """MCP server name and configuration to add to user configuration.""" @@ -11427,6 +12207,31 @@ def to_dict(self) -> dict: result["checkpoints"] = from_list(lambda x: to_class(WorkspacesCheckpoints, x), self.checkpoints) return result +@dataclass +class ModelBilling: + """Billing information""" + + multiplier: float | None = None + """Billing cost multiplier relative to the base rate""" + + token_prices: ModelBillingTokenPrices | None = None + """Token-level pricing information for this model""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelBilling': + assert isinstance(obj, dict) + multiplier = from_union([from_float, from_none], obj.get("multiplier")) + token_prices = from_union([ModelBillingTokenPrices.from_dict, from_none], obj.get("tokenPrices")) + return ModelBilling(multiplier, token_prices) + + def to_dict(self) -> dict: + result: dict = {} + if self.multiplier is not None: + result["multiplier"] = from_union([to_float, from_none], self.multiplier) + if self.token_prices is not None: + result["tokenPrices"] = from_union([lambda x: to_class(ModelBillingTokenPrices, x), from_none], self.token_prices) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelCapabilitiesOverride: @@ -11509,202 +12314,65 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class CommandsRespondToQueuedCommandRequest: - """Queued-command request ID and the result indicating whether the host executed it (and - whether to stop processing further queued commands). - """ - request_id: str - """Request ID from the `command.queued` event the host is responding to.""" +class SendAttachmentSelection: + """Code selection attachment from an editor""" - result: QueuedCommandResult - """Result of the queued command execution.""" + display_name: str + """User-facing display name for the selection""" + + file_path: str + """Absolute path to the file containing the selection""" + + selection: SendAttachmentSelectionDetails + """Position range of the selection within the file""" + + text: str + """The selected text content""" + + type: ClassVar[str] = "selection" + """Attachment type discriminator""" @staticmethod - def from_dict(obj: Any) -> 'CommandsRespondToQueuedCommandRequest': + def from_dict(obj: Any) -> 'SendAttachmentSelection': assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - result = QueuedCommandResult.from_dict(obj.get("result")) - return CommandsRespondToQueuedCommandRequest(request_id, result) + display_name = from_str(obj.get("displayName")) + file_path = from_str(obj.get("filePath")) + selection = SendAttachmentSelectionDetails.from_dict(obj.get("selection")) + text = from_str(obj.get("text")) + return SendAttachmentSelection(display_name, file_path, selection, text) def to_dict(self) -> dict: result: dict = {} - result["requestId"] = from_str(self.request_id) - result["result"] = to_class(QueuedCommandResult, self.result) + result["displayName"] = from_str(self.display_name) + result["filePath"] = from_str(self.file_path) + result["selection"] = to_class(SendAttachmentSelectionDetails, self.selection) + result["text"] = from_str(self.text) + result["type"] = self.type return result -# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SendAttachment: - """A user message attachment — a file, directory, code selection, blob, or GitHub reference +class SessionFSReadFileResult: + """File content as a UTF-8 string, or a filesystem error if the read failed.""" - File attachment + content: str + """File content as UTF-8 string""" - Directory attachment + error: SessionFSError | None = None + """Describes a filesystem error.""" - Code selection attachment from an editor + @staticmethod + def from_dict(obj: Any) -> 'SessionFSReadFileResult': + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + error = from_union([SessionFSError.from_dict, from_none], obj.get("error")) + return SessionFSReadFileResult(content, error) - GitHub issue, pull request, or discussion reference - - Blob attachment with inline base64-encoded data - """ - type: SendAttachmentType - """Attachment type discriminator""" - - display_name: str | None = None - """User-facing display name for the attachment - - User-facing display name for the selection - """ - line_range: SendAttachmentFileLineRange | None = None - """Optional line range to scope the attachment to a specific section of the file""" - - path: str | None = None - """Absolute file path - - Absolute directory path - """ - file_path: str | None = None - """Absolute path to the file containing the selection""" - - selection: SendAttachmentSelectionDetails | None = None - """Position range of the selection within the file""" - - text: str | None = None - """The selected text content""" - - number: int | None = None - """Issue, pull request, or discussion number""" - - reference_type: SendAttachmentGithubReferenceTypeEnum | None = None - """Type of GitHub reference""" - - state: str | None = None - """Current state of the referenced item (e.g., open, closed, merged)""" - - title: str | None = None - """Title of the referenced item""" - - url: str | None = None - """URL to the referenced item on GitHub""" - - data: str | None = None - """Base64-encoded content""" - - mime_type: str | None = None - """MIME type of the inline data""" - - @staticmethod - def from_dict(obj: Any) -> 'SendAttachment': - assert isinstance(obj, dict) - type = SendAttachmentType(obj.get("type")) - display_name = from_union([from_str, from_none], obj.get("displayName")) - line_range = from_union([SendAttachmentFileLineRange.from_dict, from_none], obj.get("lineRange")) - path = from_union([from_str, from_none], obj.get("path")) - file_path = from_union([from_str, from_none], obj.get("filePath")) - selection = from_union([SendAttachmentSelectionDetails.from_dict, from_none], obj.get("selection")) - text = from_union([from_str, from_none], obj.get("text")) - number = from_union([from_int, from_none], obj.get("number")) - reference_type = from_union([SendAttachmentGithubReferenceTypeEnum, from_none], obj.get("referenceType")) - state = from_union([from_str, from_none], obj.get("state")) - title = from_union([from_str, from_none], obj.get("title")) - url = from_union([from_str, from_none], obj.get("url")) - data = from_union([from_str, from_none], obj.get("data")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - return SendAttachment(type, display_name, line_range, path, file_path, selection, text, number, reference_type, state, title, url, data, mime_type) - - def to_dict(self) -> dict: - result: dict = {} - result["type"] = to_enum(SendAttachmentType, self.type) - if self.display_name is not None: - result["displayName"] = from_union([from_str, from_none], self.display_name) - if self.line_range is not None: - result["lineRange"] = from_union([lambda x: to_class(SendAttachmentFileLineRange, x), from_none], self.line_range) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.file_path is not None: - result["filePath"] = from_union([from_str, from_none], self.file_path) - if self.selection is not None: - result["selection"] = from_union([lambda x: to_class(SendAttachmentSelectionDetails, x), from_none], self.selection) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.number is not None: - result["number"] = from_union([from_int, from_none], self.number) - if self.reference_type is not None: - result["referenceType"] = from_union([lambda x: to_enum(SendAttachmentGithubReferenceTypeEnum, x), from_none], self.reference_type) - if self.state is not None: - result["state"] = from_union([from_str, from_none], self.state) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) - if self.data is not None: - result["data"] = from_union([from_str, from_none], self.data) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SendAttachmentSelection: - """Code selection attachment from an editor""" - - display_name: str - """User-facing display name for the selection""" - - file_path: str - """Absolute path to the file containing the selection""" - - selection: SendAttachmentSelectionDetails - """Position range of the selection within the file""" - - text: str - """The selected text content""" - - type: SendAttachmentSelectionType - """Attachment type discriminator""" - - @staticmethod - def from_dict(obj: Any) -> 'SendAttachmentSelection': - assert isinstance(obj, dict) - display_name = from_str(obj.get("displayName")) - file_path = from_str(obj.get("filePath")) - selection = SendAttachmentSelectionDetails.from_dict(obj.get("selection")) - text = from_str(obj.get("text")) - type = SendAttachmentSelectionType(obj.get("type")) - return SendAttachmentSelection(display_name, file_path, selection, text, type) - - def to_dict(self) -> dict: - result: dict = {} - result["displayName"] = from_str(self.display_name) - result["filePath"] = from_str(self.file_path) - result["selection"] = to_class(SendAttachmentSelectionDetails, self.selection) - result["text"] = from_str(self.text) - result["type"] = to_enum(SendAttachmentSelectionType, self.type) - return result - -@dataclass -class SessionFSReadFileResult: - """File content as a UTF-8 string, or a filesystem error if the read failed.""" - - content: str - """File content as UTF-8 string""" - - error: SessionFSError | None = None - """Describes a filesystem error.""" - - @staticmethod - def from_dict(obj: Any) -> 'SessionFSReadFileResult': - assert isinstance(obj, dict) - content = from_str(obj.get("content")) - error = from_union([SessionFSError.from_dict, from_none], obj.get("error")) - return SessionFSReadFileResult(content, error) - - def to_dict(self) -> dict: - result: dict = {} - result["content"] = from_str(self.content) - if self.error is not None: - result["error"] = from_union([lambda x: to_class(SessionFSError, x), from_none], self.error) - return result + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + if self.error is not None: + result["error"] = from_union([lambda x: to_class(SessionFSError, x), from_none], self.error) + return result @dataclass class SessionFSReaddirResult: @@ -11917,107 +12585,6 @@ def to_dict(self) -> dict: result["agent"] = to_class(AgentInfo, self.agent) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SlashCommandInvocationResult: - """Result of invoking the slash command (text output, prompt to send to the agent, or - completion). - - Schema for the `SlashCommandTextResult` type. - - Schema for the `SlashCommandAgentPromptResult` type. - - Schema for the `SlashCommandCompletedResult` type. - - Schema for the `SlashCommandSelectSubcommandResult` type. - """ - kind: SlashCommandInvocationResultKind - """Text result discriminator - - Agent prompt result discriminator - - Completed result discriminator - - Select subcommand result discriminator - """ - markdown: bool | None = None - """Whether text contains Markdown""" - - preserve_ansi: bool | None = None - """Whether ANSI sequences should be preserved""" - - runtime_settings_changed: bool | None = None - """True when the invocation mutated user runtime settings; consumers caching settings should - refresh - """ - text: str | None = None - """Text output for the client to render""" - - display_prompt: str | None = None - """Prompt text to display to the user""" - - mode: SessionMode | None = None - """Optional target session mode for the agent prompt""" - - prompt: str | None = None - """Prompt to submit to the agent""" - - message: str | None = None - """Optional user-facing message describing the completed command""" - - command: str | None = None - """Parent command name that requires subcommand selection""" - - options: list[SlashCommandSelectSubcommandOption] | None = None - """Available subcommand options for the client to present""" - - title: str | None = None - """Human-readable title for the selection UI""" - - @staticmethod - def from_dict(obj: Any) -> 'SlashCommandInvocationResult': - assert isinstance(obj, dict) - kind = SlashCommandInvocationResultKind(obj.get("kind")) - markdown = from_union([from_bool, from_none], obj.get("markdown")) - preserve_ansi = from_union([from_bool, from_none], obj.get("preserveAnsi")) - runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - text = from_union([from_str, from_none], obj.get("text")) - display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) - mode = from_union([SessionMode, from_none], obj.get("mode")) - prompt = from_union([from_str, from_none], obj.get("prompt")) - message = from_union([from_str, from_none], obj.get("message")) - command = from_union([from_str, from_none], obj.get("command")) - options = from_union([lambda x: from_list(SlashCommandSelectSubcommandOption.from_dict, x), from_none], obj.get("options")) - title = from_union([from_str, from_none], obj.get("title")) - return SlashCommandInvocationResult(kind, markdown, preserve_ansi, runtime_settings_changed, text, display_prompt, mode, prompt, message, command, options, title) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(SlashCommandInvocationResultKind, self.kind) - if self.markdown is not None: - result["markdown"] = from_union([from_bool, from_none], self.markdown) - if self.preserve_ansi is not None: - result["preserveAnsi"] = from_union([from_bool, from_none], self.preserve_ansi) - if self.runtime_settings_changed is not None: - result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.display_prompt is not None: - result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) - if self.mode is not None: - result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode) - if self.prompt is not None: - result["prompt"] = from_union([from_str, from_none], self.prompt) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) - if self.command is not None: - result["command"] = from_union([from_str, from_none], self.command) - if self.options is not None: - result["options"] = from_union([lambda x: from_list(lambda x: to_class(SlashCommandSelectSubcommandOption, x), x), from_none], self.options) - if self.title is not None: - 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. @dataclass class TaskProgress: @@ -12451,7 +13018,7 @@ class APIKeyAuthInfo: host: str """Authentication host.""" - type: APIKeyAuthInfoType + type: ClassVar[str] = "api-key" """API-key authentication for non-GitHub LLM providers (e.g. when running BYOM-style).""" copilot_user: CopilotUserResponse | None = None @@ -12465,15 +13032,14 @@ def from_dict(obj: Any) -> 'APIKeyAuthInfo': assert isinstance(obj, dict) api_key = from_str(obj.get("apiKey")) host = from_str(obj.get("host")) - type = APIKeyAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return APIKeyAuthInfo(api_key, host, type, copilot_user) + return APIKeyAuthInfo(api_key, host, copilot_user) def to_dict(self) -> dict: result: dict = {} result["apiKey"] = from_str(self.api_key) result["host"] = from_str(self.host) - result["type"] = to_enum(APIKeyAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12486,7 +13052,7 @@ class CopilotAPITokenAuthInfo: host: Host """Authentication host (always the public GitHub host).""" - type: CopilotAPITokenAuthInfoType + type: ClassVar[str] = "copilot-api-token" """Direct Copilot API authentication via the `GITHUB_COPILOT_API_TOKEN` + `COPILOT_API_URL` environment-variable pair. The token itself is read from the environment by the runtime, not carried in this struct. @@ -12501,14 +13067,13 @@ class CopilotAPITokenAuthInfo: def from_dict(obj: Any) -> 'CopilotAPITokenAuthInfo': assert isinstance(obj, dict) host = Host(obj.get("host")) - type = CopilotAPITokenAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return CopilotAPITokenAuthInfo(host, type, copilot_user) + return CopilotAPITokenAuthInfo(host, copilot_user) def to_dict(self) -> dict: result: dict = {} result["host"] = to_enum(Host, self.host) - result["type"] = to_enum(CopilotAPITokenAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12527,7 +13092,7 @@ class EnvAuthInfo: token: str """The token value itself. Treat as a secret.""" - type: EnvAuthInfoType + type: ClassVar[str] = "env" """Personal access token (PAT) or server-to-server token sourced from an environment variable. """ @@ -12547,17 +13112,16 @@ def from_dict(obj: Any) -> 'EnvAuthInfo': env_var = from_str(obj.get("envVar")) host = from_str(obj.get("host")) token = from_str(obj.get("token")) - type = EnvAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) login = from_union([from_str, from_none], obj.get("login")) - return EnvAuthInfo(env_var, host, token, type, copilot_user, login) + return EnvAuthInfo(env_var, host, token, copilot_user, login) def to_dict(self) -> dict: result: dict = {} result["envVar"] = from_str(self.env_var) result["host"] = from_str(self.host) result["token"] = from_str(self.token) - result["type"] = to_enum(EnvAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) if self.login is not None: @@ -12578,7 +13142,7 @@ class GhCLIAuthInfo: token: str """The token returned by `gh auth token`. Treat as a secret.""" - type: GhCLIAuthInfoType + type: ClassVar[str] = "gh-cli" """Authentication via the `gh` CLI's saved credentials.""" copilot_user: CopilotUserResponse | None = None @@ -12593,16 +13157,15 @@ def from_dict(obj: Any) -> 'GhCLIAuthInfo': host = from_str(obj.get("host")) login = from_str(obj.get("login")) token = from_str(obj.get("token")) - type = GhCLIAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return GhCLIAuthInfo(host, login, token, type, copilot_user) + return GhCLIAuthInfo(host, login, token, copilot_user) def to_dict(self) -> dict: result: dict = {} result["host"] = from_str(self.host) result["login"] = from_str(self.login) result["token"] = from_str(self.token) - result["type"] = to_enum(GhCLIAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12618,7 +13181,7 @@ class HMACAuthInfo: host: Host """Authentication host. HMAC auth always targets the public GitHub host.""" - type: HMACAuthInfoType + type: ClassVar[str] = "hmac" """HMAC-based authentication used by GitHub-internal services.""" copilot_user: CopilotUserResponse | None = None @@ -12632,15 +13195,14 @@ def from_dict(obj: Any) -> 'HMACAuthInfo': assert isinstance(obj, dict) hmac = from_str(obj.get("hmac")) host = Host(obj.get("host")) - type = HMACAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return HMACAuthInfo(hmac, host, type, copilot_user) + return HMACAuthInfo(hmac, host, copilot_user) def to_dict(self) -> dict: result: dict = {} result["hmac"] = from_str(self.hmac) result["host"] = to_enum(Host, self.host) - result["type"] = to_enum(HMACAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12656,7 +13218,7 @@ class TokenAuthInfo: token: str """The token value itself. Treat as a secret.""" - type: TokenAuthInfoType + type: ClassVar[str] = "token" """SDK-side token authentication; the host configured the token directly via the SDK.""" copilot_user: CopilotUserResponse | None = None @@ -12670,15 +13232,14 @@ def from_dict(obj: Any) -> 'TokenAuthInfo': assert isinstance(obj, dict) host = from_str(obj.get("host")) token = from_str(obj.get("token")) - type = TokenAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return TokenAuthInfo(host, token, type, copilot_user) + return TokenAuthInfo(host, token, copilot_user) def to_dict(self) -> dict: result: dict = {} result["host"] = from_str(self.host) result["token"] = from_str(self.token) - result["type"] = to_enum(TokenAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12694,7 +13255,7 @@ class UserAuthInfo: login: str """OAuth user login.""" - type: UserAuthInfoType + type: ClassVar[str] = "user" """OAuth user authentication. The token itself is held in the runtime's secret token store (keyed by host+login) and is NOT carried in this struct. """ @@ -12709,23 +13270,42 @@ def from_dict(obj: Any) -> 'UserAuthInfo': assert isinstance(obj, dict) host = from_str(obj.get("host")) login = from_str(obj.get("login")) - type = UserAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return UserAuthInfo(host, login, type, copilot_user) + return UserAuthInfo(host, login, copilot_user) def to_dict(self) -> dict: result: dict = {} result["host"] = from_str(self.host) result["login"] = from_str(self.login) - result["type"] = to_enum(UserAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result -# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForLocationApproval: - """Approval to persist for this location +class PermissionDecisionApproveForIonApproval: + """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) + + Schema for the `PermissionDecisionApproveForSessionApprovalCommands` type. + + Schema for the `PermissionDecisionApproveForSessionApprovalRead` type. + + Schema for the `PermissionDecisionApproveForSessionApprovalWrite` type. + + Schema for the `PermissionDecisionApproveForSessionApprovalMcp` type. + + Schema for the `PermissionDecisionApproveForSessionApprovalMcpSampling` type. + + Schema for the `PermissionDecisionApproveForSessionApprovalMemory` type. + + Schema for the `PermissionDecisionApproveForSessionApprovalCustomTool` type. + + Schema for the `PermissionDecisionApproveForSessionApprovalExtensionManagement` type. + + Schema for the `PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess` + type. + + Approval to persist for this location Schema for the `PermissionDecisionApproveForLocationApprovalCommands` type. @@ -12745,8 +13325,15 @@ class PermissionDecisionApproveForLocationApproval: Schema for the `PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess` type. + + The approval to add as a session-scoped rule + + The approval to persist for this location """ - kind: ApprovalKind + command_identifiers: list[str] | None = None + """Command identifiers covered by this approval.""" + + kind: ApprovalKind | None = None """Approval scoped to specific command identifiers. Approval covering read-only filesystem operations. @@ -12765,9 +13352,6 @@ class PermissionDecisionApproveForLocationApproval: Approval covering an extension's request to access a permission-gated capability. """ - command_identifiers: list[str] | None = None - """Command identifiers covered by this approval.""" - server_name: str | None = None """MCP server name.""" @@ -12783,22 +13367,26 @@ class PermissionDecisionApproveForLocationApproval: extension_name: str | None = None """Extension name.""" + external_ref_marker_external_ref_user_tool_session_approval: str | None = None + @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApproval': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForIonApproval': assert isinstance(obj, dict) - kind = ApprovalKind(obj.get("kind")) command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) + kind = from_union([ApprovalKind, from_none], obj.get("kind")) server_name = from_union([from_str, from_none], obj.get("serverName")) tool_name = from_union([from_none, from_str], obj.get("toolName")) operation = from_union([from_str, from_none], obj.get("operation")) extension_name = from_union([from_str, from_none], obj.get("extensionName")) - return PermissionDecisionApproveForLocationApproval(kind, command_identifiers, server_name, tool_name, operation, extension_name) + external_ref_marker_external_ref_user_tool_session_approval = from_union([from_str, from_none], obj.get("__externalRefMarker___ExternalRef_UserToolSessionApproval")) + return PermissionDecisionApproveForIonApproval(command_identifiers, kind, server_name, tool_name, operation, extension_name, external_ref_marker_external_ref_user_tool_session_approval) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(ApprovalKind, self.kind) if self.command_identifiers is not None: result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) + if self.kind is not None: + result["kind"] = from_union([lambda x: to_enum(ApprovalKind, x), from_none], self.kind) if self.server_name is not None: result["serverName"] = from_union([from_str, from_none], self.server_name) if self.tool_name is not None: @@ -12807,1050 +13395,411 @@ def to_dict(self) -> dict: result["operation"] = from_union([from_str, from_none], self.operation) if self.extension_name is not None: result["extensionName"] = from_union([from_str, from_none], self.extension_name) + if self.external_ref_marker_external_ref_user_tool_session_approval is not None: + result["__externalRefMarker___ExternalRef_UserToolSessionApproval"] = from_union([from_str, from_none], self.external_ref_marker_external_ref_user_tool_session_approval) return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForIonApproval: - """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) - - Schema for the `PermissionDecisionApproveForSessionApprovalCommands` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalRead` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalWrite` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMcp` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMcpSampling` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMemory` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalCustomTool` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalExtensionManagement` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess` - type. - - Approval to persist for this location - - Schema for the `PermissionDecisionApproveForLocationApprovalCommands` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalRead` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalWrite` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalMcp` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalMcpSampling` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalMemory` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalCustomTool` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalExtensionManagement` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess` - type. - - The approval to add as a session-scoped rule - - The approval to persist for this location - """ - command_identifiers: list[str] | None = None - """Command identifiers covered by this approval.""" - - kind: ApprovalKind | None = None - """Approval scoped to specific command identifiers. - - Approval covering read-only filesystem operations. - - Approval covering filesystem write operations. - - Approval covering an MCP tool. - - Approval covering MCP sampling requests for a server. - - Approval covering writes to long-term memory. - - Approval covering a custom tool. - - Approval covering extension lifecycle operations such as enable, disable, or reload. - - Approval covering an extension's request to access a permission-gated capability. +class HandlePendingToolCallRequest: + """Pending external tool call request ID, with the tool result or an error describing why it + failed. """ - server_name: str | None = None - """MCP server name.""" - - tool_name: str | None = None - """MCP tool name, or null to cover every tool on the server. + request_id: str + """Request ID of the pending tool call""" - Custom tool name. - """ - operation: str | None = None - """Optional operation identifier; when omitted, the approval covers all extension management - operations. - """ - extension_name: str | None = None - """Extension name.""" + error: str | None = None + """Error message if the tool call failed""" - external_ref_marker_external_ref_user_tool_session_approval: str | None = None + result: ExternalToolTextResultForLlm | str | None = None + """Tool call result (string or expanded result object)""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForIonApproval': + def from_dict(obj: Any) -> 'HandlePendingToolCallRequest': assert isinstance(obj, dict) - command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) - kind = from_union([ApprovalKind, from_none], obj.get("kind")) - server_name = from_union([from_str, from_none], obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - operation = from_union([from_str, from_none], obj.get("operation")) - extension_name = from_union([from_str, from_none], obj.get("extensionName")) - external_ref_marker_external_ref_user_tool_session_approval = from_union([from_str, from_none], obj.get("__externalRefMarker___ExternalRef_UserToolSessionApproval")) - return PermissionDecisionApproveForIonApproval(command_identifiers, kind, server_name, tool_name, operation, extension_name, external_ref_marker_external_ref_user_tool_session_approval) + request_id = from_str(obj.get("requestId")) + error = from_union([from_str, from_none], obj.get("error")) + result = from_union([ExternalToolTextResultForLlm.from_dict, from_str, from_none], obj.get("result")) + return HandlePendingToolCallRequest(request_id, error, result) def to_dict(self) -> dict: result: dict = {} - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) - if self.kind is not None: - result["kind"] = from_union([lambda x: to_enum(ApprovalKind, x), from_none], self.kind) - if self.server_name is not None: - result["serverName"] = from_union([from_str, from_none], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.operation is not None: - result["operation"] = from_union([from_str, from_none], self.operation) - if self.extension_name is not None: - result["extensionName"] = from_union([from_str, from_none], self.extension_name) - if self.external_ref_marker_external_ref_user_tool_session_approval is not None: - result["__externalRefMarker___ExternalRef_UserToolSessionApproval"] = from_union([from_str, from_none], self.external_ref_marker_external_ref_user_tool_session_approval) + result["requestId"] = from_str(self.request_id) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.result is not None: + result["result"] = from_union([lambda x: to_class(ExternalToolTextResultForLlm, x), from_str, from_none], self.result) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForSessionApproval: - """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) - - Schema for the `PermissionDecisionApproveForSessionApprovalCommands` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalRead` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalWrite` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMcp` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMcpSampling` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMemory` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalCustomTool` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalExtensionManagement` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess` - type. - """ - kind: ApprovalKind - """Approval scoped to specific command identifiers. - - Approval covering read-only filesystem operations. - - Approval covering filesystem write operations. - - Approval covering an MCP tool. - - Approval covering MCP sampling requests for a server. +class InstalledPlugin: + """Schema for the `InstalledPlugin` type.""" - Approval covering writes to long-term memory. + enabled: bool + """Whether the plugin is currently enabled""" - Approval covering a custom tool. + installed_at: str + """Installation timestamp""" - Approval covering extension lifecycle operations such as enable, disable, or reload. + marketplace: str + """Marketplace the plugin came from (empty string for direct repo installs)""" - Approval covering an extension's request to access a permission-gated capability. - """ - command_identifiers: list[str] | None = None - """Command identifiers covered by this approval.""" + name: str + """Plugin name""" - server_name: str | None = None - """MCP server name.""" + cache_path: str | None = None + """Path where the plugin is cached locally""" - tool_name: str | None = None - """MCP tool name, or null to cover every tool on the server. + source: InstalledPluginSource | str | None = None + """Source for direct repo installs (when marketplace is empty)""" - Custom tool name. - """ - operation: str | None = None - """Optional operation identifier; when omitted, the approval covers all extension management - operations. - """ - extension_name: str | None = None - """Extension name.""" + version: str | None = None + """Version installed (if available)""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApproval': + def from_dict(obj: Any) -> 'InstalledPlugin': assert isinstance(obj, dict) - kind = ApprovalKind(obj.get("kind")) - command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) - server_name = from_union([from_str, from_none], obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - operation = from_union([from_str, from_none], obj.get("operation")) - extension_name = from_union([from_str, from_none], obj.get("extensionName")) - return PermissionDecisionApproveForSessionApproval(kind, command_identifiers, server_name, tool_name, operation, extension_name) + enabled = from_bool(obj.get("enabled")) + installed_at = from_str(obj.get("installed_at")) + marketplace = from_str(obj.get("marketplace")) + name = from_str(obj.get("name")) + cache_path = from_union([from_str, from_none], obj.get("cache_path")) + source = from_union([InstalledPluginSource.from_dict, from_str, from_none], obj.get("source")) + version = from_union([from_str, from_none], obj.get("version")) + return InstalledPlugin(enabled, installed_at, marketplace, name, cache_path, source, version) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(ApprovalKind, self.kind) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) - if self.server_name is not None: - result["serverName"] = from_union([from_str, from_none], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.operation is not None: - result["operation"] = from_union([from_str, from_none], self.operation) - if self.extension_name is not None: - result["extensionName"] = from_union([from_str, from_none], self.extension_name) + result["enabled"] = from_bool(self.enabled) + result["installed_at"] = from_str(self.installed_at) + result["marketplace"] = from_str(self.marketplace) + result["name"] = from_str(self.name) + if self.cache_path is not None: + result["cache_path"] = from_union([from_str, from_none], self.cache_path) + if self.source is not None: + result["source"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str, from_none], self.source) + if self.version is not None: + result["version"] = from_union([from_str, from_none], self.version) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionsLocationsAddToolApprovalDetails: - """Tool approval to persist and apply - - Schema for the `PermissionsLocationsAddToolApprovalDetailsCommands` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsRead` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsWrite` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsMcp` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsMcpSampling` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsMemory` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsCustomTool` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsExtensionManagement` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess` type. - """ - kind: ApprovalKind - """Approval scoped to specific command identifiers. +class SessionInstalledPlugin: + """Schema for the `SessionInstalledPlugin` type.""" - Approval covering read-only filesystem operations. + enabled: bool + """Whether the plugin is currently enabled""" - Approval covering filesystem write operations. + installed_at: str + """Installation timestamp (ISO-8601)""" - Approval covering an MCP tool. + marketplace: str + """Marketplace the plugin came from (empty string for direct repo installs)""" - Approval covering MCP sampling requests for a server. + name: str + """Plugin name""" - Approval covering writes to long-term memory. + cache_path: str | None = None + """Path where the plugin is cached locally""" - Approval covering a custom tool. + source: SessionInstalledPluginSource | str | None = None + """Source descriptor for direct repo installs (when marketplace is empty)""" - Approval covering extension lifecycle operations such as enable, disable, or reload. + version: str | None = None + """Installed version, if known""" - Approval covering an extension's request to access a permission-gated capability. - """ - command_identifiers: list[str] | None = None - """Command identifiers covered by this approval.""" - - server_name: str | None = None - """MCP server name.""" - - tool_name: str | None = None - """MCP tool name, or null to cover every tool on the server. - - Custom tool name. - """ - operation: str | None = None - """Optional operation identifier; when omitted, the approval covers all extension management - operations. - """ - extension_name: str | None = None - """Extension name.""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetails': - assert isinstance(obj, dict) - kind = ApprovalKind(obj.get("kind")) - command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) - server_name = from_union([from_str, from_none], obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - operation = from_union([from_str, from_none], obj.get("operation")) - extension_name = from_union([from_str, from_none], obj.get("extensionName")) - return PermissionsLocationsAddToolApprovalDetails(kind, command_identifiers, server_name, tool_name, operation, extension_name) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(ApprovalKind, self.kind) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) - if self.server_name is not None: - result["serverName"] = from_union([from_str, from_none], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.operation is not None: - result["operation"] = from_union([from_str, from_none], self.operation) - if self.extension_name is not None: - result["extensionName"] = from_union([from_str, from_none], self.extension_name) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ExternalToolTextResultForLlm: - """Expanded external tool result payload""" - - text_result_for_llm: str - """Text result returned to the model""" - - binary_results_for_llm: list[ExternalToolTextResultForLlmBinaryResultsForLlm] | None = None - """Base64-encoded binary results returned to the model""" - - contents: list[ExternalToolTextResultForLlmContent] | None = None - """Structured content blocks from the tool""" - - error: str | None = None - """Optional error message for failed executions""" - - result_type: str | None = None - """Execution outcome classification. Optional for back-compat; normalized to 'success' (or - 'failure' when error is present) when missing or unrecognized. - """ - session_log: str | None = None - """Detailed log content for timeline display""" - - tool_telemetry: dict[str, Any] | None = None - """Optional tool-specific telemetry""" - - @staticmethod - def from_dict(obj: Any) -> 'ExternalToolTextResultForLlm': - assert isinstance(obj, dict) - text_result_for_llm = from_str(obj.get("textResultForLlm")) - binary_results_for_llm = from_union([lambda x: from_list(ExternalToolTextResultForLlmBinaryResultsForLlm.from_dict, x), from_none], obj.get("binaryResultsForLlm")) - contents = from_union([lambda x: from_list(ExternalToolTextResultForLlmContent.from_dict, x), from_none], obj.get("contents")) - error = from_union([from_str, from_none], obj.get("error")) - result_type = from_union([from_str, from_none], obj.get("resultType")) - session_log = from_union([from_str, from_none], obj.get("sessionLog")) - tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry")) - return ExternalToolTextResultForLlm(text_result_for_llm, binary_results_for_llm, contents, error, result_type, session_log, tool_telemetry) - - def to_dict(self) -> dict: - result: dict = {} - result["textResultForLlm"] = from_str(self.text_result_for_llm) - if self.binary_results_for_llm is not None: - result["binaryResultsForLlm"] = from_union([lambda x: from_list(lambda x: to_class(ExternalToolTextResultForLlmBinaryResultsForLlm, x), x), from_none], self.binary_results_for_llm) - if self.contents is not None: - result["contents"] = from_union([lambda x: from_list(lambda x: to_class(ExternalToolTextResultForLlmContent, x), x), from_none], self.contents) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.result_type is not None: - result["resultType"] = from_union([from_str, from_none], self.result_type) - if self.session_log is not None: - result["sessionLog"] = from_union([from_str, from_none], self.session_log) - if self.tool_telemetry is not None: - result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class InstalledPlugin: - """Schema for the `InstalledPlugin` type.""" - - enabled: bool - """Whether the plugin is currently enabled""" - - installed_at: str - """Installation timestamp""" - - marketplace: str - """Marketplace the plugin came from (empty string for direct repo installs)""" - - name: str - """Plugin name""" - - cache_path: str | None = None - """Path where the plugin is cached locally""" - - source: InstalledPluginSource | str | None = None - """Source for direct repo installs (when marketplace is empty)""" - - version: str | None = None - """Version installed (if available)""" - - @staticmethod - def from_dict(obj: Any) -> 'InstalledPlugin': - assert isinstance(obj, dict) - enabled = from_bool(obj.get("enabled")) - installed_at = from_str(obj.get("installed_at")) - marketplace = from_str(obj.get("marketplace")) - name = from_str(obj.get("name")) - cache_path = from_union([from_str, from_none], obj.get("cache_path")) - source = from_union([InstalledPluginSource.from_dict, from_str, from_none], obj.get("source")) - version = from_union([from_str, from_none], obj.get("version")) - return InstalledPlugin(enabled, installed_at, marketplace, name, cache_path, source, version) - - def to_dict(self) -> dict: - result: dict = {} - result["enabled"] = from_bool(self.enabled) - result["installed_at"] = from_str(self.installed_at) - result["marketplace"] = from_str(self.marketplace) - result["name"] = from_str(self.name) - if self.cache_path is not None: - result["cache_path"] = from_union([from_str, from_none], self.cache_path) - if self.source is not None: - result["source"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str, from_none], self.source) - if self.version is not None: - result["version"] = from_union([from_str, from_none], self.version) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionInstalledPlugin: - """Schema for the `SessionInstalledPlugin` type.""" - - enabled: bool - """Whether the plugin is currently enabled""" - - installed_at: str - """Installation timestamp (ISO-8601)""" - - marketplace: str - """Marketplace the plugin came from (empty string for direct repo installs)""" - - name: str - """Plugin name""" - - cache_path: str | None = None - """Path where the plugin is cached locally""" - - source: SessionInstalledPluginSource | str | None = None - """Source descriptor for direct repo installs (when marketplace is empty)""" - - version: str | None = None - """Installed version, if known""" - - @staticmethod - def from_dict(obj: Any) -> 'SessionInstalledPlugin': - assert isinstance(obj, dict) - enabled = from_bool(obj.get("enabled")) - installed_at = from_str(obj.get("installed_at")) - marketplace = from_str(obj.get("marketplace")) - name = from_str(obj.get("name")) - cache_path = from_union([from_str, from_none], obj.get("cache_path")) - source = from_union([SessionInstalledPluginSource.from_dict, from_str, from_none], obj.get("source")) - version = from_union([from_str, from_none], obj.get("version")) - return SessionInstalledPlugin(enabled, installed_at, marketplace, name, cache_path, source, version) - - def to_dict(self) -> dict: - result: dict = {} - result["enabled"] = from_bool(self.enabled) - result["installed_at"] = from_str(self.installed_at) - result["marketplace"] = from_str(self.marketplace) - result["name"] = from_str(self.name) - if self.cache_path is not None: - result["cache_path"] = from_union([from_str, from_none], self.cache_path) - if self.source is not None: - result["source"] = from_union([lambda x: to_class(SessionInstalledPluginSource, x), from_str, from_none], self.source) - if self.version is not None: - result["version"] = from_union([from_str, from_none], self.version) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionEnrichMetadataResult: - """The same metadata records, with summary and context fields backfilled where available.""" - - sessions: list[SessionMetadata] - """Same records, with summary and context backfilled""" - - @staticmethod - def from_dict(obj: Any) -> 'SessionEnrichMetadataResult': - assert isinstance(obj, dict) - sessions = from_list(SessionMetadata.from_dict, obj.get("sessions")) - return SessionEnrichMetadataResult(sessions) - - def to_dict(self) -> dict: - result: dict = {} - result["sessions"] = from_list(lambda x: to_class(SessionMetadata, x), self.sessions) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionList: - """Persisted sessions matching the filter, ordered most-recently-modified first.""" - - sessions: list[SessionMetadata] - """Sessions ordered most-recently-modified first""" - - @staticmethod - def from_dict(obj: Any) -> 'SessionList': - assert isinstance(obj, dict) - sessions = from_list(SessionMetadata.from_dict, obj.get("sessions")) - return SessionList(sessions) - - def to_dict(self) -> dict: - result: dict = {} - result["sessions"] = from_list(lambda x: to_class(SessionMetadata, x), self.sessions) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionsEnrichMetadataRequest: - """Session metadata records to enrich with summary and context information.""" - - sessions: list[SessionMetadata] - """Session metadata records to enrich. Records that already have summary and context are - returned unchanged. - """ - - @staticmethod - def from_dict(obj: Any) -> 'SessionsEnrichMetadataRequest': - assert isinstance(obj, dict) - sessions = from_list(SessionMetadata.from_dict, obj.get("sessions")) - return SessionsEnrichMetadataRequest(sessions) - - def to_dict(self) -> dict: - result: dict = {} - result["sessions"] = from_list(lambda x: to_class(SessionMetadata, x), self.sessions) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionMetadataSnapshot: - """Point-in-time snapshot of slow-changing session identifier and state fields""" - - already_in_use: bool - """True when the session was detected to be in use by another process at construction time. - Local consumers may surface a confirmation prompt before fully attaching. Always false - for new sessions. - """ - current_mode: MetadataSnapshotCurrentMode - """The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot')""" - - is_remote: bool - """Whether this is a remote session (i.e., one whose runtime executes elsewhere and is - steered through this process) - """ - modified_time: datetime - """ISO 8601 timestamp of when the session's persisted state was last modified on disk. For - new sessions, equals startTime. For resumed sessions, reflects the previous modification - time at construction. - """ - session_id: str - """The unique identifier of the session""" - - start_time: datetime - """ISO 8601 timestamp of when the session started""" - - working_directory: str - """Absolute path to the session's current working directory""" - - initial_name: str | None = None - """User-provided name supplied at session construction (via `--name`), if any. Immutable - after construction. - """ - remote_metadata: MetadataSnapshotRemoteMetadata | None = None - """Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are - immutable for the lifetime of the session. - """ - selected_model: str | None = None - """Currently selected model identifier, if any""" - - summary: str | None = None - """Short human-readable summary of the session, if known. Omitted when no summary has been - generated. - """ - workspace: WorkspaceSummary | None = None - """Public-facing workspace metadata for this session, or null if the session has no - associated workspace. Excludes runtime-internal fields (GitHub IDs, summary count, - internal flags). - """ - workspace_path: str | None = None - """Absolute path to the session's workspace directory on disk, or null if the session has no - associated workspace - """ - - @staticmethod - def from_dict(obj: Any) -> 'SessionMetadataSnapshot': - assert isinstance(obj, dict) - already_in_use = from_bool(obj.get("alreadyInUse")) - current_mode = MetadataSnapshotCurrentMode(obj.get("currentMode")) - is_remote = from_bool(obj.get("isRemote")) - modified_time = from_datetime(obj.get("modifiedTime")) - session_id = from_str(obj.get("sessionId")) - start_time = from_datetime(obj.get("startTime")) - working_directory = from_str(obj.get("workingDirectory")) - initial_name = from_union([from_str, from_none], obj.get("initialName")) - remote_metadata = from_union([MetadataSnapshotRemoteMetadata.from_dict, from_none], obj.get("remoteMetadata")) - selected_model = from_union([from_str, from_none], obj.get("selectedModel")) - summary = from_union([from_str, from_none], obj.get("summary")) - workspace = from_union([WorkspaceSummary.from_dict, from_none], obj.get("workspace")) - workspace_path = from_union([from_none, from_str], obj.get("workspacePath")) - return SessionMetadataSnapshot(already_in_use, current_mode, is_remote, modified_time, session_id, start_time, working_directory, initial_name, remote_metadata, selected_model, summary, workspace, workspace_path) - - def to_dict(self) -> dict: - result: dict = {} - result["alreadyInUse"] = from_bool(self.already_in_use) - result["currentMode"] = to_enum(MetadataSnapshotCurrentMode, self.current_mode) - result["isRemote"] = from_bool(self.is_remote) - result["modifiedTime"] = self.modified_time.isoformat() - result["sessionId"] = from_str(self.session_id) - result["startTime"] = self.start_time.isoformat() - result["workingDirectory"] = from_str(self.working_directory) - if self.initial_name is not None: - result["initialName"] = from_union([from_str, from_none], self.initial_name) - if self.remote_metadata is not None: - result["remoteMetadata"] = from_union([lambda x: to_class(MetadataSnapshotRemoteMetadata, x), from_none], self.remote_metadata) - if self.selected_model is not None: - result["selectedModel"] = from_union([from_str, from_none], self.selected_model) - if self.summary is not None: - result["summary"] = from_union([from_str, from_none], self.summary) - if self.workspace is not None: - result["workspace"] = from_union([lambda x: to_class(WorkspaceSummary, x), from_none], self.workspace) - result["workspacePath"] = from_union([from_none, from_str], self.workspace_path) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionsConfigureParams: - """Patch of permission policy fields to apply (omit a field to leave it unchanged).""" - - additional_content_exclusion_policies: list[PermissionsConfigureAdditionalContentExclusionPolicy] | None = None - """If specified, replaces the host-supplied GitHub Content Exclusion policies on the session - (combined with natively-discovered policies when evaluating tool/file access). Omit to - leave the current policies unchanged. - """ - approve_all_read_permission_requests: bool | None = None - """If specified, sets whether path/URL read permission requests are auto-approved. Omit to - leave the current value unchanged. - """ - approve_all_tool_permission_requests: bool | None = None - """If specified, sets whether tool permission requests are auto-approved without prompting. - Omit to leave the current value unchanged. - """ - paths: PermissionPathsConfig | None = None - """If specified, replaces the session's path-permission policy. The runtime constructs the - appropriate PathManager based on these inputs (rooted at the session's working - directory). Omit to leave the current path policy unchanged. - """ - rules: PermissionRulesSet | None = None - """If specified, replaces the session's approved/denied permission rules. Omit to leave the - current rules unchanged. - """ - urls: PermissionUrlsConfig | None = None - """If specified, replaces the session's URL-permission policy. The runtime constructs a - fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy - unchanged. - """ - - @staticmethod - def from_dict(obj: Any) -> 'PermissionsConfigureParams': - assert isinstance(obj, dict) - additional_content_exclusion_policies = from_union([lambda x: from_list(PermissionsConfigureAdditionalContentExclusionPolicy.from_dict, x), from_none], obj.get("additionalContentExclusionPolicies")) - approve_all_read_permission_requests = from_union([from_bool, from_none], obj.get("approveAllReadPermissionRequests")) - approve_all_tool_permission_requests = from_union([from_bool, from_none], obj.get("approveAllToolPermissionRequests")) - paths = from_union([PermissionPathsConfig.from_dict, from_none], obj.get("paths")) - rules = from_union([PermissionRulesSet.from_dict, from_none], obj.get("rules")) - urls = from_union([PermissionUrlsConfig.from_dict, from_none], obj.get("urls")) - return PermissionsConfigureParams(additional_content_exclusion_policies, approve_all_read_permission_requests, approve_all_tool_permission_requests, paths, rules, urls) - - def to_dict(self) -> dict: - result: dict = {} - if self.additional_content_exclusion_policies is not None: - result["additionalContentExclusionPolicies"] = from_union([lambda x: from_list(lambda x: to_class(PermissionsConfigureAdditionalContentExclusionPolicy, x), x), from_none], self.additional_content_exclusion_policies) - if self.approve_all_read_permission_requests is not None: - result["approveAllReadPermissionRequests"] = from_union([from_bool, from_none], self.approve_all_read_permission_requests) - if self.approve_all_tool_permission_requests is not None: - result["approveAllToolPermissionRequests"] = from_union([from_bool, from_none], self.approve_all_tool_permission_requests) - if self.paths is not None: - result["paths"] = from_union([lambda x: to_class(PermissionPathsConfig, x), from_none], self.paths) - if self.rules is not None: - result["rules"] = from_union([lambda x: to_class(PermissionRulesSet, x), from_none], self.rules) - if self.urls is not None: - result["urls"] = from_union([lambda x: to_class(PermissionUrlsConfig, x), from_none], self.urls) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SendRequest: - """Parameters for sending a user message to the session""" - - prompt: str - """The user message text""" - - agent_mode: SendAgentMode | None = None - """The UI mode the agent was in when this message was sent. Defaults to the session's - current mode. - """ - attachments: list[SendAttachment] | None = None - """Optional attachments (files, directories, selections, blobs, GitHub references) to - include with the message - """ - billable: bool | None = None - """If false, this message will not trigger a Premium Request Unit charge. User messages - default to billable. - """ - display_prompt: str | None = None - """If provided, this is shown in the timeline instead of `prompt`""" - - mode: SendMode | None = None - """How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` - interjects during an in-progress turn. - """ - prepend: bool | None = None - """If true, adds the message to the front of the queue instead of the end""" - - request_headers: dict[str, str] | None = None - """Custom HTTP headers to include in outbound model requests for this turn. Merged with - session-level provider headers; per-turn headers augment and overwrite session-level - headers with the same key. - """ - required_tool: str | None = None - """If set, the request will fail if the named tool is not available when this message is - among the user messages at the start of the current exchange - """ - source: Any = None - """Optional provenance tag copied to the resulting user.message event. Supported values are - `system`, `command-*`, and `schedule-*`. - """ - traceparent: str | None = None - """W3C Trace Context traceparent header for distributed tracing of this agent turn""" - - tracestate: str | None = None - """W3C Trace Context tracestate header for distributed tracing""" - - wait: bool | None = None - """If true, await completion of the agentic loop for this message before returning. Defaults - to false (fire-and-forget). When true, the result still contains the same `messageId`; - the caller can rely on the agent having processed the message before the call resolves. - """ - - @staticmethod - def from_dict(obj: Any) -> 'SendRequest': - assert isinstance(obj, dict) - prompt = from_str(obj.get("prompt")) - agent_mode = from_union([SendAgentMode, from_none], obj.get("agentMode")) - attachments = from_union([lambda x: from_list(SendAttachment.from_dict, x), from_none], obj.get("attachments")) - billable = from_union([from_bool, from_none], obj.get("billable")) - display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) - mode = from_union([SendMode, from_none], obj.get("mode")) - prepend = from_union([from_bool, from_none], obj.get("prepend")) - request_headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("requestHeaders")) - required_tool = from_union([from_str, from_none], obj.get("requiredTool")) - source = obj.get("source") - traceparent = from_union([from_str, from_none], obj.get("traceparent")) - tracestate = from_union([from_str, from_none], obj.get("tracestate")) - wait = from_union([from_bool, from_none], obj.get("wait")) - return SendRequest(prompt, agent_mode, attachments, billable, display_prompt, mode, prepend, request_headers, required_tool, source, traceparent, tracestate, wait) + @staticmethod + def from_dict(obj: Any) -> 'SessionInstalledPlugin': + assert isinstance(obj, dict) + enabled = from_bool(obj.get("enabled")) + installed_at = from_str(obj.get("installed_at")) + marketplace = from_str(obj.get("marketplace")) + name = from_str(obj.get("name")) + cache_path = from_union([from_str, from_none], obj.get("cache_path")) + source = from_union([SessionInstalledPluginSource.from_dict, from_str, from_none], obj.get("source")) + version = from_union([from_str, from_none], obj.get("version")) + return SessionInstalledPlugin(enabled, installed_at, marketplace, name, cache_path, source, version) def to_dict(self) -> dict: result: dict = {} - result["prompt"] = from_str(self.prompt) - if self.agent_mode is not None: - result["agentMode"] = from_union([lambda x: to_enum(SendAgentMode, x), from_none], self.agent_mode) - if self.attachments is not None: - result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(SendAttachment, x), x), from_none], self.attachments) - if self.billable is not None: - result["billable"] = from_union([from_bool, from_none], self.billable) - if self.display_prompt is not None: - result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) - if self.mode is not None: - result["mode"] = from_union([lambda x: to_enum(SendMode, x), from_none], self.mode) - if self.prepend is not None: - result["prepend"] = from_union([from_bool, from_none], self.prepend) - if self.request_headers is not None: - result["requestHeaders"] = from_union([lambda x: from_dict(from_str, x), from_none], self.request_headers) - if self.required_tool is not None: - result["requiredTool"] = from_union([from_str, from_none], self.required_tool) + result["enabled"] = from_bool(self.enabled) + result["installed_at"] = from_str(self.installed_at) + result["marketplace"] = from_str(self.marketplace) + result["name"] = from_str(self.name) + if self.cache_path is not None: + result["cache_path"] = from_union([from_str, from_none], self.cache_path) if self.source is not None: - result["source"] = self.source - if self.traceparent is not None: - result["traceparent"] = from_union([from_str, from_none], self.traceparent) - if self.tracestate is not None: - result["tracestate"] = from_union([from_str, from_none], self.tracestate) - if self.wait is not None: - result["wait"] = from_union([from_bool, from_none], self.wait) + result["source"] = from_union([lambda x: to_class(SessionInstalledPluginSource, x), from_str, from_none], self.source) + if self.version is not None: + result["version"] = from_union([from_str, from_none], self.version) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class TasksGetProgressResult: - """Progress information for the task, or null when no task with that ID is tracked.""" +class SessionEnrichMetadataResult: + """The same metadata records, with summary and context fields backfilled where available.""" - progress: TaskProgress | None = None - """Progress information for the task, discriminated by type. Returns null when no task with - this ID is currently tracked. - """ + sessions: list[SessionMetadata] + """Same records, with summary and context backfilled""" @staticmethod - def from_dict(obj: Any) -> 'TasksGetProgressResult': + def from_dict(obj: Any) -> 'SessionEnrichMetadataResult': assert isinstance(obj, dict) - progress = from_union([TaskProgress.from_dict, from_none], obj.get("progress")) - return TasksGetProgressResult(progress) + sessions = from_list(SessionMetadata.from_dict, obj.get("sessions")) + return SessionEnrichMetadataResult(sessions) def to_dict(self) -> dict: result: dict = {} - if self.progress is not None: - result["progress"] = from_union([lambda x: to_class(TaskProgress, x), from_none], self.progress) + result["sessions"] = from_list(lambda x: to_class(SessionMetadata, x), self.sessions) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class UIElicitationSchema: - """JSON Schema describing the form fields to present to the user""" - - properties: dict[str, UIElicitationSchemaProperty] - """Form field definitions, keyed by field name""" - - type: UIElicitationSchemaType - """Schema type indicator (always 'object')""" +class SessionList: + """Persisted sessions matching the filter, ordered most-recently-modified first.""" - required: list[str] | None = None - """List of required field names""" + sessions: list[SessionMetadata] + """Sessions ordered most-recently-modified first""" @staticmethod - def from_dict(obj: Any) -> 'UIElicitationSchema': + def from_dict(obj: Any) -> 'SessionList': assert isinstance(obj, dict) - properties = from_dict(UIElicitationSchemaProperty.from_dict, obj.get("properties")) - type = UIElicitationSchemaType(obj.get("type")) - required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("required")) - return UIElicitationSchema(properties, type, required) + sessions = from_list(SessionMetadata.from_dict, obj.get("sessions")) + return SessionList(sessions) def to_dict(self) -> dict: result: dict = {} - result["properties"] = from_dict(lambda x: to_class(UIElicitationSchemaProperty, x), self.properties) - result["type"] = to_enum(UIElicitationSchemaType, self.type) - if self.required is not None: - result["required"] = from_union([lambda x: from_list(from_str, x), from_none], self.required) + result["sessions"] = from_list(lambda x: to_class(SessionMetadata, x), self.sessions) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AuthInfo: - """The new auth credentials to install on the session. When omitted or `undefined`, the call - is a no-op and the session's existing credentials are preserved. The runtime stores the - value verbatim and uses it for outbound model/API requests; it does NOT re-validate or - re-fetch the associated Copilot user response. Several variants carry secret material; - treat this method's params as containing secrets at rest and in transit. - - Schema for the `HMACAuthInfo` type. - - Schema for the `EnvAuthInfo` type. +class SessionsEnrichMetadataRequest: + """Session metadata records to enrich with summary and context information.""" - Schema for the `TokenAuthInfo` type. + sessions: list[SessionMetadata] + """Session metadata records to enrich. Records that already have summary and context are + returned unchanged. + """ - Schema for the `CopilotApiTokenAuthInfo` type. + @staticmethod + def from_dict(obj: Any) -> 'SessionsEnrichMetadataRequest': + assert isinstance(obj, dict) + sessions = from_list(SessionMetadata.from_dict, obj.get("sessions")) + return SessionsEnrichMetadataRequest(sessions) - Schema for the `UserAuthInfo` type. + def to_dict(self) -> dict: + result: dict = {} + result["sessions"] = from_list(lambda x: to_class(SessionMetadata, x), self.sessions) + return result - Schema for the `GhCliAuthInfo` type. +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionMetadataSnapshot: + """Point-in-time snapshot of slow-changing session identifier and state fields""" - Schema for the `ApiKeyAuthInfo` type. + already_in_use: bool + """True when the session was detected to be in use by another process at construction time. + Local consumers may surface a confirmation prompt before fully attaching. Always false + for new sessions. """ - host: str - """Authentication host. HMAC auth always targets the public GitHub host. - - Authentication host (e.g. https://github.com or a GHES host). - - Authentication host. + current_mode: MetadataSnapshotCurrentMode + """The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot')""" - Authentication host (always the public GitHub host). + is_remote: bool + """Whether this is a remote session (i.e., one whose runtime executes elsewhere and is + steered through this process) """ - type: AuthInfoType - """HMAC-based authentication used by GitHub-internal services. - - Personal access token (PAT) or server-to-server token sourced from an environment - variable. - - SDK-side token authentication; the host configured the token directly via the SDK. - - Direct Copilot API authentication via the `GITHUB_COPILOT_API_TOKEN` + `COPILOT_API_URL` - environment-variable pair. The token itself is read from the environment by the runtime, - not carried in this struct. + modified_time: datetime + """ISO 8601 timestamp of when the session's persisted state was last modified on disk. For + new sessions, equals startTime. For resumed sessions, reflects the previous modification + time at construction. + """ + session_id: str + """The unique identifier of the session""" - OAuth user authentication. The token itself is held in the runtime's secret token store - (keyed by host+login) and is NOT carried in this struct. + start_time: datetime + """ISO 8601 timestamp of when the session started""" - Authentication via the `gh` CLI's saved credentials. + working_directory: str + """Absolute path to the session's current working directory""" - API-key authentication for non-GitHub LLM providers (e.g. when running BYOM-style). + initial_name: str | None = None + """User-provided name supplied at session construction (via `--name`), if any. Immutable + after construction. """ - copilot_user: CopilotUserResponse | None = None - """Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the - GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this - verbatim and does not re-fetch when set. + remote_metadata: MetadataSnapshotRemoteMetadata | None = None + """Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are + immutable for the lifetime of the session. """ - hmac: str | None = None - """HMAC secret used to sign requests.""" - - env_var: str | None = None - """Name of the environment variable the token was sourced from.""" - - login: str | None = None - """User login associated with the token. Undefined for server-to-server tokens (those - starting with `ghs_`). - - OAuth user login. + selected_model: str | None = None + """Currently selected model identifier, if any""" - User login as reported by `gh auth status`. + summary: str | None = None + """Short human-readable summary of the session, if known. Omitted when no summary has been + generated. """ - token: str | None = None - """The token value itself. Treat as a secret. - - The token returned by `gh auth token`. Treat as a secret. + workspace: WorkspaceSummary | None = None + """Public-facing workspace metadata for this session, or null if the session has no + associated workspace. Excludes runtime-internal fields (GitHub IDs, summary count, + internal flags). + """ + workspace_path: str | None = None + """Absolute path to the session's workspace directory on disk, or null if the session has no + associated workspace """ - api_key: str | None = None - """The API key. Treat as a secret.""" @staticmethod - def from_dict(obj: Any) -> 'AuthInfo': + def from_dict(obj: Any) -> 'SessionMetadataSnapshot': assert isinstance(obj, dict) - host = from_str(obj.get("host")) - type = AuthInfoType(obj.get("type")) - copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - hmac = from_union([from_str, from_none], obj.get("hmac")) - env_var = from_union([from_str, from_none], obj.get("envVar")) - login = from_union([from_str, from_none], obj.get("login")) - token = from_union([from_str, from_none], obj.get("token")) - api_key = from_union([from_str, from_none], obj.get("apiKey")) - return AuthInfo(host, type, copilot_user, hmac, env_var, login, token, api_key) + already_in_use = from_bool(obj.get("alreadyInUse")) + current_mode = MetadataSnapshotCurrentMode(obj.get("currentMode")) + is_remote = from_bool(obj.get("isRemote")) + modified_time = from_datetime(obj.get("modifiedTime")) + session_id = from_str(obj.get("sessionId")) + start_time = from_datetime(obj.get("startTime")) + working_directory = from_str(obj.get("workingDirectory")) + initial_name = from_union([from_str, from_none], obj.get("initialName")) + remote_metadata = from_union([MetadataSnapshotRemoteMetadata.from_dict, from_none], obj.get("remoteMetadata")) + selected_model = from_union([from_str, from_none], obj.get("selectedModel")) + summary = from_union([from_str, from_none], obj.get("summary")) + workspace = from_union([WorkspaceSummary.from_dict, from_none], obj.get("workspace")) + workspace_path = from_union([from_none, from_str], obj.get("workspacePath")) + return SessionMetadataSnapshot(already_in_use, current_mode, is_remote, modified_time, session_id, start_time, working_directory, initial_name, remote_metadata, selected_model, summary, workspace, workspace_path) def to_dict(self) -> dict: result: dict = {} - result["host"] = from_str(self.host) - result["type"] = to_enum(AuthInfoType, self.type) - if self.copilot_user is not None: - result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) - if self.hmac is not None: - result["hmac"] = from_union([from_str, from_none], self.hmac) - if self.env_var is not None: - result["envVar"] = from_union([from_str, from_none], self.env_var) - if self.login is not None: - result["login"] = from_union([from_str, from_none], self.login) - if self.token is not None: - result["token"] = from_union([from_str, from_none], self.token) - if self.api_key is not None: - result["apiKey"] = from_union([from_str, from_none], self.api_key) + result["alreadyInUse"] = from_bool(self.already_in_use) + result["currentMode"] = to_enum(MetadataSnapshotCurrentMode, self.current_mode) + result["isRemote"] = from_bool(self.is_remote) + result["modifiedTime"] = self.modified_time.isoformat() + result["sessionId"] = from_str(self.session_id) + result["startTime"] = self.start_time.isoformat() + result["workingDirectory"] = from_str(self.working_directory) + if self.initial_name is not None: + result["initialName"] = from_union([from_str, from_none], self.initial_name) + if self.remote_metadata is not None: + result["remoteMetadata"] = from_union([lambda x: to_class(MetadataSnapshotRemoteMetadata, x), from_none], self.remote_metadata) + if self.selected_model is not None: + result["selectedModel"] = from_union([from_str, from_none], self.selected_model) + if self.summary is not None: + result["summary"] = from_union([from_str, from_none], self.summary) + if self.workspace is not None: + result["workspace"] = from_union([lambda x: to_class(WorkspaceSummary, x), from_none], self.workspace) + result["workspacePath"] = from_union([from_none, from_str], self.workspace_path) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForLocation: - """Schema for the `PermissionDecisionApproveForLocation` type.""" - - approval: PermissionDecisionApproveForLocationApproval - """Approval to persist for this location""" - - kind: PermissionDecisionApproveForLocationKind - """Approve and persist for this project location""" +class PermissionsConfigureParams: + """Patch of permission policy fields to apply (omit a field to leave it unchanged).""" - location_key: str - """Location key (git root or cwd) to persist the approval to""" + additional_content_exclusion_policies: list[PermissionsConfigureAdditionalContentExclusionPolicy] | None = None + """If specified, replaces the host-supplied GitHub Content Exclusion policies on the session + (combined with natively-discovered policies when evaluating tool/file access). Omit to + leave the current policies unchanged. + """ + approve_all_read_permission_requests: bool | None = None + """If specified, sets whether path/URL read permission requests are auto-approved. Omit to + leave the current value unchanged. + """ + approve_all_tool_permission_requests: bool | None = None + """If specified, sets whether tool permission requests are auto-approved without prompting. + Omit to leave the current value unchanged. + """ + paths: PermissionPathsConfig | None = None + """If specified, replaces the session's path-permission policy. The runtime constructs the + appropriate PathManager based on these inputs (rooted at the session's working + directory). Omit to leave the current path policy unchanged. + """ + rules: PermissionRulesSet | None = None + """If specified, replaces the session's approved/denied permission rules. Omit to leave the + current rules unchanged. + """ + urls: PermissionUrlsConfig | None = None + """If specified, replaces the session's URL-permission policy. The runtime constructs a + fresh DefaultUrlManager based on these inputs. Omit to leave the current URL policy + unchanged. + """ @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocation': + def from_dict(obj: Any) -> 'PermissionsConfigureParams': assert isinstance(obj, dict) - approval = PermissionDecisionApproveForLocationApproval.from_dict(obj.get("approval")) - kind = PermissionDecisionApproveForLocationKind(obj.get("kind")) - location_key = from_str(obj.get("locationKey")) - return PermissionDecisionApproveForLocation(approval, kind, location_key) + additional_content_exclusion_policies = from_union([lambda x: from_list(PermissionsConfigureAdditionalContentExclusionPolicy.from_dict, x), from_none], obj.get("additionalContentExclusionPolicies")) + approve_all_read_permission_requests = from_union([from_bool, from_none], obj.get("approveAllReadPermissionRequests")) + approve_all_tool_permission_requests = from_union([from_bool, from_none], obj.get("approveAllToolPermissionRequests")) + paths = from_union([PermissionPathsConfig.from_dict, from_none], obj.get("paths")) + rules = from_union([PermissionRulesSet.from_dict, from_none], obj.get("rules")) + urls = from_union([PermissionUrlsConfig.from_dict, from_none], obj.get("urls")) + return PermissionsConfigureParams(additional_content_exclusion_policies, approve_all_read_permission_requests, approve_all_tool_permission_requests, paths, rules, urls) def to_dict(self) -> dict: result: dict = {} - result["approval"] = to_class(PermissionDecisionApproveForLocationApproval, self.approval) - result["kind"] = to_enum(PermissionDecisionApproveForLocationKind, self.kind) - result["locationKey"] = from_str(self.location_key) + if self.additional_content_exclusion_policies is not None: + result["additionalContentExclusionPolicies"] = from_union([lambda x: from_list(lambda x: to_class(PermissionsConfigureAdditionalContentExclusionPolicy, x), x), from_none], self.additional_content_exclusion_policies) + if self.approve_all_read_permission_requests is not None: + result["approveAllReadPermissionRequests"] = from_union([from_bool, from_none], self.approve_all_read_permission_requests) + if self.approve_all_tool_permission_requests is not None: + result["approveAllToolPermissionRequests"] = from_union([from_bool, from_none], self.approve_all_tool_permission_requests) + if self.paths is not None: + result["paths"] = from_union([lambda x: to_class(PermissionPathsConfig, x), from_none], self.paths) + if self.rules is not None: + result["rules"] = from_union([lambda x: to_class(PermissionRulesSet, x), from_none], self.rules) + if self.urls is not None: + result["urls"] = from_union([lambda x: to_class(PermissionUrlsConfig, x), from_none], self.urls) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForSession: - """Schema for the `PermissionDecisionApproveForSession` type.""" - - kind: PermissionDecisionApproveForSessionKind - """Approve and remember for the rest of the session""" - - approval: PermissionDecisionApproveForSessionApproval | None = None - """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts)""" +class TasksGetProgressResult: + """Progress information for the task, or null when no task with that ID is tracked.""" - domain: str | None = None - """URL domain to approve for the rest of the session (URL prompts only)""" + progress: TaskProgress | None = None + """Progress information for the task, discriminated by type. Returns null when no task with + this ID is currently tracked. + """ @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForSession': + def from_dict(obj: Any) -> 'TasksGetProgressResult': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForSessionKind(obj.get("kind")) - approval = from_union([PermissionDecisionApproveForSessionApproval.from_dict, from_none], obj.get("approval")) - domain = from_union([from_str, from_none], obj.get("domain")) - return PermissionDecisionApproveForSession(kind, approval, domain) + progress = from_union([TaskProgress.from_dict, from_none], obj.get("progress")) + return TasksGetProgressResult(progress) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForSessionKind, self.kind) - if self.approval is not None: - result["approval"] = from_union([lambda x: to_class(PermissionDecisionApproveForSessionApproval, x), from_none], self.approval) - if self.domain is not None: - result["domain"] = from_union([from_str, from_none], self.domain) + if self.progress is not None: + result["progress"] = from_union([lambda x: to_class(TaskProgress, x), from_none], self.progress) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionLocationAddToolApprovalParams: - """Location-scoped tool approval to persist.""" - - approval: PermissionsLocationsAddToolApprovalDetails - """Tool approval to persist and apply""" - - location_key: str - """Location key (git root or cwd) to persist the approval to""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionLocationAddToolApprovalParams': - assert isinstance(obj, dict) - approval = PermissionsLocationsAddToolApprovalDetails.from_dict(obj.get("approval")) - location_key = from_str(obj.get("locationKey")) - return PermissionLocationAddToolApprovalParams(approval, location_key) - - def to_dict(self) -> dict: - result: dict = {} - result["approval"] = to_class(PermissionsLocationsAddToolApprovalDetails, self.approval) - result["locationKey"] = from_str(self.location_key) - return result +class UIElicitationSchema: + """JSON Schema describing the form fields to present to the user""" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class HandlePendingToolCallRequest: - """Pending external tool call request ID, with the tool result or an error describing why it - failed. - """ - request_id: str - """Request ID of the pending tool call""" + properties: dict[str, UIElicitationSchemaProperty] + """Form field definitions, keyed by field name""" - error: str | None = None - """Error message if the tool call failed""" + type: UIElicitationSchemaType + """Schema type indicator (always 'object')""" - result: ExternalToolTextResultForLlm | str | None = None - """Tool call result (string or expanded result object)""" + required: list[str] | None = None + """List of required field names""" @staticmethod - def from_dict(obj: Any) -> 'HandlePendingToolCallRequest': + def from_dict(obj: Any) -> 'UIElicitationSchema': assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - error = from_union([from_str, from_none], obj.get("error")) - result = from_union([ExternalToolTextResultForLlm.from_dict, from_str, from_none], obj.get("result")) - return HandlePendingToolCallRequest(request_id, error, result) + properties = from_dict(UIElicitationSchemaProperty.from_dict, obj.get("properties")) + type = UIElicitationSchemaType(obj.get("type")) + required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("required")) + return UIElicitationSchema(properties, type, required) def to_dict(self) -> dict: result: dict = {} - result["requestId"] = from_str(self.request_id) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.result is not None: - result["result"] = from_union([lambda x: to_class(ExternalToolTextResultForLlm, x), from_str, from_none], self.result) + result["properties"] = from_dict(lambda x: to_class(UIElicitationSchemaProperty, x), self.properties) + result["type"] = to_enum(UIElicitationSchemaType, self.type) + if self.required is not None: + result["required"] = from_union([lambda x: from_list(from_str, x), from_none], self.required) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -14118,223 +14067,23 @@ def to_dict(self) -> dict: class UIElicitationRequest: """Prompt message and JSON schema describing the form fields to elicit from the user.""" - message: str - """Message describing what information is needed from the user""" - - requested_schema: UIElicitationSchema - """JSON Schema describing the form fields to present to the user""" - - @staticmethod - def from_dict(obj: Any) -> 'UIElicitationRequest': - assert isinstance(obj, dict) - message = from_str(obj.get("message")) - requested_schema = UIElicitationSchema.from_dict(obj.get("requestedSchema")) - return UIElicitationRequest(message, requested_schema) - - def to_dict(self) -> dict: - result: dict = {} - result["message"] = from_str(self.message) - result["requestedSchema"] = to_class(UIElicitationSchema, self.requested_schema) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionSetCredentialsParams: - """New auth credentials to install on the session. Omit to leave credentials unchanged.""" - - credentials: AuthInfo | None = None - """The new auth credentials to install on the session. When omitted or `undefined`, the call - is a no-op and the session's existing credentials are preserved. The runtime stores the - value verbatim and uses it for outbound model/API requests; it does NOT re-validate or - re-fetch the associated Copilot user response. Several variants carry secret material; - treat this method's params as containing secrets at rest and in transit. - """ - - @staticmethod - def from_dict(obj: Any) -> 'SessionSetCredentialsParams': - assert isinstance(obj, dict) - credentials = from_union([AuthInfo.from_dict, from_none], obj.get("credentials")) - return SessionSetCredentialsParams(credentials) - - def to_dict(self) -> dict: - result: dict = {} - if self.credentials is not None: - result["credentials"] = from_union([lambda x: to_class(AuthInfo, x), from_none], self.credentials) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionDecision: - """The client's response to the pending permission prompt - - Schema for the `PermissionDecisionApproveOnce` type. - - Schema for the `PermissionDecisionApproveForSession` type. - - Schema for the `PermissionDecisionApproveForLocation` type. - - Schema for the `PermissionDecisionApprovePermanently` type. - - Schema for the `PermissionDecisionReject` type. - - Schema for the `PermissionDecisionUserNotAvailable` type. - - Schema for the `PermissionDecisionApproved` type. - - Schema for the `PermissionDecisionApprovedForSession` type. - - Schema for the `PermissionDecisionApprovedForLocation` type. - - Schema for the `PermissionDecisionCancelled` type. - - Schema for the `PermissionDecisionDeniedByRules` type. - - Schema for the `PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser` type. - - Schema for the `PermissionDecisionDeniedInteractivelyByUser` type. - - Schema for the `PermissionDecisionDeniedByContentExclusionPolicy` type. - - Schema for the `PermissionDecisionDeniedByPermissionRequestHook` type. - """ - kind: PermissionDecisionKind - """Approve this single request only - - Approve and remember for the rest of the session - - Approve and persist for this project location - - Approve and persist across sessions (URL prompts only) - - Reject the request - - No user is available to confirm the request - - The permission request was approved - - Approved and remembered for the rest of the session - - Approved and persisted for this project location - - The permission request was cancelled before a response was used - - Denied because approval rules explicitly blocked it - - Denied because no approval rule matched and user confirmation was unavailable - - Denied by the user during an interactive prompt - - Denied by the organization's content exclusion policy - - Denied by a permission request hook registered by an extension or plugin - """ - approval: PermissionDecisionApproveForIonApproval | None = None - """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) - - Approval to persist for this location - - The approval to add as a session-scoped rule - - The approval to persist for this location - """ - domain: str | None = None - """URL domain to approve for the rest of the session (URL prompts only) - - URL domain to approve permanently - """ - location_key: str | None = None - """Location key (git root or cwd) to persist the approval to - - The location key (git root or cwd) to persist the approval to - """ - feedback: str | None = None - """Optional feedback explaining the rejection - - Optional feedback from the user explaining the denial - """ - reason: str | None = None - """Optional explanation of why the request was cancelled""" - - rules: list[PermissionRule] | None = None - """Rules that denied the request""" - - force_reject: bool | None = None - """Whether to force-reject the current agent turn""" - - message: str | None = None - """Human-readable explanation of why the path was excluded - - Optional message from the hook explaining the denial - """ - path: str | None = None - """File path that triggered the exclusion""" - - interrupt: bool | None = None - """Whether to interrupt the current agent turn""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionDecision': - assert isinstance(obj, dict) - kind = PermissionDecisionKind(obj.get("kind")) - approval = from_union([PermissionDecisionApproveForIonApproval.from_dict, from_none], obj.get("approval")) - domain = from_union([from_str, from_none], obj.get("domain")) - location_key = from_union([from_str, from_none], obj.get("locationKey")) - feedback = from_union([from_str, from_none], obj.get("feedback")) - reason = from_union([from_str, from_none], obj.get("reason")) - rules = from_union([lambda x: from_list(PermissionRule.from_dict, x), from_none], obj.get("rules")) - force_reject = from_union([from_bool, from_none], obj.get("forceReject")) - message = from_union([from_str, from_none], obj.get("message")) - path = from_union([from_str, from_none], obj.get("path")) - interrupt = from_union([from_bool, from_none], obj.get("interrupt")) - return PermissionDecision(kind, approval, domain, location_key, feedback, reason, rules, force_reject, message, path, interrupt) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionKind, self.kind) - if self.approval is not None: - result["approval"] = from_union([lambda x: to_class(PermissionDecisionApproveForIonApproval, x), from_none], self.approval) - if self.domain is not None: - result["domain"] = from_union([from_str, from_none], self.domain) - if self.location_key is not None: - result["locationKey"] = from_union([from_str, from_none], self.location_key) - if self.feedback is not None: - result["feedback"] = from_union([from_str, from_none], self.feedback) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.rules is not None: - result["rules"] = from_union([lambda x: from_list(lambda x: to_class(PermissionRule, x), x), from_none], self.rules) - if self.force_reject is not None: - result["forceReject"] = from_union([from_bool, from_none], self.force_reject) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.interrupt is not None: - result["interrupt"] = from_union([from_bool, from_none], self.interrupt) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionDecisionRequest: - """Pending permission request ID and the decision to apply (approve/reject and scope).""" - - request_id: str - """Request ID of the pending permission request""" + message: str + """Message describing what information is needed from the user""" - result: PermissionDecision - """The client's response to the pending permission prompt""" + requested_schema: UIElicitationSchema + """JSON Schema describing the form fields to present to the user""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionRequest': + def from_dict(obj: Any) -> 'UIElicitationRequest': assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - result = PermissionDecision.from_dict(obj.get("result")) - return PermissionDecisionRequest(request_id, result) + message = from_str(obj.get("message")) + requested_schema = UIElicitationSchema.from_dict(obj.get("requestedSchema")) + return UIElicitationRequest(message, requested_schema) def to_dict(self) -> dict: result: dict = {} - result["requestId"] = from_str(self.request_id) - result["result"] = to_class(PermissionDecision, self.result) + result["message"] = from_str(self.message) + result["requestedSchema"] = to_class(UIElicitationSchema, self.requested_schema) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -14642,7 +14391,7 @@ class TaskAgentInfo: tool_call_id: str """Tool call ID associated with this agent task""" - type: TaskAgentInfoType + type: ClassVar[str] = "agent" """Task kind""" active_started_at: datetime | None = None @@ -14687,7 +14436,6 @@ def from_dict(obj: Any) -> 'TaskAgentInfo': started_at = from_datetime(obj.get("startedAt")) status = TaskStatus(obj.get("status")) tool_call_id = from_str(obj.get("toolCallId")) - type = TaskAgentInfoType(obj.get("type")) active_started_at = from_union([from_datetime, from_none], obj.get("activeStartedAt")) active_time_ms = from_union([from_int, from_none], obj.get("activeTimeMs")) can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) @@ -14698,7 +14446,7 @@ def from_dict(obj: Any) -> 'TaskAgentInfo': latest_response = from_union([from_str, from_none], obj.get("latestResponse")) model = from_union([from_str, from_none], obj.get("model")) result = from_union([from_str, from_none], obj.get("result")) - return TaskAgentInfo(agent_type, description, id, prompt, started_at, status, tool_call_id, type, active_started_at, active_time_ms, can_promote_to_background, completed_at, error, execution_mode, idle_since, latest_response, model, result) + return TaskAgentInfo(agent_type, description, id, prompt, started_at, status, tool_call_id, active_started_at, active_time_ms, can_promote_to_background, completed_at, error, execution_mode, idle_since, latest_response, model, result) def to_dict(self) -> dict: result: dict = {} @@ -14709,157 +14457,11 @@ def to_dict(self) -> dict: result["startedAt"] = self.started_at.isoformat() result["status"] = to_enum(TaskStatus, self.status) result["toolCallId"] = from_str(self.tool_call_id) - result["type"] = to_enum(TaskAgentInfoType, self.type) - if self.active_started_at is not None: - result["activeStartedAt"] = from_union([lambda x: x.isoformat(), from_none], self.active_started_at) - if self.active_time_ms is not None: - result["activeTimeMs"] = from_union([from_int, from_none], self.active_time_ms) - if self.can_promote_to_background is not None: - result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) - if self.completed_at is not None: - result["completedAt"] = from_union([lambda x: x.isoformat(), from_none], self.completed_at) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.execution_mode is not None: - result["executionMode"] = from_union([lambda x: to_enum(TaskExecutionMode, x), from_none], self.execution_mode) - if self.idle_since is not None: - result["idleSince"] = from_union([lambda x: x.isoformat(), from_none], self.idle_since) - if self.latest_response is not None: - result["latestResponse"] = from_union([from_str, from_none], self.latest_response) - if self.model is not None: - result["model"] = from_union([from_str, from_none], self.model) - if self.result is not None: - result["result"] = from_union([from_str, from_none], self.result) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TaskInfo: - """Schema for the `TaskInfo` type. - - The first sync-waiting task (agent first, then shell) that can currently be promoted to - background mode. Omitted if no such task exists. The returned task is guaranteed to have - executionMode='sync' and canPromoteToBackground=true at the time of the call. - - The promoted task as it now exists in background mode, omitted if no promotable task was - waiting. Atomic operation: avoids the race window of getCurrentPromotable + - promoteToBackground. - - Schema for the `TaskAgentInfo` type. - - Schema for the `TaskShellInfo` type. - """ - description: str - """Short description of the task""" - - id: str - """Unique task identifier""" - - started_at: datetime - """ISO 8601 timestamp when the task was started""" - - status: TaskStatus - """Current lifecycle status of the task""" - - type: TaskInfoType - """Task kind""" - - active_started_at: datetime | None = None - """ISO 8601 timestamp when the current active period began""" - - active_time_ms: int | None = None - """Accumulated active execution time in milliseconds""" - - agent_type: str | None = None - """Type of agent running this task""" - - can_promote_to_background: bool | None = None - """Whether the task is currently in the original sync wait and can be moved to background - mode. False once it is already backgrounded, idle, finished, or no longer has a - promotable sync waiter. - - Whether this shell task can be promoted to background mode - """ - completed_at: datetime | None = None - """ISO 8601 timestamp when the task finished""" - - error: str | None = None - """Error message when the task failed""" - - execution_mode: TaskExecutionMode | None = None - """Whether task execution is synchronously awaited or managed in the background""" - - idle_since: datetime | None = None - """ISO 8601 timestamp when the agent entered idle state""" - - latest_response: str | None = None - """Most recent response text from the agent""" - - model: str | None = None - """Model used for the task when specified""" - - prompt: str | None = None - """Prompt passed to the agent""" - - result: str | None = None - """Result text from the task when available""" - - tool_call_id: str | None = None - """Tool call ID associated with this agent task""" - - attachment_mode: TaskShellInfoAttachmentMode | None = None - """Whether the shell runs inside a managed PTY session or as an independent background - process - """ - command: str | None = None - """Command being executed""" - - log_path: str | None = None - """Path to the detached shell log, when available""" - - pid: int | None = None - """Process ID when available""" - - @staticmethod - def from_dict(obj: Any) -> 'TaskInfo': - assert isinstance(obj, dict) - description = from_str(obj.get("description")) - id = from_str(obj.get("id")) - started_at = from_datetime(obj.get("startedAt")) - status = TaskStatus(obj.get("status")) - type = TaskInfoType(obj.get("type")) - active_started_at = from_union([from_datetime, from_none], obj.get("activeStartedAt")) - active_time_ms = from_union([from_int, from_none], obj.get("activeTimeMs")) - agent_type = from_union([from_str, from_none], obj.get("agentType")) - can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) - completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) - error = from_union([from_str, from_none], obj.get("error")) - execution_mode = from_union([TaskExecutionMode, from_none], obj.get("executionMode")) - idle_since = from_union([from_datetime, from_none], obj.get("idleSince")) - latest_response = from_union([from_str, from_none], obj.get("latestResponse")) - model = from_union([from_str, from_none], obj.get("model")) - prompt = from_union([from_str, from_none], obj.get("prompt")) - result = from_union([from_str, from_none], obj.get("result")) - tool_call_id = from_union([from_str, from_none], obj.get("toolCallId")) - attachment_mode = from_union([TaskShellInfoAttachmentMode, from_none], obj.get("attachmentMode")) - command = from_union([from_str, from_none], obj.get("command")) - log_path = from_union([from_str, from_none], obj.get("logPath")) - pid = from_union([from_int, from_none], obj.get("pid")) - return TaskInfo(description, id, started_at, status, type, active_started_at, active_time_ms, agent_type, can_promote_to_background, completed_at, error, execution_mode, idle_since, latest_response, model, prompt, result, tool_call_id, attachment_mode, command, log_path, pid) - - def to_dict(self) -> dict: - result: dict = {} - result["description"] = from_str(self.description) - result["id"] = from_str(self.id) - result["startedAt"] = self.started_at.isoformat() - result["status"] = to_enum(TaskStatus, self.status) - result["type"] = to_enum(TaskInfoType, self.type) + result["type"] = self.type if self.active_started_at is not None: result["activeStartedAt"] = from_union([lambda x: x.isoformat(), from_none], self.active_started_at) if self.active_time_ms is not None: result["activeTimeMs"] = from_union([from_int, from_none], self.active_time_ms) - if self.agent_type is not None: - result["agentType"] = from_union([from_str, from_none], self.agent_type) if self.can_promote_to_background is not None: result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) if self.completed_at is not None: @@ -14874,86 +14476,8 @@ def to_dict(self) -> dict: result["latestResponse"] = from_union([from_str, from_none], self.latest_response) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) - if self.prompt is not None: - result["prompt"] = from_union([from_str, from_none], self.prompt) if self.result is not None: result["result"] = from_union([from_str, from_none], self.result) - if self.tool_call_id is not None: - result["toolCallId"] = from_union([from_str, from_none], self.tool_call_id) - if self.attachment_mode is not None: - result["attachmentMode"] = from_union([lambda x: to_enum(TaskShellInfoAttachmentMode, x), from_none], self.attachment_mode) - if self.command is not None: - result["command"] = from_union([from_str, from_none], self.command) - if self.log_path is not None: - result["logPath"] = from_union([from_str, from_none], self.log_path) - if self.pid is not None: - result["pid"] = from_union([from_int, from_none], self.pid) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TaskList: - """Background tasks currently tracked by the session.""" - - tasks: list[TaskInfo] - """Currently tracked tasks""" - - @staticmethod - def from_dict(obj: Any) -> 'TaskList': - assert isinstance(obj, dict) - tasks = from_list(TaskInfo.from_dict, obj.get("tasks")) - return TaskList(tasks) - - def to_dict(self) -> dict: - result: dict = {} - result["tasks"] = from_list(lambda x: to_class(TaskInfo, x), self.tasks) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TasksGetCurrentPromotableResult: - """The first sync-waiting task that can currently be promoted to background mode.""" - - task: TaskInfo | None = None - """The first sync-waiting task (agent first, then shell) that can currently be promoted to - background mode. Omitted if no such task exists. The returned task is guaranteed to have - executionMode='sync' and canPromoteToBackground=true at the time of the call. - """ - - @staticmethod - def from_dict(obj: Any) -> 'TasksGetCurrentPromotableResult': - assert isinstance(obj, dict) - task = from_union([TaskInfo.from_dict, from_none], obj.get("task")) - return TasksGetCurrentPromotableResult(task) - - def to_dict(self) -> dict: - result: dict = {} - if self.task is not None: - result["task"] = from_union([lambda x: to_class(TaskInfo, x), from_none], self.task) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TasksPromoteCurrentToBackgroundResult: - """The promoted task as it now exists in background mode, omitted if no promotable task was - waiting. - """ - task: TaskInfo | None = None - """The promoted task as it now exists in background mode, omitted if no promotable task was - waiting. Atomic operation: avoids the race window of getCurrentPromotable + - promoteToBackground. - """ - - @staticmethod - def from_dict(obj: Any) -> 'TasksPromoteCurrentToBackgroundResult': - assert isinstance(obj, dict) - task = from_union([TaskInfo.from_dict, from_none], obj.get("task")) - return TasksPromoteCurrentToBackgroundResult(task) - - def to_dict(self) -> dict: - result: dict = {} - if self.task is not None: - result["task"] = from_union([lambda x: to_class(TaskInfo, x), from_none], self.task) return result @dataclass @@ -14973,6 +14497,15 @@ class RPC: api_key_auth_info: APIKeyAuthInfo auth_info: AuthInfo auth_info_type: AuthInfoType + canvas_action: CanvasAction + canvas_close_request: CanvasCloseRequest + canvas_instance_availability: CanvasInstanceAvailability + canvas_invoke_action_request: CanvasInvokeActionRequest + canvas_invoke_action_result: CanvasInvokeActionResult + canvas_json_schema: Any + canvas_list: CanvasList + canvas_list_open_result: CanvasListOpenResult + canvas_open_request: CanvasOpenRequest command_list: CommandList commands_handle_pending_command_request: CommandsHandlePendingCommandRequest commands_handle_pending_command_result: CommandsHandlePendingCommandResult @@ -14984,8 +14517,8 @@ class RPC: connected_remote_session_metadata_kind: ConnectedRemoteSessionMetadataKind connected_remote_session_metadata_repository: ConnectedRemoteSessionMetadataRepository connect_remote_session_params: ConnectRemoteSessionParams - connect_request: ConnectRequest - connect_result: ConnectResult + connect_request: _ConnectRequest + connect_result: _ConnectResult content_filter_mode: ContentFilterMode copilot_api_token_auth_info: CopilotAPITokenAuthInfo copilot_user_response: CopilotUserResponse @@ -14995,6 +14528,7 @@ class RPC: copilot_user_response_quota_snapshots_completions: CopilotUserResponseQuotaSnapshotsCompletions copilot_user_response_quota_snapshots_premium_interactions: CopilotUserResponseQuotaSnapshotsPremiumInteractions current_model: CurrentModel + discovered_canvas: DiscoveredCanvas discovered_mcp_server: DiscoveredMCPServer discovered_mcp_server_type: DiscoveredMCPServerType enqueue_command_params: EnqueueCommandParams @@ -15026,7 +14560,7 @@ class RPC: external_tool_text_result_for_llm_content_resource_details: ExternalToolTextResultForLlmContentResourceDetails external_tool_text_result_for_llm_content_resource_link: ExternalToolTextResultForLlmContentResourceLink external_tool_text_result_for_llm_content_resource_link_icon: ExternalToolTextResultForLlmContentResourceLinkIcon - external_tool_text_result_for_llm_content_resource_link_icon_theme: ExternalToolTextResultForLlmContentResourceLinkIconTheme + external_tool_text_result_for_llm_content_resource_link_icon_theme: Theme external_tool_text_result_for_llm_content_terminal: ExternalToolTextResultForLlmContentTerminal external_tool_text_result_for_llm_content_text: ExternalToolTextResultForLlmContentText filter_mapping: dict[str, ContentFilterMode] | ContentFilterMode @@ -15059,6 +14593,28 @@ class RPC: log_request: LogRequest log_result: LogResult lsp_initialize_request: LspInitializeRequest + mcp_apps_call_tool_request: MCPAppsCallToolRequest + mcp_apps_diagnose_capability: MCPAppsDiagnoseCapability + mcp_apps_diagnose_request: MCPAppsDiagnoseRequest + mcp_apps_diagnose_result: MCPAppsDiagnoseResult + mcp_apps_diagnose_server: MCPAppsDiagnoseServer + mcp_apps_host_context: MCPAppsHostContext + mcp_apps_host_context_details: MCPAppsHostContextDetails + mcp_apps_host_context_details_available_display_mode: MCPAppsDisplayMode + mcp_apps_host_context_details_display_mode: MCPAppsDisplayMode + mcp_apps_host_context_details_platform: MCPAppsHostContextDetailsPlatform + mcp_apps_host_context_details_theme: Theme + mcp_apps_list_tools_request: MCPAppsListToolsRequest + mcp_apps_list_tools_result: MCPAppsListToolsResult + mcp_apps_read_resource_request: MCPAppsReadResourceRequest + mcp_apps_read_resource_result: MCPAppsReadResourceResult + mcp_apps_resource_content: MCPAppsResourceContent + mcp_apps_set_host_context_details: MCPAppsSetHostContextDetails + mcp_apps_set_host_context_details_available_display_mode: MCPAppsDisplayMode + mcp_apps_set_host_context_details_display_mode: MCPAppsDisplayMode + mcp_apps_set_host_context_details_platform: MCPAppsHostContextDetailsPlatform + mcp_apps_set_host_context_details_theme: Theme + mcp_apps_set_host_context_request: MCPAppsSetHostContextRequest mcp_cancel_sampling_execution_params: MCPCancelSamplingExecutionParams mcp_cancel_sampling_execution_result: MCPCancelSamplingExecutionResult mcp_config_add_request: MCPConfigAddRequest @@ -15106,6 +14662,7 @@ class RPC: model: Model model_billing: ModelBilling model_billing_token_prices: ModelBillingTokenPrices + model_billing_token_prices_long_context: ModelBillingTokenPricesLongContext model_capabilities: ModelCapabilities model_capabilities_limits: ModelCapabilitiesLimits model_capabilities_limits_vision: ModelCapabilitiesLimitsVision @@ -15129,6 +14686,7 @@ class RPC: name_set_auto_request: NameSetAutoRequest name_set_auto_result: NameSetAutoResult name_set_request: NameSetRequest + open_canvas_instance: OpenCanvasInstance options_update_env_value_mode: MCPSetEnvValueModeDetails pending_permission_request: PendingPermissionRequest pending_permission_request_list: PendingPermissionRequestList @@ -15309,6 +14867,7 @@ class RPC: session_list_filter: SessionListFilter session_load_deferred_repo_hooks_result: SessionLoadDeferredRepoHooksResult session_log_level: SessionLogLevel + session_mcp_apps_call_tool_result: dict[str, Any] session_metadata: SessionMetadata session_metadata_snapshot: SessionMetadataSnapshot session_mode: SessionMode @@ -15449,14 +15008,6 @@ class RPC: usage_metrics_model_metric_usage: UsageMetricsModelMetricUsage usage_metrics_token_detail: UsageMetricsTokenDetail user_auth_info: UserAuthInfo - user_tool_session_approval_commands: UserToolSessionApprovalCommands - user_tool_session_approval_custom_tool: UserToolSessionApprovalCustomTool - user_tool_session_approval_extension_management: UserToolSessionApprovalExtensionManagement - user_tool_session_approval_extension_permission_access: UserToolSessionApprovalExtensionPermissionAccess - user_tool_session_approval_mcp: UserToolSessionApprovalMCP - user_tool_session_approval_memory: UserToolSessionApprovalMemory - user_tool_session_approval_read: UserToolSessionApprovalRead - user_tool_session_approval_write: UserToolSessionApprovalWrite workspaces_checkpoints: WorkspacesCheckpoints workspaces_create_file_request: WorkspacesCreateFileRequest workspaces_get_workspace_result: WorkspacesGetWorkspaceResult @@ -15490,8 +15041,17 @@ def from_dict(obj: Any) -> 'RPC': agent_select_request = AgentSelectRequest.from_dict(obj.get("AgentSelectRequest")) agent_select_result = AgentSelectResult.from_dict(obj.get("AgentSelectResult")) api_key_auth_info = APIKeyAuthInfo.from_dict(obj.get("ApiKeyAuthInfo")) - auth_info = AuthInfo.from_dict(obj.get("AuthInfo")) + auth_info = _load_AuthInfo(obj.get("AuthInfo")) auth_info_type = AuthInfoType(obj.get("AuthInfoType")) + canvas_action = CanvasAction.from_dict(obj.get("CanvasAction")) + canvas_close_request = CanvasCloseRequest.from_dict(obj.get("CanvasCloseRequest")) + canvas_instance_availability = CanvasInstanceAvailability(obj.get("CanvasInstanceAvailability")) + canvas_invoke_action_request = CanvasInvokeActionRequest.from_dict(obj.get("CanvasInvokeActionRequest")) + canvas_invoke_action_result = CanvasInvokeActionResult.from_dict(obj.get("CanvasInvokeActionResult")) + canvas_json_schema = obj.get("CanvasJsonSchema") + canvas_list = CanvasList.from_dict(obj.get("CanvasList")) + canvas_list_open_result = CanvasListOpenResult.from_dict(obj.get("CanvasListOpenResult")) + canvas_open_request = CanvasOpenRequest.from_dict(obj.get("CanvasOpenRequest")) command_list = CommandList.from_dict(obj.get("CommandList")) commands_handle_pending_command_request = CommandsHandlePendingCommandRequest.from_dict(obj.get("CommandsHandlePendingCommandRequest")) commands_handle_pending_command_result = CommandsHandlePendingCommandResult.from_dict(obj.get("CommandsHandlePendingCommandResult")) @@ -15503,8 +15063,8 @@ def from_dict(obj: Any) -> 'RPC': connected_remote_session_metadata_kind = ConnectedRemoteSessionMetadataKind(obj.get("ConnectedRemoteSessionMetadataKind")) connected_remote_session_metadata_repository = ConnectedRemoteSessionMetadataRepository.from_dict(obj.get("ConnectedRemoteSessionMetadataRepository")) connect_remote_session_params = ConnectRemoteSessionParams.from_dict(obj.get("ConnectRemoteSessionParams")) - connect_request = ConnectRequest.from_dict(obj.get("ConnectRequest")) - connect_result = ConnectResult.from_dict(obj.get("ConnectResult")) + connect_request = _ConnectRequest.from_dict(obj.get("ConnectRequest")) + connect_result = _ConnectResult.from_dict(obj.get("ConnectResult")) content_filter_mode = ContentFilterMode(obj.get("ContentFilterMode")) copilot_api_token_auth_info = CopilotAPITokenAuthInfo.from_dict(obj.get("CopilotApiTokenAuthInfo")) copilot_user_response = CopilotUserResponse.from_dict(obj.get("CopilotUserResponse")) @@ -15514,6 +15074,7 @@ def from_dict(obj: Any) -> 'RPC': copilot_user_response_quota_snapshots_completions = CopilotUserResponseQuotaSnapshotsCompletions.from_dict(obj.get("CopilotUserResponseQuotaSnapshotsCompletions")) copilot_user_response_quota_snapshots_premium_interactions = CopilotUserResponseQuotaSnapshotsPremiumInteractions.from_dict(obj.get("CopilotUserResponseQuotaSnapshotsPremiumInteractions")) current_model = CurrentModel.from_dict(obj.get("CurrentModel")) + discovered_canvas = DiscoveredCanvas.from_dict(obj.get("DiscoveredCanvas")) 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")) @@ -15538,14 +15099,14 @@ def from_dict(obj: Any) -> 'RPC': external_tool_text_result_for_llm = ExternalToolTextResultForLlm.from_dict(obj.get("ExternalToolTextResultForLlm")) external_tool_text_result_for_llm_binary_results_for_llm = ExternalToolTextResultForLlmBinaryResultsForLlm.from_dict(obj.get("ExternalToolTextResultForLlmBinaryResultsForLlm")) external_tool_text_result_for_llm_binary_results_for_llm_type = ExternalToolTextResultForLlmBinaryResultsForLlmType(obj.get("ExternalToolTextResultForLlmBinaryResultsForLlmType")) - external_tool_text_result_for_llm_content = ExternalToolTextResultForLlmContent.from_dict(obj.get("ExternalToolTextResultForLlmContent")) + external_tool_text_result_for_llm_content = _load_ExternalToolTextResultForLlmContent(obj.get("ExternalToolTextResultForLlmContent")) external_tool_text_result_for_llm_content_audio = ExternalToolTextResultForLlmContentAudio.from_dict(obj.get("ExternalToolTextResultForLlmContentAudio")) external_tool_text_result_for_llm_content_image = ExternalToolTextResultForLlmContentImage.from_dict(obj.get("ExternalToolTextResultForLlmContentImage")) external_tool_text_result_for_llm_content_resource = ExternalToolTextResultForLlmContentResource.from_dict(obj.get("ExternalToolTextResultForLlmContentResource")) external_tool_text_result_for_llm_content_resource_details = (lambda x: from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], x))(obj.get("ExternalToolTextResultForLlmContentResourceDetails")) external_tool_text_result_for_llm_content_resource_link = ExternalToolTextResultForLlmContentResourceLink.from_dict(obj.get("ExternalToolTextResultForLlmContentResourceLink")) external_tool_text_result_for_llm_content_resource_link_icon = ExternalToolTextResultForLlmContentResourceLinkIcon.from_dict(obj.get("ExternalToolTextResultForLlmContentResourceLinkIcon")) - external_tool_text_result_for_llm_content_resource_link_icon_theme = ExternalToolTextResultForLlmContentResourceLinkIconTheme(obj.get("ExternalToolTextResultForLlmContentResourceLinkIconTheme")) + external_tool_text_result_for_llm_content_resource_link_icon_theme = Theme(obj.get("ExternalToolTextResultForLlmContentResourceLinkIconTheme")) external_tool_text_result_for_llm_content_terminal = ExternalToolTextResultForLlmContentTerminal.from_dict(obj.get("ExternalToolTextResultForLlmContentTerminal")) external_tool_text_result_for_llm_content_text = ExternalToolTextResultForLlmContentText.from_dict(obj.get("ExternalToolTextResultForLlmContentText")) filter_mapping = from_union([lambda x: from_dict(ContentFilterMode, x), ContentFilterMode], obj.get("FilterMapping")) @@ -15578,6 +15139,28 @@ def from_dict(obj: Any) -> 'RPC': log_request = LogRequest.from_dict(obj.get("LogRequest")) log_result = LogResult.from_dict(obj.get("LogResult")) lsp_initialize_request = LspInitializeRequest.from_dict(obj.get("LspInitializeRequest")) + mcp_apps_call_tool_request = MCPAppsCallToolRequest.from_dict(obj.get("McpAppsCallToolRequest")) + mcp_apps_diagnose_capability = MCPAppsDiagnoseCapability.from_dict(obj.get("McpAppsDiagnoseCapability")) + mcp_apps_diagnose_request = MCPAppsDiagnoseRequest.from_dict(obj.get("McpAppsDiagnoseRequest")) + mcp_apps_diagnose_result = MCPAppsDiagnoseResult.from_dict(obj.get("McpAppsDiagnoseResult")) + mcp_apps_diagnose_server = MCPAppsDiagnoseServer.from_dict(obj.get("McpAppsDiagnoseServer")) + mcp_apps_host_context = MCPAppsHostContext.from_dict(obj.get("McpAppsHostContext")) + mcp_apps_host_context_details = MCPAppsHostContextDetails.from_dict(obj.get("McpAppsHostContextDetails")) + mcp_apps_host_context_details_available_display_mode = MCPAppsDisplayMode(obj.get("McpAppsHostContextDetailsAvailableDisplayMode")) + mcp_apps_host_context_details_display_mode = MCPAppsDisplayMode(obj.get("McpAppsHostContextDetailsDisplayMode")) + mcp_apps_host_context_details_platform = MCPAppsHostContextDetailsPlatform(obj.get("McpAppsHostContextDetailsPlatform")) + mcp_apps_host_context_details_theme = Theme(obj.get("McpAppsHostContextDetailsTheme")) + mcp_apps_list_tools_request = MCPAppsListToolsRequest.from_dict(obj.get("McpAppsListToolsRequest")) + mcp_apps_list_tools_result = MCPAppsListToolsResult.from_dict(obj.get("McpAppsListToolsResult")) + mcp_apps_read_resource_request = MCPAppsReadResourceRequest.from_dict(obj.get("McpAppsReadResourceRequest")) + mcp_apps_read_resource_result = MCPAppsReadResourceResult.from_dict(obj.get("McpAppsReadResourceResult")) + mcp_apps_resource_content = MCPAppsResourceContent.from_dict(obj.get("McpAppsResourceContent")) + mcp_apps_set_host_context_details = MCPAppsSetHostContextDetails.from_dict(obj.get("McpAppsSetHostContextDetails")) + mcp_apps_set_host_context_details_available_display_mode = MCPAppsDisplayMode(obj.get("McpAppsSetHostContextDetailsAvailableDisplayMode")) + mcp_apps_set_host_context_details_display_mode = MCPAppsDisplayMode(obj.get("McpAppsSetHostContextDetailsDisplayMode")) + mcp_apps_set_host_context_details_platform = MCPAppsHostContextDetailsPlatform(obj.get("McpAppsSetHostContextDetailsPlatform")) + mcp_apps_set_host_context_details_theme = Theme(obj.get("McpAppsSetHostContextDetailsTheme")) + mcp_apps_set_host_context_request = MCPAppsSetHostContextRequest.from_dict(obj.get("McpAppsSetHostContextRequest")) mcp_cancel_sampling_execution_params = MCPCancelSamplingExecutionParams.from_dict(obj.get("McpCancelSamplingExecutionParams")) mcp_cancel_sampling_execution_result = MCPCancelSamplingExecutionResult.from_dict(obj.get("McpCancelSamplingExecutionResult")) mcp_config_add_request = MCPConfigAddRequest.from_dict(obj.get("McpConfigAddRequest")) @@ -15625,6 +15208,7 @@ def from_dict(obj: Any) -> 'RPC': model = Model.from_dict(obj.get("Model")) model_billing = ModelBilling.from_dict(obj.get("ModelBilling")) model_billing_token_prices = ModelBillingTokenPrices.from_dict(obj.get("ModelBillingTokenPrices")) + model_billing_token_prices_long_context = ModelBillingTokenPricesLongContext.from_dict(obj.get("ModelBillingTokenPricesLongContext")) model_capabilities = ModelCapabilities.from_dict(obj.get("ModelCapabilities")) model_capabilities_limits = ModelCapabilitiesLimits.from_dict(obj.get("ModelCapabilitiesLimits")) model_capabilities_limits_vision = ModelCapabilitiesLimitsVision.from_dict(obj.get("ModelCapabilitiesLimitsVision")) @@ -15648,15 +15232,16 @@ def from_dict(obj: Any) -> 'RPC': name_set_auto_request = NameSetAutoRequest.from_dict(obj.get("NameSetAutoRequest")) name_set_auto_result = NameSetAutoResult.from_dict(obj.get("NameSetAutoResult")) name_set_request = NameSetRequest.from_dict(obj.get("NameSetRequest")) + open_canvas_instance = OpenCanvasInstance.from_dict(obj.get("OpenCanvasInstance")) options_update_env_value_mode = MCPSetEnvValueModeDetails(obj.get("OptionsUpdateEnvValueMode")) pending_permission_request = PendingPermissionRequest.from_dict(obj.get("PendingPermissionRequest")) pending_permission_request_list = PendingPermissionRequestList.from_dict(obj.get("PendingPermissionRequestList")) - permission_decision = PermissionDecision.from_dict(obj.get("PermissionDecision")) + permission_decision = _load_PermissionDecision(obj.get("PermissionDecision")) permission_decision_approved = PermissionDecisionApproved.from_dict(obj.get("PermissionDecisionApproved")) permission_decision_approved_for_location = PermissionDecisionApprovedForLocation.from_dict(obj.get("PermissionDecisionApprovedForLocation")) permission_decision_approved_for_session = PermissionDecisionApprovedForSession.from_dict(obj.get("PermissionDecisionApprovedForSession")) permission_decision_approve_for_location = PermissionDecisionApproveForLocation.from_dict(obj.get("PermissionDecisionApproveForLocation")) - permission_decision_approve_for_location_approval = PermissionDecisionApproveForLocationApproval.from_dict(obj.get("PermissionDecisionApproveForLocationApproval")) + permission_decision_approve_for_location_approval = _load_PermissionDecisionApproveForLocationApproval(obj.get("PermissionDecisionApproveForLocationApproval")) permission_decision_approve_for_location_approval_commands = PermissionDecisionApproveForLocationApprovalCommands.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalCommands")) permission_decision_approve_for_location_approval_custom_tool = PermissionDecisionApproveForLocationApprovalCustomTool.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalCustomTool")) permission_decision_approve_for_location_approval_extension_management = PermissionDecisionApproveForLocationApprovalExtensionManagement.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalExtensionManagement")) @@ -15667,7 +15252,7 @@ def from_dict(obj: Any) -> 'RPC': permission_decision_approve_for_location_approval_read = PermissionDecisionApproveForLocationApprovalRead.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalRead")) permission_decision_approve_for_location_approval_write = PermissionDecisionApproveForLocationApprovalWrite.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalWrite")) permission_decision_approve_for_session = PermissionDecisionApproveForSession.from_dict(obj.get("PermissionDecisionApproveForSession")) - permission_decision_approve_for_session_approval = PermissionDecisionApproveForSessionApproval.from_dict(obj.get("PermissionDecisionApproveForSessionApproval")) + permission_decision_approve_for_session_approval = _load_PermissionDecisionApproveForSessionApproval(obj.get("PermissionDecisionApproveForSessionApproval")) permission_decision_approve_for_session_approval_commands = PermissionDecisionApproveForSessionApprovalCommands.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalCommands")) permission_decision_approve_for_session_approval_custom_tool = PermissionDecisionApproveForSessionApprovalCustomTool.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalCustomTool")) permission_decision_approve_for_session_approval_extension_management = PermissionDecisionApproveForSessionApprovalExtensionManagement.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalExtensionManagement")) @@ -15712,7 +15297,7 @@ def from_dict(obj: Any) -> 'RPC': permissions_configure_params = PermissionsConfigureParams.from_dict(obj.get("PermissionsConfigureParams")) permissions_configure_result = PermissionsConfigureResult.from_dict(obj.get("PermissionsConfigureResult")) permissions_folder_trust_add_trusted_result = PermissionsFolderTrustAddTrustedResult.from_dict(obj.get("PermissionsFolderTrustAddTrustedResult")) - permissions_locations_add_tool_approval_details = PermissionsLocationsAddToolApprovalDetails.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetails")) + permissions_locations_add_tool_approval_details = _load_PermissionsLocationsAddToolApprovalDetails(obj.get("PermissionsLocationsAddToolApprovalDetails")) permissions_locations_add_tool_approval_details_commands = PermissionsLocationsAddToolApprovalDetailsCommands.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsCommands")) permissions_locations_add_tool_approval_details_custom_tool = PermissionsLocationsAddToolApprovalDetailsCustomTool.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsCustomTool")) permissions_locations_add_tool_approval_details_extension_management = PermissionsLocationsAddToolApprovalDetailsExtensionManagement.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsExtensionManagement")) @@ -15749,7 +15334,7 @@ def from_dict(obj: Any) -> 'RPC': plugin_list = PluginList.from_dict(obj.get("PluginList")) queued_command_handled = QueuedCommandHandled.from_dict(obj.get("QueuedCommandHandled")) queued_command_not_handled = QueuedCommandNotHandled.from_dict(obj.get("QueuedCommandNotHandled")) - queued_command_result = QueuedCommandResult.from_dict(obj.get("QueuedCommandResult")) + queued_command_result = _load_QueuedCommandResult(obj.get("QueuedCommandResult")) queue_pending_items = QueuePendingItems.from_dict(obj.get("QueuePendingItems")) queue_pending_items_kind = QueuePendingItemsKind(obj.get("QueuePendingItemsKind")) queue_pending_items_result = QueuePendingItemsResult.from_dict(obj.get("QueuePendingItemsResult")) @@ -15770,7 +15355,7 @@ def from_dict(obj: Any) -> 'RPC': secrets_add_filter_values_request = SecretsAddFilterValuesRequest.from_dict(obj.get("SecretsAddFilterValuesRequest")) secrets_add_filter_values_result = SecretsAddFilterValuesResult.from_dict(obj.get("SecretsAddFilterValuesResult")) send_agent_mode = SendAgentMode(obj.get("SendAgentMode")) - send_attachment = SendAttachment.from_dict(obj.get("SendAttachment")) + send_attachment = _load_SendAttachment(obj.get("SendAttachment")) send_attachment_blob = SendAttachmentBlob.from_dict(obj.get("SendAttachmentBlob")) send_attachment_directory = SendAttachmentDirectory.from_dict(obj.get("SendAttachmentDirectory")) send_attachment_file = SendAttachmentFile.from_dict(obj.get("SendAttachmentFile")) @@ -15828,6 +15413,7 @@ def from_dict(obj: Any) -> 'RPC': session_list_filter = SessionListFilter.from_dict(obj.get("SessionListFilter")) session_load_deferred_repo_hooks_result = SessionLoadDeferredRepoHooksResult.from_dict(obj.get("SessionLoadDeferredRepoHooksResult")) session_log_level = SessionLogLevel(obj.get("SessionLogLevel")) + session_mcp_apps_call_tool_result = from_dict(lambda x: x, obj.get("SessionMcpAppsCallToolResult")) session_metadata = SessionMetadata.from_dict(obj.get("SessionMetadata")) session_metadata_snapshot = SessionMetadataSnapshot.from_dict(obj.get("SessionMetadataSnapshot")) session_mode = SessionMode(obj.get("SessionMode")) @@ -15888,7 +15474,7 @@ def from_dict(obj: Any) -> 'RPC': slash_command_info = SlashCommandInfo.from_dict(obj.get("SlashCommandInfo")) slash_command_input = SlashCommandInput.from_dict(obj.get("SlashCommandInput")) slash_command_input_completion = SlashCommandInputCompletion(obj.get("SlashCommandInputCompletion")) - slash_command_invocation_result = SlashCommandInvocationResult.from_dict(obj.get("SlashCommandInvocationResult")) + slash_command_invocation_result = _load_SlashCommandInvocationResult(obj.get("SlashCommandInvocationResult")) slash_command_kind = SlashCommandKind(obj.get("SlashCommandKind")) slash_command_select_subcommand_option = SlashCommandSelectSubcommandOption.from_dict(obj.get("SlashCommandSelectSubcommandOption")) slash_command_select_subcommand_result = SlashCommandSelectSubcommandResult.from_dict(obj.get("SlashCommandSelectSubcommandResult")) @@ -15896,7 +15482,7 @@ def from_dict(obj: Any) -> 'RPC': task_agent_info = TaskAgentInfo.from_dict(obj.get("TaskAgentInfo")) task_agent_progress = TaskAgentProgress.from_dict(obj.get("TaskAgentProgress")) task_execution_mode = TaskExecutionMode(obj.get("TaskExecutionMode")) - task_info = TaskInfo.from_dict(obj.get("TaskInfo")) + task_info = _load_TaskInfo(obj.get("TaskInfo")) task_list = TaskList.from_dict(obj.get("TaskList")) task_progress_line = TaskProgressLine.from_dict(obj.get("TaskProgressLine")) tasks_cancel_request = TasksCancelRequest.from_dict(obj.get("TasksCancelRequest")) @@ -15968,14 +15554,6 @@ def from_dict(obj: Any) -> 'RPC': usage_metrics_model_metric_usage = UsageMetricsModelMetricUsage.from_dict(obj.get("UsageMetricsModelMetricUsage")) usage_metrics_token_detail = UsageMetricsTokenDetail.from_dict(obj.get("UsageMetricsTokenDetail")) user_auth_info = UserAuthInfo.from_dict(obj.get("UserAuthInfo")) - user_tool_session_approval_commands = UserToolSessionApprovalCommands.from_dict(obj.get("UserToolSessionApprovalCommands")) - user_tool_session_approval_custom_tool = UserToolSessionApprovalCustomTool.from_dict(obj.get("UserToolSessionApprovalCustomTool")) - user_tool_session_approval_extension_management = UserToolSessionApprovalExtensionManagement.from_dict(obj.get("UserToolSessionApprovalExtensionManagement")) - user_tool_session_approval_extension_permission_access = UserToolSessionApprovalExtensionPermissionAccess.from_dict(obj.get("UserToolSessionApprovalExtensionPermissionAccess")) - user_tool_session_approval_mcp = UserToolSessionApprovalMCP.from_dict(obj.get("UserToolSessionApprovalMcp")) - user_tool_session_approval_memory = UserToolSessionApprovalMemory.from_dict(obj.get("UserToolSessionApprovalMemory")) - user_tool_session_approval_read = UserToolSessionApprovalRead.from_dict(obj.get("UserToolSessionApprovalRead")) - user_tool_session_approval_write = UserToolSessionApprovalWrite.from_dict(obj.get("UserToolSessionApprovalWrite")) workspaces_checkpoints = WorkspacesCheckpoints.from_dict(obj.get("WorkspacesCheckpoints")) workspaces_create_file_request = WorkspacesCreateFileRequest.from_dict(obj.get("WorkspacesCreateFileRequest")) workspaces_get_workspace_result = WorkspacesGetWorkspaceResult.from_dict(obj.get("WorkspacesGetWorkspaceResult")) @@ -15992,7 +15570,7 @@ def from_dict(obj: Any) -> 'RPC': session_context_info = from_union([SessionContextInfo.from_dict, from_none], obj.get("SessionContextInfo")) 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_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_reload_result, agent_select_request, agent_select_result, api_key_auth_info, auth_info, auth_info_type, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_filter_mode, 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, 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_result, execute_command_params, execute_command_result, extension, 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_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, installed_plugin, installed_plugin_source, installed_plugin_source_github, installed_plugin_source_local, installed_plugin_source_url, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, lsp_initialize_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_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_oauth_login_request, mcp_oauth_login_result, mcp_remove_git_hub_result, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_auth, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_stdio, mcp_server_list, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_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_billing, model_billing_token_prices, 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_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, options_update_env_value_mode, 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_management, permission_decision_approve_for_location_approval_extension_permission_access, 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_management, permission_decision_approve_for_session_approval_extension_permission_access, 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_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_reject, permission_decision_request, 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_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_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_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_management, permissions_locations_add_tool_approval_details_extension_permission_access, 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_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_update_request, plugin, plugin_list, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_mode, schedule_entry, schedule_list, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachment, send_attachment_blob, send_attachment_directory, send_attachment_file, send_attachment_file_line_range, send_attachment_github_reference, send_attachment_github_reference_type, send_attachment_selection, send_attachment_selection_details, send_attachment_selection_details_end, send_attachment_selection_details_start, send_mode, send_request, send_result, server_skill, server_skill_list, session_auth_status, session_bulk_delete_result, 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_stat_request, session_fs_stat_result, session_fs_write_file_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_github, session_installed_plugin_source_local, session_installed_plugin_source_url, session_list, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_metadata, session_metadata_snapshot, session_mode, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, 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_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_prune_old_request, 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, session_update_options_params, session_update_options_result, session_working_directory_context, session_working_directory_context_host_type, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, task_agent_info, task_agent_progress, 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, tool, tool_list, tools_initialize_and_validate_result, tools_list_request, 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_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_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, usage_get_metrics_result, 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_tool_session_approval_commands, user_tool_session_approval_custom_tool, user_tool_session_approval_extension_management, user_tool_session_approval_extension_permission_access, user_tool_session_approval_mcp, user_tool_session_approval_memory, user_tool_session_approval_read, user_tool_session_approval_write, workspaces_checkpoints, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_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, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_info, task_progress, workspace_summary) + return RPC(abort_request, abort_result, account_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_reload_result, agent_select_request, agent_select_result, api_key_auth_info, auth_info, auth_info_type, canvas_action, canvas_close_request, canvas_instance_availability, canvas_invoke_action_request, canvas_invoke_action_result, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_filter_mode, 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, discovered_canvas, 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_result, execute_command_params, execute_command_result, extension, 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_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, installed_plugin, installed_plugin_source, installed_plugin_source_github, installed_plugin_source_local, installed_plugin_source_url, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, lsp_initialize_request, 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_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_oauth_login_request, mcp_oauth_login_result, mcp_remove_git_hub_result, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_auth, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_stdio, mcp_server_list, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_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_billing, 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_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_env_value_mode, 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_management, permission_decision_approve_for_location_approval_extension_permission_access, 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_management, permission_decision_approve_for_session_approval_extension_permission_access, 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_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_reject, permission_decision_request, 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_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_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_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_management, permissions_locations_add_tool_approval_details_extension_permission_access, 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_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_update_request, plugin, plugin_list, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_mode, schedule_entry, schedule_list, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachment, send_attachment_blob, send_attachment_directory, send_attachment_file, send_attachment_file_line_range, send_attachment_github_reference, send_attachment_github_reference_type, send_attachment_selection, send_attachment_selection_details, send_attachment_selection_details_end, send_attachment_selection_details_start, send_mode, send_request, send_result, server_skill, server_skill_list, session_auth_status, session_bulk_delete_result, 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_stat_request, session_fs_stat_result, session_fs_write_file_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_github, session_installed_plugin_source_local, session_installed_plugin_source_url, session_list, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_mcp_apps_call_tool_result, session_metadata, session_metadata_snapshot, session_mode, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, 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_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_prune_old_request, 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, session_update_options_params, session_update_options_result, session_working_directory_context, session_working_directory_context_host_type, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, task_agent_info, task_agent_progress, 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, tool, tool_list, tools_initialize_and_validate_result, tools_list_request, 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_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_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, usage_get_metrics_result, 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, workspaces_checkpoints, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_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, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_info, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -16009,8 +15587,17 @@ def to_dict(self) -> dict: result["AgentSelectRequest"] = to_class(AgentSelectRequest, self.agent_select_request) result["AgentSelectResult"] = to_class(AgentSelectResult, self.agent_select_result) result["ApiKeyAuthInfo"] = to_class(APIKeyAuthInfo, self.api_key_auth_info) - result["AuthInfo"] = to_class(AuthInfo, self.auth_info) + result["AuthInfo"] = (self.auth_info).to_dict() result["AuthInfoType"] = to_enum(AuthInfoType, self.auth_info_type) + result["CanvasAction"] = to_class(CanvasAction, self.canvas_action) + result["CanvasCloseRequest"] = to_class(CanvasCloseRequest, self.canvas_close_request) + result["CanvasInstanceAvailability"] = to_enum(CanvasInstanceAvailability, self.canvas_instance_availability) + result["CanvasInvokeActionRequest"] = to_class(CanvasInvokeActionRequest, self.canvas_invoke_action_request) + result["CanvasInvokeActionResult"] = to_class(CanvasInvokeActionResult, self.canvas_invoke_action_result) + result["CanvasJsonSchema"] = self.canvas_json_schema + result["CanvasList"] = to_class(CanvasList, self.canvas_list) + result["CanvasListOpenResult"] = to_class(CanvasListOpenResult, self.canvas_list_open_result) + result["CanvasOpenRequest"] = to_class(CanvasOpenRequest, self.canvas_open_request) result["CommandList"] = to_class(CommandList, self.command_list) result["CommandsHandlePendingCommandRequest"] = to_class(CommandsHandlePendingCommandRequest, self.commands_handle_pending_command_request) result["CommandsHandlePendingCommandResult"] = to_class(CommandsHandlePendingCommandResult, self.commands_handle_pending_command_result) @@ -16022,8 +15609,8 @@ def to_dict(self) -> dict: result["ConnectedRemoteSessionMetadataKind"] = to_enum(ConnectedRemoteSessionMetadataKind, self.connected_remote_session_metadata_kind) result["ConnectedRemoteSessionMetadataRepository"] = to_class(ConnectedRemoteSessionMetadataRepository, self.connected_remote_session_metadata_repository) result["ConnectRemoteSessionParams"] = to_class(ConnectRemoteSessionParams, self.connect_remote_session_params) - result["ConnectRequest"] = to_class(ConnectRequest, self.connect_request) - result["ConnectResult"] = to_class(ConnectResult, self.connect_result) + result["ConnectRequest"] = to_class(_ConnectRequest, self.connect_request) + result["ConnectResult"] = to_class(_ConnectResult, self.connect_result) result["ContentFilterMode"] = to_enum(ContentFilterMode, self.content_filter_mode) result["CopilotApiTokenAuthInfo"] = to_class(CopilotAPITokenAuthInfo, self.copilot_api_token_auth_info) result["CopilotUserResponse"] = to_class(CopilotUserResponse, self.copilot_user_response) @@ -16033,6 +15620,7 @@ def to_dict(self) -> dict: result["CopilotUserResponseQuotaSnapshotsCompletions"] = to_class(CopilotUserResponseQuotaSnapshotsCompletions, self.copilot_user_response_quota_snapshots_completions) result["CopilotUserResponseQuotaSnapshotsPremiumInteractions"] = to_class(CopilotUserResponseQuotaSnapshotsPremiumInteractions, self.copilot_user_response_quota_snapshots_premium_interactions) result["CurrentModel"] = to_class(CurrentModel, self.current_model) + result["DiscoveredCanvas"] = to_class(DiscoveredCanvas, self.discovered_canvas) 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) @@ -16057,14 +15645,14 @@ def to_dict(self) -> dict: result["ExternalToolTextResultForLlm"] = to_class(ExternalToolTextResultForLlm, self.external_tool_text_result_for_llm) result["ExternalToolTextResultForLlmBinaryResultsForLlm"] = to_class(ExternalToolTextResultForLlmBinaryResultsForLlm, self.external_tool_text_result_for_llm_binary_results_for_llm) result["ExternalToolTextResultForLlmBinaryResultsForLlmType"] = to_enum(ExternalToolTextResultForLlmBinaryResultsForLlmType, self.external_tool_text_result_for_llm_binary_results_for_llm_type) - result["ExternalToolTextResultForLlmContent"] = to_class(ExternalToolTextResultForLlmContent, self.external_tool_text_result_for_llm_content) + result["ExternalToolTextResultForLlmContent"] = (self.external_tool_text_result_for_llm_content).to_dict() result["ExternalToolTextResultForLlmContentAudio"] = to_class(ExternalToolTextResultForLlmContentAudio, self.external_tool_text_result_for_llm_content_audio) result["ExternalToolTextResultForLlmContentImage"] = to_class(ExternalToolTextResultForLlmContentImage, self.external_tool_text_result_for_llm_content_image) result["ExternalToolTextResultForLlmContentResource"] = to_class(ExternalToolTextResultForLlmContentResource, self.external_tool_text_result_for_llm_content_resource) result["ExternalToolTextResultForLlmContentResourceDetails"] = from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], self.external_tool_text_result_for_llm_content_resource_details) result["ExternalToolTextResultForLlmContentResourceLink"] = to_class(ExternalToolTextResultForLlmContentResourceLink, self.external_tool_text_result_for_llm_content_resource_link) result["ExternalToolTextResultForLlmContentResourceLinkIcon"] = to_class(ExternalToolTextResultForLlmContentResourceLinkIcon, self.external_tool_text_result_for_llm_content_resource_link_icon) - result["ExternalToolTextResultForLlmContentResourceLinkIconTheme"] = to_enum(ExternalToolTextResultForLlmContentResourceLinkIconTheme, self.external_tool_text_result_for_llm_content_resource_link_icon_theme) + result["ExternalToolTextResultForLlmContentResourceLinkIconTheme"] = to_enum(Theme, self.external_tool_text_result_for_llm_content_resource_link_icon_theme) result["ExternalToolTextResultForLlmContentTerminal"] = to_class(ExternalToolTextResultForLlmContentTerminal, self.external_tool_text_result_for_llm_content_terminal) result["ExternalToolTextResultForLlmContentText"] = to_class(ExternalToolTextResultForLlmContentText, self.external_tool_text_result_for_llm_content_text) result["FilterMapping"] = from_union([lambda x: from_dict(lambda x: to_enum(ContentFilterMode, x), x), lambda x: to_enum(ContentFilterMode, x)], self.filter_mapping) @@ -16097,6 +15685,28 @@ def to_dict(self) -> dict: result["LogRequest"] = to_class(LogRequest, self.log_request) result["LogResult"] = to_class(LogResult, self.log_result) result["LspInitializeRequest"] = to_class(LspInitializeRequest, self.lsp_initialize_request) + result["McpAppsCallToolRequest"] = to_class(MCPAppsCallToolRequest, self.mcp_apps_call_tool_request) + result["McpAppsDiagnoseCapability"] = to_class(MCPAppsDiagnoseCapability, self.mcp_apps_diagnose_capability) + result["McpAppsDiagnoseRequest"] = to_class(MCPAppsDiagnoseRequest, self.mcp_apps_diagnose_request) + result["McpAppsDiagnoseResult"] = to_class(MCPAppsDiagnoseResult, self.mcp_apps_diagnose_result) + result["McpAppsDiagnoseServer"] = to_class(MCPAppsDiagnoseServer, self.mcp_apps_diagnose_server) + result["McpAppsHostContext"] = to_class(MCPAppsHostContext, self.mcp_apps_host_context) + result["McpAppsHostContextDetails"] = to_class(MCPAppsHostContextDetails, self.mcp_apps_host_context_details) + result["McpAppsHostContextDetailsAvailableDisplayMode"] = to_enum(MCPAppsDisplayMode, self.mcp_apps_host_context_details_available_display_mode) + result["McpAppsHostContextDetailsDisplayMode"] = to_enum(MCPAppsDisplayMode, self.mcp_apps_host_context_details_display_mode) + result["McpAppsHostContextDetailsPlatform"] = to_enum(MCPAppsHostContextDetailsPlatform, self.mcp_apps_host_context_details_platform) + result["McpAppsHostContextDetailsTheme"] = to_enum(Theme, self.mcp_apps_host_context_details_theme) + result["McpAppsListToolsRequest"] = to_class(MCPAppsListToolsRequest, self.mcp_apps_list_tools_request) + result["McpAppsListToolsResult"] = to_class(MCPAppsListToolsResult, self.mcp_apps_list_tools_result) + result["McpAppsReadResourceRequest"] = to_class(MCPAppsReadResourceRequest, self.mcp_apps_read_resource_request) + result["McpAppsReadResourceResult"] = to_class(MCPAppsReadResourceResult, self.mcp_apps_read_resource_result) + result["McpAppsResourceContent"] = to_class(MCPAppsResourceContent, self.mcp_apps_resource_content) + result["McpAppsSetHostContextDetails"] = to_class(MCPAppsSetHostContextDetails, self.mcp_apps_set_host_context_details) + result["McpAppsSetHostContextDetailsAvailableDisplayMode"] = to_enum(MCPAppsDisplayMode, self.mcp_apps_set_host_context_details_available_display_mode) + result["McpAppsSetHostContextDetailsDisplayMode"] = to_enum(MCPAppsDisplayMode, self.mcp_apps_set_host_context_details_display_mode) + result["McpAppsSetHostContextDetailsPlatform"] = to_enum(MCPAppsHostContextDetailsPlatform, self.mcp_apps_set_host_context_details_platform) + result["McpAppsSetHostContextDetailsTheme"] = to_enum(Theme, self.mcp_apps_set_host_context_details_theme) + result["McpAppsSetHostContextRequest"] = to_class(MCPAppsSetHostContextRequest, self.mcp_apps_set_host_context_request) result["McpCancelSamplingExecutionParams"] = to_class(MCPCancelSamplingExecutionParams, self.mcp_cancel_sampling_execution_params) result["McpCancelSamplingExecutionResult"] = to_class(MCPCancelSamplingExecutionResult, self.mcp_cancel_sampling_execution_result) result["McpConfigAddRequest"] = to_class(MCPConfigAddRequest, self.mcp_config_add_request) @@ -16144,6 +15754,7 @@ def to_dict(self) -> dict: result["Model"] = to_class(Model, self.model) result["ModelBilling"] = to_class(ModelBilling, self.model_billing) result["ModelBillingTokenPrices"] = to_class(ModelBillingTokenPrices, self.model_billing_token_prices) + result["ModelBillingTokenPricesLongContext"] = to_class(ModelBillingTokenPricesLongContext, self.model_billing_token_prices_long_context) result["ModelCapabilities"] = to_class(ModelCapabilities, self.model_capabilities) result["ModelCapabilitiesLimits"] = to_class(ModelCapabilitiesLimits, self.model_capabilities_limits) result["ModelCapabilitiesLimitsVision"] = to_class(ModelCapabilitiesLimitsVision, self.model_capabilities_limits_vision) @@ -16167,15 +15778,16 @@ def to_dict(self) -> dict: result["NameSetAutoRequest"] = to_class(NameSetAutoRequest, self.name_set_auto_request) result["NameSetAutoResult"] = to_class(NameSetAutoResult, self.name_set_auto_result) result["NameSetRequest"] = to_class(NameSetRequest, self.name_set_request) + result["OpenCanvasInstance"] = to_class(OpenCanvasInstance, self.open_canvas_instance) result["OptionsUpdateEnvValueMode"] = to_enum(MCPSetEnvValueModeDetails, self.options_update_env_value_mode) result["PendingPermissionRequest"] = to_class(PendingPermissionRequest, self.pending_permission_request) result["PendingPermissionRequestList"] = to_class(PendingPermissionRequestList, self.pending_permission_request_list) - result["PermissionDecision"] = to_class(PermissionDecision, self.permission_decision) + result["PermissionDecision"] = (self.permission_decision).to_dict() result["PermissionDecisionApproved"] = to_class(PermissionDecisionApproved, self.permission_decision_approved) result["PermissionDecisionApprovedForLocation"] = to_class(PermissionDecisionApprovedForLocation, self.permission_decision_approved_for_location) result["PermissionDecisionApprovedForSession"] = to_class(PermissionDecisionApprovedForSession, self.permission_decision_approved_for_session) result["PermissionDecisionApproveForLocation"] = to_class(PermissionDecisionApproveForLocation, self.permission_decision_approve_for_location) - result["PermissionDecisionApproveForLocationApproval"] = to_class(PermissionDecisionApproveForLocationApproval, self.permission_decision_approve_for_location_approval) + result["PermissionDecisionApproveForLocationApproval"] = (self.permission_decision_approve_for_location_approval).to_dict() result["PermissionDecisionApproveForLocationApprovalCommands"] = to_class(PermissionDecisionApproveForLocationApprovalCommands, self.permission_decision_approve_for_location_approval_commands) result["PermissionDecisionApproveForLocationApprovalCustomTool"] = to_class(PermissionDecisionApproveForLocationApprovalCustomTool, self.permission_decision_approve_for_location_approval_custom_tool) result["PermissionDecisionApproveForLocationApprovalExtensionManagement"] = to_class(PermissionDecisionApproveForLocationApprovalExtensionManagement, self.permission_decision_approve_for_location_approval_extension_management) @@ -16186,7 +15798,7 @@ def to_dict(self) -> dict: result["PermissionDecisionApproveForLocationApprovalRead"] = to_class(PermissionDecisionApproveForLocationApprovalRead, self.permission_decision_approve_for_location_approval_read) result["PermissionDecisionApproveForLocationApprovalWrite"] = to_class(PermissionDecisionApproveForLocationApprovalWrite, self.permission_decision_approve_for_location_approval_write) result["PermissionDecisionApproveForSession"] = to_class(PermissionDecisionApproveForSession, self.permission_decision_approve_for_session) - result["PermissionDecisionApproveForSessionApproval"] = to_class(PermissionDecisionApproveForSessionApproval, self.permission_decision_approve_for_session_approval) + result["PermissionDecisionApproveForSessionApproval"] = (self.permission_decision_approve_for_session_approval).to_dict() result["PermissionDecisionApproveForSessionApprovalCommands"] = to_class(PermissionDecisionApproveForSessionApprovalCommands, self.permission_decision_approve_for_session_approval_commands) result["PermissionDecisionApproveForSessionApprovalCustomTool"] = to_class(PermissionDecisionApproveForSessionApprovalCustomTool, self.permission_decision_approve_for_session_approval_custom_tool) result["PermissionDecisionApproveForSessionApprovalExtensionManagement"] = to_class(PermissionDecisionApproveForSessionApprovalExtensionManagement, self.permission_decision_approve_for_session_approval_extension_management) @@ -16231,7 +15843,7 @@ def to_dict(self) -> dict: result["PermissionsConfigureParams"] = to_class(PermissionsConfigureParams, self.permissions_configure_params) result["PermissionsConfigureResult"] = to_class(PermissionsConfigureResult, self.permissions_configure_result) result["PermissionsFolderTrustAddTrustedResult"] = to_class(PermissionsFolderTrustAddTrustedResult, self.permissions_folder_trust_add_trusted_result) - result["PermissionsLocationsAddToolApprovalDetails"] = to_class(PermissionsLocationsAddToolApprovalDetails, self.permissions_locations_add_tool_approval_details) + result["PermissionsLocationsAddToolApprovalDetails"] = (self.permissions_locations_add_tool_approval_details).to_dict() result["PermissionsLocationsAddToolApprovalDetailsCommands"] = to_class(PermissionsLocationsAddToolApprovalDetailsCommands, self.permissions_locations_add_tool_approval_details_commands) result["PermissionsLocationsAddToolApprovalDetailsCustomTool"] = to_class(PermissionsLocationsAddToolApprovalDetailsCustomTool, self.permissions_locations_add_tool_approval_details_custom_tool) result["PermissionsLocationsAddToolApprovalDetailsExtensionManagement"] = to_class(PermissionsLocationsAddToolApprovalDetailsExtensionManagement, self.permissions_locations_add_tool_approval_details_extension_management) @@ -16268,7 +15880,7 @@ def to_dict(self) -> dict: result["PluginList"] = to_class(PluginList, self.plugin_list) result["QueuedCommandHandled"] = to_class(QueuedCommandHandled, self.queued_command_handled) result["QueuedCommandNotHandled"] = to_class(QueuedCommandNotHandled, self.queued_command_not_handled) - result["QueuedCommandResult"] = to_class(QueuedCommandResult, self.queued_command_result) + result["QueuedCommandResult"] = (self.queued_command_result).to_dict() result["QueuePendingItems"] = to_class(QueuePendingItems, self.queue_pending_items) result["QueuePendingItemsKind"] = to_enum(QueuePendingItemsKind, self.queue_pending_items_kind) result["QueuePendingItemsResult"] = to_class(QueuePendingItemsResult, self.queue_pending_items_result) @@ -16289,7 +15901,7 @@ def to_dict(self) -> dict: result["SecretsAddFilterValuesRequest"] = to_class(SecretsAddFilterValuesRequest, self.secrets_add_filter_values_request) result["SecretsAddFilterValuesResult"] = to_class(SecretsAddFilterValuesResult, self.secrets_add_filter_values_result) result["SendAgentMode"] = to_enum(SendAgentMode, self.send_agent_mode) - result["SendAttachment"] = to_class(SendAttachment, self.send_attachment) + result["SendAttachment"] = (self.send_attachment).to_dict() result["SendAttachmentBlob"] = to_class(SendAttachmentBlob, self.send_attachment_blob) result["SendAttachmentDirectory"] = to_class(SendAttachmentDirectory, self.send_attachment_directory) result["SendAttachmentFile"] = to_class(SendAttachmentFile, self.send_attachment_file) @@ -16347,6 +15959,7 @@ def to_dict(self) -> dict: result["SessionListFilter"] = to_class(SessionListFilter, self.session_list_filter) result["SessionLoadDeferredRepoHooksResult"] = to_class(SessionLoadDeferredRepoHooksResult, self.session_load_deferred_repo_hooks_result) result["SessionLogLevel"] = to_enum(SessionLogLevel, self.session_log_level) + result["SessionMcpAppsCallToolResult"] = from_dict(lambda x: x, self.session_mcp_apps_call_tool_result) result["SessionMetadata"] = to_class(SessionMetadata, self.session_metadata) result["SessionMetadataSnapshot"] = to_class(SessionMetadataSnapshot, self.session_metadata_snapshot) result["SessionMode"] = to_enum(SessionMode, self.session_mode) @@ -16407,7 +16020,7 @@ def to_dict(self) -> dict: result["SlashCommandInfo"] = to_class(SlashCommandInfo, self.slash_command_info) result["SlashCommandInput"] = to_class(SlashCommandInput, self.slash_command_input) result["SlashCommandInputCompletion"] = to_enum(SlashCommandInputCompletion, self.slash_command_input_completion) - result["SlashCommandInvocationResult"] = to_class(SlashCommandInvocationResult, self.slash_command_invocation_result) + result["SlashCommandInvocationResult"] = (self.slash_command_invocation_result).to_dict() result["SlashCommandKind"] = to_enum(SlashCommandKind, self.slash_command_kind) result["SlashCommandSelectSubcommandOption"] = to_class(SlashCommandSelectSubcommandOption, self.slash_command_select_subcommand_option) result["SlashCommandSelectSubcommandResult"] = to_class(SlashCommandSelectSubcommandResult, self.slash_command_select_subcommand_result) @@ -16415,7 +16028,7 @@ def to_dict(self) -> dict: result["TaskAgentInfo"] = to_class(TaskAgentInfo, self.task_agent_info) result["TaskAgentProgress"] = to_class(TaskAgentProgress, self.task_agent_progress) result["TaskExecutionMode"] = to_enum(TaskExecutionMode, self.task_execution_mode) - result["TaskInfo"] = to_class(TaskInfo, self.task_info) + result["TaskInfo"] = (self.task_info).to_dict() result["TaskList"] = to_class(TaskList, self.task_list) result["TaskProgressLine"] = to_class(TaskProgressLine, self.task_progress_line) result["TasksCancelRequest"] = to_class(TasksCancelRequest, self.tasks_cancel_request) @@ -16487,14 +16100,6 @@ def to_dict(self) -> dict: result["UsageMetricsModelMetricUsage"] = to_class(UsageMetricsModelMetricUsage, self.usage_metrics_model_metric_usage) result["UsageMetricsTokenDetail"] = to_class(UsageMetricsTokenDetail, self.usage_metrics_token_detail) result["UserAuthInfo"] = to_class(UserAuthInfo, self.user_auth_info) - result["UserToolSessionApprovalCommands"] = to_class(UserToolSessionApprovalCommands, self.user_tool_session_approval_commands) - result["UserToolSessionApprovalCustomTool"] = to_class(UserToolSessionApprovalCustomTool, self.user_tool_session_approval_custom_tool) - result["UserToolSessionApprovalExtensionManagement"] = to_class(UserToolSessionApprovalExtensionManagement, self.user_tool_session_approval_extension_management) - result["UserToolSessionApprovalExtensionPermissionAccess"] = to_class(UserToolSessionApprovalExtensionPermissionAccess, self.user_tool_session_approval_extension_permission_access) - result["UserToolSessionApprovalMcp"] = to_class(UserToolSessionApprovalMCP, self.user_tool_session_approval_mcp) - result["UserToolSessionApprovalMemory"] = to_class(UserToolSessionApprovalMemory, self.user_tool_session_approval_memory) - result["UserToolSessionApprovalRead"] = to_class(UserToolSessionApprovalRead, self.user_tool_session_approval_read) - result["UserToolSessionApprovalWrite"] = to_class(UserToolSessionApprovalWrite, self.user_tool_session_approval_write) result["WorkspacesCheckpoints"] = to_class(WorkspacesCheckpoints, self.workspaces_checkpoints) result["WorkspacesCreateFileRequest"] = to_class(WorkspacesCreateFileRequest, self.workspaces_create_file_request) result["WorkspacesGetWorkspaceResult"] = to_class(WorkspacesGetWorkspaceResult, self.workspaces_get_workspace_result) @@ -16519,13 +16124,181 @@ def rpc_from_dict(s: Any) -> RPC: def rpc_to_dict(x: RPC) -> Any: return to_class(RPC, x) - +# The new auth credentials to install on the session. When omitted or `undefined`, the call is a no-op and the session's existing credentials are preserved. The runtime stores the value verbatim and uses it for outbound model/API requests; it does NOT re-validate or re-fetch the associated Copilot user response. Several variants carry secret material; treat this method's params as containing secrets at rest and in transit. +AuthInfo = HMACAuthInfo | EnvAuthInfo | TokenAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo + +def _load_AuthInfo(obj: Any) -> "AuthInfo": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "hmac": return HMACAuthInfo.from_dict(obj) + case "env": return EnvAuthInfo.from_dict(obj) + case "token": return TokenAuthInfo.from_dict(obj) + case "copilot-api-token": return CopilotAPITokenAuthInfo.from_dict(obj) + case "user": return UserAuthInfo.from_dict(obj) + case "gh-cli": return GhCLIAuthInfo.from_dict(obj) + case "api-key": return APIKeyAuthInfo.from_dict(obj) + case _: raise ValueError(f"Unknown AuthInfo type: {kind!r}") + +# A content block within a tool result, which may be text, terminal output, image, audio, or a resource +ExternalToolTextResultForLlmContent = ExternalToolTextResultForLlmContentText | ExternalToolTextResultForLlmContentTerminal | ExternalToolTextResultForLlmContentImage | ExternalToolTextResultForLlmContentAudio | ExternalToolTextResultForLlmContentResourceLink | ExternalToolTextResultForLlmContentResource + +def _load_ExternalToolTextResultForLlmContent(obj: Any) -> "ExternalToolTextResultForLlmContent": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "text": return ExternalToolTextResultForLlmContentText.from_dict(obj) + case "terminal": return ExternalToolTextResultForLlmContentTerminal.from_dict(obj) + case "image": return ExternalToolTextResultForLlmContentImage.from_dict(obj) + case "audio": return ExternalToolTextResultForLlmContentAudio.from_dict(obj) + case "resource_link": return ExternalToolTextResultForLlmContentResourceLink.from_dict(obj) + case "resource": return ExternalToolTextResultForLlmContentResource.from_dict(obj) + case _: raise ValueError(f"Unknown ExternalToolTextResultForLlmContent type: {kind!r}") + +# The client's response to the pending permission prompt +PermissionDecision = PermissionDecisionApproveOnce | PermissionDecisionApproveForSession | PermissionDecisionApproveForLocation | PermissionDecisionApprovePermanently | PermissionDecisionReject | PermissionDecisionUserNotAvailable | PermissionDecisionApproved | PermissionDecisionApprovedForSession | PermissionDecisionApprovedForLocation | PermissionDecisionCancelled | PermissionDecisionDeniedByRules | PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDecisionDeniedInteractivelyByUser | PermissionDecisionDeniedByContentExclusionPolicy | PermissionDecisionDeniedByPermissionRequestHook + +def _load_PermissionDecision(obj: Any) -> "PermissionDecision": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "approve-once": return PermissionDecisionApproveOnce.from_dict(obj) + case "approve-for-session": return PermissionDecisionApproveForSession.from_dict(obj) + case "approve-for-location": return PermissionDecisionApproveForLocation.from_dict(obj) + case "approve-permanently": return PermissionDecisionApprovePermanently.from_dict(obj) + case "reject": return PermissionDecisionReject.from_dict(obj) + case "user-not-available": return PermissionDecisionUserNotAvailable.from_dict(obj) + case "approved": return PermissionDecisionApproved.from_dict(obj) + case "approved-for-session": return PermissionDecisionApprovedForSession.from_dict(obj) + case "approved-for-location": return PermissionDecisionApprovedForLocation.from_dict(obj) + case "cancelled": return PermissionDecisionCancelled.from_dict(obj) + case "denied-by-rules": return PermissionDecisionDeniedByRules.from_dict(obj) + case "denied-no-approval-rule-and-could-not-request-from-user": return PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser.from_dict(obj) + case "denied-interactively-by-user": return PermissionDecisionDeniedInteractivelyByUser.from_dict(obj) + case "denied-by-content-exclusion-policy": return PermissionDecisionDeniedByContentExclusionPolicy.from_dict(obj) + case "denied-by-permission-request-hook": return PermissionDecisionDeniedByPermissionRequestHook.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionDecision kind: {kind!r}") + +# Approval to persist for this location +PermissionDecisionApproveForLocationApproval = PermissionDecisionApproveForLocationApprovalCommands | PermissionDecisionApproveForLocationApprovalRead | PermissionDecisionApproveForLocationApprovalWrite | PermissionDecisionApproveForLocationApprovalMCP | PermissionDecisionApproveForLocationApprovalMCPSampling | PermissionDecisionApproveForLocationApprovalMemory | PermissionDecisionApproveForLocationApprovalCustomTool | PermissionDecisionApproveForLocationApprovalExtensionManagement | PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess + +def _load_PermissionDecisionApproveForLocationApproval(obj: Any) -> "PermissionDecisionApproveForLocationApproval": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return PermissionDecisionApproveForLocationApprovalCommands.from_dict(obj) + case "read": return PermissionDecisionApproveForLocationApprovalRead.from_dict(obj) + case "write": return PermissionDecisionApproveForLocationApprovalWrite.from_dict(obj) + case "mcp": return PermissionDecisionApproveForLocationApprovalMCP.from_dict(obj) + case "mcp-sampling": return PermissionDecisionApproveForLocationApprovalMCPSampling.from_dict(obj) + case "memory": return PermissionDecisionApproveForLocationApprovalMemory.from_dict(obj) + case "custom-tool": return PermissionDecisionApproveForLocationApprovalCustomTool.from_dict(obj) + case "extension-management": return PermissionDecisionApproveForLocationApprovalExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionDecisionApproveForLocationApproval kind: {kind!r}") + +# Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) +PermissionDecisionApproveForSessionApproval = PermissionDecisionApproveForSessionApprovalCommands | PermissionDecisionApproveForSessionApprovalRead | PermissionDecisionApproveForSessionApprovalWrite | PermissionDecisionApproveForSessionApprovalMCP | PermissionDecisionApproveForSessionApprovalMCPSampling | PermissionDecisionApproveForSessionApprovalMemory | PermissionDecisionApproveForSessionApprovalCustomTool | PermissionDecisionApproveForSessionApprovalExtensionManagement | PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess + +def _load_PermissionDecisionApproveForSessionApproval(obj: Any) -> "PermissionDecisionApproveForSessionApproval": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return PermissionDecisionApproveForSessionApprovalCommands.from_dict(obj) + case "read": return PermissionDecisionApproveForSessionApprovalRead.from_dict(obj) + case "write": return PermissionDecisionApproveForSessionApprovalWrite.from_dict(obj) + case "mcp": return PermissionDecisionApproveForSessionApprovalMCP.from_dict(obj) + case "mcp-sampling": return PermissionDecisionApproveForSessionApprovalMCPSampling.from_dict(obj) + case "memory": return PermissionDecisionApproveForSessionApprovalMemory.from_dict(obj) + case "custom-tool": return PermissionDecisionApproveForSessionApprovalCustomTool.from_dict(obj) + case "extension-management": return PermissionDecisionApproveForSessionApprovalExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionDecisionApproveForSessionApproval kind: {kind!r}") + +# Tool approval to persist and apply +PermissionsLocationsAddToolApprovalDetails = PermissionsLocationsAddToolApprovalDetailsCommands | PermissionsLocationsAddToolApprovalDetailsRead | PermissionsLocationsAddToolApprovalDetailsWrite | PermissionsLocationsAddToolApprovalDetailsMCP | PermissionsLocationsAddToolApprovalDetailsMCPSampling | PermissionsLocationsAddToolApprovalDetailsMemory | PermissionsLocationsAddToolApprovalDetailsCustomTool | PermissionsLocationsAddToolApprovalDetailsExtensionManagement | PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess + +def _load_PermissionsLocationsAddToolApprovalDetails(obj: Any) -> "PermissionsLocationsAddToolApprovalDetails": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return PermissionsLocationsAddToolApprovalDetailsCommands.from_dict(obj) + case "read": return PermissionsLocationsAddToolApprovalDetailsRead.from_dict(obj) + case "write": return PermissionsLocationsAddToolApprovalDetailsWrite.from_dict(obj) + case "mcp": return PermissionsLocationsAddToolApprovalDetailsMCP.from_dict(obj) + case "mcp-sampling": return PermissionsLocationsAddToolApprovalDetailsMCPSampling.from_dict(obj) + case "memory": return PermissionsLocationsAddToolApprovalDetailsMemory.from_dict(obj) + case "custom-tool": return PermissionsLocationsAddToolApprovalDetailsCustomTool.from_dict(obj) + case "extension-management": return PermissionsLocationsAddToolApprovalDetailsExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionsLocationsAddToolApprovalDetails kind: {kind!r}") + +# Result of the queued command execution. +QueuedCommandResult = QueuedCommandHandled | QueuedCommandNotHandled + +def _load_QueuedCommandResult(obj: Any) -> "QueuedCommandResult": + assert isinstance(obj, dict) + kind = obj.get("handled") + match kind: + case "true": return QueuedCommandHandled.from_dict(obj) + case "false": return QueuedCommandNotHandled.from_dict(obj) + case _: raise ValueError(f"Unknown QueuedCommandResult handled: {kind!r}") + +# A user message attachment — a file, directory, code selection, blob, or GitHub reference +SendAttachment = SendAttachmentFile | SendAttachmentDirectory | SendAttachmentSelection | SendAttachmentGithubReference | SendAttachmentBlob + +def _load_SendAttachment(obj: Any) -> "SendAttachment": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "file": return SendAttachmentFile.from_dict(obj) + case "directory": return SendAttachmentDirectory.from_dict(obj) + case "selection": return SendAttachmentSelection.from_dict(obj) + case "github_reference": return SendAttachmentGithubReference.from_dict(obj) + case "blob": return SendAttachmentBlob.from_dict(obj) + case _: raise ValueError(f"Unknown SendAttachment type: {kind!r}") + +# Result of invoking the slash command (text output, prompt to send to the agent, or completion). +SlashCommandInvocationResult = SlashCommandTextResult | SlashCommandAgentPromptResult | SlashCommandCompletedResult | SlashCommandSelectSubcommandResult + +def _load_SlashCommandInvocationResult(obj: Any) -> "SlashCommandInvocationResult": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "text": return SlashCommandTextResult.from_dict(obj) + case "agent-prompt": return SlashCommandAgentPromptResult.from_dict(obj) + case "completed": return SlashCommandCompletedResult.from_dict(obj) + case "select-subcommand": return SlashCommandSelectSubcommandResult.from_dict(obj) + case _: raise ValueError(f"Unknown SlashCommandInvocationResult kind: {kind!r}") + +# Schema for the `TaskInfo` type. +TaskInfo = TaskAgentInfo | TaskShellInfo + +def _load_TaskInfo(obj: Any) -> "TaskInfo": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "agent": return TaskAgentInfo.from_dict(obj) + case "shell": return TaskShellInfo.from_dict(obj) + case _: raise ValueError(f"Unknown TaskInfo type: {kind!r}") + + +CanvasJsonSchema = Any ExternalToolResult = ExternalToolTextResultForLlm +ExternalToolTextResultForLlmContentResourceLinkIconTheme = Theme FilterMapping = dict +McpAppsHostContextDetailsAvailableDisplayMode = MCPAppsDisplayMode +McpAppsHostContextDetailsDisplayMode = MCPAppsDisplayMode +McpAppsHostContextDetailsTheme = Theme +McpAppsSetHostContextDetailsAvailableDisplayMode = MCPAppsDisplayMode +McpAppsSetHostContextDetailsDisplayMode = MCPAppsDisplayMode +McpAppsSetHostContextDetailsPlatform = MCPAppsHostContextDetailsPlatform +McpAppsSetHostContextDetailsTheme = Theme McpExecuteSamplingRequest = dict McpExecuteSamplingResult = dict OptionsUpdateEnvValueMode = MCPSetEnvValueModeDetails SessionContextHostType = HostType +SessionMcpAppsCallToolResult = dict SessionWorkingDirectoryContextHostType = HostType TaskInfoExecutionMode = TaskExecutionMode TaskInfoStatus = TaskStatus @@ -16800,10 +16573,10 @@ class _InternalServerRpc: def __init__(self, client: "JsonRpcClient"): self._client = client - async def connect(self, params: ConnectRequest, *, timeout: float | None = None) -> ConnectResult: + async def _connect(self, params: _ConnectRequest, *, timeout: float | None = None) -> _ConnectResult: "Performs the SDK server connection handshake and validates the optional connection token.\n\nArgs:\n params: Optional connection token presented by the SDK client during the handshake.\n\nReturns:\n Handshake result reporting the server's protocol version and package version on success.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - return ConnectResult.from_dict(await self._client.request("connect", params_dict, **_timeout_kwargs(timeout))) + 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. @@ -16823,6 +16596,39 @@ async def set_credentials(self, params: SessionSetCredentialsParams, *, timeout: return SessionSetCredentialsResult.from_dict(await self._client.request("session.auth.setCredentials", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class CanvasApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self, *, timeout: float | None = None) -> CanvasList: + "Lists canvases declared for the session.\n\nReturns:\n Declared canvases available in this session." + return CanvasList.from_dict(await self._client.request("session.canvas.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def list_open(self, *, timeout: float | None = None) -> CanvasListOpenResult: + "Lists currently open canvas instances for the live session.\n\nReturns:\n Live open-canvas snapshot." + return CanvasListOpenResult.from_dict(await self._client.request("session.canvas.listOpen", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def open(self, params: CanvasOpenRequest, *, timeout: float | None = None) -> OpenCanvasInstance: + "Opens or focuses a canvas instance.\n\nArgs:\n params: Canvas open parameters.\n\nReturns:\n Open canvas instance snapshot." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return OpenCanvasInstance.from_dict(await self._client.request("session.canvas.open", params_dict, **_timeout_kwargs(timeout))) + + async def close(self, params: CanvasCloseRequest, *, timeout: float | None = None) -> None: + "Closes an open canvas instance.\n\nArgs:\n params: Canvas close parameters." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + await self._client.request("session.canvas.close", params_dict, **_timeout_kwargs(timeout)) + + async def invoke_action(self, params: CanvasInvokeActionRequest, *, timeout: float | None = None) -> CanvasInvokeActionResult: + "Invokes an action on an open canvas instance.\n\nArgs:\n params: Canvas action invocation parameters.\n\nReturns:\n Canvas action invocation result." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return CanvasInvokeActionResult.from_dict(await self._client.request("session.canvas.invokeAction", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ModelApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -17114,12 +16920,54 @@ async def login(self, params: MCPOauthLoginRequest, *, timeout: float | None = N return MCPOauthLoginResult.from_dict(await self._client.request("session.mcp.oauth.login", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class McpAppsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def read_resource(self, params: MCPAppsReadResourceRequest, *, timeout: float | None = None) -> MCPAppsReadResourceResult: + "Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability.\n\nArgs:\n params: MCP server and resource URI to fetch.\n\nReturns:\n Resource contents returned by the MCP server." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return MCPAppsReadResourceResult.from_dict(await self._client.request("session.mcp.apps.readResource", params_dict, **_timeout_kwargs(timeout))) + + async def list_tools(self, params: MCPAppsListToolsRequest, *, timeout: float | None = None) -> MCPAppsListToolsResult: + "List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `[\"model\",\"app\"]`) or includes `\"app\"`.\n\nArgs:\n params: MCP server to list app-callable tools for.\n\nReturns:\n App-callable tools from the named MCP server." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return MCPAppsListToolsResult.from_dict(await self._client.request("session.mcp.apps.listTools", params_dict, **_timeout_kwargs(timeout))) + + async def call_tool(self, params: MCPAppsCallToolRequest, *, timeout: float | None = None) -> dict: + "Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`.\n\nArgs:\n params: MCP server, tool name, and arguments to invoke from an MCP App view.\n\nReturns:\n Standard MCP CallToolResult" + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return dict(await self._client.request("session.mcp.apps.callTool", params_dict, **_timeout_kwargs(timeout))) + + async def set_host_context(self, params: MCPAppsSetHostContextRequest, *, timeout: float | None = None) -> None: + "Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI.\n\nArgs:\n params: Host context to advertise to MCP App guests." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + await self._client.request("session.mcp.apps.setHostContext", params_dict, **_timeout_kwargs(timeout)) + + async def get_host_context(self, *, timeout: float | None = None) -> MCPAppsHostContext: + "Read the current host context advertised to MCP App guests.\n\nReturns:\n Current host context advertised to MCP App guests." + return MCPAppsHostContext.from_dict(await self._client.request("session.mcp.apps.getHostContext", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def diagnose(self, params: MCPAppsDiagnoseRequest, *, timeout: float | None = None) -> MCPAppsDiagnoseResult: + "Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated.\n\nArgs:\n params: MCP server to diagnose MCP Apps wiring for.\n\nReturns:\n Diagnostic snapshot of MCP Apps wiring for the named server." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return MCPAppsDiagnoseResult.from_dict(await self._client.request("session.mcp.apps.diagnose", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class McpApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id self.oauth = McpOauthApi(client, session_id) + self.apps = McpAppsApi(client, session_id) async def list(self, *, timeout: float | None = None) -> MCPServerList: "Lists MCP servers configured for the session and their connection status.\n\nReturns:\n MCP servers configured for the session, with their connection status." @@ -17261,7 +17109,7 @@ async def invoke(self, params: CommandsInvokeRequest, *, timeout: float | None = "Invokes a slash command in the session.\n\nArgs:\n params: Slash command name and optional raw input string to invoke.\n\nReturns:\n Result of invoking the slash command (text output, prompt to send to the agent, or completion)." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id - return SlashCommandInvocationResult.from_dict(await self._client.request("session.commands.invoke", params_dict, **_timeout_kwargs(timeout))) + return _load_SlashCommandInvocationResult(await self._client.request("session.commands.invoke", params_dict, **_timeout_kwargs(timeout))) async def handle_pending_command(self, params: CommandsHandlePendingCommandRequest, *, timeout: float | None = None) -> CommandsHandlePendingCommandResult: "Reports completion of a pending client-handled slash command.\n\nArgs:\n params: Pending command request ID and an optional error if the client handler failed.\n\nReturns:\n Indicates whether the pending client-handled command was completed successfully." @@ -17695,6 +17543,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id self.auth = AuthApi(client, session_id) + self.canvas = CanvasApi(client, session_id) self.model = ModelApi(client, session_id) self.mode = ModeApi(client, session_id) self.name = NameApi(client, session_id) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 515f9c317..219e6a365 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum -from typing import Any, TypeVar, cast +from typing import Any, ClassVar, TypeVar, cast from uuid import UUID import dateutil.parser @@ -202,6 +202,9 @@ class SessionEventType(Enum): SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded" SESSION_MCP_SERVER_STATUS_CHANGED = "session.mcp_server_status_changed" SESSION_EXTENSIONS_LOADED = "session.extensions_loaded" + SESSION_CANVAS_OPENED = "session.canvas.opened" + SESSION_CANVAS_REGISTRY_CHANGED = "session.canvas.registry_changed" + MCP_APP_TOOL_CALL_COMPLETE = "mcp_app.tool_call_complete" UNKNOWN = "unknown" @classmethod @@ -320,7 +323,9 @@ class AssistantMessageData: "Assistant response containing text content, optional tool requests, and interaction metadata" content: str message_id: str + # Experimental: this field is part of an experimental API and may change or be removed. anthropic_advisor_blocks: list[Any] | None = None + # Experimental: this field is part of an experimental API and may change or be removed. anthropic_advisor_model: str | None = None encrypted_content: str | None = None interaction_id: str | None = None @@ -332,6 +337,7 @@ class AssistantMessageData: reasoning_opaque: str | None = None reasoning_text: str | None = None request_id: str | None = None + service_request_id: str | None = None tool_requests: list[AssistantMessageToolRequest] | None = None turn_id: str | None = None @@ -351,6 +357,7 @@ def from_dict(obj: Any) -> "AssistantMessageData": reasoning_opaque = from_union([from_none, from_str], obj.get("reasoningOpaque")) reasoning_text = from_union([from_none, from_str], obj.get("reasoningText")) request_id = from_union([from_none, from_str], obj.get("requestId")) + service_request_id = from_union([from_none, from_str], obj.get("serviceRequestId")) tool_requests = from_union([from_none, lambda x: from_list(AssistantMessageToolRequest.from_dict, x)], obj.get("toolRequests")) turn_id = from_union([from_none, from_str], obj.get("turnId")) return AssistantMessageData( @@ -367,6 +374,7 @@ def from_dict(obj: Any) -> "AssistantMessageData": reasoning_opaque=reasoning_opaque, reasoning_text=reasoning_text, request_id=request_id, + service_request_id=service_request_id, tool_requests=tool_requests, turn_id=turn_id, ) @@ -397,6 +405,8 @@ def to_dict(self) -> dict: result["reasoningText"] = from_union([from_none, from_str], self.reasoning_text) if self.request_id is not None: result["requestId"] = from_union([from_none, from_str], self.request_id) + if self.service_request_id is not None: + result["serviceRequestId"] = from_union([from_none, from_str], self.service_request_id) if self.tool_requests is not None: result["toolRequests"] = from_union([from_none, lambda x: from_list(lambda x: to_class(AssistantMessageToolRequest, x), x)], self.tool_requests) if self.turn_id is not None: @@ -619,17 +629,17 @@ def to_dict(self) -> dict: @dataclass -class AssistantUsageCopilotUsage: +class _AssistantUsageCopilotUsage: "Per-request cost and usage data from the CAPI copilot_usage response field" token_details: list[AssistantUsageCopilotUsageTokenDetail] total_nano_aiu: float @staticmethod - def from_dict(obj: Any) -> "AssistantUsageCopilotUsage": + def from_dict(obj: Any) -> "_AssistantUsageCopilotUsage": assert isinstance(obj, dict) token_details = from_list(AssistantUsageCopilotUsageTokenDetail.from_dict, obj.get("tokenDetails")) total_nano_aiu = from_float(obj.get("totalNanoAiu")) - return AssistantUsageCopilotUsage( + return _AssistantUsageCopilotUsage( token_details=token_details, total_nano_aiu=total_nano_aiu, ) @@ -680,7 +690,9 @@ class AssistantUsageData: api_endpoint: AssistantUsageApiEndpoint | None = None cache_read_tokens: int | None = None cache_write_tokens: int | None = None - copilot_usage: AssistantUsageCopilotUsage | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _copilot_usage: _AssistantUsageCopilotUsage | None = None + # Experimental: this field is part of an experimental API and may change or be removed. cost: float | None = None duration: timedelta | None = None initiator: str | None = None @@ -690,9 +702,11 @@ class AssistantUsageData: # Deprecated: this field is deprecated. parent_tool_call_id: str | None = None provider_call_id: str | None = None - quota_snapshots: dict[str, AssistantUsageQuotaSnapshot] | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _quota_snapshots: dict[str, _AssistantUsageQuotaSnapshot] | None = None reasoning_effort: str | None = None reasoning_tokens: int | None = None + service_request_id: str | None = None time_to_first_token: timedelta | None = None @staticmethod @@ -703,7 +717,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": api_endpoint = from_union([from_none, lambda x: parse_enum(AssistantUsageApiEndpoint, x)], obj.get("apiEndpoint")) cache_read_tokens = from_union([from_none, from_int], obj.get("cacheReadTokens")) cache_write_tokens = from_union([from_none, from_int], obj.get("cacheWriteTokens")) - copilot_usage = from_union([from_none, AssistantUsageCopilotUsage.from_dict], obj.get("copilotUsage")) + _copilot_usage = from_union([from_none, _AssistantUsageCopilotUsage.from_dict], obj.get("copilotUsage")) cost = from_union([from_none, from_float], obj.get("cost")) duration = from_union([from_none, from_timedelta], obj.get("duration")) initiator = from_union([from_none, from_str], obj.get("initiator")) @@ -712,9 +726,10 @@ def from_dict(obj: Any) -> "AssistantUsageData": output_tokens = from_union([from_none, from_int], obj.get("outputTokens")) parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) - quota_snapshots = from_union([from_none, lambda x: from_dict(AssistantUsageQuotaSnapshot.from_dict, x)], obj.get("quotaSnapshots")) + _quota_snapshots = from_union([from_none, lambda x: from_dict(_AssistantUsageQuotaSnapshot.from_dict, x)], obj.get("quotaSnapshots")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) reasoning_tokens = from_union([from_none, from_int], obj.get("reasoningTokens")) + service_request_id = from_union([from_none, from_str], obj.get("serviceRequestId")) time_to_first_token = from_union([from_none, from_timedelta], obj.get("timeToFirstTokenMs")) return AssistantUsageData( model=model, @@ -722,7 +737,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": api_endpoint=api_endpoint, cache_read_tokens=cache_read_tokens, cache_write_tokens=cache_write_tokens, - copilot_usage=copilot_usage, + _copilot_usage=_copilot_usage, cost=cost, duration=duration, initiator=initiator, @@ -731,9 +746,10 @@ def from_dict(obj: Any) -> "AssistantUsageData": output_tokens=output_tokens, parent_tool_call_id=parent_tool_call_id, provider_call_id=provider_call_id, - quota_snapshots=quota_snapshots, + _quota_snapshots=_quota_snapshots, reasoning_effort=reasoning_effort, reasoning_tokens=reasoning_tokens, + service_request_id=service_request_id, time_to_first_token=time_to_first_token, ) @@ -748,8 +764,8 @@ def to_dict(self) -> dict: result["cacheReadTokens"] = from_union([from_none, to_int], self.cache_read_tokens) if self.cache_write_tokens is not None: result["cacheWriteTokens"] = from_union([from_none, to_int], self.cache_write_tokens) - if self.copilot_usage is not None: - result["copilotUsage"] = from_union([from_none, lambda x: to_class(AssistantUsageCopilotUsage, x)], self.copilot_usage) + if self._copilot_usage is not None: + result["copilotUsage"] = from_union([from_none, lambda x: to_class(_AssistantUsageCopilotUsage, x)], self._copilot_usage) if self.cost is not None: result["cost"] = from_union([from_none, to_float], self.cost) if self.duration is not None: @@ -766,62 +782,72 @@ def to_dict(self) -> dict: result["parentToolCallId"] = from_union([from_none, from_str], self.parent_tool_call_id) if self.provider_call_id is not None: result["providerCallId"] = from_union([from_none, from_str], self.provider_call_id) - if self.quota_snapshots is not None: - result["quotaSnapshots"] = from_union([from_none, lambda x: from_dict(lambda x: to_class(AssistantUsageQuotaSnapshot, x), x)], self.quota_snapshots) + if self._quota_snapshots is not None: + result["quotaSnapshots"] = from_union([from_none, lambda x: from_dict(lambda x: to_class(_AssistantUsageQuotaSnapshot, x), x)], self._quota_snapshots) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) if self.reasoning_tokens is not None: result["reasoningTokens"] = from_union([from_none, to_int], self.reasoning_tokens) + if self.service_request_id is not None: + result["serviceRequestId"] = from_union([from_none, from_str], self.service_request_id) if self.time_to_first_token is not None: result["timeToFirstTokenMs"] = from_union([from_none, to_timedelta_int], self.time_to_first_token) return result @dataclass -class AssistantUsageQuotaSnapshot: - "Schema for the `AssistantUsageQuotaSnapshot` type." - entitlement_requests: int - is_unlimited_entitlement: bool - overage: float - overage_allowed_with_exhausted_quota: bool - remaining_percentage: float - usage_allowed_with_exhausted_quota: bool - used_requests: int - reset_date: datetime | None = None - - @staticmethod - def from_dict(obj: Any) -> "AssistantUsageQuotaSnapshot": +class _AssistantUsageQuotaSnapshot: + "Schema for the `_AssistantUsageQuotaSnapshot` type." + # Internal: this field is an internal SDK API and is not part of the public surface. + _entitlement_requests: int + # Internal: this field is an internal SDK API and is not part of the public surface. + _is_unlimited_entitlement: bool + # Internal: this field is an internal SDK API and is not part of the public surface. + _overage: float + # Internal: this field is an internal SDK API and is not part of the public surface. + _overage_allowed_with_exhausted_quota: bool + # Internal: this field is an internal SDK API and is not part of the public surface. + _remaining_percentage: float + # Internal: this field is an internal SDK API and is not part of the public surface. + _usage_allowed_with_exhausted_quota: bool + # Internal: this field is an internal SDK API and is not part of the public surface. + _used_requests: int + # Internal: this field is an internal SDK API and is not part of the public surface. + _reset_date: datetime | None = None + + @staticmethod + def from_dict(obj: Any) -> "_AssistantUsageQuotaSnapshot": assert isinstance(obj, dict) - entitlement_requests = from_int(obj.get("entitlementRequests")) - is_unlimited_entitlement = from_bool(obj.get("isUnlimitedEntitlement")) - overage = from_float(obj.get("overage")) - overage_allowed_with_exhausted_quota = from_bool(obj.get("overageAllowedWithExhaustedQuota")) - remaining_percentage = from_float(obj.get("remainingPercentage")) - usage_allowed_with_exhausted_quota = from_bool(obj.get("usageAllowedWithExhaustedQuota")) - used_requests = from_int(obj.get("usedRequests")) - reset_date = from_union([from_none, from_datetime], obj.get("resetDate")) - return AssistantUsageQuotaSnapshot( - entitlement_requests=entitlement_requests, - is_unlimited_entitlement=is_unlimited_entitlement, - overage=overage, - overage_allowed_with_exhausted_quota=overage_allowed_with_exhausted_quota, - remaining_percentage=remaining_percentage, - usage_allowed_with_exhausted_quota=usage_allowed_with_exhausted_quota, - used_requests=used_requests, - reset_date=reset_date, + _entitlement_requests = from_int(obj.get("entitlementRequests")) + _is_unlimited_entitlement = from_bool(obj.get("isUnlimitedEntitlement")) + _overage = from_float(obj.get("overage")) + _overage_allowed_with_exhausted_quota = from_bool(obj.get("overageAllowedWithExhaustedQuota")) + _remaining_percentage = from_float(obj.get("remainingPercentage")) + _usage_allowed_with_exhausted_quota = from_bool(obj.get("usageAllowedWithExhaustedQuota")) + _used_requests = from_int(obj.get("usedRequests")) + _reset_date = from_union([from_none, from_datetime], obj.get("resetDate")) + return _AssistantUsageQuotaSnapshot( + _entitlement_requests=_entitlement_requests, + _is_unlimited_entitlement=_is_unlimited_entitlement, + _overage=_overage, + _overage_allowed_with_exhausted_quota=_overage_allowed_with_exhausted_quota, + _remaining_percentage=_remaining_percentage, + _usage_allowed_with_exhausted_quota=_usage_allowed_with_exhausted_quota, + _used_requests=_used_requests, + _reset_date=_reset_date, ) def to_dict(self) -> dict: result: dict = {} - result["entitlementRequests"] = to_int(self.entitlement_requests) - result["isUnlimitedEntitlement"] = from_bool(self.is_unlimited_entitlement) - result["overage"] = to_float(self.overage) - result["overageAllowedWithExhaustedQuota"] = from_bool(self.overage_allowed_with_exhausted_quota) - result["remainingPercentage"] = to_float(self.remaining_percentage) - result["usageAllowedWithExhaustedQuota"] = from_bool(self.usage_allowed_with_exhausted_quota) - result["usedRequests"] = to_int(self.used_requests) - if self.reset_date is not None: - result["resetDate"] = from_union([from_none, to_datetime], self.reset_date) + result["entitlementRequests"] = to_int(self._entitlement_requests) + result["isUnlimitedEntitlement"] = from_bool(self._is_unlimited_entitlement) + result["overage"] = to_float(self._overage) + result["overageAllowedWithExhaustedQuota"] = from_bool(self._overage_allowed_with_exhausted_quota) + result["remainingPercentage"] = to_float(self._remaining_percentage) + result["usageAllowedWithExhaustedQuota"] = from_bool(self._usage_allowed_with_exhausted_quota) + result["usedRequests"] = to_int(self._used_requests) + if self._reset_date is not None: + result["resetDate"] = from_union([from_none, to_datetime], self._reset_date) return result @@ -877,6 +903,81 @@ def to_dict(self) -> dict: return result +@dataclass +class CanvasRegistryChangedCanvas: + "Schema for the `CanvasRegistryChangedCanvas` type." + canvas_id: str + description: str + display_name: str + extension_id: str + actions: list[CanvasRegistryChangedCanvasAction] | None = None + extension_name: str | None = None + input_schema: dict[str, Any] | None = None + + @staticmethod + def from_dict(obj: Any) -> "CanvasRegistryChangedCanvas": + assert isinstance(obj, dict) + canvas_id = from_str(obj.get("canvasId")) + description = from_str(obj.get("description")) + display_name = from_str(obj.get("displayName")) + extension_id = from_str(obj.get("extensionId")) + actions = from_union([from_none, lambda x: from_list(CanvasRegistryChangedCanvasAction.from_dict, x)], obj.get("actions")) + extension_name = from_union([from_none, from_str], obj.get("extensionName")) + input_schema = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("inputSchema")) + return CanvasRegistryChangedCanvas( + canvas_id=canvas_id, + description=description, + display_name=display_name, + extension_id=extension_id, + actions=actions, + extension_name=extension_name, + input_schema=input_schema, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["canvasId"] = from_str(self.canvas_id) + result["description"] = from_str(self.description) + result["displayName"] = from_str(self.display_name) + result["extensionId"] = from_str(self.extension_id) + if self.actions is not None: + result["actions"] = from_union([from_none, lambda x: from_list(lambda x: to_class(CanvasRegistryChangedCanvasAction, x), x)], self.actions) + if self.extension_name is not None: + result["extensionName"] = from_union([from_none, from_str], self.extension_name) + if self.input_schema is not None: + result["inputSchema"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.input_schema) + return result + + +@dataclass +class CanvasRegistryChangedCanvasAction: + "Schema for the `CanvasRegistryChangedCanvasAction` type." + name: str + description: str | None = None + input_schema: dict[str, Any] | None = None + + @staticmethod + def from_dict(obj: Any) -> "CanvasRegistryChangedCanvasAction": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + description = from_union([from_none, from_str], obj.get("description")) + input_schema = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("inputSchema")) + return CanvasRegistryChangedCanvasAction( + name=name, + description=description, + input_schema=input_schema, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + if self.input_schema is not None: + result["inputSchema"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.input_schema) + return result + + @dataclass class CapabilitiesChangedData: "Session capability change notification" @@ -900,20 +1001,30 @@ def to_dict(self) -> dict: @dataclass class CapabilitiesChangedUI: "UI capability changes" + canvases: bool | None = None elicitation: bool | None = None + mcp_apps: bool | None = None @staticmethod def from_dict(obj: Any) -> "CapabilitiesChangedUI": assert isinstance(obj, dict) + canvases = from_union([from_none, from_bool], obj.get("canvases")) elicitation = from_union([from_none, from_bool], obj.get("elicitation")) + mcp_apps = from_union([from_none, from_bool], obj.get("mcpApps")) return CapabilitiesChangedUI( + canvases=canvases, elicitation=elicitation, + mcp_apps=mcp_apps, ) def to_dict(self) -> dict: result: dict = {} + if self.canvases is not None: + result["canvases"] = from_union([from_none, from_bool], self.canvases) if self.elicitation is not None: result["elicitation"] = from_union([from_none, from_bool], self.elicitation) + if self.mcp_apps is not None: + result["mcpApps"] = from_union([from_none, from_bool], self.mcp_apps) return result @@ -1038,7 +1149,8 @@ class CompactionCompleteCompactionTokensUsed: "Token usage breakdown for the compaction LLM call (aligned with assistant.usage format)" cache_read_tokens: int | None = None cache_write_tokens: int | None = None - copilot_usage: CompactionCompleteCompactionTokensUsedCopilotUsage | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _copilot_usage: _CompactionCompleteCompactionTokensUsedCopilotUsage | None = None duration: timedelta | None = None input_tokens: int | None = None model: str | None = None @@ -1049,7 +1161,7 @@ def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsed": assert isinstance(obj, dict) cache_read_tokens = from_union([from_none, from_int], obj.get("cacheReadTokens")) cache_write_tokens = from_union([from_none, from_int], obj.get("cacheWriteTokens")) - copilot_usage = from_union([from_none, CompactionCompleteCompactionTokensUsedCopilotUsage.from_dict], obj.get("copilotUsage")) + _copilot_usage = from_union([from_none, _CompactionCompleteCompactionTokensUsedCopilotUsage.from_dict], obj.get("copilotUsage")) duration = from_union([from_none, from_timedelta], obj.get("duration")) input_tokens = from_union([from_none, from_int], obj.get("inputTokens")) model = from_union([from_none, from_str], obj.get("model")) @@ -1057,7 +1169,7 @@ def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsed": return CompactionCompleteCompactionTokensUsed( cache_read_tokens=cache_read_tokens, cache_write_tokens=cache_write_tokens, - copilot_usage=copilot_usage, + _copilot_usage=_copilot_usage, duration=duration, input_tokens=input_tokens, model=model, @@ -1070,8 +1182,8 @@ def to_dict(self) -> dict: result["cacheReadTokens"] = from_union([from_none, to_int], self.cache_read_tokens) if self.cache_write_tokens is not None: result["cacheWriteTokens"] = from_union([from_none, to_int], self.cache_write_tokens) - if self.copilot_usage is not None: - result["copilotUsage"] = from_union([from_none, lambda x: to_class(CompactionCompleteCompactionTokensUsedCopilotUsage, x)], self.copilot_usage) + if self._copilot_usage is not None: + result["copilotUsage"] = from_union([from_none, lambda x: to_class(_CompactionCompleteCompactionTokensUsedCopilotUsage, x)], self._copilot_usage) if self.duration is not None: result["duration"] = from_union([from_none, to_timedelta_int], self.duration) if self.input_tokens is not None: @@ -1084,17 +1196,17 @@ def to_dict(self) -> dict: @dataclass -class CompactionCompleteCompactionTokensUsedCopilotUsage: +class _CompactionCompleteCompactionTokensUsedCopilotUsage: "Per-request cost and usage data from the CAPI copilot_usage response field" token_details: list[CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail] total_nano_aiu: float @staticmethod - def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsedCopilotUsage": + def from_dict(obj: Any) -> "_CompactionCompleteCompactionTokensUsedCopilotUsage": assert isinstance(obj, dict) token_details = from_list(CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.from_dict, obj.get("tokenDetails")) total_nano_aiu = from_float(obj.get("totalNanoAiu")) - return CompactionCompleteCompactionTokensUsedCopilotUsage( + return _CompactionCompleteCompactionTokensUsedCopilotUsage( token_details=token_details, total_nano_aiu=total_nano_aiu, ) @@ -1633,6 +1745,121 @@ def to_dict(self) -> dict: return result +@dataclass +class McpAppToolCallCompleteData: + "MCP App view called a tool on a connected MCP server (SEP-1865)" + duration_ms: float + server_name: str + success: bool + tool_name: str + arguments: dict[str, Any] | None = None + error: McpAppToolCallCompleteError | None = None + result: dict[str, Any] | None = None + tool_meta: McpAppToolCallCompleteToolMeta | None = None + + @staticmethod + def from_dict(obj: Any) -> "McpAppToolCallCompleteData": + assert isinstance(obj, dict) + duration_ms = from_float(obj.get("durationMs")) + server_name = from_str(obj.get("serverName")) + success = from_bool(obj.get("success")) + tool_name = from_str(obj.get("toolName")) + arguments = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("arguments")) + error = from_union([from_none, McpAppToolCallCompleteError.from_dict], obj.get("error")) + result = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("result")) + tool_meta = from_union([from_none, McpAppToolCallCompleteToolMeta.from_dict], obj.get("toolMeta")) + return McpAppToolCallCompleteData( + duration_ms=duration_ms, + server_name=server_name, + success=success, + tool_name=tool_name, + arguments=arguments, + error=error, + result=result, + tool_meta=tool_meta, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["durationMs"] = to_float(self.duration_ms) + result["serverName"] = from_str(self.server_name) + result["success"] = from_bool(self.success) + result["toolName"] = from_str(self.tool_name) + if self.arguments is not None: + result["arguments"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.arguments) + if self.error is not None: + result["error"] = from_union([from_none, lambda x: to_class(McpAppToolCallCompleteError, x)], self.error) + if self.result is not None: + result["result"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.result) + if self.tool_meta is not None: + result["toolMeta"] = from_union([from_none, lambda x: to_class(McpAppToolCallCompleteToolMeta, x)], self.tool_meta) + return result + + +@dataclass +class McpAppToolCallCompleteError: + "Set when the underlying tools/call threw an error before returning a CallToolResult" + message: str + + @staticmethod + def from_dict(obj: Any) -> "McpAppToolCallCompleteError": + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + return McpAppToolCallCompleteError( + message=message, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + return result + + +@dataclass +class McpAppToolCallCompleteToolMeta: + "The tool's `_meta.ui` block at the time of the call, so consumers can decide whether to forward the result to the model without re-listing tools." + ui: McpAppToolCallCompleteToolMetaUI | None = None + + @staticmethod + def from_dict(obj: Any) -> "McpAppToolCallCompleteToolMeta": + assert isinstance(obj, dict) + ui = from_union([from_none, McpAppToolCallCompleteToolMetaUI.from_dict], obj.get("ui")) + return McpAppToolCallCompleteToolMeta( + ui=ui, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.ui is not None: + result["ui"] = from_union([from_none, lambda x: to_class(McpAppToolCallCompleteToolMetaUI, x)], self.ui) + return result + + +@dataclass +class McpAppToolCallCompleteToolMetaUI: + "Schema for the `McpAppToolCallCompleteToolMetaUI` type." + resource_uri: str | None = None + visibility: list[str] | None = None + + @staticmethod + def from_dict(obj: Any) -> "McpAppToolCallCompleteToolMetaUI": + assert isinstance(obj, dict) + resource_uri = from_union([from_none, from_str], obj.get("resourceUri")) + visibility = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("visibility")) + return McpAppToolCallCompleteToolMetaUI( + resource_uri=resource_uri, + visibility=visibility, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.resource_uri is not None: + result["resourceUri"] = from_union([from_none, from_str], self.resource_uri) + if self.visibility is not None: + result["visibility"] = from_union([from_none, lambda x: from_list(from_str, x)], self.visibility) + return result + + @dataclass class McpOauthCompletedData: "MCP OAuth request completion notification" @@ -1719,7 +1946,10 @@ class McpServersLoadedServer: name: str status: McpServerStatus error: str | None = None + plugin_name: str | None = None + plugin_version: str | None = None source: McpServerSource | None = None + transport: McpServerTransport | None = None @staticmethod def from_dict(obj: Any) -> "McpServersLoadedServer": @@ -1727,12 +1957,18 @@ def from_dict(obj: Any) -> "McpServersLoadedServer": name = from_str(obj.get("name")) status = parse_enum(McpServerStatus, obj.get("status")) error = from_union([from_none, from_str], obj.get("error")) + plugin_name = from_union([from_none, from_str], obj.get("pluginName")) + plugin_version = from_union([from_none, from_str], obj.get("pluginVersion")) source = from_union([from_none, lambda x: parse_enum(McpServerSource, x)], obj.get("source")) + transport = from_union([from_none, lambda x: parse_enum(McpServerTransport, x)], obj.get("transport")) return McpServersLoadedServer( name=name, status=status, error=error, + plugin_name=plugin_name, + plugin_version=plugin_version, source=source, + transport=transport, ) def to_dict(self) -> dict: @@ -1741,8 +1977,14 @@ def to_dict(self) -> dict: result["status"] = to_enum(McpServerStatus, self.status) if self.error is not None: result["error"] = from_union([from_none, from_str], self.error) + if self.plugin_name is not None: + result["pluginName"] = from_union([from_none, from_str], self.plugin_name) + if self.plugin_version is not None: + result["pluginVersion"] = from_union([from_none, from_str], self.plugin_version) if self.source is not None: result["source"] = from_union([from_none, lambda x: to_enum(McpServerSource, x)], self.source) + if self.transport is not None: + result["transport"] = from_union([from_none, lambda x: to_enum(McpServerTransport, x)], self.transport) return result @@ -1756,6 +1998,7 @@ class ModelCallFailureData: initiator: str | None = None model: str | None = None provider_call_id: str | None = None + service_request_id: str | None = None status_code: int | None = None @staticmethod @@ -1768,6 +2011,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData": initiator = from_union([from_none, from_str], obj.get("initiator")) model = from_union([from_none, from_str], obj.get("model")) provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) + service_request_id = from_union([from_none, from_str], obj.get("serviceRequestId")) status_code = from_union([from_none, from_int], obj.get("statusCode")) return ModelCallFailureData( source=source, @@ -1777,6 +2021,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData": initiator=initiator, model=model, provider_call_id=provider_call_id, + service_request_id=service_request_id, status_code=status_code, ) @@ -1795,6 +2040,8 @@ def to_dict(self) -> dict: result["model"] = from_union([from_none, from_str], self.model) if self.provider_call_id is not None: result["providerCallId"] = from_union([from_none, from_str], self.provider_call_id) + if self.service_request_id is not None: + result["serviceRequestId"] = from_union([from_none, from_str], self.service_request_id) if self.status_code is not None: result["statusCode"] = from_union([from_none, to_int], self.status_code) return result @@ -1812,6 +2059,91 @@ def to_dict(self) -> dict: return {} +@dataclass +class PermissionApproved: + "Schema for the `PermissionApproved` type." + kind: ClassVar[str] = "approved" + + @staticmethod + def from_dict(obj: Any) -> "PermissionApproved": + assert isinstance(obj, dict) + return PermissionApproved( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + return result + + +@dataclass +class PermissionApprovedForLocation: + "Schema for the `PermissionApprovedForLocation` type." + approval: UserToolSessionApproval + kind: ClassVar[str] = "approved-for-location" + location_key: str + + @staticmethod + def from_dict(obj: Any) -> "PermissionApprovedForLocation": + assert isinstance(obj, dict) + approval = _load_UserToolSessionApproval(obj.get("approval")) + location_key = from_str(obj.get("locationKey")) + return PermissionApprovedForLocation( + approval=approval, + location_key=location_key, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["approval"] = self.approval.to_dict() + result["kind"] = self.kind + result["locationKey"] = from_str(self.location_key) + return result + + +@dataclass +class PermissionApprovedForSession: + "Schema for the `PermissionApprovedForSession` type." + approval: UserToolSessionApproval + kind: ClassVar[str] = "approved-for-session" + + @staticmethod + def from_dict(obj: Any) -> "PermissionApprovedForSession": + assert isinstance(obj, dict) + approval = _load_UserToolSessionApproval(obj.get("approval")) + return PermissionApprovedForSession( + approval=approval, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["approval"] = self.approval.to_dict() + result["kind"] = self.kind + return result + + +@dataclass +class PermissionCancelled: + "Schema for the `PermissionCancelled` type." + kind: ClassVar[str] = "cancelled" + reason: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionCancelled": + assert isinstance(obj, dict) + reason = from_union([from_none, from_str], obj.get("reason")) + return PermissionCancelled( + reason=reason, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.reason is not None: + result["reason"] = from_union([from_none, from_str], self.reason) + return result + + @dataclass class PermissionCompletedData: "Permission request completion notification signaling UI dismissal" @@ -1823,7 +2155,7 @@ class PermissionCompletedData: def from_dict(obj: Any) -> "PermissionCompletedData": assert isinstance(obj, dict) request_id = from_str(obj.get("requestId")) - result = PermissionResult.from_dict(obj.get("result")) + result = _load_PermissionResult(obj.get("result")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) return PermissionCompletedData( request_id=request_id, @@ -1834,345 +2166,833 @@ def from_dict(obj: Any) -> "PermissionCompletedData": def to_dict(self) -> dict: result: dict = {} result["requestId"] = from_str(self.request_id) - result["result"] = to_class(PermissionResult, self.result) + result["result"] = self.result.to_dict() if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) return result @dataclass -class PermissionPromptRequest: - "Derived user-facing permission prompt details for UI consumers" - kind: PermissionPromptRequestKind - access_kind: PermissionPromptRequestPathAccessKind | None = None - action: PermissionRequestMemoryAction | None = None - args: Any | None = None - can_offer_session_approval: bool | None = None - capabilities: list[str] | None = None - citations: str | None = None - command_identifiers: list[str] | None = None - diff: str | None = None - direction: PermissionRequestMemoryDirection | None = None - extension_name: str | None = None - fact: str | None = None - file_name: str | None = None - full_command_text: str | None = None - hook_message: str | None = None - intention: str | None = None - new_file_contents: str | None = None - operation: str | None = None - path: str | None = None - paths: list[str] | None = None - reason: str | None = None - server_name: str | None = None - subject: str | None = None - tool_args: Any = None - tool_call_id: str | None = None - tool_description: str | None = None - tool_name: str | None = None - tool_title: str | None = None - url: str | None = None - warning: str | None = None +class PermissionDeniedByContentExclusionPolicy: + "Schema for the `PermissionDeniedByContentExclusionPolicy` type." + kind: ClassVar[str] = "denied-by-content-exclusion-policy" + message: str + path: str @staticmethod - def from_dict(obj: Any) -> "PermissionPromptRequest": + def from_dict(obj: Any) -> "PermissionDeniedByContentExclusionPolicy": assert isinstance(obj, dict) - kind = parse_enum(PermissionPromptRequestKind, obj.get("kind")) - access_kind = from_union([from_none, lambda x: parse_enum(PermissionPromptRequestPathAccessKind, x)], obj.get("accessKind")) - action = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryAction, x)], obj.get("action")) - args = from_union([from_none, lambda x: x], obj.get("args")) - can_offer_session_approval = from_union([from_none, from_bool], obj.get("canOfferSessionApproval")) - capabilities = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("capabilities")) - citations = from_union([from_none, from_str], obj.get("citations")) - command_identifiers = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("commandIdentifiers")) - diff = from_union([from_none, from_str], obj.get("diff")) - direction = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryDirection, x)], obj.get("direction")) - extension_name = from_union([from_none, from_str], obj.get("extensionName")) - fact = from_union([from_none, from_str], obj.get("fact")) - file_name = from_union([from_none, from_str], obj.get("fileName")) - full_command_text = from_union([from_none, from_str], obj.get("fullCommandText")) - hook_message = from_union([from_none, from_str], obj.get("hookMessage")) - intention = from_union([from_none, from_str], obj.get("intention")) - new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) - operation = from_union([from_none, from_str], obj.get("operation")) - path = from_union([from_none, from_str], obj.get("path")) - paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("paths")) - reason = from_union([from_none, from_str], obj.get("reason")) - server_name = from_union([from_none, from_str], obj.get("serverName")) - subject = from_union([from_none, from_str], obj.get("subject")) - tool_args = obj.get("toolArgs") - tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) - tool_description = from_union([from_none, from_str], obj.get("toolDescription")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - tool_title = from_union([from_none, from_str], obj.get("toolTitle")) - url = from_union([from_none, from_str], obj.get("url")) - warning = from_union([from_none, from_str], obj.get("warning")) - return PermissionPromptRequest( - kind=kind, - access_kind=access_kind, - action=action, - args=args, - can_offer_session_approval=can_offer_session_approval, - capabilities=capabilities, - citations=citations, - command_identifiers=command_identifiers, - diff=diff, - direction=direction, - extension_name=extension_name, - fact=fact, - file_name=file_name, - full_command_text=full_command_text, - hook_message=hook_message, - intention=intention, - new_file_contents=new_file_contents, - operation=operation, + message = from_str(obj.get("message")) + path = from_str(obj.get("path")) + return PermissionDeniedByContentExclusionPolicy( + message=message, path=path, - paths=paths, - reason=reason, - server_name=server_name, - subject=subject, - tool_args=tool_args, - tool_call_id=tool_call_id, - tool_description=tool_description, - tool_name=tool_name, - tool_title=tool_title, - url=url, - warning=warning, ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionPromptRequestKind, self.kind) - if self.access_kind is not None: - result["accessKind"] = from_union([from_none, lambda x: to_enum(PermissionPromptRequestPathAccessKind, x)], self.access_kind) - if self.action is not None: - result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryAction, x)], self.action) - if self.args is not None: - result["args"] = from_union([from_none, lambda x: x], self.args) - if self.can_offer_session_approval is not None: - result["canOfferSessionApproval"] = from_union([from_none, from_bool], self.can_offer_session_approval) - if self.capabilities is not None: - result["capabilities"] = from_union([from_none, lambda x: from_list(from_str, x)], self.capabilities) - if self.citations is not None: - result["citations"] = from_union([from_none, from_str], self.citations) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([from_none, lambda x: from_list(from_str, x)], self.command_identifiers) - if self.diff is not None: - result["diff"] = from_union([from_none, from_str], self.diff) - if self.direction is not None: - result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryDirection, x)], self.direction) - if self.extension_name is not None: - result["extensionName"] = from_union([from_none, from_str], self.extension_name) - if self.fact is not None: - result["fact"] = from_union([from_none, from_str], self.fact) - if self.file_name is not None: - result["fileName"] = from_union([from_none, from_str], self.file_name) - if self.full_command_text is not None: - result["fullCommandText"] = from_union([from_none, from_str], self.full_command_text) - if self.hook_message is not None: - result["hookMessage"] = from_union([from_none, from_str], self.hook_message) - if self.intention is not None: - result["intention"] = from_union([from_none, from_str], self.intention) - if self.new_file_contents is not None: - result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) - if self.operation is not None: - result["operation"] = from_union([from_none, from_str], self.operation) - if self.path is not None: - result["path"] = from_union([from_none, from_str], self.path) - if self.paths is not None: - result["paths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.paths) + result["kind"] = self.kind + result["message"] = from_str(self.message) + result["path"] = from_str(self.path) + return result + + +@dataclass +class PermissionDeniedByPermissionRequestHook: + "Schema for the `PermissionDeniedByPermissionRequestHook` type." + kind: ClassVar[str] = "denied-by-permission-request-hook" + interrupt: bool | None = None + message: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedByPermissionRequestHook": + assert isinstance(obj, dict) + interrupt = from_union([from_none, from_bool], obj.get("interrupt")) + message = from_union([from_none, from_str], obj.get("message")) + return PermissionDeniedByPermissionRequestHook( + interrupt=interrupt, + message=message, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.interrupt is not None: + result["interrupt"] = from_union([from_none, from_bool], self.interrupt) + if self.message is not None: + result["message"] = from_union([from_none, from_str], self.message) + return result + + +@dataclass +class PermissionDeniedByRules: + "Schema for the `PermissionDeniedByRules` type." + kind: ClassVar[str] = "denied-by-rules" + rules: list[PermissionRule] + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedByRules": + assert isinstance(obj, dict) + rules = from_list(PermissionRule.from_dict, obj.get("rules")) + return PermissionDeniedByRules( + rules=rules, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["rules"] = from_list(lambda x: to_class(PermissionRule, x), self.rules) + return result + + +@dataclass +class PermissionDeniedInteractivelyByUser: + "Schema for the `PermissionDeniedInteractivelyByUser` type." + kind: ClassVar[str] = "denied-interactively-by-user" + feedback: str | None = None + force_reject: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedInteractivelyByUser": + assert isinstance(obj, dict) + feedback = from_union([from_none, from_str], obj.get("feedback")) + force_reject = from_union([from_none, from_bool], obj.get("forceReject")) + return PermissionDeniedInteractivelyByUser( + feedback=feedback, + force_reject=force_reject, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.feedback is not None: + result["feedback"] = from_union([from_none, from_str], self.feedback) + if self.force_reject is not None: + result["forceReject"] = from_union([from_none, from_bool], self.force_reject) + return result + + +@dataclass +class PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser: + "Schema for the `PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser` type." + kind: ClassVar[str] = "denied-no-approval-rule-and-could-not-request-from-user" + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser": + assert isinstance(obj, dict) + return PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + return result + + +@dataclass +class PermissionPromptRequestCommands: + "Shell command permission prompt" + can_offer_session_approval: bool + command_identifiers: list[str] + full_command_text: str + intention: str + kind: ClassVar[str] = "commands" + tool_call_id: str | None = None + warning: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestCommands": + assert isinstance(obj, dict) + can_offer_session_approval = from_bool(obj.get("canOfferSessionApproval")) + command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) + full_command_text = from_str(obj.get("fullCommandText")) + intention = from_str(obj.get("intention")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + warning = from_union([from_none, from_str], obj.get("warning")) + return PermissionPromptRequestCommands( + can_offer_session_approval=can_offer_session_approval, + command_identifiers=command_identifiers, + full_command_text=full_command_text, + intention=intention, + tool_call_id=tool_call_id, + warning=warning, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["canOfferSessionApproval"] = from_bool(self.can_offer_session_approval) + result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) + result["fullCommandText"] = from_str(self.full_command_text) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + if self.warning is not None: + result["warning"] = from_union([from_none, from_str], self.warning) + return result + + +@dataclass +class PermissionPromptRequestCustomTool: + "Custom tool invocation permission prompt" + kind: ClassVar[str] = "custom-tool" + tool_description: str + tool_name: str + args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestCustomTool": + assert isinstance(obj, dict) + tool_description = from_str(obj.get("toolDescription")) + tool_name = from_str(obj.get("toolName")) + args = obj.get("args") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestCustomTool( + tool_description=tool_description, + tool_name=tool_name, + args=args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolDescription"] = from_str(self.tool_description) + result["toolName"] = from_str(self.tool_name) + if self.args is not None: + result["args"] = self.args + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestExtensionManagement: + "Extension management permission prompt" + kind: ClassVar[str] = "extension-management" + operation: str + extension_name: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestExtensionManagement": + assert isinstance(obj, dict) + operation = from_str(obj.get("operation")) + extension_name = from_union([from_none, from_str], obj.get("extensionName")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestExtensionManagement( + operation=operation, + extension_name=extension_name, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["operation"] = from_str(self.operation) + if self.extension_name is not None: + result["extensionName"] = from_union([from_none, from_str], self.extension_name) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestExtensionPermissionAccess: + "Extension permission access prompt" + capabilities: list[str] + extension_name: str + kind: ClassVar[str] = "extension-permission-access" + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestExtensionPermissionAccess": + assert isinstance(obj, dict) + capabilities = from_list(from_str, obj.get("capabilities")) + extension_name = from_str(obj.get("extensionName")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestExtensionPermissionAccess( + capabilities=capabilities, + extension_name=extension_name, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["capabilities"] = from_list(from_str, self.capabilities) + result["extensionName"] = from_str(self.extension_name) + result["kind"] = self.kind + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestHook: + "Hook confirmation permission prompt" + kind: ClassVar[str] = "hook" + tool_name: str + hook_message: str | None = None + tool_args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestHook": + assert isinstance(obj, dict) + tool_name = from_str(obj.get("toolName")) + hook_message = from_union([from_none, from_str], obj.get("hookMessage")) + tool_args = obj.get("toolArgs") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestHook( + tool_name=tool_name, + hook_message=hook_message, + tool_args=tool_args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolName"] = from_str(self.tool_name) + if self.hook_message is not None: + result["hookMessage"] = from_union([from_none, from_str], self.hook_message) + if self.tool_args is not None: + result["toolArgs"] = self.tool_args + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestMcp: + "MCP tool invocation permission prompt" + kind: ClassVar[str] = "mcp" + server_name: str + tool_name: str + tool_title: str + args: Any | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestMcp": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + tool_name = from_str(obj.get("toolName")) + tool_title = from_str(obj.get("toolTitle")) + args = from_union([from_none, lambda x: x], obj.get("args")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestMcp( + server_name=server_name, + tool_name=tool_name, + tool_title=tool_title, + args=args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_str(self.tool_name) + result["toolTitle"] = from_str(self.tool_title) + if self.args is not None: + result["args"] = from_union([from_none, lambda x: x], self.args) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestMemory: + "Memory operation permission prompt" + fact: str + kind: ClassVar[str] = "memory" + action: PermissionRequestMemoryAction | None = None + citations: str | None = None + direction: PermissionRequestMemoryDirection | None = None + reason: str | None = None + subject: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestMemory": + assert isinstance(obj, dict) + fact = from_str(obj.get("fact")) + action = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryAction, x)], obj.get("action")) + citations = from_union([from_none, from_str], obj.get("citations")) + direction = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryDirection, x)], obj.get("direction")) + reason = from_union([from_none, from_str], obj.get("reason")) + subject = from_union([from_none, from_str], obj.get("subject")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestMemory( + fact=fact, + action=action, + citations=citations, + direction=direction, + reason=reason, + subject=subject, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["fact"] = from_str(self.fact) + result["kind"] = self.kind + if self.action is not None: + result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryAction, x)], self.action) + if self.citations is not None: + result["citations"] = from_union([from_none, from_str], self.citations) + if self.direction is not None: + result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryDirection, x)], self.direction) if self.reason is not None: result["reason"] = from_union([from_none, from_str], self.reason) - if self.server_name is not None: - result["serverName"] = from_union([from_none, from_str], self.server_name) if self.subject is not None: result["subject"] = from_union([from_none, from_str], self.subject) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestPath: + "Path access permission prompt" + access_kind: PermissionPromptRequestPathAccessKind + kind: ClassVar[str] = "path" + paths: list[str] + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestPath": + assert isinstance(obj, dict) + access_kind = parse_enum(PermissionPromptRequestPathAccessKind, obj.get("accessKind")) + paths = from_list(from_str, obj.get("paths")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestPath( + access_kind=access_kind, + paths=paths, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["accessKind"] = to_enum(PermissionPromptRequestPathAccessKind, self.access_kind) + result["kind"] = self.kind + result["paths"] = from_list(from_str, self.paths) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestRead: + "File read permission prompt" + intention: str + kind: ClassVar[str] = "read" + path: str + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestRead": + assert isinstance(obj, dict) + intention = from_str(obj.get("intention")) + path = from_str(obj.get("path")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestRead( + intention=intention, + path=path, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["path"] = from_str(self.path) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestUrl: + "URL access permission prompt" + intention: str + kind: ClassVar[str] = "url" + url: str + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestUrl": + assert isinstance(obj, dict) + intention = from_str(obj.get("intention")) + url = from_str(obj.get("url")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestUrl( + intention=intention, + url=url, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["url"] = from_str(self.url) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestWrite: + "File write permission prompt" + can_offer_session_approval: bool + diff: str + file_name: str + intention: str + kind: ClassVar[str] = "write" + new_file_contents: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestWrite": + assert isinstance(obj, dict) + can_offer_session_approval = from_bool(obj.get("canOfferSessionApproval")) + diff = from_str(obj.get("diff")) + file_name = from_str(obj.get("fileName")) + intention = from_str(obj.get("intention")) + new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestWrite( + can_offer_session_approval=can_offer_session_approval, + diff=diff, + file_name=file_name, + intention=intention, + new_file_contents=new_file_contents, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["canOfferSessionApproval"] = from_bool(self.can_offer_session_approval) + result["diff"] = from_str(self.diff) + result["fileName"] = from_str(self.file_name) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + if self.new_file_contents is not None: + result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestCustomTool: + "Custom tool invocation permission request" + kind: ClassVar[str] = "custom-tool" + tool_description: str + tool_name: str + args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestCustomTool": + assert isinstance(obj, dict) + tool_description = from_str(obj.get("toolDescription")) + tool_name = from_str(obj.get("toolName")) + args = obj.get("args") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestCustomTool( + tool_description=tool_description, + tool_name=tool_name, + args=args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolDescription"] = from_str(self.tool_description) + result["toolName"] = from_str(self.tool_name) + if self.args is not None: + result["args"] = self.args + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestExtensionManagement: + "Extension management permission request" + kind: ClassVar[str] = "extension-management" + operation: str + extension_name: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestExtensionManagement": + assert isinstance(obj, dict) + operation = from_str(obj.get("operation")) + extension_name = from_union([from_none, from_str], obj.get("extensionName")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestExtensionManagement( + operation=operation, + extension_name=extension_name, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["operation"] = from_str(self.operation) + if self.extension_name is not None: + result["extensionName"] = from_union([from_none, from_str], self.extension_name) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestExtensionPermissionAccess: + "Extension permission access request" + capabilities: list[str] + extension_name: str + kind: ClassVar[str] = "extension-permission-access" + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestExtensionPermissionAccess": + assert isinstance(obj, dict) + capabilities = from_list(from_str, obj.get("capabilities")) + extension_name = from_str(obj.get("extensionName")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestExtensionPermissionAccess( + capabilities=capabilities, + extension_name=extension_name, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["capabilities"] = from_list(from_str, self.capabilities) + result["extensionName"] = from_str(self.extension_name) + result["kind"] = self.kind + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestHook: + "Hook confirmation permission request" + kind: ClassVar[str] = "hook" + tool_name: str + hook_message: str | None = None + tool_args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestHook": + assert isinstance(obj, dict) + tool_name = from_str(obj.get("toolName")) + hook_message = from_union([from_none, from_str], obj.get("hookMessage")) + tool_args = obj.get("toolArgs") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestHook( + tool_name=tool_name, + hook_message=hook_message, + tool_args=tool_args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolName"] = from_str(self.tool_name) + if self.hook_message is not None: + result["hookMessage"] = from_union([from_none, from_str], self.hook_message) if self.tool_args is not None: result["toolArgs"] = self.tool_args if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) - if self.tool_description is not None: - result["toolDescription"] = from_union([from_none, from_str], self.tool_description) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.tool_title is not None: - result["toolTitle"] = from_union([from_none, from_str], self.tool_title) - if self.url is not None: - result["url"] = from_union([from_none, from_str], self.url) - if self.warning is not None: - result["warning"] = from_union([from_none, from_str], self.warning) return result @dataclass -class PermissionRequest: - "Details of the permission being requested" - kind: PermissionRequestKind - action: PermissionRequestMemoryAction | None = None +class PermissionRequestMcp: + "MCP tool invocation permission request" + kind: ClassVar[str] = "mcp" + read_only: bool + server_name: str + tool_name: str + tool_title: str args: Any = None - can_offer_session_approval: bool | None = None - capabilities: list[str] | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestMcp": + assert isinstance(obj, dict) + read_only = from_bool(obj.get("readOnly")) + server_name = from_str(obj.get("serverName")) + tool_name = from_str(obj.get("toolName")) + tool_title = from_str(obj.get("toolTitle")) + args = obj.get("args") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestMcp( + read_only=read_only, + server_name=server_name, + tool_name=tool_name, + tool_title=tool_title, + args=args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["readOnly"] = from_bool(self.read_only) + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_str(self.tool_name) + result["toolTitle"] = from_str(self.tool_title) + if self.args is not None: + result["args"] = self.args + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestMemory: + "Memory operation permission request" + fact: str + kind: ClassVar[str] = "memory" + action: PermissionRequestMemoryAction | None = None citations: str | None = None - commands: list[PermissionRequestShellCommand] | None = None - diff: str | None = None direction: PermissionRequestMemoryDirection | None = None - extension_name: str | None = None - fact: str | None = None - file_name: str | None = None - full_command_text: str | None = None - has_write_file_redirection: bool | None = None - hook_message: str | None = None - intention: str | None = None - new_file_contents: str | None = None - operation: str | None = None - path: str | None = None - possible_paths: list[str] | None = None - possible_urls: list[PermissionRequestShellPossibleUrl] | None = None - read_only: bool | None = None reason: str | None = None - server_name: str | None = None subject: str | None = None - tool_args: Any = None tool_call_id: str | None = None - tool_description: str | None = None - tool_name: str | None = None - tool_title: str | None = None - url: str | None = None - warning: str | None = None @staticmethod - def from_dict(obj: Any) -> "PermissionRequest": + def from_dict(obj: Any) -> "PermissionRequestMemory": assert isinstance(obj, dict) - kind = parse_enum(PermissionRequestKind, obj.get("kind")) + fact = from_str(obj.get("fact")) action = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryAction, x)], obj.get("action")) - args = obj.get("args") - can_offer_session_approval = from_union([from_none, from_bool], obj.get("canOfferSessionApproval")) - capabilities = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("capabilities")) citations = from_union([from_none, from_str], obj.get("citations")) - commands = from_union([from_none, lambda x: from_list(PermissionRequestShellCommand.from_dict, x)], obj.get("commands")) - diff = from_union([from_none, from_str], obj.get("diff")) direction = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryDirection, x)], obj.get("direction")) - extension_name = from_union([from_none, from_str], obj.get("extensionName")) - fact = from_union([from_none, from_str], obj.get("fact")) - file_name = from_union([from_none, from_str], obj.get("fileName")) - full_command_text = from_union([from_none, from_str], obj.get("fullCommandText")) - has_write_file_redirection = from_union([from_none, from_bool], obj.get("hasWriteFileRedirection")) - hook_message = from_union([from_none, from_str], obj.get("hookMessage")) - intention = from_union([from_none, from_str], obj.get("intention")) - new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) - operation = from_union([from_none, from_str], obj.get("operation")) - path = from_union([from_none, from_str], obj.get("path")) - possible_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("possiblePaths")) - possible_urls = from_union([from_none, lambda x: from_list(PermissionRequestShellPossibleUrl.from_dict, x)], obj.get("possibleUrls")) - read_only = from_union([from_none, from_bool], obj.get("readOnly")) reason = from_union([from_none, from_str], obj.get("reason")) - server_name = from_union([from_none, from_str], obj.get("serverName")) subject = from_union([from_none, from_str], obj.get("subject")) - tool_args = obj.get("toolArgs") tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) - tool_description = from_union([from_none, from_str], obj.get("toolDescription")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - tool_title = from_union([from_none, from_str], obj.get("toolTitle")) - url = from_union([from_none, from_str], obj.get("url")) - warning = from_union([from_none, from_str], obj.get("warning")) - return PermissionRequest( - kind=kind, + return PermissionRequestMemory( + fact=fact, action=action, - args=args, - can_offer_session_approval=can_offer_session_approval, - capabilities=capabilities, citations=citations, - commands=commands, - diff=diff, direction=direction, - extension_name=extension_name, - fact=fact, - file_name=file_name, - full_command_text=full_command_text, - has_write_file_redirection=has_write_file_redirection, - hook_message=hook_message, - intention=intention, - new_file_contents=new_file_contents, - operation=operation, - path=path, - possible_paths=possible_paths, - possible_urls=possible_urls, - read_only=read_only, reason=reason, - server_name=server_name, subject=subject, - tool_args=tool_args, tool_call_id=tool_call_id, - tool_description=tool_description, - tool_name=tool_name, - tool_title=tool_title, - url=url, - warning=warning, ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionRequestKind, self.kind) + result["fact"] = from_str(self.fact) + result["kind"] = self.kind if self.action is not None: result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryAction, x)], self.action) - if self.args is not None: - result["args"] = self.args - if self.can_offer_session_approval is not None: - result["canOfferSessionApproval"] = from_union([from_none, from_bool], self.can_offer_session_approval) - if self.capabilities is not None: - result["capabilities"] = from_union([from_none, lambda x: from_list(from_str, x)], self.capabilities) if self.citations is not None: result["citations"] = from_union([from_none, from_str], self.citations) - if self.commands is not None: - result["commands"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestShellCommand, x), x)], self.commands) - if self.diff is not None: - result["diff"] = from_union([from_none, from_str], self.diff) if self.direction is not None: result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryDirection, x)], self.direction) - if self.extension_name is not None: - result["extensionName"] = from_union([from_none, from_str], self.extension_name) - if self.fact is not None: - result["fact"] = from_union([from_none, from_str], self.fact) - if self.file_name is not None: - result["fileName"] = from_union([from_none, from_str], self.file_name) - if self.full_command_text is not None: - result["fullCommandText"] = from_union([from_none, from_str], self.full_command_text) - if self.has_write_file_redirection is not None: - result["hasWriteFileRedirection"] = from_union([from_none, from_bool], self.has_write_file_redirection) - if self.hook_message is not None: - result["hookMessage"] = from_union([from_none, from_str], self.hook_message) - if self.intention is not None: - result["intention"] = from_union([from_none, from_str], self.intention) - if self.new_file_contents is not None: - result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) - if self.operation is not None: - result["operation"] = from_union([from_none, from_str], self.operation) - if self.path is not None: - result["path"] = from_union([from_none, from_str], self.path) - if self.possible_paths is not None: - result["possiblePaths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.possible_paths) - if self.possible_urls is not None: - result["possibleUrls"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestShellPossibleUrl, x), x)], self.possible_urls) - if self.read_only is not None: - result["readOnly"] = from_union([from_none, from_bool], self.read_only) if self.reason is not None: result["reason"] = from_union([from_none, from_str], self.reason) - if self.server_name is not None: - result["serverName"] = from_union([from_none, from_str], self.server_name) if self.subject is not None: result["subject"] = from_union([from_none, from_str], self.subject) - if self.tool_args is not None: - result["toolArgs"] = self.tool_args if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) - if self.tool_description is not None: - result["toolDescription"] = from_union([from_none, from_str], self.tool_description) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.tool_title is not None: - result["toolTitle"] = from_union([from_none, from_str], self.tool_title) - if self.url is not None: - result["url"] = from_union([from_none, from_str], self.url) + return result + + +@dataclass +class PermissionRequestRead: + "File or directory read permission request" + intention: str + kind: ClassVar[str] = "read" + path: str + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestRead": + assert isinstance(obj, dict) + intention = from_str(obj.get("intention")) + path = from_str(obj.get("path")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestRead( + intention=intention, + path=path, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["path"] = from_str(self.path) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestShell: + "Shell command permission request" + can_offer_session_approval: bool + commands: list[PermissionRequestShellCommand] + full_command_text: str + has_write_file_redirection: bool + intention: str + kind: ClassVar[str] = "shell" + possible_paths: list[str] + possible_urls: list[PermissionRequestShellPossibleUrl] + tool_call_id: str | None = None + warning: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestShell": + assert isinstance(obj, dict) + can_offer_session_approval = from_bool(obj.get("canOfferSessionApproval")) + commands = from_list(PermissionRequestShellCommand.from_dict, obj.get("commands")) + full_command_text = from_str(obj.get("fullCommandText")) + has_write_file_redirection = from_bool(obj.get("hasWriteFileRedirection")) + intention = from_str(obj.get("intention")) + possible_paths = from_list(from_str, obj.get("possiblePaths")) + possible_urls = from_list(PermissionRequestShellPossibleUrl.from_dict, obj.get("possibleUrls")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + warning = from_union([from_none, from_str], obj.get("warning")) + return PermissionRequestShell( + can_offer_session_approval=can_offer_session_approval, + commands=commands, + full_command_text=full_command_text, + has_write_file_redirection=has_write_file_redirection, + intention=intention, + possible_paths=possible_paths, + possible_urls=possible_urls, + tool_call_id=tool_call_id, + warning=warning, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["canOfferSessionApproval"] = from_bool(self.can_offer_session_approval) + result["commands"] = from_list(lambda x: to_class(PermissionRequestShellCommand, x), self.commands) + result["fullCommandText"] = from_str(self.full_command_text) + result["hasWriteFileRedirection"] = from_bool(self.has_write_file_redirection) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["possiblePaths"] = from_list(from_str, self.possible_paths) + result["possibleUrls"] = from_list(lambda x: to_class(PermissionRequestShellPossibleUrl, x), self.possible_urls) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) if self.warning is not None: result["warning"] = from_union([from_none, from_str], self.warning) return result @@ -2207,16 +3027,89 @@ class PermissionRequestShellPossibleUrl: url: str @staticmethod - def from_dict(obj: Any) -> "PermissionRequestShellPossibleUrl": + def from_dict(obj: Any) -> "PermissionRequestShellPossibleUrl": + assert isinstance(obj, dict) + url = from_str(obj.get("url")) + return PermissionRequestShellPossibleUrl( + url=url, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["url"] = from_str(self.url) + return result + + +@dataclass +class PermissionRequestUrl: + "URL access permission request" + intention: str + kind: ClassVar[str] = "url" + url: str + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestUrl": + assert isinstance(obj, dict) + intention = from_str(obj.get("intention")) + url = from_str(obj.get("url")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestUrl( + intention=intention, + url=url, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["url"] = from_str(self.url) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestWrite: + "File write permission request" + can_offer_session_approval: bool + diff: str + file_name: str + intention: str + kind: ClassVar[str] = "write" + new_file_contents: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestWrite": assert isinstance(obj, dict) - url = from_str(obj.get("url")) - return PermissionRequestShellPossibleUrl( - url=url, + can_offer_session_approval = from_bool(obj.get("canOfferSessionApproval")) + diff = from_str(obj.get("diff")) + file_name = from_str(obj.get("fileName")) + intention = from_str(obj.get("intention")) + new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestWrite( + can_offer_session_approval=can_offer_session_approval, + diff=diff, + file_name=file_name, + intention=intention, + new_file_contents=new_file_contents, + tool_call_id=tool_call_id, ) def to_dict(self) -> dict: result: dict = {} - result["url"] = from_str(self.url) + result["canOfferSessionApproval"] = from_bool(self.can_offer_session_approval) + result["diff"] = from_str(self.diff) + result["fileName"] = from_str(self.file_name) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + if self.new_file_contents is not None: + result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) return result @@ -2231,9 +3124,9 @@ class PermissionRequestedData: @staticmethod def from_dict(obj: Any) -> "PermissionRequestedData": assert isinstance(obj, dict) - permission_request = PermissionRequest.from_dict(obj.get("permissionRequest")) + permission_request = _load_PermissionRequest(obj.get("permissionRequest")) request_id = from_str(obj.get("requestId")) - prompt_request = from_union([from_none, PermissionPromptRequest.from_dict], obj.get("promptRequest")) + prompt_request = from_union([from_none, _load_PermissionPromptRequest], obj.get("promptRequest")) resolved_by_hook = from_union([from_none, from_bool], obj.get("resolvedByHook")) return PermissionRequestedData( permission_request=permission_request, @@ -2244,79 +3137,15 @@ def from_dict(obj: Any) -> "PermissionRequestedData": def to_dict(self) -> dict: result: dict = {} - result["permissionRequest"] = to_class(PermissionRequest, self.permission_request) + result["permissionRequest"] = self.permission_request.to_dict() result["requestId"] = from_str(self.request_id) if self.prompt_request is not None: - result["promptRequest"] = from_union([from_none, lambda x: to_class(PermissionPromptRequest, x)], self.prompt_request) + result["promptRequest"] = from_union([from_none, lambda x: x.to_dict()], self.prompt_request) if self.resolved_by_hook is not None: result["resolvedByHook"] = from_union([from_none, from_bool], self.resolved_by_hook) return result -@dataclass -class PermissionResult: - "The result of the permission request" - kind: PermissionResultKind - approval: UserToolSessionApproval | None = None - feedback: str | None = None - force_reject: bool | None = None - interrupt: bool | None = None - location_key: str | None = None - message: str | None = None - path: str | None = None - reason: str | None = None - rules: list[PermissionRule] | None = None - - @staticmethod - def from_dict(obj: Any) -> "PermissionResult": - assert isinstance(obj, dict) - kind = parse_enum(PermissionResultKind, obj.get("kind")) - approval = from_union([from_none, UserToolSessionApproval.from_dict], obj.get("approval")) - feedback = from_union([from_none, from_str], obj.get("feedback")) - force_reject = from_union([from_none, from_bool], obj.get("forceReject")) - interrupt = from_union([from_none, from_bool], obj.get("interrupt")) - location_key = from_union([from_none, from_str], obj.get("locationKey")) - message = from_union([from_none, from_str], obj.get("message")) - path = from_union([from_none, from_str], obj.get("path")) - reason = from_union([from_none, from_str], obj.get("reason")) - rules = from_union([from_none, lambda x: from_list(PermissionRule.from_dict, x)], obj.get("rules")) - return PermissionResult( - kind=kind, - approval=approval, - feedback=feedback, - force_reject=force_reject, - interrupt=interrupt, - location_key=location_key, - message=message, - path=path, - reason=reason, - rules=rules, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionResultKind, self.kind) - if self.approval is not None: - result["approval"] = from_union([from_none, lambda x: to_class(UserToolSessionApproval, x)], self.approval) - if self.feedback is not None: - result["feedback"] = from_union([from_none, from_str], self.feedback) - if self.force_reject is not None: - result["forceReject"] = from_union([from_none, from_bool], self.force_reject) - if self.interrupt is not None: - result["interrupt"] = from_union([from_none, from_bool], self.interrupt) - if self.location_key is not None: - result["locationKey"] = from_union([from_none, from_str], self.location_key) - if self.message is not None: - result["message"] = from_union([from_none, from_str], self.message) - if self.path is not None: - result["path"] = from_union([from_none, from_str], self.path) - if self.reason is not None: - result["reason"] = from_union([from_none, from_str], self.reason) - if self.rules is not None: - result["rules"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRule, x), x)], self.rules) - return result - - @dataclass class PermissionRule: "Schema for the `PermissionRule` type." @@ -2398,6 +3227,85 @@ def to_dict(self) -> dict: return {} +@dataclass +class SessionCanvasOpenedData: + "Schema for the `CanvasOpenedData` type." + availability: CanvasOpenedAvailability + canvas_id: str + extension_id: str + instance_id: str + reopen: bool + extension_name: str | None = None + input: Any = None + status: str | None = None + title: str | None = None + url: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionCanvasOpenedData": + assert isinstance(obj, dict) + availability = parse_enum(CanvasOpenedAvailability, obj.get("availability")) + canvas_id = from_str(obj.get("canvasId")) + extension_id = from_str(obj.get("extensionId")) + instance_id = from_str(obj.get("instanceId")) + reopen = from_bool(obj.get("reopen")) + extension_name = from_union([from_none, from_str], obj.get("extensionName")) + input = obj.get("input") + status = from_union([from_none, from_str], obj.get("status")) + title = from_union([from_none, from_str], obj.get("title")) + url = from_union([from_none, from_str], obj.get("url")) + return SessionCanvasOpenedData( + availability=availability, + canvas_id=canvas_id, + extension_id=extension_id, + instance_id=instance_id, + reopen=reopen, + extension_name=extension_name, + input=input, + status=status, + title=title, + url=url, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["availability"] = to_enum(CanvasOpenedAvailability, self.availability) + result["canvasId"] = from_str(self.canvas_id) + result["extensionId"] = from_str(self.extension_id) + result["instanceId"] = from_str(self.instance_id) + result["reopen"] = from_bool(self.reopen) + if self.extension_name is not None: + result["extensionName"] = from_union([from_none, from_str], self.extension_name) + if self.input is not None: + result["input"] = self.input + if self.status is not None: + result["status"] = from_union([from_none, from_str], self.status) + if self.title is not None: + result["title"] = from_union([from_none, from_str], self.title) + if self.url is not None: + result["url"] = from_union([from_none, from_str], self.url) + return result + + +@dataclass +class SessionCanvasRegistryChangedData: + "Schema for the `CanvasRegistryChangedData` type." + canvases: list[CanvasRegistryChangedCanvas] + + @staticmethod + def from_dict(obj: Any) -> "SessionCanvasRegistryChangedData": + assert isinstance(obj, dict) + canvases = from_list(CanvasRegistryChangedCanvas.from_dict, obj.get("canvases")) + return SessionCanvasRegistryChangedData( + canvases=canvases, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["canvases"] = from_list(lambda x: to_class(CanvasRegistryChangedCanvas, x), self.canvases) + return result + + @dataclass class SessionCompactionCompleteData: "Conversation compaction results including success status, metrics, and optional error details" @@ -2413,6 +3321,7 @@ class SessionCompactionCompleteData: pre_compaction_messages_length: int | None = None pre_compaction_tokens: int | None = None request_id: str | None = None + service_request_id: str | None = None summary_content: str | None = None system_tokens: int | None = None tokens_removed: int | None = None @@ -2433,6 +3342,7 @@ def from_dict(obj: Any) -> "SessionCompactionCompleteData": pre_compaction_messages_length = from_union([from_none, from_int], obj.get("preCompactionMessagesLength")) pre_compaction_tokens = from_union([from_none, from_int], obj.get("preCompactionTokens")) request_id = from_union([from_none, from_str], obj.get("requestId")) + service_request_id = from_union([from_none, from_str], obj.get("serviceRequestId")) summary_content = from_union([from_none, from_str], obj.get("summaryContent")) system_tokens = from_union([from_none, from_int], obj.get("systemTokens")) tokens_removed = from_union([from_none, from_int], obj.get("tokensRemoved")) @@ -2450,6 +3360,7 @@ def from_dict(obj: Any) -> "SessionCompactionCompleteData": pre_compaction_messages_length=pre_compaction_messages_length, pre_compaction_tokens=pre_compaction_tokens, request_id=request_id, + service_request_id=service_request_id, summary_content=summary_content, system_tokens=system_tokens, tokens_removed=tokens_removed, @@ -2481,6 +3392,8 @@ def to_dict(self) -> dict: result["preCompactionTokens"] = from_union([from_none, to_int], self.pre_compaction_tokens) if self.request_id is not None: result["requestId"] = from_union([from_none, from_str], self.request_id) + if self.service_request_id is not None: + result["serviceRequestId"] = from_union([from_none, from_str], self.service_request_id) if self.summary_content is not None: result["summaryContent"] = from_union([from_none, from_str], self.summary_content) if self.system_tokens is not None: @@ -2648,6 +3561,7 @@ class SessionErrorData: eligible_for_auto_switch: bool | None = None error_code: str | None = None provider_call_id: str | None = None + service_request_id: str | None = None stack: str | None = None status_code: int | None = None url: str | None = None @@ -2660,6 +3574,7 @@ def from_dict(obj: Any) -> "SessionErrorData": eligible_for_auto_switch = from_union([from_none, from_bool], obj.get("eligibleForAutoSwitch")) error_code = from_union([from_none, from_str], obj.get("errorCode")) provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) + service_request_id = from_union([from_none, from_str], obj.get("serviceRequestId")) stack = from_union([from_none, from_str], obj.get("stack")) status_code = from_union([from_none, from_int], obj.get("statusCode")) url = from_union([from_none, from_str], obj.get("url")) @@ -2669,6 +3584,7 @@ def from_dict(obj: Any) -> "SessionErrorData": eligible_for_auto_switch=eligible_for_auto_switch, error_code=error_code, provider_call_id=provider_call_id, + service_request_id=service_request_id, stack=stack, status_code=status_code, url=url, @@ -2684,6 +3600,8 @@ def to_dict(self) -> dict: result["errorCode"] = from_union([from_none, from_str], self.error_code) if self.provider_call_id is not None: result["providerCallId"] = from_union([from_none, from_str], self.provider_call_id) + if self.service_request_id is not None: + result["serviceRequestId"] = from_union([from_none, from_str], self.service_request_id) if self.stack is not None: result["stack"] = from_union([from_none, from_str], self.stack) if self.status_code is not None: @@ -2818,21 +3736,26 @@ class SessionMcpServerStatusChangedData: "Schema for the `McpServerStatusChangedData` type." server_name: str status: McpServerStatus + error: str | None = None @staticmethod def from_dict(obj: Any) -> "SessionMcpServerStatusChangedData": assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) status = parse_enum(McpServerStatus, obj.get("status")) + error = from_union([from_none, from_str], obj.get("error")) return SessionMcpServerStatusChangedData( server_name=server_name, status=status, + error=error, ) def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) result["status"] = to_enum(McpServerStatus, self.status) + if self.error is not None: + result["error"] = from_union([from_none, from_str], self.error) return result @@ -2883,6 +3806,7 @@ class SessionModelChangeData: "Model change details including previous and new model identifiers" new_model: str cause: str | None = None + context_tier: SessionModelChangeDataContextTier | None = None previous_model: str | None = None previous_reasoning_effort: str | None = None previous_reasoning_summary: ReasoningSummary | None = None @@ -2894,6 +3818,7 @@ def from_dict(obj: Any) -> "SessionModelChangeData": assert isinstance(obj, dict) new_model = from_str(obj.get("newModel")) cause = from_union([from_none, from_str], obj.get("cause")) + context_tier = from_union([from_none, lambda x: parse_enum(SessionModelChangeDataContextTier, x)], obj.get("contextTier")) previous_model = from_union([from_none, from_str], obj.get("previousModel")) previous_reasoning_effort = from_union([from_none, from_str], obj.get("previousReasoningEffort")) previous_reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("previousReasoningSummary")) @@ -2902,6 +3827,7 @@ def from_dict(obj: Any) -> "SessionModelChangeData": return SessionModelChangeData( new_model=new_model, cause=cause, + context_tier=context_tier, previous_model=previous_model, previous_reasoning_effort=previous_reasoning_effort, previous_reasoning_summary=previous_reasoning_summary, @@ -2914,6 +3840,8 @@ def to_dict(self) -> dict: result["newModel"] = from_str(self.new_model) if self.cause is not None: result["cause"] = from_union([from_none, from_str], self.cause) + if self.context_tier is not None: + result["contextTier"] = from_union([from_none, lambda x: to_enum(SessionModelChangeDataContextTier, x)], self.context_tier) if self.previous_model is not None: result["previousModel"] = from_union([from_none, from_str], self.previous_model) if self.previous_reasoning_effort is not None: @@ -3099,8 +4027,10 @@ class SessionShutdownData: system_tokens: int | None = None token_details: dict[str, ShutdownTokenDetail] | None = None tool_definitions_tokens: int | None = None + # Experimental: this field is part of an experimental API and may change or be removed. total_nano_aiu: float | None = None - total_premium_requests: float | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _total_premium_requests: float | None = None @staticmethod def from_dict(obj: Any) -> "SessionShutdownData": @@ -3118,7 +4048,7 @@ def from_dict(obj: Any) -> "SessionShutdownData": token_details = from_union([from_none, lambda x: from_dict(ShutdownTokenDetail.from_dict, x)], obj.get("tokenDetails")) tool_definitions_tokens = from_union([from_none, from_int], obj.get("toolDefinitionsTokens")) total_nano_aiu = from_union([from_none, from_float], obj.get("totalNanoAiu")) - total_premium_requests = from_union([from_none, from_float], obj.get("totalPremiumRequests")) + _total_premium_requests = from_union([from_none, from_float], obj.get("totalPremiumRequests")) return SessionShutdownData( code_changes=code_changes, model_metrics=model_metrics, @@ -3133,7 +4063,7 @@ def from_dict(obj: Any) -> "SessionShutdownData": token_details=token_details, tool_definitions_tokens=tool_definitions_tokens, total_nano_aiu=total_nano_aiu, - total_premium_requests=total_premium_requests, + _total_premium_requests=_total_premium_requests, ) def to_dict(self) -> dict: @@ -3159,8 +4089,8 @@ def to_dict(self) -> dict: result["toolDefinitionsTokens"] = from_union([from_none, to_int], self.tool_definitions_tokens) if self.total_nano_aiu is not None: result["totalNanoAiu"] = from_union([from_none, to_float], self.total_nano_aiu) - if self.total_premium_requests is not None: - result["totalPremiumRequests"] = from_union([from_none, to_float], self.total_premium_requests) + if self._total_premium_requests is not None: + result["totalPremiumRequests"] = from_union([from_none, to_float], self._total_premium_requests) return result @@ -3517,6 +4447,7 @@ class ShutdownModelMetric: requests: ShutdownModelMetricRequests usage: ShutdownModelMetricUsage token_details: dict[str, ShutdownModelMetricTokenDetail] | None = None + # Experimental: this field is part of an experimental API and may change or be removed. total_nano_aiu: float | None = None @staticmethod @@ -3547,7 +4478,9 @@ def to_dict(self) -> dict: @dataclass class ShutdownModelMetricRequests: "Request count and cost metrics" + # Experimental: this field is part of an experimental API and may change or be removed. cost: float | None = None + # Experimental: this field is part of an experimental API and may change or be removed. count: int | None = None @staticmethod @@ -3653,6 +4586,8 @@ class SkillInvokedData: description: str | None = None plugin_name: str | None = None plugin_version: str | None = None + source: str | None = None + trigger: SkillInvokedTrigger | None = None @staticmethod def from_dict(obj: Any) -> "SkillInvokedData": @@ -3664,6 +4599,8 @@ def from_dict(obj: Any) -> "SkillInvokedData": description = from_union([from_none, from_str], obj.get("description")) plugin_name = from_union([from_none, from_str], obj.get("pluginName")) plugin_version = from_union([from_none, from_str], obj.get("pluginVersion")) + source = from_union([from_none, from_str], obj.get("source")) + trigger = from_union([from_none, lambda x: parse_enum(SkillInvokedTrigger, x)], obj.get("trigger")) return SkillInvokedData( content=content, name=name, @@ -3672,6 +4609,8 @@ def from_dict(obj: Any) -> "SkillInvokedData": description=description, plugin_name=plugin_name, plugin_version=plugin_version, + source=source, + trigger=trigger, ) def to_dict(self) -> dict: @@ -3687,6 +4626,10 @@ def to_dict(self) -> dict: result["pluginName"] = from_union([from_none, from_str], self.plugin_name) if self.plugin_version is not None: result["pluginVersion"] = from_union([from_none, from_str], self.plugin_version) + if self.source is not None: + result["source"] = from_union([from_none, from_str], self.source) + if self.trigger is not None: + result["trigger"] = from_union([from_none, lambda x: to_enum(SkillInvokedTrigger, x)], self.trigger) return result @@ -3962,193 +4905,339 @@ def to_dict(self) -> dict: @dataclass -class SystemNotification: - "Structured metadata identifying what triggered this notification" - type: SystemNotificationType - agent_id: str | None = None - agent_type: str | None = None +class SystemNotificationAgentCompleted: + "Schema for the `SystemNotificationAgentCompleted` type." + agent_id: str + agent_type: str + status: SystemNotificationAgentCompletedStatus + type: ClassVar[str] = "agent_completed" description: str | None = None - entry_id: str | None = None - exit_code: int | None = None prompt: str | None = None - sender_name: str | None = None - sender_type: str | None = None - shell_id: str | None = None - source_path: str | None = None - status: SystemNotificationAgentCompletedStatus | None = None - summary: str | None = None - trigger_file: str | None = None - trigger_tool: str | None = None @staticmethod - def from_dict(obj: Any) -> "SystemNotification": + def from_dict(obj: Any) -> "SystemNotificationAgentCompleted": assert isinstance(obj, dict) - type = parse_enum(SystemNotificationType, obj.get("type")) - agent_id = from_union([from_none, from_str], obj.get("agentId")) - agent_type = from_union([from_none, from_str], obj.get("agentType")) + agent_id = from_str(obj.get("agentId")) + agent_type = from_str(obj.get("agentType")) + status = parse_enum(SystemNotificationAgentCompletedStatus, obj.get("status")) description = from_union([from_none, from_str], obj.get("description")) - entry_id = from_union([from_none, from_str], obj.get("entryId")) - exit_code = from_union([from_none, from_int], obj.get("exitCode")) prompt = from_union([from_none, from_str], obj.get("prompt")) - sender_name = from_union([from_none, from_str], obj.get("senderName")) - sender_type = from_union([from_none, from_str], obj.get("senderType")) - shell_id = from_union([from_none, from_str], obj.get("shellId")) - source_path = from_union([from_none, from_str], obj.get("sourcePath")) - status = from_union([from_none, lambda x: parse_enum(SystemNotificationAgentCompletedStatus, x)], obj.get("status")) - summary = from_union([from_none, from_str], obj.get("summary")) - trigger_file = from_union([from_none, from_str], obj.get("triggerFile")) - trigger_tool = from_union([from_none, from_str], obj.get("triggerTool")) - return SystemNotification( - type=type, + return SystemNotificationAgentCompleted( agent_id=agent_id, agent_type=agent_type, + status=status, description=description, - entry_id=entry_id, - exit_code=exit_code, prompt=prompt, - sender_name=sender_name, - sender_type=sender_type, - shell_id=shell_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["agentId"] = from_str(self.agent_id) + result["agentType"] = from_str(self.agent_type) + result["status"] = to_enum(SystemNotificationAgentCompletedStatus, self.status) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + if self.prompt is not None: + result["prompt"] = from_union([from_none, from_str], self.prompt) + return result + + +@dataclass +class SystemNotificationAgentIdle: + "Schema for the `SystemNotificationAgentIdle` type." + agent_id: str + agent_type: str + type: ClassVar[str] = "agent_idle" + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationAgentIdle": + assert isinstance(obj, dict) + agent_id = from_str(obj.get("agentId")) + agent_type = from_str(obj.get("agentType")) + description = from_union([from_none, from_str], obj.get("description")) + return SystemNotificationAgentIdle( + agent_id=agent_id, + agent_type=agent_type, + description=description, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["agentId"] = from_str(self.agent_id) + result["agentType"] = from_str(self.agent_type) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + return result + + +@dataclass +class SystemNotificationData: + "System-generated notification for runtime events like background task completion" + content: str + kind: SystemNotification + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationData": + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + kind = _load_SystemNotification(obj.get("kind")) + return SystemNotificationData( + content=content, + kind=kind, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + result["kind"] = self.kind.to_dict() + return result + + +@dataclass +class SystemNotificationInstructionDiscovered: + "Schema for the `SystemNotificationInstructionDiscovered` type." + source_path: str + trigger_file: str + trigger_tool: str + type: ClassVar[str] = "instruction_discovered" + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationInstructionDiscovered": + assert isinstance(obj, dict) + source_path = from_str(obj.get("sourcePath")) + trigger_file = from_str(obj.get("triggerFile")) + trigger_tool = from_str(obj.get("triggerTool")) + description = from_union([from_none, from_str], obj.get("description")) + return SystemNotificationInstructionDiscovered( source_path=source_path, - status=status, - summary=summary, trigger_file=trigger_file, trigger_tool=trigger_tool, + description=description, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["sourcePath"] = from_str(self.source_path) + result["triggerFile"] = from_str(self.trigger_file) + result["triggerTool"] = from_str(self.trigger_tool) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + return result + + +@dataclass +class SystemNotificationNewInboxMessage: + "Schema for the `SystemNotificationNewInboxMessage` type." + entry_id: str + sender_name: str + sender_type: str + summary: str + type: ClassVar[str] = "new_inbox_message" + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationNewInboxMessage": + assert isinstance(obj, dict) + entry_id = from_str(obj.get("entryId")) + sender_name = from_str(obj.get("senderName")) + sender_type = from_str(obj.get("senderType")) + summary = from_str(obj.get("summary")) + return SystemNotificationNewInboxMessage( + entry_id=entry_id, + sender_name=sender_name, + sender_type=sender_type, + summary=summary, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["entryId"] = from_str(self.entry_id) + result["senderName"] = from_str(self.sender_name) + result["senderType"] = from_str(self.sender_type) + result["summary"] = from_str(self.summary) + result["type"] = self.type + return result + + +@dataclass +class SystemNotificationShellCompleted: + "Schema for the `SystemNotificationShellCompleted` type." + shell_id: str + type: ClassVar[str] = "shell_completed" + description: str | None = None + exit_code: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationShellCompleted": + assert isinstance(obj, dict) + shell_id = from_str(obj.get("shellId")) + description = from_union([from_none, from_str], obj.get("description")) + exit_code = from_union([from_none, from_int], obj.get("exitCode")) + return SystemNotificationShellCompleted( + shell_id=shell_id, + description=description, + exit_code=exit_code, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["shellId"] = from_str(self.shell_id) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, to_int], self.exit_code) + return result + + +@dataclass +class SystemNotificationShellDetachedCompleted: + "Schema for the `SystemNotificationShellDetachedCompleted` type." + shell_id: str + type: ClassVar[str] = "shell_detached_completed" + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationShellDetachedCompleted": + assert isinstance(obj, dict) + shell_id = from_str(obj.get("shellId")) + description = from_union([from_none, from_str], obj.get("description")) + return SystemNotificationShellDetachedCompleted( + shell_id=shell_id, + description=description, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["shellId"] = from_str(self.shell_id) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + return result + + +@dataclass +class ToolExecutionCompleteContentAudio: + "Audio content block with base64-encoded data" + data: str + mime_type: str + type: ClassVar[str] = "audio" + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentAudio": + assert isinstance(obj, dict) + data = from_str(obj.get("data")) + mime_type = from_str(obj.get("mimeType")) + return ToolExecutionCompleteContentAudio( + data=data, + mime_type=mime_type, ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(SystemNotificationType, self.type) - if self.agent_id is not None: - result["agentId"] = from_union([from_none, from_str], self.agent_id) - if self.agent_type is not None: - result["agentType"] = from_union([from_none, from_str], self.agent_type) - if self.description is not None: - result["description"] = from_union([from_none, from_str], self.description) - if self.entry_id is not None: - result["entryId"] = from_union([from_none, from_str], self.entry_id) - if self.exit_code is not None: - result["exitCode"] = from_union([from_none, to_int], self.exit_code) - if self.prompt is not None: - result["prompt"] = from_union([from_none, from_str], self.prompt) - if self.sender_name is not None: - result["senderName"] = from_union([from_none, from_str], self.sender_name) - if self.sender_type is not None: - result["senderType"] = from_union([from_none, from_str], self.sender_type) - if self.shell_id is not None: - result["shellId"] = from_union([from_none, from_str], self.shell_id) - if self.source_path is not None: - result["sourcePath"] = from_union([from_none, from_str], self.source_path) - if self.status is not None: - result["status"] = from_union([from_none, lambda x: to_enum(SystemNotificationAgentCompletedStatus, x)], self.status) - if self.summary is not None: - result["summary"] = from_union([from_none, from_str], self.summary) - if self.trigger_file is not None: - result["triggerFile"] = from_union([from_none, from_str], self.trigger_file) - if self.trigger_tool is not None: - result["triggerTool"] = from_union([from_none, from_str], self.trigger_tool) + result["data"] = from_str(self.data) + result["mimeType"] = from_str(self.mime_type) + result["type"] = self.type return result @dataclass -class SystemNotificationData: - "System-generated notification for runtime events like background task completion" - content: str - kind: SystemNotification +class ToolExecutionCompleteContentImage: + "Image content block with base64-encoded data" + data: str + mime_type: str + type: ClassVar[str] = "image" @staticmethod - def from_dict(obj: Any) -> "SystemNotificationData": + def from_dict(obj: Any) -> "ToolExecutionCompleteContentImage": assert isinstance(obj, dict) - content = from_str(obj.get("content")) - kind = SystemNotification.from_dict(obj.get("kind")) - return SystemNotificationData( - content=content, - kind=kind, + data = from_str(obj.get("data")) + mime_type = from_str(obj.get("mimeType")) + return ToolExecutionCompleteContentImage( + data=data, + mime_type=mime_type, ) def to_dict(self) -> dict: result: dict = {} - result["content"] = from_str(self.content) - result["kind"] = to_class(SystemNotification, self.kind) + result["data"] = from_str(self.data) + result["mimeType"] = from_str(self.mime_type) + result["type"] = self.type return result @dataclass -class ToolExecutionCompleteContent: - "A content block within a tool result, which may be text, terminal output, image, audio, or a resource" - type: ToolExecutionCompleteContentType - cwd: str | None = None - data: str | None = None +class ToolExecutionCompleteContentResource: + "Embedded resource content block with inline text or binary data" + resource: ToolExecutionCompleteContentResourceDetails + type: ClassVar[str] = "resource" + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentResource": + assert isinstance(obj, dict) + resource = from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], obj.get("resource")) + return ToolExecutionCompleteContentResource( + resource=resource, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["resource"] = from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], self.resource) + result["type"] = self.type + return result + + +@dataclass +class ToolExecutionCompleteContentResourceLink: + "Resource link content block referencing an external resource" + name: str + type: ClassVar[str] = "resource_link" + uri: str description: str | None = None - exit_code: int | None = None icons: list[ToolExecutionCompleteContentResourceLinkIcon] | None = None mime_type: str | None = None - name: str | None = None - resource: ToolExecutionCompleteContentResourceDetails | None = None size: int | None = None - text: str | None = None title: str | None = None - uri: str | None = None @staticmethod - def from_dict(obj: Any) -> "ToolExecutionCompleteContent": + def from_dict(obj: Any) -> "ToolExecutionCompleteContentResourceLink": assert isinstance(obj, dict) - type = parse_enum(ToolExecutionCompleteContentType, obj.get("type")) - cwd = from_union([from_none, from_str], obj.get("cwd")) - data = from_union([from_none, from_str], obj.get("data")) + name = from_str(obj.get("name")) + uri = from_str(obj.get("uri")) description = from_union([from_none, from_str], obj.get("description")) - exit_code = from_union([from_none, from_int], obj.get("exitCode")) icons = from_union([from_none, lambda x: from_list(ToolExecutionCompleteContentResourceLinkIcon.from_dict, x)], obj.get("icons")) mime_type = from_union([from_none, from_str], obj.get("mimeType")) - name = from_union([from_none, from_str], obj.get("name")) - resource = from_union([from_none, lambda x: from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], x)], obj.get("resource")) size = from_union([from_none, from_int], obj.get("size")) - text = from_union([from_none, from_str], obj.get("text")) title = from_union([from_none, from_str], obj.get("title")) - uri = from_union([from_none, from_str], obj.get("uri")) - return ToolExecutionCompleteContent( - type=type, - cwd=cwd, - data=data, + return ToolExecutionCompleteContentResourceLink( + name=name, + uri=uri, description=description, - exit_code=exit_code, icons=icons, mime_type=mime_type, - name=name, - resource=resource, size=size, - text=text, title=title, - uri=uri, ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(ToolExecutionCompleteContentType, self.type) - if self.cwd is not None: - result["cwd"] = from_union([from_none, from_str], self.cwd) - if self.data is not None: - result["data"] = from_union([from_none, from_str], self.data) + result["name"] = from_str(self.name) + result["type"] = self.type + result["uri"] = from_str(self.uri) if self.description is not None: result["description"] = from_union([from_none, from_str], self.description) - if self.exit_code is not None: - result["exitCode"] = from_union([from_none, to_int], self.exit_code) if self.icons is not None: result["icons"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ToolExecutionCompleteContentResourceLinkIcon, x), x)], self.icons) if self.mime_type is not None: result["mimeType"] = from_union([from_none, from_str], self.mime_type) - if self.name is not None: - result["name"] = from_union([from_none, from_str], self.name) - if self.resource is not None: - result["resource"] = from_union([from_none, lambda x: from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], x)], self.resource) if self.size is not None: result["size"] = from_union([from_none, to_int], self.size) - if self.text is not None: - result["text"] = from_union([from_none, from_str], self.text) if self.title is not None: result["title"] = from_union([from_none, from_str], self.title) - if self.uri is not None: - result["uri"] = from_union([from_none, from_str], self.uri) return result @@ -4186,6 +5275,58 @@ def to_dict(self) -> dict: return result +@dataclass +class ToolExecutionCompleteContentTerminal: + "Terminal/shell output content block with optional exit code and working directory" + text: str + type: ClassVar[str] = "terminal" + cwd: str | None = None + exit_code: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentTerminal": + assert isinstance(obj, dict) + text = from_str(obj.get("text")) + cwd = from_union([from_none, from_str], obj.get("cwd")) + exit_code = from_union([from_none, from_int], obj.get("exitCode")) + return ToolExecutionCompleteContentTerminal( + text=text, + cwd=cwd, + exit_code=exit_code, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["text"] = from_str(self.text) + result["type"] = self.type + if self.cwd is not None: + result["cwd"] = from_union([from_none, from_str], self.cwd) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, to_int], self.exit_code) + return result + + +@dataclass +class ToolExecutionCompleteContentText: + "Plain text content block" + text: str + type: ClassVar[str] = "text" + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentText": + assert isinstance(obj, dict) + text = from_str(obj.get("text")) + return ToolExecutionCompleteContentText( + text=text, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["text"] = from_str(self.text) + result["type"] = self.type + return result + + @dataclass class ToolExecutionCompleteData: "Tool execution completion results including success status, detailed output, and error information" @@ -4199,6 +5340,7 @@ class ToolExecutionCompleteData: parent_tool_call_id: str | None = None result: ToolExecutionCompleteResult | None = None sandboxed: bool | None = None + tool_description: ToolExecutionCompleteToolDescription | None = None tool_telemetry: dict[str, Any] | None = None turn_id: str | None = None @@ -4214,6 +5356,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteData": parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) result = from_union([from_none, ToolExecutionCompleteResult.from_dict], obj.get("result")) sandboxed = from_union([from_none, from_bool], obj.get("sandboxed")) + tool_description = from_union([from_none, ToolExecutionCompleteToolDescription.from_dict], obj.get("toolDescription")) tool_telemetry = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("toolTelemetry")) turn_id = from_union([from_none, from_str], obj.get("turnId")) return ToolExecutionCompleteData( @@ -4226,6 +5369,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteData": parent_tool_call_id=parent_tool_call_id, result=result, sandboxed=sandboxed, + tool_description=tool_description, tool_telemetry=tool_telemetry, turn_id=turn_id, ) @@ -4248,6 +5392,8 @@ def to_dict(self) -> dict: result["result"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteResult, x)], self.result) if self.sandboxed is not None: result["sandboxed"] = from_union([from_none, from_bool], self.sandboxed) + if self.tool_description is not None: + result["toolDescription"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteToolDescription, x)], self.tool_description) if self.tool_telemetry is not None: result["toolTelemetry"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.tool_telemetry) if self.turn_id is not None: @@ -4285,29 +5431,319 @@ class ToolExecutionCompleteResult: content: str contents: list[ToolExecutionCompleteContent] | None = None detailed_content: str | None = None + ui_resource: ToolExecutionCompleteUIResource | None = None @staticmethod def from_dict(obj: Any) -> "ToolExecutionCompleteResult": assert isinstance(obj, dict) content = from_str(obj.get("content")) - contents = from_union([from_none, lambda x: from_list(ToolExecutionCompleteContent.from_dict, x)], obj.get("contents")) + contents = from_union([from_none, lambda x: from_list(_load_ToolExecutionCompleteContent, x)], obj.get("contents")) detailed_content = from_union([from_none, from_str], obj.get("detailedContent")) + ui_resource = from_union([from_none, ToolExecutionCompleteUIResource.from_dict], obj.get("uiResource")) return ToolExecutionCompleteResult( content=content, contents=contents, detailed_content=detailed_content, + ui_resource=ui_resource, ) def to_dict(self) -> dict: result: dict = {} result["content"] = from_str(self.content) if self.contents is not None: - result["contents"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ToolExecutionCompleteContent, x), x)], self.contents) + result["contents"] = from_union([from_none, lambda x: from_list(lambda x: x.to_dict(), x)], self.contents) if self.detailed_content is not None: result["detailedContent"] = from_union([from_none, from_str], self.detailed_content) + if self.ui_resource is not None: + result["uiResource"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResource, x)], self.ui_resource) + return result + + +@dataclass +class ToolExecutionCompleteToolDescription: + "Tool definition metadata, present for MCP tools with MCP Apps support" + name: str + _meta: ToolExecutionCompleteToolDescriptionMeta | None = None + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteToolDescription": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + _meta = from_union([from_none, ToolExecutionCompleteToolDescriptionMeta.from_dict], obj.get("_meta")) + description = from_union([from_none, from_str], obj.get("description")) + return ToolExecutionCompleteToolDescription( + name=name, + _meta=_meta, + description=description, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self._meta is not None: + result["_meta"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteToolDescriptionMeta, x)], self._meta) + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + return result + + +@dataclass +class ToolExecutionCompleteToolDescriptionMeta: + "MCP Apps metadata for UI resource association" + ui: ToolExecutionCompleteToolDescriptionMetaUI | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteToolDescriptionMeta": + assert isinstance(obj, dict) + ui = from_union([from_none, ToolExecutionCompleteToolDescriptionMetaUI.from_dict], obj.get("ui")) + return ToolExecutionCompleteToolDescriptionMeta( + ui=ui, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.ui is not None: + result["ui"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteToolDescriptionMetaUI, x)], self.ui) + return result + + +@dataclass +class ToolExecutionCompleteToolDescriptionMetaUI: + "Schema for the `ToolExecutionCompleteToolDescriptionMetaUI` type." + resource_uri: str | None = None + visibility: list[ToolExecutionCompleteToolDescriptionMetaUIVisibility] | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteToolDescriptionMetaUI": + assert isinstance(obj, dict) + resource_uri = from_union([from_none, from_str], obj.get("resourceUri")) + visibility = from_union([from_none, lambda x: from_list(lambda x: parse_enum(ToolExecutionCompleteToolDescriptionMetaUIVisibility, x), x)], obj.get("visibility")) + return ToolExecutionCompleteToolDescriptionMetaUI( + resource_uri=resource_uri, + visibility=visibility, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.resource_uri is not None: + result["resourceUri"] = from_union([from_none, from_str], self.resource_uri) + if self.visibility is not None: + result["visibility"] = from_union([from_none, lambda x: from_list(lambda x: to_enum(ToolExecutionCompleteToolDescriptionMetaUIVisibility, x), x)], self.visibility) + return result + + +@dataclass +class ToolExecutionCompleteUIResource: + "MCP Apps UI resource content for rendering in a sandboxed iframe" + mime_type: str + uri: str + _meta: ToolExecutionCompleteUIResourceMeta | None = None + blob: str | None = None + text: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResource": + assert isinstance(obj, dict) + mime_type = from_str(obj.get("mimeType")) + uri = from_str(obj.get("uri")) + _meta = from_union([from_none, ToolExecutionCompleteUIResourceMeta.from_dict], obj.get("_meta")) + blob = from_union([from_none, from_str], obj.get("blob")) + text = from_union([from_none, from_str], obj.get("text")) + return ToolExecutionCompleteUIResource( + mime_type=mime_type, + uri=uri, + _meta=_meta, + blob=blob, + text=text, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["mimeType"] = from_str(self.mime_type) + result["uri"] = from_str(self.uri) + if self._meta is not None: + result["_meta"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResourceMeta, x)], self._meta) + if self.blob is not None: + result["blob"] = from_union([from_none, from_str], self.blob) + if self.text is not None: + result["text"] = from_union([from_none, from_str], self.text) + return result + + +@dataclass +class ToolExecutionCompleteUIResourceMeta: + "Resource-level UI metadata (CSP, permissions, visual preferences)" + ui: ToolExecutionCompleteUIResourceMetaUI | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResourceMeta": + assert isinstance(obj, dict) + ui = from_union([from_none, ToolExecutionCompleteUIResourceMetaUI.from_dict], obj.get("ui")) + return ToolExecutionCompleteUIResourceMeta( + ui=ui, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.ui is not None: + result["ui"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResourceMetaUI, x)], self.ui) return result +@dataclass +class ToolExecutionCompleteUIResourceMetaUI: + "Schema for the `ToolExecutionCompleteUIResourceMetaUI` type." + csp: ToolExecutionCompleteUIResourceMetaUICsp | None = None + domain: str | None = None + permissions: ToolExecutionCompleteUIResourceMetaUIPermissions | None = None + prefers_border: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResourceMetaUI": + assert isinstance(obj, dict) + csp = from_union([from_none, ToolExecutionCompleteUIResourceMetaUICsp.from_dict], obj.get("csp")) + domain = from_union([from_none, from_str], obj.get("domain")) + permissions = from_union([from_none, ToolExecutionCompleteUIResourceMetaUIPermissions.from_dict], obj.get("permissions")) + prefers_border = from_union([from_none, from_bool], obj.get("prefersBorder")) + return ToolExecutionCompleteUIResourceMetaUI( + csp=csp, + domain=domain, + permissions=permissions, + prefers_border=prefers_border, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.csp is not None: + result["csp"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResourceMetaUICsp, x)], self.csp) + if self.domain is not None: + result["domain"] = from_union([from_none, from_str], self.domain) + if self.permissions is not None: + result["permissions"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResourceMetaUIPermissions, x)], self.permissions) + if self.prefers_border is not None: + result["prefersBorder"] = from_union([from_none, from_bool], self.prefers_border) + return result + + +@dataclass +class ToolExecutionCompleteUIResourceMetaUICsp: + "Schema for the `ToolExecutionCompleteUIResourceMetaUICsp` type." + base_uri_domains: list[str] | None = None + connect_domains: list[str] | None = None + frame_domains: list[str] | None = None + resource_domains: list[str] | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResourceMetaUICsp": + assert isinstance(obj, dict) + base_uri_domains = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("baseUriDomains")) + connect_domains = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("connectDomains")) + frame_domains = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("frameDomains")) + resource_domains = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("resourceDomains")) + return ToolExecutionCompleteUIResourceMetaUICsp( + base_uri_domains=base_uri_domains, + connect_domains=connect_domains, + frame_domains=frame_domains, + resource_domains=resource_domains, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.base_uri_domains is not None: + result["baseUriDomains"] = from_union([from_none, lambda x: from_list(from_str, x)], self.base_uri_domains) + if self.connect_domains is not None: + result["connectDomains"] = from_union([from_none, lambda x: from_list(from_str, x)], self.connect_domains) + if self.frame_domains is not None: + result["frameDomains"] = from_union([from_none, lambda x: from_list(from_str, x)], self.frame_domains) + if self.resource_domains is not None: + result["resourceDomains"] = from_union([from_none, lambda x: from_list(from_str, x)], self.resource_domains) + return result + + +@dataclass +class ToolExecutionCompleteUIResourceMetaUIPermissions: + "Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissions` type." + camera: ToolExecutionCompleteUIResourceMetaUIPermissionsCamera | None = None + clipboard_write: ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite | None = None + geolocation: ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation | None = None + microphone: ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResourceMetaUIPermissions": + assert isinstance(obj, dict) + camera = from_union([from_none, ToolExecutionCompleteUIResourceMetaUIPermissionsCamera.from_dict], obj.get("camera")) + clipboard_write = from_union([from_none, ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite.from_dict], obj.get("clipboardWrite")) + geolocation = from_union([from_none, ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation.from_dict], obj.get("geolocation")) + microphone = from_union([from_none, ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone.from_dict], obj.get("microphone")) + return ToolExecutionCompleteUIResourceMetaUIPermissions( + camera=camera, + clipboard_write=clipboard_write, + geolocation=geolocation, + microphone=microphone, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.camera is not None: + result["camera"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResourceMetaUIPermissionsCamera, x)], self.camera) + if self.clipboard_write is not None: + result["clipboardWrite"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite, x)], self.clipboard_write) + if self.geolocation is not None: + result["geolocation"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation, x)], self.geolocation) + if self.microphone is not None: + result["microphone"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone, x)], self.microphone) + return result + + +@dataclass +class ToolExecutionCompleteUIResourceMetaUIPermissionsCamera: + "Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsCamera` type." + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResourceMetaUIPermissionsCamera": + assert isinstance(obj, dict) + return ToolExecutionCompleteUIResourceMetaUIPermissionsCamera() + + def to_dict(self) -> dict: + return {} + + +@dataclass +class ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite: + "Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite` type." + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite": + assert isinstance(obj, dict) + return ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite() + + def to_dict(self) -> dict: + return {} + + +@dataclass +class ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation: + "Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation` type." + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation": + assert isinstance(obj, dict) + return ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation() + + def to_dict(self) -> dict: + return {} + + +@dataclass +class ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone: + "Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone` type." + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone": + assert isinstance(obj, dict) + return ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone() + + def to_dict(self) -> dict: + return {} + + @dataclass class ToolExecutionPartialResultData: "Streaming tool execution output for incremental result display" @@ -4499,86 +5935,87 @@ def to_dict(self) -> dict: @dataclass -class UserMessageAttachment: - "A user message attachment — a file, directory, code selection, blob, or GitHub reference" - type: UserMessageAttachmentType - data: str | None = None +class UserMessageAttachmentBlob: + "Blob attachment with inline base64-encoded data" + data: str + mime_type: str + type: ClassVar[str] = "blob" display_name: str | None = None - file_path: str | None = None - line_range: UserMessageAttachmentFileLineRange | None = None - mime_type: str | None = None - number: int | None = None - path: str | None = None - reference_type: UserMessageAttachmentGithubReferenceType | None = None - selection: UserMessageAttachmentSelectionDetails | None = None - state: str | None = None - text: str | None = None - title: str | None = None - url: str | None = None @staticmethod - def from_dict(obj: Any) -> "UserMessageAttachment": + def from_dict(obj: Any) -> "UserMessageAttachmentBlob": assert isinstance(obj, dict) - type = parse_enum(UserMessageAttachmentType, obj.get("type")) - data = from_union([from_none, from_str], obj.get("data")) + data = from_str(obj.get("data")) + mime_type = from_str(obj.get("mimeType")) display_name = from_union([from_none, from_str], obj.get("displayName")) - file_path = from_union([from_none, from_str], obj.get("filePath")) - line_range = from_union([from_none, UserMessageAttachmentFileLineRange.from_dict], obj.get("lineRange")) - mime_type = from_union([from_none, from_str], obj.get("mimeType")) - number = from_union([from_none, from_int], obj.get("number")) - path = from_union([from_none, from_str], obj.get("path")) - reference_type = from_union([from_none, lambda x: parse_enum(UserMessageAttachmentGithubReferenceType, x)], obj.get("referenceType")) - selection = from_union([from_none, UserMessageAttachmentSelectionDetails.from_dict], obj.get("selection")) - state = from_union([from_none, from_str], obj.get("state")) - text = from_union([from_none, from_str], obj.get("text")) - title = from_union([from_none, from_str], obj.get("title")) - url = from_union([from_none, from_str], obj.get("url")) - return UserMessageAttachment( - type=type, + return UserMessageAttachmentBlob( data=data, - display_name=display_name, - file_path=file_path, - line_range=line_range, mime_type=mime_type, - number=number, - path=path, - reference_type=reference_type, - selection=selection, - state=state, - text=text, - title=title, - url=url, + display_name=display_name, ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(UserMessageAttachmentType, self.type) - if self.data is not None: - result["data"] = from_union([from_none, from_str], self.data) + result["data"] = from_str(self.data) + result["mimeType"] = from_str(self.mime_type) + result["type"] = self.type if self.display_name is not None: result["displayName"] = from_union([from_none, from_str], self.display_name) - if self.file_path is not None: - result["filePath"] = from_union([from_none, from_str], self.file_path) + return result + + +@dataclass +class UserMessageAttachmentDirectory: + "Directory attachment" + display_name: str + path: str + type: ClassVar[str] = "directory" + + @staticmethod + def from_dict(obj: Any) -> "UserMessageAttachmentDirectory": + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + path = from_str(obj.get("path")) + return UserMessageAttachmentDirectory( + display_name=display_name, + path=path, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["path"] = from_str(self.path) + result["type"] = self.type + return result + + +@dataclass +class UserMessageAttachmentFile: + "File attachment" + display_name: str + path: str + type: ClassVar[str] = "file" + line_range: UserMessageAttachmentFileLineRange | None = None + + @staticmethod + def from_dict(obj: Any) -> "UserMessageAttachmentFile": + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + path = from_str(obj.get("path")) + line_range = from_union([from_none, UserMessageAttachmentFileLineRange.from_dict], obj.get("lineRange")) + return UserMessageAttachmentFile( + display_name=display_name, + path=path, + line_range=line_range, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["path"] = from_str(self.path) + result["type"] = self.type if self.line_range is not None: result["lineRange"] = from_union([from_none, lambda x: to_class(UserMessageAttachmentFileLineRange, x)], self.line_range) - if self.mime_type is not None: - result["mimeType"] = from_union([from_none, from_str], self.mime_type) - if self.number is not None: - result["number"] = from_union([from_none, to_int], self.number) - if self.path is not None: - result["path"] = from_union([from_none, from_str], self.path) - if self.reference_type is not None: - result["referenceType"] = from_union([from_none, lambda x: to_enum(UserMessageAttachmentGithubReferenceType, x)], self.reference_type) - if self.selection is not None: - result["selection"] = from_union([from_none, lambda x: to_class(UserMessageAttachmentSelectionDetails, x)], self.selection) - if self.state is not None: - result["state"] = from_union([from_none, from_str], self.state) - if self.text is not None: - result["text"] = from_union([from_none, from_str], self.text) - if self.title is not None: - result["title"] = from_union([from_none, from_str], self.title) - if self.url is not None: - result["url"] = from_union([from_none, from_str], self.url) return result @@ -4605,6 +6042,76 @@ def to_dict(self) -> dict: return result +@dataclass +class UserMessageAttachmentGithubReference: + "GitHub issue, pull request, or discussion reference" + number: int + reference_type: UserMessageAttachmentGithubReferenceType + state: str + title: str + type: ClassVar[str] = "github_reference" + url: str + + @staticmethod + def from_dict(obj: Any) -> "UserMessageAttachmentGithubReference": + assert isinstance(obj, dict) + number = from_int(obj.get("number")) + reference_type = parse_enum(UserMessageAttachmentGithubReferenceType, obj.get("referenceType")) + state = from_str(obj.get("state")) + title = from_str(obj.get("title")) + url = from_str(obj.get("url")) + return UserMessageAttachmentGithubReference( + number=number, + reference_type=reference_type, + state=state, + title=title, + url=url, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["number"] = to_int(self.number) + result["referenceType"] = to_enum(UserMessageAttachmentGithubReferenceType, self.reference_type) + result["state"] = from_str(self.state) + result["title"] = from_str(self.title) + result["type"] = self.type + result["url"] = from_str(self.url) + return result + + +@dataclass +class UserMessageAttachmentSelection: + "Code selection attachment from an editor" + display_name: str + file_path: str + selection: UserMessageAttachmentSelectionDetails + text: str + type: ClassVar[str] = "selection" + + @staticmethod + def from_dict(obj: Any) -> "UserMessageAttachmentSelection": + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + file_path = from_str(obj.get("filePath")) + selection = UserMessageAttachmentSelectionDetails.from_dict(obj.get("selection")) + text = from_str(obj.get("text")) + return UserMessageAttachmentSelection( + display_name=display_name, + file_path=file_path, + selection=selection, + text=text, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["filePath"] = from_str(self.file_path) + result["selection"] = to_class(UserMessageAttachmentSelectionDetails, self.selection) + result["text"] = from_str(self.text) + result["type"] = self.type + return result + + @dataclass class UserMessageAttachmentSelectionDetails: "Position range of the selection within the file" @@ -4693,7 +6200,7 @@ def from_dict(obj: Any) -> "UserMessageData": assert isinstance(obj, dict) content = from_str(obj.get("content")) agent_mode = from_union([from_none, lambda x: parse_enum(UserMessageAgentMode, x)], obj.get("agentMode")) - attachments = from_union([from_none, lambda x: from_list(UserMessageAttachment.from_dict, x)], obj.get("attachments")) + attachments = from_union([from_none, lambda x: from_list(_load_UserMessageAttachment, x)], obj.get("attachments")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_autopilot_continuation = from_union([from_none, from_bool], obj.get("isAutopilotContinuation")) native_document_path_fallback_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("nativeDocumentPathFallbackPaths")) @@ -4720,7 +6227,7 @@ def to_dict(self) -> dict: if self.agent_mode is not None: result["agentMode"] = from_union([from_none, lambda x: to_enum(UserMessageAgentMode, x)], self.agent_mode) if self.attachments is not None: - result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: to_class(UserMessageAttachment, x), x)], self.attachments) + result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: x.to_dict(), x)], self.attachments) if self.interaction_id is not None: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) if self.is_autopilot_continuation is not None: @@ -4739,46 +6246,163 @@ def to_dict(self) -> dict: @dataclass -class UserToolSessionApproval: - "The approval to add as a session-scoped rule" - kind: UserToolSessionApprovalKind - command_identifiers: list[str] | None = None - extension_name: str | None = None +class UserToolSessionApprovalCommands: + "Schema for the `UserToolSessionApprovalCommands` type." + command_identifiers: list[str] + kind: ClassVar[str] = "commands" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalCommands": + assert isinstance(obj, dict) + command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) + return UserToolSessionApprovalCommands( + command_identifiers=command_identifiers, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) + result["kind"] = self.kind + return result + + +@dataclass +class UserToolSessionApprovalCustomTool: + "Schema for the `UserToolSessionApprovalCustomTool` type." + kind: ClassVar[str] = "custom-tool" + tool_name: str + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalCustomTool": + assert isinstance(obj, dict) + tool_name = from_str(obj.get("toolName")) + return UserToolSessionApprovalCustomTool( + tool_name=tool_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolName"] = from_str(self.tool_name) + return result + + +@dataclass +class UserToolSessionApprovalExtensionManagement: + "Schema for the `UserToolSessionApprovalExtensionManagement` type." + kind: ClassVar[str] = "extension-management" operation: str | None = None - server_name: str | None = None - tool_name: str | None = None @staticmethod - def from_dict(obj: Any) -> "UserToolSessionApproval": + def from_dict(obj: Any) -> "UserToolSessionApprovalExtensionManagement": assert isinstance(obj, dict) - kind = parse_enum(UserToolSessionApprovalKind, obj.get("kind")) - command_identifiers = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("commandIdentifiers")) - extension_name = from_union([from_none, from_str], obj.get("extensionName")) operation = from_union([from_none, from_str], obj.get("operation")) - server_name = from_union([from_none, from_str], obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - return UserToolSessionApproval( - kind=kind, - command_identifiers=command_identifiers, - extension_name=extension_name, + return UserToolSessionApprovalExtensionManagement( operation=operation, - server_name=server_name, - tool_name=tool_name, ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(UserToolSessionApprovalKind, self.kind) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([from_none, lambda x: from_list(from_str, x)], self.command_identifiers) - if self.extension_name is not None: - result["extensionName"] = from_union([from_none, from_str], self.extension_name) + result["kind"] = self.kind if self.operation is not None: result["operation"] = from_union([from_none, from_str], self.operation) - if self.server_name is not None: - result["serverName"] = from_union([from_none, from_str], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + + +@dataclass +class UserToolSessionApprovalExtensionPermissionAccess: + "Schema for the `UserToolSessionApprovalExtensionPermissionAccess` type." + extension_name: str + kind: ClassVar[str] = "extension-permission-access" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalExtensionPermissionAccess": + assert isinstance(obj, dict) + extension_name = from_str(obj.get("extensionName")) + return UserToolSessionApprovalExtensionPermissionAccess( + extension_name=extension_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["extensionName"] = from_str(self.extension_name) + result["kind"] = self.kind + return result + + +@dataclass +class UserToolSessionApprovalMcp: + "Schema for the `UserToolSessionApprovalMcp` type." + kind: ClassVar[str] = "mcp" + server_name: str + tool_name: str | None + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalMcp": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return UserToolSessionApprovalMcp( + server_name=server_name, + tool_name=tool_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + + +@dataclass +class UserToolSessionApprovalMemory: + "Schema for the `UserToolSessionApprovalMemory` type." + kind: ClassVar[str] = "memory" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalMemory": + assert isinstance(obj, dict) + return UserToolSessionApprovalMemory( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + return result + + +@dataclass +class UserToolSessionApprovalRead: + "Schema for the `UserToolSessionApprovalRead` type." + kind: ClassVar[str] = "read" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalRead": + assert isinstance(obj, dict) + return UserToolSessionApprovalRead( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + return result + + +@dataclass +class UserToolSessionApprovalWrite: + "Schema for the `UserToolSessionApprovalWrite` type." + kind: ClassVar[str] = "write" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalWrite": + assert isinstance(obj, dict) + return UserToolSessionApprovalWrite( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind return result @@ -4836,10 +6460,142 @@ def to_dict(self) -> dict: return result +def _load_PermissionPromptRequest(obj: Any) -> "PermissionPromptRequest": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return PermissionPromptRequestCommands.from_dict(obj) + case "write": return PermissionPromptRequestWrite.from_dict(obj) + case "read": return PermissionPromptRequestRead.from_dict(obj) + case "mcp": return PermissionPromptRequestMcp.from_dict(obj) + case "url": return PermissionPromptRequestUrl.from_dict(obj) + case "memory": return PermissionPromptRequestMemory.from_dict(obj) + case "custom-tool": return PermissionPromptRequestCustomTool.from_dict(obj) + case "path": return PermissionPromptRequestPath.from_dict(obj) + case "hook": return PermissionPromptRequestHook.from_dict(obj) + case "extension-management": return PermissionPromptRequestExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionPromptRequestExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionPromptRequest kind: {kind!r}") + + +def _load_PermissionRequest(obj: Any) -> "PermissionRequest": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "shell": return PermissionRequestShell.from_dict(obj) + case "write": return PermissionRequestWrite.from_dict(obj) + case "read": return PermissionRequestRead.from_dict(obj) + case "mcp": return PermissionRequestMcp.from_dict(obj) + case "url": return PermissionRequestUrl.from_dict(obj) + case "memory": return PermissionRequestMemory.from_dict(obj) + case "custom-tool": return PermissionRequestCustomTool.from_dict(obj) + case "hook": return PermissionRequestHook.from_dict(obj) + case "extension-management": return PermissionRequestExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionRequestExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionRequest kind: {kind!r}") + + +def _load_PermissionResult(obj: Any) -> "PermissionResult": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "approved": return PermissionApproved.from_dict(obj) + case "approved-for-session": return PermissionApprovedForSession.from_dict(obj) + case "approved-for-location": return PermissionApprovedForLocation.from_dict(obj) + case "cancelled": return PermissionCancelled.from_dict(obj) + case "denied-by-rules": return PermissionDeniedByRules.from_dict(obj) + case "denied-no-approval-rule-and-could-not-request-from-user": return PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser.from_dict(obj) + case "denied-interactively-by-user": return PermissionDeniedInteractivelyByUser.from_dict(obj) + case "denied-by-content-exclusion-policy": return PermissionDeniedByContentExclusionPolicy.from_dict(obj) + case "denied-by-permission-request-hook": return PermissionDeniedByPermissionRequestHook.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionResult kind: {kind!r}") + + +def _load_SystemNotification(obj: Any) -> "SystemNotification": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "agent_completed": return SystemNotificationAgentCompleted.from_dict(obj) + case "agent_idle": return SystemNotificationAgentIdle.from_dict(obj) + case "new_inbox_message": return SystemNotificationNewInboxMessage.from_dict(obj) + case "shell_completed": return SystemNotificationShellCompleted.from_dict(obj) + case "shell_detached_completed": return SystemNotificationShellDetachedCompleted.from_dict(obj) + case "instruction_discovered": return SystemNotificationInstructionDiscovered.from_dict(obj) + case _: raise ValueError(f"Unknown SystemNotification type: {kind!r}") + + +def _load_ToolExecutionCompleteContent(obj: Any) -> "ToolExecutionCompleteContent": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "text": return ToolExecutionCompleteContentText.from_dict(obj) + case "terminal": return ToolExecutionCompleteContentTerminal.from_dict(obj) + case "image": return ToolExecutionCompleteContentImage.from_dict(obj) + case "audio": return ToolExecutionCompleteContentAudio.from_dict(obj) + case "resource_link": return ToolExecutionCompleteContentResourceLink.from_dict(obj) + case "resource": return ToolExecutionCompleteContentResource.from_dict(obj) + case _: raise ValueError(f"Unknown ToolExecutionCompleteContent type: {kind!r}") + + +def _load_UserMessageAttachment(obj: Any) -> "UserMessageAttachment": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "file": return UserMessageAttachmentFile.from_dict(obj) + case "directory": return UserMessageAttachmentDirectory.from_dict(obj) + case "selection": return UserMessageAttachmentSelection.from_dict(obj) + case "github_reference": return UserMessageAttachmentGithubReference.from_dict(obj) + case "blob": return UserMessageAttachmentBlob.from_dict(obj) + case _: raise ValueError(f"Unknown UserMessageAttachment type: {kind!r}") + + +def _load_UserToolSessionApproval(obj: Any) -> "UserToolSessionApproval": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return UserToolSessionApprovalCommands.from_dict(obj) + case "read": return UserToolSessionApprovalRead.from_dict(obj) + case "write": return UserToolSessionApprovalWrite.from_dict(obj) + case "mcp": return UserToolSessionApprovalMcp.from_dict(obj) + case "memory": return UserToolSessionApprovalMemory.from_dict(obj) + case "custom-tool": return UserToolSessionApprovalCustomTool.from_dict(obj) + case "extension-management": return UserToolSessionApprovalExtensionManagement.from_dict(obj) + case "extension-permission-access": return UserToolSessionApprovalExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown UserToolSessionApproval kind: {kind!r}") + + +# A content block within a tool result, which may be text, terminal output, image, audio, or a resource +ToolExecutionCompleteContent = ToolExecutionCompleteContentText | ToolExecutionCompleteContentTerminal | ToolExecutionCompleteContentImage | ToolExecutionCompleteContentAudio | ToolExecutionCompleteContentResourceLink | ToolExecutionCompleteContentResource + + +# A user message attachment — a file, directory, code selection, blob, or GitHub reference +UserMessageAttachment = UserMessageAttachmentFile | UserMessageAttachmentDirectory | UserMessageAttachmentSelection | UserMessageAttachmentGithubReference | UserMessageAttachmentBlob + + +# Derived user-facing permission prompt details for UI consumers +PermissionPromptRequest = PermissionPromptRequestCommands | PermissionPromptRequestWrite | PermissionPromptRequestRead | PermissionPromptRequestMcp | PermissionPromptRequestUrl | PermissionPromptRequestMemory | PermissionPromptRequestCustomTool | PermissionPromptRequestPath | PermissionPromptRequestHook | PermissionPromptRequestExtensionManagement | PermissionPromptRequestExtensionPermissionAccess + + +# Details of the permission being requested +PermissionRequest = PermissionRequestShell | PermissionRequestWrite | PermissionRequestRead | PermissionRequestMcp | PermissionRequestUrl | PermissionRequestMemory | PermissionRequestCustomTool | PermissionRequestHook | PermissionRequestExtensionManagement | PermissionRequestExtensionPermissionAccess + + +# Structured metadata identifying what triggered this notification +SystemNotification = SystemNotificationAgentCompleted | SystemNotificationAgentIdle | SystemNotificationNewInboxMessage | SystemNotificationShellCompleted | SystemNotificationShellDetachedCompleted | SystemNotificationInstructionDiscovered + + +# The approval to add as a session-scoped rule +UserToolSessionApproval = UserToolSessionApprovalCommands | UserToolSessionApprovalRead | UserToolSessionApprovalWrite | UserToolSessionApprovalMcp | UserToolSessionApprovalMemory | UserToolSessionApprovalCustomTool | UserToolSessionApprovalExtensionManagement | UserToolSessionApprovalExtensionPermissionAccess + + # The embedded resource contents, either text or base64-encoded binary ToolExecutionCompleteContentResourceDetails = EmbeddedTextResourceContents | EmbeddedBlobResourceContents +# The result of the permission request +PermissionResult = PermissionApproved | PermissionApprovedForSession | PermissionApprovedForLocation | PermissionCancelled | PermissionDeniedByRules | PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDeniedInteractivelyByUser | PermissionDeniedByContentExclusionPolicy | PermissionDeniedByPermissionRequestHook + + class AbortReason(Enum): "Finite reason code describing why the current turn was aborted" # The local user requested the abort, for example by pressing Ctrl+C in the CLI. @@ -4880,6 +6636,14 @@ class AutoModeSwitchResponse(Enum): NO = "no" +class CanvasOpenedAvailability(Enum): + "Runtime-controlled routing state for the instance. \"ready\" when the provider connection is live; \"stale\" when the provider has gone away and the instance is awaiting rebinding." + # Provider connection is live; actions can be invoked. + READY = "ready" + # Provider has gone away; the instance is awaiting rebinding. + STALE = "stale" + + class ElicitationCompletedAction(Enum): "The user action: \"accept\" (submitted form), \"decline\" (explicitly refused), or \"cancel\" (dismissed)" # The user submitted the requested form. @@ -4966,6 +6730,18 @@ class McpServerStatus(Enum): NOT_CONFIGURED = "not_configured" +class McpServerTransport(Enum): + "Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server)" + # Server communicates over stdio with a local child process. + STDIO = "stdio" + # Server communicates over streamable HTTP. + HTTP = "http" + # Server communicates over Server-Sent Events (deprecated). + SSE = "sse" + # Server is backed by an in-memory runtime implementation. + MEMORY = "memory" + + class ModelCallFailureSource(Enum): "Where the failed model call originated" # Model call from the top-level agent. @@ -4976,21 +6752,6 @@ class ModelCallFailureSource(Enum): MCP_SAMPLING = "mcp_sampling" -class PermissionPromptRequestKind(Enum): - "Derived user-facing permission prompt details for UI consumers discriminator" - COMMANDS = "commands" - WRITE = "write" - READ = "read" - MCP = "mcp" - URL = "url" - MEMORY = "memory" - CUSTOM_TOOL = "custom-tool" - PATH = "path" - HOOK = "hook" - EXTENSION_MANAGEMENT = "extension-management" - EXTENSION_PERMISSION_ACCESS = "extension-permission-access" - - class PermissionPromptRequestPathAccessKind(Enum): "Underlying permission kind that needs path approval" # Read access to a filesystem path. @@ -5001,20 +6762,6 @@ class PermissionPromptRequestPathAccessKind(Enum): WRITE = "write" -class PermissionRequestKind(Enum): - "Details of the permission being requested discriminator" - SHELL = "shell" - WRITE = "write" - READ = "read" - MCP = "mcp" - URL = "url" - MEMORY = "memory" - CUSTOM_TOOL = "custom-tool" - HOOK = "hook" - EXTENSION_MANAGEMENT = "extension-management" - EXTENSION_PERMISSION_ACCESS = "extension-permission-access" - - class PermissionRequestMemoryAction(Enum): "Whether this is a store or vote memory operation" # Store a new memory. @@ -5031,19 +6778,6 @@ class PermissionRequestMemoryDirection(Enum): DOWNVOTE = "downvote" -class PermissionResultKind(Enum): - "The result of the permission request discriminator" - APPROVED = "approved" - APPROVED_FOR_SESSION = "approved-for-session" - APPROVED_FOR_LOCATION = "approved-for-location" - CANCELLED = "cancelled" - DENIED_BY_RULES = "denied-by-rules" - DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user" - DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user" - DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" - DENIED_BY_PERMISSION_REQUEST_HOOK = "denied-by-permission-request-hook" - - class PlanChangedOperation(Enum): "The type of operation performed on the plan file" # The plan file was created. @@ -5074,6 +6808,13 @@ class SessionMode(Enum): AUTOPILOT = "autopilot" +class SessionModelChangeDataContextTier(Enum): + # Default context tier with standard context window size. + DEFAULT = "default" + # Extended context tier with a larger context window. + LONG_CONTEXT = "long_context" + + class ShutdownType(Enum): "Whether the session ended normally (\"routine\") or due to a crash/fatal error (\"error\")" # The session ended normally. @@ -5082,6 +6823,16 @@ class ShutdownType(Enum): ERROR = "error" +class SkillInvokedTrigger(Enum): + "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)" + # Skill invocation requested explicitly by the user, such as via a slash command or UI affordance. + USER_INVOKED = "user-invoked" + # Skill invocation requested by the agent. + AGENT_INVOKED = "agent-invoked" + # Skill content loaded as part of another context, such as a configured custom agent or subagent. + CONTEXT_LOAD = "context-load" + + class SkillSource(Enum): "Source location type (e.g., project, personal-copilot, plugin, builtin)" # Skill defined in the current project's skill directories. @@ -5116,16 +6867,6 @@ class SystemNotificationAgentCompletedStatus(Enum): FAILED = "failed" -class SystemNotificationType(Enum): - "Structured metadata identifying what triggered this notification discriminator" - AGENT_COMPLETED = "agent_completed" - AGENT_IDLE = "agent_idle" - NEW_INBOX_MESSAGE = "new_inbox_message" - SHELL_COMPLETED = "shell_completed" - SHELL_DETACHED_COMPLETED = "shell_detached_completed" - INSTRUCTION_DISCOVERED = "instruction_discovered" - - class ToolExecutionCompleteContentResourceLinkIconTheme(Enum): "Theme variant this icon is intended for" # Icon intended for light themes. @@ -5134,14 +6875,12 @@ class ToolExecutionCompleteContentResourceLinkIconTheme(Enum): DARK = "dark" -class ToolExecutionCompleteContentType(Enum): - "A content block within a tool result, which may be text, terminal output, image, audio, or a resource discriminator" - TEXT = "text" - TERMINAL = "terminal" - IMAGE = "image" - AUDIO = "audio" - RESOURCE_LINK = "resource_link" - RESOURCE = "resource" +class ToolExecutionCompleteToolDescriptionMetaUIVisibility(Enum): + "Allowed values for the `ToolExecutionCompleteToolDescriptionMetaUIVisibility` enumeration." + # Tool is callable by the model (LLM tool surface) + MODEL = "model" + # Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool + APP = "app" class UserMessageAgentMode(Enum): @@ -5166,27 +6905,6 @@ class UserMessageAttachmentGithubReferenceType(Enum): DISCUSSION = "discussion" -class UserMessageAttachmentType(Enum): - "A user message attachment — a file, directory, code selection, blob, or GitHub reference discriminator" - FILE = "file" - DIRECTORY = "directory" - SELECTION = "selection" - GITHUB_REFERENCE = "github_reference" - BLOB = "blob" - - -class UserToolSessionApprovalKind(Enum): - "The approval to add as a session-scoped rule discriminator" - COMMANDS = "commands" - READ = "read" - WRITE = "write" - MCP = "mcp" - MEMORY = "memory" - CUSTOM_TOOL = "custom-tool" - EXTENSION_MANAGEMENT = "extension-management" - EXTENSION_PERMISSION_ACCESS = "extension-permission-access" - - class WorkingDirectoryContextHostType(Enum): "Hosting platform type of the repository (github or ado)" # Repository is hosted on GitHub. @@ -5203,7 +6921,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | ModelCallFailureData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | SessionCustomNotificationData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | ModelCallFailureData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | SessionCustomNotificationData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -5310,6 +7028,9 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_MCP_SERVERS_LOADED: data = SessionMcpServersLoadedData.from_dict(data_obj) case SessionEventType.SESSION_MCP_SERVER_STATUS_CHANGED: data = SessionMcpServerStatusChangedData.from_dict(data_obj) case SessionEventType.SESSION_EXTENSIONS_LOADED: data = SessionExtensionsLoadedData.from_dict(data_obj) + case SessionEventType.SESSION_CANVAS_OPENED: data = SessionCanvasOpenedData.from_dict(data_obj) + case SessionEventType.SESSION_CANVAS_REGISTRY_CHANGED: data = SessionCanvasRegistryChangedData.from_dict(data_obj) + case SessionEventType.MCP_APP_TOOL_CALL_COMPLETE: data = McpAppToolCallCompleteData.from_dict(data_obj) case _: data = RawSessionEventData.from_dict(data_obj) return SessionEvent( data=data, diff --git a/python/copilot/session.py b/python/copilot/session.py index caf2e3020..90134a151 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -18,12 +18,14 @@ import time from collections.abc import Awaitable, Callable from dataclasses import dataclass +from datetime import UTC, datetime from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, NotRequired, Required, TypedDict, cast from ._diagnostics import log_timing from ._jsonrpc import JsonRpcError, ProcessExitedError from ._telemetry import get_trace_context, trace_context +from .canvas import CanvasHandler, OpenCanvasInstance from .generated.rpc import ( ClientSessionApiHandlers, CommandsHandlePendingCommandRequest, @@ -32,8 +34,9 @@ LogRequest, ModelSwitchToRequest, PermissionDecision, - PermissionDecisionKind, + PermissionDecisionApproveOnce, PermissionDecisionRequest, + PermissionDecisionUserNotAvailable, SessionLogLevel, SessionRpc, UIElicitationRequest, @@ -168,7 +171,7 @@ class SystemMessageReplaceConfig(TypedDict): content: str -# Known system prompt section identifiers for the "customize" mode. +# Known system message section identifiers for the "customize" mode. SectionTransformFn = Callable[[str], str | Awaitable[str]] """Transform callback: receives current section content, returns new content.""" @@ -176,7 +179,7 @@ class SystemMessageReplaceConfig(TypedDict): SectionOverrideAction = Literal["replace", "remove", "append", "prepend"] | SectionTransformFn """Override action: a string literal for static overrides, or a callback for transforms.""" -SystemPromptSection = Literal[ +SystemMessageSection = Literal[ "identity", "tone", "tool_efficiency", @@ -186,10 +189,11 @@ class SystemMessageReplaceConfig(TypedDict): "safety", "tool_instructions", "custom_instructions", + "runtime_instructions", "last_instructions", ] -SYSTEM_PROMPT_SECTIONS: dict[SystemPromptSection, str] = { +SYSTEM_MESSAGE_SECTIONS: dict[SystemMessageSection, str] = { "identity": "Agent identity preamble and mode statement", "tone": "Response style, conciseness rules, output formatting preferences", "tool_efficiency": "Tool usage patterns, parallel calling, batching guidelines", @@ -199,6 +203,11 @@ class SystemMessageReplaceConfig(TypedDict): "safety": "Environment limitations, prohibited actions, security policies", "tool_instructions": "Per-tool usage instructions", "custom_instructions": "Repository and organization custom instructions", + "runtime_instructions": ( + "Runtime-provided context and instructions" + " (e.g. system notifications, memories, workspace context," + " mode-specific instructions, content-exclusion policy)" + ), "last_instructions": ( "End-of-prompt instructions: parallel tool calling, persistence, task completion" ), @@ -206,7 +215,7 @@ class SystemMessageReplaceConfig(TypedDict): class SectionOverride(TypedDict, total=False): - """Override operation for a single system prompt section.""" + """Override operation for a single system message section.""" action: Required[SectionOverrideAction] content: NotRequired[str] @@ -219,7 +228,7 @@ class SystemMessageCustomizeConfig(TypedDict, total=False): """ mode: Required[Literal["customize"]] - sections: NotRequired[dict[SystemPromptSection, SectionOverride]] + sections: NotRequired[dict[SystemMessageSection, SectionOverride]] content: NotRequired[str] @@ -231,19 +240,27 @@ class SystemMessageCustomizeConfig(TypedDict, total=False): # Permission Types # ============================================================================ -PermissionRequestResultKind = Literal[ - "approve-once", - "reject", - "user-not-available", - "no-result", -] - @dataclass -class PermissionRequestResult: - """Result of a permission request.""" +class PermissionNoResult: + """Sentinel returned by a permission handler to leave the request unanswered. + + Only meaningful against protocol-v1 servers. v2 servers reject ``no-result`` + responses; the SDK raises :class:`ValueError` if a v2 server receives one. + Mirrors the ``{kind: "no-result"}`` extension TS adds to its ``PermissionDecision`` + union (see ``nodejs/src/types.ts:883``). + """ + + kind: Literal["no-result"] = "no-result" - kind: PermissionRequestResultKind = "user-not-available" + +# The decision returned by a permission handler. Identical shape to the wire +# ``PermissionDecision`` discriminated union, plus a :class:`PermissionNoResult` +# sentinel for v1 servers. Construct via the generated variant classes: +# ``PermissionDecisionApproveOnce()``, ``PermissionDecisionReject(feedback=...)``, +# etc. The ``kind`` discriminator is baked in as a ``ClassVar`` default by +# codegen, so callers must not pass it. +PermissionRequestResult = PermissionDecision | PermissionNoResult _PermissionHandlerFn = Callable[ @@ -257,7 +274,7 @@ class PermissionHandler: def approve_all( request: PermissionRequest, invocation: dict[str, str] ) -> PermissionRequestResult: - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() # ============================================================================ @@ -619,7 +636,7 @@ class PreToolUseHookInput(TypedDict): """Input for pre-tool-use hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str toolName: str toolArgs: Any @@ -645,7 +662,7 @@ class PreMcpToolCallHookInput(TypedDict): """Input for pre-MCP-tool-call hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str serverName: str toolName: str @@ -676,7 +693,7 @@ class PostToolUseHookInput(TypedDict): """Input for post-tool-use hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str toolName: str toolArgs: Any @@ -701,7 +718,7 @@ class UserPromptSubmittedHookInput(TypedDict): """Input for user-prompt-submitted hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str prompt: str @@ -724,7 +741,7 @@ class SessionStartHookInput(TypedDict): """Input for session-start hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str source: Literal["startup", "resume", "new"] initialPrompt: NotRequired[str] @@ -747,7 +764,7 @@ class SessionEndHookInput(TypedDict): """Input for session-end hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str reason: Literal["complete", "error", "abort", "timeout", "user_exit"] finalMessage: NotRequired[str] @@ -772,7 +789,7 @@ class ErrorOccurredHookInput(TypedDict): """Input for error-occurred hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str error: str errorContext: Literal["model_call", "tool_execution", "system", "user_input"] @@ -929,193 +946,12 @@ class ProviderConfig(TypedDict, total=False): # triggers conversation compaction before sending a request when the prompt # (system message, history, tool definitions, user message) would exceed # this limit. - max_input_tokens: int + max_prompt_tokens: int # Overrides the resolved model's default max output tokens. When hit, the # model stops generating and returns a truncated response. max_output_tokens: int -class SessionConfig(TypedDict, total=False): - """Configuration for creating a session""" - - session_id: str # Optional custom session ID - # Client name to identify the application using the SDK. - # Included in the User-Agent header for API requests. - client_name: str - model: str # Model to use for this session. Use client.list_models() to see available models. - # Reasoning effort level for models that support it. - # Only valid for models where capabilities.supports.reasoning_effort is True. - reasoning_effort: ReasoningEffort - tools: list[Tool] - system_message: SystemMessageConfig # System message configuration - # List of tool names to allow. When specified, only these tools will be available. - # Applies to the full merged tool catalog (built-in, MCP, and custom tools - # registered via tools=). Takes precedence over excluded_tools. - available_tools: list[str] - # List of tool names to disable. Applies to all tools including custom tools - # registered via tools=. Ignored if available_tools is set. - excluded_tools: list[str] - # Optional handler for permission requests from the server. When omitted, - # requests are surfaced as events and left pending for manual resolution. - on_permission_request: _PermissionHandlerFn | None - # Handler for user input requests from the agent (enables ask_user tool) - on_user_input_request: UserInputHandler - # Hook handlers for intercepting session lifecycle events - hooks: SessionHooks - # Working directory for the session. Tool operations will be relative to this directory. - working_directory: str - # Custom provider configuration (BYOK - Bring Your Own Key) - provider: ProviderConfig - # Enables or disables internal session telemetry for this session. When False, - # disables session telemetry. When omitted (the default) or True, telemetry is enabled for - # GitHub-authenticated sessions. When a custom provider (BYOK) is configured, - # session telemetry is always disabled regardless of this setting. - # This is independent of the client OpenTelemetry configuration. - enable_session_telemetry: bool - # Enable streaming of assistant message and reasoning chunks - # When True, assistant.message_delta and assistant.reasoning_delta events - # with delta_content are sent as the response is generated - streaming: bool - # Include sub-agent streaming events in the event stream. When True, streaming - # delta events from sub-agents (e.g., assistant.message_delta, - # assistant.reasoning_delta, assistant.streaming_delta with agentId set) are - # forwarded to this connection. When False, only non-streaming sub-agent events - # and subagent.* lifecycle events are forwarded; streaming deltas from sub-agents - # are suppressed. Defaults to True. - include_sub_agent_streaming_events: bool - # MCP server configurations for the session - mcp_servers: dict[str, MCPServerConfig] - # Custom agent configurations for the session - custom_agents: list[CustomAgentConfig] - # Configuration for the default agent. - # Use excluded_tools to hide tools from the default agent - # while keeping them available to sub-agents. - default_agent: DefaultAgentConfig - # Name of the custom agent to activate when the session starts. - # Must match the name of one of the agents in custom_agents. - agent: str - # Override the default configuration directory location. - # When specified, the session will use this directory for storing config and state. - config_dir: str - # Directories to load skills from - skill_directories: list[str] - # Additional directories to search for custom instruction files. - instruction_directories: list[str] - # List of skill names to disable - disabled_skills: list[str] - # Infinite session configuration for persistent workspaces and automatic compaction. - # When enabled (default), sessions automatically manage context limits and persist state. - # Set to {"enabled": False} to disable. - infinite_sessions: InfiniteSessionConfig - # Optional event handler that is registered on the session before the - # session.create RPC is issued, ensuring early events (e.g. session.start) - # are delivered. Equivalent to calling session.on(handler) immediately - # after creation, but executes earlier in the lifecycle so no events are missed. - on_event: Callable[[SessionEvent], None] - # Slash commands to register with the session. - # When the CLI has a TUI, each command appears as /name for the user to invoke. - commands: list[CommandDefinition] - # Handler for elicitation requests from the server. - # When provided, the server calls back to this client for form-based UI dialogs. - on_elicitation_request: ElicitationHandler - # Handler for exit-plan-mode requests from the server. - on_exit_plan_mode: ExitPlanModeHandler - # Handler for auto-mode-switch requests from the server. - on_auto_mode_switch: AutoModeSwitchHandler - # Handler factory for session-scoped sessionFs operations. - create_session_fs_handler: CreateSessionFsHandler - - -class ResumeSessionConfig(TypedDict, total=False): - """Configuration for resuming a session""" - - # Client name to identify the application using the SDK. - # Included in the User-Agent header for API requests. - client_name: str - # Model to use for this session. Can change the model when resuming. - model: str - tools: list[Tool] - system_message: SystemMessageConfig # System message configuration - # List of tool names to allow. When specified, only these tools will be available. - # Applies to the full merged tool catalog (built-in, MCP, and custom tools - # registered via tools=). Takes precedence over excluded_tools. - available_tools: list[str] - # List of tool names to disable. Applies to all tools including custom tools - # registered via tools=. Ignored if available_tools is set. - excluded_tools: list[str] - provider: ProviderConfig - # Enables or disables internal session telemetry for this session. When False, - # disables session telemetry. When omitted (the default) or True, telemetry is enabled for - # GitHub-authenticated sessions. When a custom provider (BYOK) is configured, - # session telemetry is always disabled regardless of this setting. - # This is independent of the client OpenTelemetry configuration. - enable_session_telemetry: bool - # Reasoning effort level for models that support it. - reasoning_effort: ReasoningEffort - # Optional handler for permission requests from the server. When omitted, - # requests are surfaced as events and left pending for manual resolution. - on_permission_request: _PermissionHandlerFn | None - # Handler for user input requestsfrom the agent (enables ask_user tool) - on_user_input_request: UserInputHandler - # Hook handlers for intercepting session lifecycle events - hooks: SessionHooks - # Working directory for the session. Tool operations will be relative to this directory. - working_directory: str - # Override the default configuration directory location. - config_dir: str - # Enable streaming of assistant message chunks - streaming: bool - # Include sub-agent streaming events in the event stream. When True, streaming - # delta events from sub-agents (e.g., assistant.message_delta, - # assistant.reasoning_delta, assistant.streaming_delta with agentId set) are - # forwarded to this connection. When False, only non-streaming sub-agent events - # and subagent.* lifecycle events are forwarded; streaming deltas from sub-agents - # are suppressed. Defaults to True. - include_sub_agent_streaming_events: bool - # MCP server configurations for the session - mcp_servers: dict[str, MCPServerConfig] - # Custom agent configurations for the session - custom_agents: list[CustomAgentConfig] - # Configuration for the default agent. - default_agent: DefaultAgentConfig - # Name of the custom agent to activate when the session starts. - # Must match the name of one of the agents in custom_agents. - agent: str - # Directories to load skills from - skill_directories: list[str] - # Additional directories to search for custom instruction files. - instruction_directories: list[str] - # List of skill names to disable - disabled_skills: list[str] - # Infinite session configuration for persistent workspaces and automatic compaction. - infinite_sessions: InfiniteSessionConfig - # When True, skips emitting the session.resume event. - # Useful for reconnecting to a session without triggering resume-related side effects. - disable_resume: bool - # When True, instructs the runtime to continue any 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. - # - # For permission requests, the runtime re-emits ``permission.requested`` so the - # registered ``on_permission_request`` handler can re-prompt; for external tool - # calls, the consumer is expected to supply the result via the corresponding - # low-level RPC method. - continue_pending_work: bool - # Optional event handler registered before the session.resume RPC is issued, - # ensuring early events are delivered. See SessionConfig.on_event. - on_event: Callable[[SessionEvent], None] - # Slash commands to register with the session. - commands: list[CommandDefinition] - # Handler for elicitation requests from the server. - on_elicitation_request: ElicitationHandler - # Handler for exit-plan-mode requests from the server. - on_exit_plan_mode: ExitPlanModeHandler - # Handler for auto-mode-switch requests from the server. - on_auto_mode_switch: AutoModeSwitchHandler - # Handler factory for session-scoped sessionFs operations. - create_session_fs_handler: CreateSessionFsHandler - - SessionEventHandler = Callable[[SessionEvent], None] @@ -1188,6 +1024,10 @@ def __init__( self._elicitation_handler_lock = threading.Lock() self._capabilities: SessionCapabilities = {} self._client_session_apis = ClientSessionApiHandlers() + self._canvas_handler: CanvasHandler | None = None + self._canvas_handler_lock = threading.Lock() + self._open_canvases: list[OpenCanvasInstance] = [] + self._open_canvases_lock = threading.Lock() self._rpc: SessionRpc | None = None self._destroyed = False @@ -1703,18 +1543,14 @@ async def _execute_permission_and_respond( ) result = cast(PermissionRequestResult, result) - if result.kind == "no-result": + if isinstance(result, PermissionNoResult): return - perm_result = PermissionDecision( - kind=PermissionDecisionKind(result.kind), - ) - rpc_start = time.perf_counter() await self.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id=request_id, - result=perm_result, + result=result, ) ) log_timing( @@ -1730,9 +1566,7 @@ async def _execute_permission_and_respond( await self.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id=request_id, - result=PermissionDecision( - kind=PermissionDecisionKind.USER_NOT_AVAILABLE, - ), + result=PermissionDecisionUserNotAvailable(), ) ) except (JsonRpcError, ProcessExitedError, OSError): @@ -1910,6 +1744,29 @@ def _register_auto_mode_switch_handler(self, handler: AutoModeSwitchHandler | No with self._auto_mode_switch_handler_lock: self._auto_mode_switch_handler = handler + def _register_canvas_handler(self, handler: CanvasHandler | None) -> None: + """Register the canvas handler for this session.""" + with self._canvas_handler_lock: + self._canvas_handler = handler + + def _get_canvas_handler(self) -> CanvasHandler | None: + with self._canvas_handler_lock: + return self._canvas_handler + + def _set_open_canvases(self, instances: list[OpenCanvasInstance]) -> None: + with self._open_canvases_lock: + self._open_canvases = list(instances) + + @property + def open_canvases(self) -> list[OpenCanvasInstance]: + """Open canvas instances reported by the most recent ``session.resume``. + + Returns an empty list for sessions created via ``session.create`` or + when the server did not include any open canvases on resume. + """ + with self._open_canvases_lock: + return list(self._open_canvases) + def _set_capabilities(self, capabilities: SessionCapabilities | None) -> None: """Set the host capabilities for this session. @@ -1995,8 +1852,8 @@ async def _handle_permission_request( handler = self._permission_handler if not handler: - # No handler registered, deny permission - return PermissionRequestResult() + # No handler registered, deny permission. + return PermissionDecisionUserNotAvailable() try: handler_start = time.perf_counter() @@ -2012,13 +1869,13 @@ async def _handle_permission_request( ) return cast(PermissionRequestResult, result) except Exception: # pylint: disable=broad-except - # Handler failed, deny permission + # Handler failed, deny permission. logger.debug( "Error handling permission request", extra={"session_id": self.session_id}, exc_info=True, ) - return PermissionRequestResult() + return PermissionDecisionUserNotAvailable() def _register_user_input_handler(self, handler: UserInputHandler | None) -> None: """ @@ -2226,9 +2083,21 @@ async def _handle_hooks_invoke(self, hook_type: str, input_data: Any) -> Any: try: handler_start = time.perf_counter() - # Remap wire key "cwd" to public API key "workingDirectory" - if "cwd" in input_data: - input_data = {**input_data, "workingDirectory": input_data.pop("cwd")} + # Normalize input from the wire format: + # - Remap wire key "cwd" to public API key "workingDirectory". + # - Convert "timestamp" from epoch milliseconds to ``datetime`` so + # hook handlers see a timezone-aware ``datetime`` rather than a + # raw integer (matches TS PR #1357 Phase E). + transformed: dict[str, Any] = dict(input_data) + if "cwd" in transformed: + transformed["workingDirectory"] = transformed.pop("cwd") + timestamp = transformed.get("timestamp") + if isinstance(timestamp, (int, float)): + transformed["timestamp"] = datetime.fromtimestamp(timestamp / 1000, tz=UTC) + # Each per-hook-type TypedDict is structurally compatible with the + # normalized dict; cast to ``Any`` so ty doesn't try to narrow the + # specific TypedDict variant from the runtime ``dict``. + input_data = cast(Any, transformed) result = handler(input_data, {"session_id": self.session_id}) if inspect.isawaitable(result): result = await result @@ -2250,7 +2119,7 @@ async def _handle_hooks_invoke(self, hook_type: str, input_data: Any) -> Any: ) return None - async def get_messages(self) -> list[SessionEvent]: + async def get_events(self) -> list[SessionEvent]: """ Retrieve all events and messages from this session's history. @@ -2265,7 +2134,7 @@ async def get_messages(self) -> list[SessionEvent]: Example: >>> from copilot.generated.session_events import AssistantMessageData - >>> events = await session.get_messages() + >>> events = await session.get_events() >>> for event in events: ... match event.data: ... case AssistantMessageData() as data: @@ -2325,26 +2194,6 @@ async def disconnect(self) -> None: with self._auto_mode_switch_handler_lock: self._auto_mode_switch_handler = None - async def destroy(self) -> None: - """ - .. deprecated:: - Use :meth:`disconnect` instead. This method will be removed in a future release. - - Disconnect this session and release all in-memory resources. - Session data on disk is preserved for later resumption. - - Raises: - Exception: If the connection fails. - """ - import warnings - - warnings.warn( - "destroy() is deprecated, use disconnect() instead", - DeprecationWarning, - stacklevel=2, - ) - await self.disconnect() - async def __aenter__(self) -> CopilotSession: """Enable use as an async context manager.""" return self diff --git a/python/copilot/tools.py b/python/copilot/tools.py index 3f8eb9c1b..c6a29dc61 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -24,7 +24,7 @@ class ToolBinaryResult: data: str = "" mime_type: str = "" - type: str = "" + type: Literal["image", "resource"] = "image" description: str = "" diff --git a/python/e2e/test_agent_and_compact_rpc_e2e.py b/python/e2e/test_agent_and_compact_rpc_e2e.py index f4773a798..14ea01ff2 100644 --- a/python/e2e/test_agent_and_compact_rpc_e2e.py +++ b/python/e2e/test_agent_and_compact_rpc_e2e.py @@ -4,8 +4,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.rpc import AgentSelectRequest from copilot.session import PermissionHandler @@ -18,7 +17,7 @@ class TestAgentSelectionRpc: @pytest.mark.asyncio async def test_should_list_available_custom_agents(self): """Test listing available custom agents via RPC.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -56,7 +55,7 @@ async def test_should_list_available_custom_agents(self): @pytest.mark.asyncio async def test_should_return_null_when_no_agent_is_selected(self): """Test getCurrent returns null when no agent is selected.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -83,7 +82,7 @@ async def test_should_return_null_when_no_agent_is_selected(self): @pytest.mark.asyncio async def test_should_select_and_get_current_agent(self): """Test selecting an agent and verifying getCurrent returns it.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -118,7 +117,7 @@ async def test_should_select_and_get_current_agent(self): @pytest.mark.asyncio async def test_should_deselect_current_agent(self): """Test deselecting the current agent.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -150,7 +149,7 @@ async def test_should_deselect_current_agent(self): @pytest.mark.asyncio async def test_should_return_empty_list_when_no_custom_agents_configured(self): """Test listing agents returns no custom agents when none configured.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -175,7 +174,7 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): @pytest.mark.asyncio async def test_should_call_agent_reload(self): """Test reloading agents via RPC.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) reload_agent = { "name": f"reload-test-agent-{uuid.uuid4().hex}", "display_name": "Reload Agent", diff --git a/python/e2e/test_client_api_e2e.py b/python/e2e/test_client_api_e2e.py index 1699bb8cf..217352817 100644 --- a/python/e2e/test_client_api_e2e.py +++ b/python/e2e/test_client_api_e2e.py @@ -44,6 +44,13 @@ async def test_should_get_null_last_session_id_before_any_sessions_exist( self, ctx: E2ETestContext ): await ctx.client.start() + + # Other tests in this class create sessions, and pytest doesn't + # guarantee test execution order. Clear any leftover sessions so this + # test sees a genuinely empty state regardless of order. + for existing in await ctx.client.list_sessions(): + await ctx.client.delete_session(existing.session_id) + result = await ctx.client.get_last_session_id() assert result is None diff --git a/python/e2e/test_client_e2e.py b/python/e2e/test_client_e2e.py index fc7315a58..1e8ea82e5 100644 --- a/python/e2e/test_client_e2e.py +++ b/python/e2e/test_client_e2e.py @@ -2,14 +2,13 @@ import pytest -from copilot import CopilotClient +from copilot import CopilotClient, RuntimeConnection from copilot.client import ( ModelCapabilities, ModelInfo, ModelLimits, ModelSupports, StopError, - SubprocessConfig, ) from copilot.session import PermissionHandler @@ -19,35 +18,31 @@ class TestClient: @pytest.mark.asyncio async def test_should_start_and_connect_to_server_using_stdio(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() - assert client.get_state() == "connected" pong = await client.ping("test message") assert pong.message == "pong: test message" assert pong.timestamp is not None await client.stop() - assert client.get_state() == "disconnected" finally: await client.force_stop() @pytest.mark.asyncio async def test_should_start_and_connect_to_server_using_tcp(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=False)) + client = CopilotClient(connection=RuntimeConnection.for_tcp(path=CLI_PATH)) try: await client.start() - assert client.get_state() == "connected" pong = await client.ping("test message") assert pong.message == "pong: test message" assert pong.timestamp is not None await client.stop() - assert client.get_state() == "disconnected" finally: await client.force_stop() @@ -55,7 +50,7 @@ async def test_should_start_and_connect_to_server_using_tcp(self): async def test_should_raise_exception_group_on_failed_cleanup(self): import asyncio - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.create_session(on_permission_request=PermissionHandler.approve_all) @@ -72,22 +67,19 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): assert len(exc.exceptions) > 0 assert isinstance(exc.exceptions[0], StopError) assert "Failed to disconnect session" in exc.exceptions[0].message - else: - assert client.get_state() == "disconnected" finally: await client.force_stop() @pytest.mark.asyncio async def test_should_force_stop_without_cleanup(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.create_session(on_permission_request=PermissionHandler.approve_all) await client.force_stop() - assert client.get_state() == "disconnected" @pytest.mark.asyncio async def test_should_get_status_with_version_and_protocol_info(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -95,9 +87,9 @@ async def test_should_get_status_with_version_and_protocol_info(self): status = await client.get_status() assert hasattr(status, "version") assert isinstance(status.version, str) - assert hasattr(status, "protocolVersion") - assert isinstance(status.protocolVersion, int) - assert status.protocolVersion >= 1 + assert hasattr(status, "protocol_version") + assert isinstance(status.protocol_version, int) + assert status.protocol_version >= 1 await client.stop() finally: @@ -105,7 +97,7 @@ async def test_should_get_status_with_version_and_protocol_info(self): @pytest.mark.asyncio async def test_should_get_auth_status(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -123,7 +115,7 @@ async def test_should_get_auth_status(self): @pytest.mark.asyncio async def test_should_list_models_when_authenticated(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -151,7 +143,7 @@ async def test_should_list_models_when_authenticated(self): @pytest.mark.asyncio async def test_should_cache_models_list(self): """Test that list_models caches results to avoid rate limiting""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -196,10 +188,8 @@ async def test_should_cache_models_list(self): async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): """Test that CLI startup errors include stderr output in the error message.""" client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - cli_args=["--nonexistent-flag-for-testing"], - use_stdio=True, + connection=RuntimeConnection.for_stdio( + path=CLI_PATH, args=["--nonexistent-flag-for-testing"] ) ) @@ -231,7 +221,7 @@ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): @pytest.mark.asyncio async def test_should_not_throw_when_disposing_session_after_stopping_client(self): """Disconnecting a session after the client is stopped must not raise.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -250,7 +240,7 @@ async def test_should_not_throw_when_disposing_session_after_stopping_client(sel @pytest.mark.asyncio async def test_should_create_session_without_permission_handler(self): """`create_session` allows omitting an `on_permission_request` handler.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -265,7 +255,7 @@ async def test_should_create_session_without_permission_handler(self): @pytest.mark.asyncio async def test_should_resume_session_without_permission_handler(self): """`resume_session` allows omitting an `on_permission_request` handler.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -300,7 +290,7 @@ def on_list_models(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH, use_stdio=True), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=on_list_models, ) @@ -338,7 +328,7 @@ def on_list_models(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH, use_stdio=True), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=on_list_models, ) diff --git a/python/e2e/test_client_lifecycle_e2e.py b/python/e2e/test_client_lifecycle_e2e.py index 90b96d822..d5a2fb681 100644 --- a/python/e2e/test_client_lifecycle_e2e.py +++ b/python/e2e/test_client_lifecycle_e2e.py @@ -1,5 +1,5 @@ """ -Client lifecycle tests covering ``client.on(...)`` lifecycle event subscriptions +Client lifecycle tests covering ``client.on_lifecycle(...)`` lifecycle event subscriptions and connection-state transitions across ``start``/``stop``. Mirrors ``dotnet/test/ClientLifecycleTests.cs`` plus the existing ``client_lifecycle`` @@ -14,8 +14,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness import E2ETestContext @@ -60,12 +59,10 @@ def _make_isolated_client(ctx: E2ETestContext) -> CopilotClient: "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) return CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) @@ -84,7 +81,7 @@ async def test_should_return_last_session_id_after_sending_a_message(self, ctx: async def test_should_emit_session_lifecycle_events(self, ctx: E2ETestContext): events: list = [] - unsubscribe = ctx.client.on(events.append) + unsubscribe = ctx.client.on_lifecycle(events.append) try: session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -94,7 +91,7 @@ async def test_should_emit_session_lifecycle_events(self, ctx: E2ETestContext): await _wait_for_condition( lambda: any( - getattr(e, "sessionId", None) == session.session_id for e in events + getattr(e, "session_id", None) == session.session_id for e in events ), timeout=10.0, ) @@ -111,7 +108,7 @@ def handler(event): if event.type == "session.created" and not created.done(): created.set_result(event) - unsubscribe = ctx.client.on(handler) + unsubscribe = ctx.client.on_lifecycle(handler) try: session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -119,7 +116,7 @@ def handler(event): try: event = await asyncio.wait_for(created, 10.0) assert event.type == "session.created" - assert event.sessionId == session.session_id + assert event.session_id == session.session_id finally: await session.disconnect() finally: @@ -133,7 +130,7 @@ def handler(event): if not created.done(): created.set_result(event) - unsubscribe = ctx.client.on("session.created", handler) + unsubscribe = ctx.client.on_lifecycle("session.created", handler) try: session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -141,7 +138,7 @@ def handler(event): try: event = await asyncio.wait_for(created, 10.0) assert event.type == "session.created" - assert event.sessionId == session.session_id + assert event.session_id == session.session_id finally: await session.disconnect() finally: @@ -157,11 +154,11 @@ def disposed_handler(_event): nonlocal unsubscribed_count unsubscribed_count += 1 - unsubscribe_disposed = ctx.client.on(disposed_handler) + unsubscribe_disposed = ctx.client.on_lifecycle(disposed_handler) unsubscribe_disposed() # Immediately dispose first subscription. active_event: asyncio.Future = loop.create_future() - unsubscribe_active = ctx.client.on( + unsubscribe_active = ctx.client.on_lifecycle( "session.created", lambda evt: active_event.set_result(evt) if not active_event.done() else None, ) @@ -171,7 +168,7 @@ def disposed_handler(_event): ) try: event = await asyncio.wait_for(active_event, 10.0) - assert event.sessionId == session.session_id + assert event.session_id == session.session_id assert unsubscribed_count == 0, "Disposed handler should not have fired" finally: await session.disconnect() @@ -181,12 +178,7 @@ def disposed_handler(_event): async def test_stop_disconnects_client_and_disposes_rpc_surface(self, ctx: E2ETestContext): client = _make_isolated_client(ctx) await client.start() - try: - assert client.get_state() == "connected" - finally: - await client.stop() - - assert client.get_state() == "disconnected" + await client.stop() with pytest.raises(RuntimeError): _ = client.rpc @@ -207,17 +199,17 @@ async def test_should_receive_session_updated_lifecycle_event_for_non_ephemeral_ def handler(event): if ( event.type == "session.updated" - and event.sessionId == session.session_id + and event.session_id == session.session_id and not updated.done() ): updated.set_result(event) - unsubscribe = ctx.client.on(handler) + unsubscribe = ctx.client.on_lifecycle(handler) try: await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN)) event = await asyncio.wait_for(updated, timeout=15.0) assert event.type == "session.updated" - assert event.sessionId == session.session_id + assert event.session_id == session.session_id finally: unsubscribe() await session.disconnect() @@ -242,18 +234,18 @@ async def test_should_receive_session_deleted_lifecycle_event_when_deleted( def handler(event): if ( event.type == "session.deleted" - and event.sessionId == session_id + and event.session_id == session_id and not deleted.done() ): deleted.set_result(event) - unsubscribe = ctx.client.on(handler) + unsubscribe = ctx.client.on_lifecycle(handler) try: await session.disconnect() await ctx.client.delete_session(session_id) event = await asyncio.wait_for(deleted, timeout=15.0) assert event.type == "session.deleted" - assert event.sessionId == session_id + assert event.session_id == session_id finally: unsubscribe() diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py index 80a3bf394..614aec5df 100644 --- a/python/e2e/test_client_options_e2e.py +++ b/python/e2e/test_client_options_e2e.py @@ -1,14 +1,15 @@ """ E2E coverage for ``CopilotClient`` configuration options exposed via -``SubprocessConfig`` and ``CopilotClient(..., auto_start=...)``. +``CopilotClientOptions`` and ``RuntimeConnection``. Mirrors ``dotnet/test/ClientOptionsTests.cs``. The two CliUrl-conflict tests (``Should_Throw_When_GitHubToken_Used_With_CliUrl`` and ``Should_Throw_When_UseLoggedInUser_Used_With_CliUrl``) have no Python -equivalent because Python's ``ExternalServerConfig`` does not accept -``github_token`` / ``use_logged_in_user`` fields at all (so the conflict cannot -be expressed in code), and the configurations are therefore intentionally -omitted. +equivalent because Python's ``RuntimeConnection.for_uri(...)`` does not accept +``github_token`` / ``use_logged_in_user`` fields at all (those live on +``CopilotClientOptions``, but a Uri-connected runtime ignores them), so the +conflict cannot be expressed in code and the configurations are therefore +intentionally omitted. """ from __future__ import annotations @@ -19,8 +20,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.rpc import PingRequest from copilot.session import PermissionHandler @@ -29,9 +29,31 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") -def _make_subprocess_config(ctx: E2ETestContext, **overrides) -> SubprocessConfig: - base = { - "cli_path": ctx.cli_path, +def _make_options( + ctx: E2ETestContext, + *, + use_tcp: bool = False, + port: int = 0, + connection_token: str | None = None, + cli_path: str | None = None, + cli_args: list[str] | None = None, + **overrides, +) -> dict[str, object]: + """Build CopilotClient kwargs pre-populated for the test harness.""" + if use_tcp: + connection: RuntimeConnection = RuntimeConnection.for_tcp( + port=port, + connection_token=connection_token, + path=cli_path if cli_path is not None else ctx.cli_path, + args=tuple(cli_args or []), + ) + else: + connection = RuntimeConnection.for_stdio( + path=cli_path if cli_path is not None else ctx.cli_path, + args=tuple(cli_args or []), + ) + base: dict[str, object] = { + "connection": connection, "working_directory": ctx.work_dir, "env": ctx.get_env(), "github_token": ( @@ -39,7 +61,7 @@ def _make_subprocess_config(ctx: E2ETestContext, **overrides) -> SubprocessConfi ), } base.update(overrides) - return SubprocessConfig(**base) + return base def _get_available_port() -> int: @@ -120,7 +142,7 @@ def _get_available_port() -> int: return; } if (message.method === "session.create") { - const sessionId = message.params?.sessionId ?? "fake-session"; + const sessionId = message.params?.session_id ?? "fake-session"; writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null }); return; } @@ -142,39 +164,12 @@ def _assert_arg_value(args: list[str], name: str, expected_value: str) -> None: class TestClientOptions: - async def test_autostart_false_requires_explicit_start(self, ctx: E2ETestContext): - client = CopilotClient(_make_subprocess_config(ctx), auto_start=False) - try: - assert client.get_state() == "disconnected" - - with pytest.raises(RuntimeError) as exc_info: - await client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - # Python raises "Client not connected" — equivalent intent to C#'s "StartAsync". - assert ( - "not connected" in str(exc_info.value).lower() - or "start" in str(exc_info.value).lower() - ) - - await client.start() - assert client.get_state() == "connected" - - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - assert session.session_id - await session.disconnect() - finally: - await client.stop() - async def test_should_listen_on_configured_tcp_port(self, ctx: E2ETestContext): port = _get_available_port() - client = CopilotClient(_make_subprocess_config(ctx, use_stdio=False, port=port)) + client = CopilotClient(**_make_options(ctx, use_tcp=True, port=port)) try: await client.start() - assert client.get_state() == "connected" - assert client.actual_port == port + assert client.runtime_port == port response = await client.rpc.ping(PingRequest(message="fixed-port")) assert "pong" in response.message @@ -187,7 +182,7 @@ async def test_should_use_client_cwd_for_default_workingdirectory(self, ctx: E2E with open(os.path.join(client_cwd, "marker.txt"), "w") as f: f.write("I am in the client cwd") - client = CopilotClient(_make_subprocess_config(ctx, working_directory=client_cwd)) + client = CopilotClient(**_make_options(ctx, working_directory=client_cwd)) try: session = await client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -212,10 +207,10 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes f.write(FAKE_STDIO_CLI_SCRIPT) client = CopilotClient( - _make_subprocess_config( + **_make_options( ctx, cli_path=cli_path, - copilot_home=copilot_home_from_option, + base_directory=copilot_home_from_option, cli_args=["--capture-file", capture_path], env={**ctx.get_env(), "COPILOT_HOME": copilot_home_from_env}, github_token="process-option-token", @@ -230,7 +225,6 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes }, use_logged_in_user=False, ), - auto_start=False, ) try: await client.start() @@ -278,51 +272,3 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes await client.stop() except Exception: await client.force_stop() - - -# --------------------------------------------------------------------------- -# Unit-style tests mirroring the property-only tests in -# dotnet/test/ClientOptionsTests.cs. These exercise the SubprocessConfig -# dataclass shape only — no client / proxy required. -# --------------------------------------------------------------------------- - - -class TestSubprocessConfigOptions: - """Mirrors the unit-style ClientOptions tests in the C# baseline.""" - - async def test_should_accept_github_token_option(self): - # Mirrors: Should_Accept_GitHubToken_Option - config = SubprocessConfig(github_token="gho_test_token") - assert config.github_token == "gho_test_token" - - async def test_should_default_use_logged_in_user_to_none(self): - # Mirrors: Should_Default_UseLoggedInUser_To_Null - config = SubprocessConfig() - assert config.use_logged_in_user is None - - async def test_should_allow_explicit_use_logged_in_user_false(self): - # Mirrors: Should_Allow_Explicit_UseLoggedInUser_False - config = SubprocessConfig(use_logged_in_user=False) - assert config.use_logged_in_user is False - - async def test_should_allow_explicit_use_logged_in_user_true_with_github_token(self): - # Mirrors: Should_Allow_Explicit_UseLoggedInUser_True_With_GitHubToken - config = SubprocessConfig(github_token="gho_test_token", use_logged_in_user=True) - assert config.use_logged_in_user is True - assert config.github_token == "gho_test_token" - - # NOTE: Should_Throw_When_GitHubToken_Used_With_CliUrl and - # Should_Throw_When_UseLoggedInUser_Used_With_CliUrl from the C# baseline - # do not apply to Python: ExternalServerConfig has no github_token / - # use_logged_in_user fields at all (they live only on SubprocessConfig), - # so the conflicting configuration is impossible to express. - - async def test_should_default_session_idle_timeout_seconds_to_none(self): - # Mirrors: Should_Default_SessionIdleTimeoutSeconds_To_Null - config = SubprocessConfig() - assert config.session_idle_timeout_seconds is None - - async def test_should_accept_session_idle_timeout_seconds_option(self): - # Mirrors: Should_Accept_SessionIdleTimeoutSeconds_Option - config = SubprocessConfig(session_idle_timeout_seconds=600) - assert config.session_idle_timeout_seconds == 600 diff --git a/python/e2e/test_commands_e2e.py b/python/e2e/test_commands_e2e.py index 5bf1a274e..e0a0d63f1 100644 --- a/python/e2e/test_commands_e2e.py +++ b/python/e2e/test_commands_e2e.py @@ -15,8 +15,7 @@ import pytest import pytest_asyncio -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import CommandDefinition, PermissionHandler from .testharness.context import SNAPSHOTS_DIR, get_cli_path_for_tests @@ -26,7 +25,7 @@ # --------------------------------------------------------------------------- -# Multi-client context (TCP mode) — same pattern as test_multi_client.py +# Multi-client context (TCP mode) — same pattern as test_multi_client.py # --------------------------------------------------------------------------- @@ -56,14 +55,12 @@ async def setup(self): # Client 1 uses TCP mode so a second client can connect self._client1 = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - working_directory=self.work_dir, - env=self._get_env(), - use_stdio=False, - github_token=github_token, - tcp_connection_token="py-tcp-shared-test-token", - ) + connection=RuntimeConnection.for_tcp( + path=self.cli_path, connection_token="py-tcp-shared-test-token" + ), + working_directory=self.work_dir, + env=self._get_env(), + github_token=github_token, ) # Trigger connection to get the port @@ -72,12 +69,12 @@ async def setup(self): ) await init_session.disconnect() - actual_port = self._client1.actual_port + actual_port = self._client1.runtime_port assert actual_port is not None self._client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + f"localhost:{actual_port}", connection_token="py-tcp-shared-test-token" ) ) diff --git a/python/e2e/test_connection_token.py b/python/e2e/test_connection_token.py index 195baaecc..1c7addbd9 100644 --- a/python/e2e/test_connection_token.py +++ b/python/e2e/test_connection_token.py @@ -11,8 +11,7 @@ import pytest import pytest_asyncio -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness.proxy import CapiProxy @@ -47,14 +46,10 @@ async def setup(self): ) self._client = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - working_directory=self.work_dir, - env=self.get_env(), - use_stdio=False, - tcp_connection_token=self.token, - github_token=github_token, - ) + connection=RuntimeConnection.for_tcp(path=self.cli_path, connection_token=self.token), + working_directory=self.work_dir, + env=self.get_env(), + github_token=github_token, ) # Trigger the spawn + connect handshake so the server is listening. @@ -133,11 +128,11 @@ async def test_auto_generated_token_round_trips(self, auto_token_ctx: Connection async def test_wrong_token_is_rejected(self, explicit_token_ctx: ConnectionTokenContext): """A sibling client connecting with the wrong token is rejected.""" - port = explicit_token_ctx.client.actual_port + port = explicit_token_ctx.client.runtime_port assert port is not None wrong = CopilotClient( - ExternalServerConfig(url=f"localhost:{port}", tcp_connection_token="wrong") + connection=RuntimeConnection.for_uri(f"localhost:{port}", connection_token="wrong") ) try: with pytest.raises(Exception, match="AUTHENTICATION_FAILED"): @@ -152,10 +147,10 @@ async def test_wrong_token_is_rejected(self, explicit_token_ctx: ConnectionToken async def test_missing_token_is_rejected(self, explicit_token_ctx: ConnectionTokenContext): """A sibling client with no token is rejected when the server requires one.""" - port = explicit_token_ctx.client.actual_port + port = explicit_token_ctx.client.runtime_port assert port is not None - no_token = CopilotClient(ExternalServerConfig(url=f"localhost:{port}")) + no_token = CopilotClient(connection=RuntimeConnection.for_uri(f"localhost:{port}")) try: with pytest.raises(Exception, match="AUTHENTICATION_FAILED"): await no_token.start() diff --git a/python/e2e/test_error_resilience_e2e.py b/python/e2e/test_error_resilience_e2e.py index 4afb78a6e..ab031842c 100644 --- a/python/e2e/test_error_resilience_e2e.py +++ b/python/e2e/test_error_resilience_e2e.py @@ -30,7 +30,7 @@ async def test_should_throw_when_getting_messages_from_disconnected_session( await session.disconnect() with pytest.raises(Exception): - await session.get_messages() + await session.get_events() async def test_should_handle_double_abort_without_error(self, ctx: E2ETestContext): session = await ctx.client.create_session( diff --git a/python/e2e/test_event_fidelity_e2e.py b/python/e2e/test_event_fidelity_e2e.py index 17193a308..b85609640 100644 --- a/python/e2e/test_event_fidelity_e2e.py +++ b/python/e2e/test_event_fidelity_e2e.py @@ -207,7 +207,7 @@ async def test_should_preserve_message_order_in_getmessages_after_tool_use( try: await session.send_and_wait("Read the file 'order.txt' and tell me what the number is.") - messages = await session.get_messages() + messages = await session.get_events() types = [m.type.value for m in messages] # Verify complete event ordering contract: diff --git a/python/e2e/test_mcp_and_agents_e2e.py b/python/e2e/test_mcp_and_agents_e2e.py index 5033524f2..be017a1e5 100644 --- a/python/e2e/test_mcp_and_agents_e2e.py +++ b/python/e2e/test_mcp_and_agents_e2e.py @@ -2,10 +2,13 @@ Tests for MCP servers and custom agents functionality """ +import asyncio +import time from pathlib import Path import pytest +from copilot.generated.rpc import McpServerStatus from copilot.session import CustomAgentConfig, MCPServerConfig, PermissionHandler from .testharness import E2ETestContext @@ -18,24 +21,50 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") +def _test_mcp_servers(*server_names: str) -> dict[str, MCPServerConfig]: + return { + server_name: { + "command": "node", + "args": [TEST_MCP_SERVER], + "tools": ["*"], + "working_directory": TEST_HARNESS_DIR, + } + for server_name in server_names + } + + +async def _wait_for_mcp_server_status( + session, server_name: str, expected_status: McpServerStatus = McpServerStatus.CONNECTED +) -> None: + deadline = time.monotonic() + 60 + last_status = "" + + while time.monotonic() < deadline: + result = await session.rpc.mcp.list() + server = next((s for s in result.servers if s.name == server_name), None) + if server is not None and server.status == expected_status: + return + last_status = server.status if server is not None else "" + await asyncio.sleep(0.2) + + raise AssertionError( + f"{server_name} did not reach {expected_status.value}; last status was {last_status}" + ) + + class TestMCPServers: async def test_should_accept_mcp_server_configuration_on_session_create( self, ctx: E2ETestContext ): """Test that MCP server configuration is accepted on session create""" - mcp_servers: dict[str, MCPServerConfig] = { - "test-server": { - "command": "echo", - "args": ["hello"], - "tools": ["*"], - } - } + mcp_servers = _test_mcp_servers("test-server") session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers ) assert session.session_id is not None + await _wait_for_mcp_server_status(session, "test-server") # Simple interaction to verify session works message = await session.send_and_wait("What is 2+2?") @@ -48,7 +77,7 @@ async def test_should_accept_mcp_server_configuration_without_args(self, ctx: E2 """Test that MCP server configuration works without args field""" mcp_servers: dict[str, MCPServerConfig] = { "test-server": { - "command": "echo", + "command": "git", "tools": ["*"], } } @@ -59,10 +88,6 @@ async def test_should_accept_mcp_server_configuration_without_args(self, ctx: E2 assert session.session_id is not None - message = await session.send_and_wait("What is 2+2?") - assert message is not None - assert "4" in message.data.content - await session.disconnect() async def test_should_accept_mcp_server_configuration_on_session_resume( @@ -77,13 +102,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( await session1.send_and_wait("What is 1+1?") # Resume with MCP servers - mcp_servers: dict[str, MCPServerConfig] = { - "test-server": { - "command": "echo", - "args": ["hello"], - "tools": ["*"], - } - } + mcp_servers = _test_mcp_servers("test-server") session2 = await ctx.client.resume_session( session_id, @@ -92,10 +111,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( ) assert session2.session_id == session_id - - message = await session2.send_and_wait("What is 3+3?") - assert message is not None - assert "6" in message.data.content + await _wait_for_mcp_server_status(session2, "test-server") await session2.disconnect() @@ -118,6 +134,7 @@ async def test_should_pass_literal_env_values_to_mcp_server_subprocess( ) assert session.session_id is not None + await _wait_for_mcp_server_status(session, "env-echo") message = await session.send_and_wait( "Use the env-echo/get_env tool to read the TEST_SECRET " @@ -194,10 +211,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( async def test_should_handle_multiple_mcp_servers(self, ctx: E2ETestContext): """Multiple MCP servers can be configured at once.""" - mcp_servers: dict[str, MCPServerConfig] = { - "server1": {"command": "echo", "args": ["server1"], "tools": ["*"]}, - "server2": {"command": "echo", "args": ["server2"], "tools": ["*"]}, - } + mcp_servers = _test_mcp_servers("server1", "server2") session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -205,6 +219,8 @@ async def test_should_handle_multiple_mcp_servers(self, ctx: E2ETestContext): ) try: assert session.session_id is not None + await _wait_for_mcp_server_status(session, "server1") + await _wait_for_mcp_server_status(session, "server2") import re assert re.match(r"^[a-f0-9-]+$", session.session_id) @@ -215,13 +231,7 @@ async def test_should_handle_multiple_mcp_servers(self, ctx: E2ETestContext): class TestCombinedConfiguration: async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETestContext): """Test that both MCP servers and custom agents can be configured together""" - mcp_servers: dict[str, MCPServerConfig] = { - "shared-server": { - "command": "echo", - "args": ["shared"], - "tools": ["*"], - } - } + mcp_servers = _test_mcp_servers("shared-server") custom_agents: list[CustomAgentConfig] = [ { @@ -239,6 +249,7 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe ) assert session.session_id is not None + await _wait_for_mcp_server_status(session, "shared-server") await session.disconnect() @@ -275,13 +286,7 @@ async def test_should_handle_custom_agent_with_mcp_servers(self, ctx: E2ETestCon "display_name": "MCP Agent", "description": "An agent with its own MCP servers", "prompt": "You are an agent with MCP servers.", - "mcp_servers": { - "agent-server": { - "command": "echo", - "args": ["agent-mcp"], - "tools": ["*"], - } - }, + "mcp_servers": _test_mcp_servers("agent-server"), } ] diff --git a/python/e2e/test_mode_handlers_e2e.py b/python/e2e/test_mode_handlers_e2e.py index c0e19da13..d9917e9f3 100644 --- a/python/e2e/test_mode_handlers_e2e.py +++ b/python/e2e/test_mode_handlers_e2e.py @@ -35,7 +35,7 @@ async def mode_ctx(ctx: E2ETestContext): """Configure per-token user responses for mode-handler tests.""" proxy_url = ctx.proxy_url - ctx.client._config.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url + ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url await ctx.set_copilot_user_by_token( MODE_HANDLER_TOKEN, @@ -75,7 +75,7 @@ async def test_should_invoke_exit_plan_mode_handler_when_model_uses_tool( ): exit_plan_mode_requests = [] - async def on_exit_plan_mode(request, invocation): + async def on_exit_plan_mode_request(request, invocation): exit_plan_mode_requests.append(request) assert invocation["session_id"] == session.session_id return { @@ -87,7 +87,7 @@ async def on_exit_plan_mode(request, invocation): session = await mode_ctx.client.create_session( github_token=MODE_HANDLER_TOKEN, on_permission_request=PermissionHandler.approve_all, - on_exit_plan_mode=on_exit_plan_mode, + on_exit_plan_mode_request=on_exit_plan_mode_request, ) try: @@ -139,7 +139,7 @@ async def test_should_invoke_auto_mode_switch_handler_when_rate_limited( ): auto_mode_switch_requests = [] - async def on_auto_mode_switch(request, invocation): + async def on_auto_mode_switch_request(request, invocation): auto_mode_switch_requests.append(request) assert invocation["session_id"] == session.session_id return "yes" @@ -147,7 +147,7 @@ async def on_auto_mode_switch(request, invocation): session = await mode_ctx.client.create_session( github_token=MODE_HANDLER_TOKEN, on_permission_request=PermissionHandler.approve_all, - on_auto_mode_switch=on_auto_mode_switch, + on_auto_mode_switch_request=on_auto_mode_switch_request, ) try: diff --git a/python/e2e/test_multi_client_e2e.py b/python/e2e/test_multi_client_e2e.py index 06f671e94..deadbfc86 100644 --- a/python/e2e/test_multi_client_e2e.py +++ b/python/e2e/test_multi_client_e2e.py @@ -14,9 +14,9 @@ import pytest_asyncio from pydantic import BaseModel, Field -from copilot import CopilotClient, define_tool -from copilot.client import ExternalServerConfig, SubprocessConfig -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot import CopilotClient, RuntimeConnection, define_tool +from copilot.generated.rpc import PermissionDecisionApproveOnce, PermissionDecisionReject +from copilot.session import PermissionHandler, PermissionNoResult from copilot.tools import ToolInvocation from .testharness import get_final_assistant_message @@ -53,14 +53,12 @@ async def setup(self): # Client 1 uses TCP mode so a second client can connect to the same server self._client1 = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - working_directory=self.work_dir, - env=self.get_env(), - use_stdio=False, - github_token=github_token, - tcp_connection_token="py-tcp-shared-test-token", - ) + connection=RuntimeConnection.for_tcp( + path=self.cli_path, connection_token="py-tcp-shared-test-token" + ), + working_directory=self.work_dir, + env=self.get_env(), + github_token=github_token, ) # Trigger connection by creating and disconnecting an init session @@ -70,12 +68,12 @@ async def setup(self): await init_session.disconnect() # Read the actual port from client 1 and create client 2 - actual_port = self._client1.actual_port + actual_port = self._client1.runtime_port assert actual_port is not None, "Client 1 should have an actual port after connecting" self._client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + f"localhost:{actual_port}", connection_token="py-tcp-shared-test-token" ) ) @@ -204,7 +202,7 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str: on_permission_request=PermissionHandler.approve_all, tools=[magic_number] ) - # Client 2 resumes with NO tools — should not overwrite client 1's tools + # Client 2 resumes with NO tools — should not overwrite client 1's tools session2 = await mctx.client2.resume_session( session1.session_id, on_permission_request=PermissionHandler.approve_all ) @@ -242,16 +240,14 @@ async def test_one_client_approves_permission_and_both_see_the_result( # Client 1 creates a session and manually approves permission requests session1 = await mctx.client1.create_session( on_permission_request=lambda request, invocation: ( - permission_requests.append(request) or PermissionRequestResult(kind="approve-once") + permission_requests.append(request) or PermissionDecisionApproveOnce() ), ) # Client 2 observes the permission request but leaves the decision to client 1. session2 = await mctx.client2.resume_session( session1.session_id, - on_permission_request=lambda request, invocation: PermissionRequestResult( - kind="no-result" - ), + on_permission_request=lambda request, invocation: PermissionNoResult(), ) client1_events = [] @@ -279,7 +275,7 @@ async def test_one_client_approves_permission_and_both_see_the_result( assert len(c1_perm_completed) > 0 assert len(c2_perm_completed) > 0 for event in c1_perm_completed + c2_perm_completed: - assert event.data.result.kind.value == "approved" + assert event.data.result.kind == "approved" await session2.disconnect() @@ -289,17 +285,13 @@ async def test_one_client_rejects_permission_and_both_see_the_result( """One client rejects a permission request and both see the result.""" # Client 1 creates a session and denies all permission requests session1 = await mctx.client1.create_session( - on_permission_request=lambda request, invocation: PermissionRequestResult( - kind="reject" - ), + on_permission_request=lambda request, invocation: PermissionDecisionReject(), ) # Client 2 observes the permission request but leaves the decision to client 1. session2 = await mctx.client2.resume_session( session1.session_id, - on_permission_request=lambda request, invocation: PermissionRequestResult( - kind="no-result" - ), + on_permission_request=lambda request, invocation: PermissionNoResult(), ) client1_events = [] @@ -332,7 +324,7 @@ async def test_one_client_rejects_permission_and_both_see_the_result( assert len(c1_perm_completed) > 0 assert len(c2_perm_completed) > 0 for event in c1_perm_completed + c2_perm_completed: - assert event.data.result.kind.value == "denied-interactively-by-user" + assert event.data.result.kind == "denied-interactively-by-user" await session2.disconnect() @@ -431,10 +423,10 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: await asyncio.sleep(0.5) # Recreate client2 for future tests (but don't rejoin the session) - actual_port = mctx.client1.actual_port + actual_port = mctx.client1.runtime_port mctx._client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + f"localhost:{actual_port}", connection_token="py-tcp-shared-test-token" ) ) diff --git a/python/e2e/test_pending_work_resume_e2e.py b/python/e2e/test_pending_work_resume_e2e.py index be0e4feec..4b1dfbff8 100644 --- a/python/e2e/test_pending_work_resume_e2e.py +++ b/python/e2e/test_pending_work_resume_e2e.py @@ -16,10 +16,13 @@ import pytest -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig -from copilot.generated.rpc import HandlePendingToolCallRequest, PermissionDecisionRequest -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot import CopilotClient, RuntimeConnection +from copilot.generated.rpc import ( + HandlePendingToolCallRequest, + PermissionDecisionRequest, + PermissionDecisionUserNotAvailable, +) +from copilot.session import PermissionHandler from copilot.tools import Tool, ToolInvocation, ToolResult from .testharness import E2ETestContext, get_final_assistant_message @@ -33,15 +36,17 @@ def _make_subprocess_client(ctx: E2ETestContext, *, use_stdio: bool = True) -> C github_token = ( "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) - return CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - use_stdio=use_stdio, - tcp_connection_token="py-tcp-shared-test-token", + if use_stdio: + connection = RuntimeConnection.for_stdio(path=ctx.cli_path) + else: + connection = RuntimeConnection.for_tcp( + path=ctx.cli_path, connection_token="py-tcp-shared-test-token" ) + return CopilotClient( + connection=connection, + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) @@ -134,7 +139,7 @@ async def test_should_continue_pending_permission_request_after_resume( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" release_original: asyncio.Future = asyncio.get_event_loop().create_future() captured_request: asyncio.Future = asyncio.get_event_loop().create_future() @@ -149,7 +154,9 @@ def original_tool_handler(args): return f"ORIGINAL_SHOULD_NOT_RUN_{args.get('value', '')}" suspended_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) session1 = await suspended_client.create_session( on_permission_request=hold_permission, @@ -175,16 +182,14 @@ def resumed_tool_handler(args): return f"PERMISSION_RESUMED_{args['value'].upper()}" resumed_client = CopilotClient( - ExternalServerConfig( - url=cli_url, tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" ) ) try: session2 = await resumed_client.resume_session( session_id, - on_permission_request=lambda req, inv: PermissionRequestResult( - kind="user-not-available" - ), + on_permission_request=lambda req, inv: PermissionDecisionUserNotAvailable(), continue_pending_work=True, tools=[_make_pending_tool("resume_permission_tool", resumed_tool_handler)], ) @@ -212,7 +217,7 @@ def resumed_tool_handler(args): await _safe_force_stop(resumed_client) finally: if not release_original.done(): - release_original.set_result(PermissionRequestResult(kind="user-not-available")) + release_original.set_result(PermissionDecisionUserNotAvailable()) finally: await _safe_force_stop(server) @@ -222,7 +227,7 @@ async def test_should_continue_pending_external_tool_request_after_resume( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" tool_started: asyncio.Future = asyncio.get_event_loop().create_future() release_original: asyncio.Future = asyncio.get_event_loop().create_future() @@ -234,7 +239,9 @@ async def blocking_external_tool(args): return await release_original suspended_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) session1 = await suspended_client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -255,8 +262,8 @@ async def blocking_external_tool(args): await suspended_client.force_stop() resumed_client = CopilotClient( - ExternalServerConfig( - url=cli_url, tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" ) ) try: @@ -294,7 +301,7 @@ async def test_should_continue_parallel_pending_external_tool_requests_after_res server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" tool_a_started: asyncio.Future = asyncio.get_event_loop().create_future() tool_b_started: asyncio.Future = asyncio.get_event_loop().create_future() @@ -312,7 +319,9 @@ async def tool_b(args): return await release_b suspended_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) session1 = await suspended_client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -343,8 +352,8 @@ async def tool_b(args): await suspended_client.force_stop() resumed_client = CopilotClient( - ExternalServerConfig( - url=cli_url, tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" ) ) try: @@ -386,10 +395,12 @@ async def test_should_resume_successfully_when_no_pending_work_exists( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" first_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: first_session = await first_client.create_session( @@ -405,7 +416,9 @@ async def test_should_resume_successfully_when_no_pending_work_exists( await _safe_force_stop(first_client) resumed_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: resumed_session = await resumed_client.resume_session( @@ -443,10 +456,12 @@ async def blocking_external_tool(args): server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" suspended_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) session1 = await suspended_client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -467,8 +482,8 @@ async def blocking_external_tool(args): await suspended_client.force_stop() resumed_client = CopilotClient( - ExternalServerConfig( - url=cli_url, tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" ) ) try: @@ -479,7 +494,7 @@ async def blocking_external_tool(args): ) # Verify resume event: continue_pending_work=False and session_was_active=True - messages = await session2.get_messages() + messages = await session2.get_events() resume_events = [m for m in messages if isinstance(m.data, SessionResumeData)] assert len(resume_events) == 1, "Expected exactly one session.resume event" resume_event = resume_events[0] @@ -518,10 +533,12 @@ async def test_should_report_continuependingwork_true_in_resume_event( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" first_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: first_session = await first_client.create_session( @@ -538,7 +555,9 @@ async def test_should_report_continuependingwork_true_in_resume_event( await _safe_force_stop(first_client) resumed_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: resumed_session = await resumed_client.resume_session( @@ -547,7 +566,7 @@ async def test_should_report_continuependingwork_true_in_resume_event( continue_pending_work=True, ) - messages = await resumed_session.get_messages() + messages = await resumed_session.get_events() resume_events = [m for m in messages if isinstance(m.data, SessionResumeData)] assert len(resume_events) == 1, "Expected exactly one session.resume event" resume_event = resume_events[0] diff --git a/python/e2e/test_per_session_auth_e2e.py b/python/e2e/test_per_session_auth_e2e.py index b03945deb..0aa42cdaa 100644 --- a/python/e2e/test_per_session_auth_e2e.py +++ b/python/e2e/test_per_session_auth_e2e.py @@ -2,7 +2,7 @@ import pytest -from copilot.client import CopilotClient, SubprocessConfig +from copilot.client import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness import E2ETestContext @@ -18,7 +18,7 @@ async def auth_ctx(ctx: E2ETestContext): # Redirect GitHub API calls to the proxy so per-session auth token # resolution (fetchCopilotUser) is intercepted. Must be set before the # CLI subprocess is spawned (i.e., before the first create_session call). - ctx.client._config.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url + ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url await ctx.set_copilot_user_by_token( "token-alice", @@ -97,12 +97,10 @@ async def test_should_return_unauthenticated_when_no_token_provided( env = without_auth_env(auth_ctx.get_env()) env["COPILOT_DEBUG_GITHUB_API_URL"] = auth_ctx.proxy_url no_token_client = CopilotClient( - SubprocessConfig( - cli_path=auth_ctx.cli_path, - working_directory=auth_ctx.work_dir, - env=env, - use_logged_in_user=False, - ) + connection=RuntimeConnection.for_stdio(path=auth_ctx.cli_path), + working_directory=auth_ctx.work_dir, + env=env, + use_logged_in_user=False, ) try: diff --git a/python/e2e/test_permissions_e2e.py b/python/e2e/test_permissions_e2e.py index 46cf2f3d4..dbea6d384 100644 --- a/python/e2e/test_permissions_e2e.py +++ b/python/e2e/test_permissions_e2e.py @@ -6,12 +6,17 @@ import pytest +from copilot.generated.rpc import ( + PermissionDecisionApproveOnce, + PermissionDecisionReject, + PermissionDecisionUserNotAvailable, +) from copilot.generated.session_events import ( PermissionRequest, SessionIdleData, ToolExecutionCompleteData, ) -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session import PermissionHandler, PermissionNoResult, PermissionRequestResult from .testharness import E2ETestContext from .testharness.helper import read_file, write_file @@ -29,7 +34,7 @@ def on_permission_request( ) -> PermissionRequestResult: permission_requests.append(request) assert invocation["session_id"] == session.session_id - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=on_permission_request) @@ -41,7 +46,7 @@ def on_permission_request( assert len(permission_requests) > 0 # Should include write permission request - write_requests = [req for req in permission_requests if req.kind.value == "write"] + write_requests = [req for req in permission_requests if req.kind == "write"] assert len(write_requests) > 0 await session.disconnect() @@ -52,7 +57,7 @@ async def test_should_deny_permission_when_handler_returns_denied(self, ctx: E2E def on_permission_request( request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: - return PermissionRequestResult(kind="reject") + return PermissionDecisionReject() session = await ctx.client.create_session(on_permission_request=on_permission_request) @@ -97,7 +102,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies( """Test that tool operations are denied when handler explicitly denies""" def deny_all(request, invocation): - return PermissionRequestResult() + return PermissionDecisionUserNotAvailable() session = await ctx.client.create_session(on_permission_request=deny_all) @@ -138,7 +143,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ await session1.send_and_wait("What is 1+1?") def deny_all(request, invocation): - return PermissionRequestResult() + return PermissionDecisionUserNotAvailable() session2 = await ctx.client.resume_session(session_id, on_permission_request=deny_all) @@ -190,7 +195,7 @@ async def on_permission_request( ) -> PermissionRequestResult: permission_requests.append(request) await asyncio.sleep(0) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=on_permission_request) @@ -216,7 +221,7 @@ def on_permission_request( request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: permission_requests.append(request) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session2 = await ctx.client.resume_session( session_id, on_permission_request=on_permission_request @@ -260,7 +265,7 @@ def on_permission_request( received_tool_call_id = True assert isinstance(request.tool_call_id, str) assert len(request.tool_call_id) > 0 - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=on_permission_request) @@ -289,7 +294,7 @@ async def slow_permission(request: PermissionRequest, invocation: dict): handler_entered.set_result(True) await asyncio.wait_for(release_handler, timeout=30.0) add_event("permission-complete", tool_call_id) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=slow_permission) @@ -376,7 +381,7 @@ async def test_should_deny_permission_with_noresult_kind(self, ctx: E2ETestConte def deny_noresult(request: PermissionRequest, invocation: dict) -> PermissionRequestResult: if not permission_called.done(): permission_called.set_result(True) - return PermissionRequestResult(kind="no-result") + return PermissionNoResult() session = await ctx.client.create_session(on_permission_request=deny_noresult) try: @@ -399,7 +404,7 @@ def counting_handler( ) -> PermissionRequestResult: nonlocal handler_call_count handler_call_count += 1 - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=counting_handler) try: @@ -458,7 +463,7 @@ async def concurrent_permission(request: PermissionRequest, invocation: dict): if permission_request_count >= 2 and not both_started.done(): both_started.set_result(True) await asyncio.wait_for(both_started, timeout=30.0) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() def first_tool_handler(invocation: ToolInvocation) -> ToolResult: nonlocal first_tool_called diff --git a/python/e2e/test_pre_mcp_tool_call_hook_e2e.py b/python/e2e/test_pre_mcp_tool_call_hook_e2e.py index 9a140dd38..c59994437 100644 --- a/python/e2e/test_pre_mcp_tool_call_hook_e2e.py +++ b/python/e2e/test_pre_mcp_tool_call_hook_e2e.py @@ -5,6 +5,7 @@ from __future__ import annotations +from datetime import datetime from pathlib import Path import pytest @@ -58,7 +59,7 @@ async def on_pre_mcp_tool_call(input_data, invocation): assert inputs[0].get("serverName") == "meta-echo" assert inputs[0].get("toolName") == "echo_meta" assert inputs[0].get("workingDirectory") - assert inputs[0].get("timestamp", 0) > 0 + assert isinstance(inputs[0].get("timestamp"), datetime) finally: await session.disconnect() diff --git a/python/e2e/test_rpc_e2e.py b/python/e2e/test_rpc_e2e.py index 511b9d1d1..b825db060 100644 --- a/python/e2e/test_rpc_e2e.py +++ b/python/e2e/test_rpc_e2e.py @@ -2,8 +2,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.rpc import ModelsListRequest, PingRequest from copilot.session import PermissionHandler @@ -16,7 +15,7 @@ class TestRpc: @pytest.mark.asyncio async def test_should_call_rpc_ping_with_typed_params(self): """Test calling rpc.ping with typed params and result""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -32,7 +31,7 @@ async def test_should_call_rpc_ping_with_typed_params(self): @pytest.mark.asyncio async def test_should_call_rpc_models_list(self): """Test calling rpc.models.list with typed result""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -55,7 +54,7 @@ async def test_should_call_rpc_models_list(self): @pytest.mark.asyncio async def test_should_call_rpc_account_get_quota(self): """Test calling rpc.account.getQuota when authenticated""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -116,7 +115,7 @@ async def test_get_and_set_session_mode(self): """Test getting and setting session mode""" from copilot.generated.rpc import ModeSetRequest, SessionMode - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -148,7 +147,7 @@ async def test_read_update_and_delete_plan(self): """Test reading, updating, and deleting plan""" from copilot.generated.rpc import PlanUpdateRequest - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -191,7 +190,7 @@ async def test_create_list_and_read_workspace_files(self): WorkspacesReadFileRequest, ) - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() diff --git a/python/e2e/test_rpc_event_side_effects_e2e.py b/python/e2e/test_rpc_event_side_effects_e2e.py index b4a5b2790..9725e211a 100644 --- a/python/e2e/test_rpc_event_side_effects_e2e.py +++ b/python/e2e/test_rpc_event_side_effects_e2e.py @@ -215,7 +215,7 @@ async def test_should_emit_snapshot_rewind_event_and_remove_events_on_truncate( try: await session.send_and_wait("Say SNAPSHOT_REWIND_TARGET exactly.", timeout=60.0) - events = await session.get_messages() + events = await session.get_events() user_msgs = [e for e in events if isinstance(e.data, UserMessageData)] assert len(user_msgs) >= 1 first_user_event_id = str(user_msgs[0].id) @@ -236,7 +236,7 @@ def on_event(event): assert evt.data.events_removed >= 1 assert evt.data.up_to_event_id.lower() == first_user_event_id.lower() - messages_after = await session.get_messages() + messages_after = await session.get_events() assert not any(e.id == user_msgs[0].id for e in messages_after) except Exception as exc: if "unhandled method" in str(exc).lower(): @@ -257,7 +257,7 @@ async def test_should_allow_session_use_after_truncate(self, ctx: E2ETestContext try: await session.send_and_wait("Say SNAPSHOT_REWIND_TARGET exactly.", timeout=60.0) - events = await session.get_messages() + events = await session.get_events() user_msgs = [e for e in events if isinstance(e.data, UserMessageData)] assert len(user_msgs) >= 1 first_user_event_id = str(user_msgs[0].id) diff --git a/python/e2e/test_rpc_mcp_and_skills_e2e.py b/python/e2e/test_rpc_mcp_and_skills_e2e.py index 6c7d66208..dee98b1dd 100644 --- a/python/e2e/test_rpc_mcp_and_skills_e2e.py +++ b/python/e2e/test_rpc_mcp_and_skills_e2e.py @@ -7,7 +7,9 @@ from __future__ import annotations +import asyncio import os +import time import uuid from pathlib import Path @@ -19,6 +21,7 @@ ExtensionsEnableRequest, MCPDisableRequest, MCPEnableRequest, + McpServerStatus, SkillsDisableRequest, SkillsEnableRequest, ) @@ -28,6 +31,11 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") +TEST_MCP_SERVER = str( + (Path(__file__).parents[2] / "test" / "harness" / "test-mcp-server.mjs").resolve() +) +TEST_HARNESS_DIR = str((Path(__file__).parents[2] / "test" / "harness").resolve()) + # --yolo auto-approves extension permission gates at the CLI level, # preventing breakage from new gates (e.g., extension-permission-access). @@ -62,6 +70,37 @@ def _create_skill_directory(work_dir: str, skill_name: str, description: str) -> return str(skills_dir) +def _test_mcp_servers(*server_names: str) -> dict: + return { + server_name: { + "command": "node", + "args": [TEST_MCP_SERVER], + "tools": ["*"], + "working_directory": TEST_HARNESS_DIR, + } + for server_name in server_names + } + + +async def _wait_for_mcp_server_status( + session, server_name: str, expected_status: McpServerStatus = McpServerStatus.CONNECTED +) -> None: + deadline = time.monotonic() + 60 + last_status = "" + + while time.monotonic() < deadline: + result = await session.rpc.mcp.list() + server = next((s for s in result.servers if s.name == server_name), None) + if server is not None and server.status == expected_status: + return + last_status = server.status if server is not None else "" + await asyncio.sleep(0.2) + + raise AssertionError( + f"{server_name} did not reach {expected_status.value}; last status was {last_status}" + ) + + def _assert_skill(skills, skill_name: str, *, enabled: bool): matching = [s for s in skills if s.name == skill_name] assert len(matching) == 1, f"Expected exactly one skill named {skill_name!r}" @@ -130,15 +169,10 @@ async def test_should_list_mcp_servers_with_configured_server(self, ctx: E2ETest server_name = "rpc-list-mcp-server" session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - mcp_servers={ - server_name: { - "command": "echo", - "args": ["rpc-list-mcp-server"], - "tools": ["*"], - } - }, + mcp_servers=_test_mcp_servers(server_name), ) try: + await _wait_for_mcp_server_status(session, server_name) result = await session.rpc.mcp.list() matching = [s for s in result.servers if s.name == server_name] assert len(matching) == 1 diff --git a/python/e2e/test_rpc_server_e2e.py b/python/e2e/test_rpc_server_e2e.py index f5dc9920d..481e50d7b 100644 --- a/python/e2e/test_rpc_server_e2e.py +++ b/python/e2e/test_rpc_server_e2e.py @@ -12,8 +12,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.rpc import ( AccountGetQuotaRequest, MCPDiscoverRequest, @@ -48,7 +47,7 @@ def _create_skill_directory(work_dir: str, skill_name: str, description: str) -> @pytest.fixture(scope="module") async def authed_ctx(ctx: E2ETestContext): """Configure proxy to redirect GitHub user lookups so per-token auth works.""" - ctx.client._config.env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url + ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url return ctx @@ -56,12 +55,10 @@ def _make_authed_client(ctx: E2ETestContext, token: str) -> CopilotClient: env = ctx.get_env() env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url return CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=env, - github_token=token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=env, + github_token=token, ) diff --git a/python/e2e/test_rpc_session_state_e2e.py b/python/e2e/test_rpc_session_state_e2e.py index b7329158c..f5b11f6fa 100644 --- a/python/e2e/test_rpc_session_state_e2e.py +++ b/python/e2e/test_rpc_session_state_e2e.py @@ -171,7 +171,7 @@ async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestCon assert initial_answer is not None assert "FORK_SOURCE_ALPHA" in (initial_answer.data.content or "") - source_messages = await session.get_messages() + source_messages = await session.get_events() source_conversation = _conversation_messages(source_messages) assert any( role == "user" and content == source_prompt for role, content in source_conversation @@ -192,7 +192,7 @@ async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestCon on_permission_request=PermissionHandler.approve_all, ) try: - forked_messages = await forked_session.get_messages() + forked_messages = await forked_session.get_events() forked_conversation = _conversation_messages(forked_messages) assert forked_conversation[: len(source_conversation)] == source_conversation @@ -200,10 +200,10 @@ async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestCon assert fork_answer is not None assert "FORK_CHILD_BETA" in (fork_answer.data.content or "") - source_after_fork = _conversation_messages(await session.get_messages()) + source_after_fork = _conversation_messages(await session.get_events()) assert all(content != fork_prompt for _, content in source_after_fork) - fork_after_prompt = _conversation_messages(await forked_session.get_messages()) + fork_after_prompt = _conversation_messages(await forked_session.get_events()) assert any( role == "user" and content == fork_prompt for role, content in fork_after_prompt ) @@ -241,7 +241,7 @@ async def test_should_handle_forking_session_without_persisted_events( on_permission_request=PermissionHandler.approve_all, ) try: - assert _conversation_messages(await forked_session.get_messages()) == [] + assert _conversation_messages(await forked_session.get_events()) == [] finally: await forked_session.disconnect() finally: @@ -506,7 +506,7 @@ async def test_should_fork_session_to_event_id_excluding_boundary_event( await session.send_and_wait(first_prompt, timeout=60.0) await session.send_and_wait(second_prompt, timeout=60.0) - source_events = await session.get_messages() + source_events = await session.get_events() second_user_event = next( ( e @@ -531,7 +531,7 @@ async def test_should_fork_session_to_event_id_excluding_boundary_event( on_permission_request=PermissionHandler.approve_all, ) try: - forked_events = await forked_session.get_messages() + forked_events = await forked_session.get_events() forked_ids = {str(e.id) for e in forked_events} assert boundary_event_id not in forked_ids, ( "toEventId is exclusive — boundary event must not be in forked session" diff --git a/python/e2e/test_rpc_shell_and_fleet_e2e.py b/python/e2e/test_rpc_shell_and_fleet_e2e.py index c5384825b..32177cbbd 100644 --- a/python/e2e/test_rpc_shell_and_fleet_e2e.py +++ b/python/e2e/test_rpc_shell_and_fleet_e2e.py @@ -128,7 +128,7 @@ def record_fleet_completion(invocation: ToolInvocation) -> ToolResult: async def _wait_for_messages(timeout: float = 120.0): deadline = asyncio.get_event_loop().time() + timeout while asyncio.get_event_loop().time() < deadline: - messages = await session.get_messages() + messages = await session.get_events() if any( isinstance(m.data, AssistantMessageData) and "fleet task" in (m.data.content or "").lower() diff --git a/python/e2e/test_rpc_tasks_and_handlers_e2e.py b/python/e2e/test_rpc_tasks_and_handlers_e2e.py index 707c8b781..23b9f9896 100644 --- a/python/e2e/test_rpc_tasks_and_handlers_e2e.py +++ b/python/e2e/test_rpc_tasks_and_handlers_e2e.py @@ -12,14 +12,15 @@ import pytest from copilot.generated.rpc import ( - ApprovalKind, CommandsHandlePendingCommandRequest, HandlePendingToolCallRequest, - PermissionDecision, - PermissionDecisionApproveForIonApproval, - PermissionDecisionKind, + PermissionDecisionApproveForLocation, + PermissionDecisionApproveForLocationApprovalCustomTool, + PermissionDecisionApproveForSession, + PermissionDecisionApproveForSessionApprovalCustomTool, + PermissionDecisionApprovePermanently, + PermissionDecisionReject, PermissionDecisionRequest, - TaskInfoType, TasksCancelRequest, TasksPromoteToBackgroundRequest, TasksRemoveRequest, @@ -137,10 +138,7 @@ async def test_should_return_expected_results_for_missing_pending_handler_reques permission = await session.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id="missing-permission-request", - result=PermissionDecision( - kind=PermissionDecisionKind.REJECT, - feedback="not approved", - ), + result=PermissionDecisionReject(feedback="not approved"), ) ) assert permission.success is False @@ -148,10 +146,7 @@ async def test_should_return_expected_results_for_missing_pending_handler_reques permanent = await session.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id="missing-permanent-permission-request", - result=PermissionDecision( - kind=PermissionDecisionKind.APPROVE_PERMANENTLY, - domain="example.com", - ), + result=PermissionDecisionApprovePermanently(domain="example.com"), ) ) assert permanent.success is False @@ -159,10 +154,8 @@ async def test_should_return_expected_results_for_missing_pending_handler_reques session_approval = await session.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id="missing-session-approval-request", - result=PermissionDecision( - kind=PermissionDecisionKind.APPROVE_FOR_SESSION, - approval=PermissionDecisionApproveForIonApproval( - kind=ApprovalKind.CUSTOM_TOOL, + result=PermissionDecisionApproveForSession( + approval=PermissionDecisionApproveForSessionApprovalCustomTool( tool_name="missing-tool", ), ), @@ -173,11 +166,9 @@ async def test_should_return_expected_results_for_missing_pending_handler_reques location_approval = await session.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id="missing-location-approval-request", - result=PermissionDecision( - kind=PermissionDecisionKind.APPROVE_FOR_LOCATION, + result=PermissionDecisionApproveForLocation( location_key="missing-location", - approval=PermissionDecisionApproveForIonApproval( - kind=ApprovalKind.CUSTOM_TOOL, + approval=PermissionDecisionApproveForLocationApprovalCustomTool( tool_name="missing-tool", ), ), @@ -215,7 +206,7 @@ async def test_should_report_implemented_error_for_invalid_task_agent_model( async def test_should_start_background_agent_and_report_task_details(self, ctx: E2ETestContext): """Start a background agent task and verify task details then remove it.""" - from copilot.generated.rpc import TaskInfoExecutionMode, TaskInfoStatus + from copilot.generated.rpc import TaskAgentInfo, TaskInfoExecutionMode, TaskInfoStatus session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -248,7 +239,7 @@ async def test_should_start_background_agent_and_report_task_details(self, ctx: ) assert found_task.id == task_id assert found_task.description == "SDK background agent coverage" - assert found_task.type == TaskInfoType.AGENT + assert isinstance(found_task, TaskAgentInfo) assert found_task.agent_type == "general-purpose" assert found_task.execution_mode == TaskInfoExecutionMode.BACKGROUND assert found_task.prompt == "Reply with TASK_AGENT_DONE exactly." diff --git a/python/e2e/test_session_config_e2e.py b/python/e2e/test_session_config_e2e.py index 1fd2cd0a2..0a5a4a1e4 100644 --- a/python/e2e/test_session_config_e2e.py +++ b/python/e2e/test_session_config_e2e.py @@ -171,7 +171,7 @@ async def test_should_use_custom_sessionid(self, ctx: E2ETestContext): ) assert session.session_id == requested_session_id - messages = await session.get_messages() + messages = await session.get_events() assert messages start_event = messages[0] assert isinstance(start_event.data, SessionStartData) @@ -422,11 +422,11 @@ async def test_should_apply_availabletools_on_session_resume(self, ctx: E2ETestC available_tools=["view"], ) - await session2.send_and_wait("What is 1+1?") - - exchanges = await ctx.get_exchanges() - assert exchanges - assert _get_tool_names(exchanges[-1]) == ["view"] + try: + await session2.send("What is 1+1?") - await session2.disconnect() - await session1.disconnect() + exchanges = await ctx.wait_for_exchanges() + assert _get_tool_names(exchanges[-1]) == ["view"] + finally: + await session2.disconnect() + await session1.disconnect() diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index d5a0c970e..f21144332 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -2,11 +2,11 @@ import base64 import os +from datetime import datetime import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.session_events import SessionModelChangeData from copilot.session import PermissionHandler from copilot.tools import Tool, ToolResult @@ -23,7 +23,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): ) assert session.session_id - messages = await session.get_messages() + messages = await session.get_events() assert len(messages) > 0 assert messages[0].type.value == "session.start" assert messages[0].data.session_id == session.session_id @@ -32,7 +32,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): await session.disconnect() with pytest.raises(Exception, match="Session not found"): - await session.get_messages() + await session.get_events() async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): session = await ctx.client.create_session( @@ -119,32 +119,36 @@ async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestCon available_tools=["view", "edit"], ) - await session.send("What is 1+1?") - await get_final_assistant_message(session) - - # It only tells the model about the specified tools and no others - traffic = await ctx.get_exchanges() - tools = traffic[0]["request"]["tools"] - tool_names = [t["function"]["name"] for t in tools] - assert len(tool_names) == 2 - assert "view" in tool_names - assert "edit" in tool_names + try: + await session.send("What is 1+1?") + + # It only tells the model about the specified tools and no others + traffic = await ctx.wait_for_exchanges() + tools = traffic[0]["request"]["tools"] + tool_names = [t["function"]["name"] for t in tools] + assert len(tool_names) == 2 + assert "view" in tool_names + assert "edit" in tool_names + finally: + await session.disconnect() async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, excluded_tools=["view"] ) - await session.send("What is 1+1?") - await get_final_assistant_message(session) - - # It has other tools, but not the one we excluded - traffic = await ctx.get_exchanges() - tools = traffic[0]["request"]["tools"] - tool_names = [t["function"]["name"] for t in tools] - assert "edit" in tool_names - assert "grep" in tool_names - assert "view" not in tool_names + try: + await session.send("What is 1+1?") + + # It has other tools, but not the one we excluded + traffic = await ctx.wait_for_exchanges() + tools = traffic[0]["request"]["tools"] + tool_names = [t["function"]["name"] for t in tools] + assert "edit" in tool_names + assert "grep" in tool_names + assert "view" not in tool_names + finally: + await session.disconnect() async def test_should_create_a_session_with_defaultAgent_excludedTools( self, ctx: E2ETestContext @@ -165,14 +169,16 @@ async def test_should_create_a_session_with_defaultAgent_excludedTools( default_agent={"excluded_tools": ["secret_tool"]}, ) - await session.send("What is 1+1?") - await get_final_assistant_message(session) + try: + await session.send("What is 1+1?") - # The real assertion: verify the runtime excluded the tool from the CAPI request - traffic = await ctx.get_exchanges() - tools = traffic[0]["request"]["tools"] - tool_names = [t["function"]["name"] for t in tools] - assert "secret_tool" not in tool_names + # The real assertion: verify the runtime excluded the tool from the CAPI request + traffic = await ctx.wait_for_exchanges() + tools = traffic[0]["request"]["tools"] + tool_names = [t["function"]["name"] for t in tools] + assert "secret_tool" not in tool_names + finally: + await session.disconnect() # TODO: This test shows there's a race condition inside client.ts. If createSession # is called concurrently and autoStart is on, it may start multiple child processes. @@ -194,7 +200,7 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont # All are connected for s in [s1, s2, s3]: - messages = await s.get_messages() + messages = await s.get_events() assert len(messages) > 0 assert messages[0].type.value == "session.start" assert messages[0].data.session_id == s.session_id @@ -203,7 +209,7 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont await asyncio.gather(s1.disconnect(), s2.disconnect(), s3.disconnect()) for s in [s1, s2, s3]: with pytest.raises(Exception, match="Session not found"): - await s.get_messages() + await s.get_events() async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext): # Create initial session @@ -243,12 +249,10 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) new_client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) try: @@ -257,7 +261,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont ) assert session2.session_id == session_id - messages = await session2.get_messages() + messages = await session2.get_events() message_types = [m.type.value for m in messages] assert "user.message" in message_types assert "session.resume" in message_types @@ -295,21 +299,21 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): sessions = await ctx.client.list_sessions() assert isinstance(sessions, list) - session_ids = [s.sessionId for s in sessions] + session_ids = [s.session_id for s in sessions] assert session1.session_id in session_ids assert session2.session_id in session_ids # Verify session metadata structure for session_data in sessions: - assert hasattr(session_data, "sessionId") - assert hasattr(session_data, "startTime") - assert hasattr(session_data, "modifiedTime") - assert hasattr(session_data, "isRemote") + assert hasattr(session_data, "session_id") + assert hasattr(session_data, "start_time") + assert hasattr(session_data, "modified_time") + assert hasattr(session_data, "is_remote") # summary is optional - assert isinstance(session_data.sessionId, str) - assert isinstance(session_data.startTime, str) - assert isinstance(session_data.modifiedTime, str) - assert isinstance(session_data.isRemote, bool) + assert isinstance(session_data.session_id, str) + assert isinstance(session_data.start_time, datetime) + assert isinstance(session_data.modified_time, datetime) + assert isinstance(session_data.is_remote, bool) # Verify context field is present for session_data in sessions: @@ -333,7 +337,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify session exists in the list sessions = await ctx.client.list_sessions() - session_ids = [s.sessionId for s in sessions] + session_ids = [s.session_id for s in sessions] assert session_id in session_ids # Delete the session @@ -341,7 +345,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify session no longer exists in the list sessions_after = await ctx.client.list_sessions() - session_ids_after = [s.sessionId for s in sessions_after] + session_ids_after = [s.session_id for s in sessions_after] assert session_id not in session_ids_after # Verify we cannot resume the deleted session @@ -365,10 +369,10 @@ async def test_should_get_session_metadata(self, ctx: E2ETestContext): # Get metadata for the session we just created metadata = await ctx.client.get_session_metadata(session.session_id) assert metadata is not None - assert metadata.sessionId == session.session_id - assert isinstance(metadata.startTime, str) - assert isinstance(metadata.modifiedTime, str) - assert isinstance(metadata.isRemote, bool) + assert metadata.session_id == session.session_id + assert isinstance(metadata.start_time, datetime) + assert isinstance(metadata.modified_time, datetime) + assert isinstance(metadata.is_remote, bool) # Verify context field is present if metadata.context is not None: @@ -499,7 +503,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): _ = await wait_for_session_idle # The session should still be alive and usable after abort - messages = await session.get_messages() + messages = await session.get_events() assert len(messages) > 0 # Verify an abort event exists in messages @@ -555,7 +559,7 @@ def on_event(event): assert "session.idle" in event_types # Verify the assistant response contains the expected answer. - # session.idle is ephemeral and not in get_messages(), but we already + # session.idle is ephemeral and not in get_events(), but we already # confirmed idle via the live event handler above. assistant_message = await get_final_assistant_message(session, already_idle=True) assert "300" in assistant_message.data.content @@ -696,13 +700,13 @@ async def test_should_send_with_file_attachment(self, ctx: E2ETestContext): ], ) - messages = await session.get_messages() + messages = await session.get_events() user_messages = [m for m in messages if isinstance(m.data, UserMessageData)] assert user_messages attachments = user_messages[-1].data.attachments assert attachments is not None and len(attachments) == 1 attachment = attachments[0] - assert attachment.type.value == "file" + assert attachment.type == "file" assert attachment.display_name == "attached-file.txt" assert attachment.path == file_path assert attachment.line_range is not None @@ -734,13 +738,13 @@ async def test_should_send_with_directory_attachment(self, ctx: E2ETestContext): ], ) - messages = await session.get_messages() + messages = await session.get_events() user_messages = [m for m in messages if isinstance(m.data, UserMessageData)] assert user_messages attachments = user_messages[-1].data.attachments assert attachments is not None and len(attachments) == 1 attachment = attachments[0] - assert attachment.type.value == "directory" + assert attachment.type == "directory" assert attachment.display_name == "attached-directory" assert attachment.path == directory_path @@ -773,13 +777,13 @@ async def test_should_send_with_selection_attachment(self, ctx: E2ETestContext): ], ) - messages = await session.get_messages() + messages = await session.get_events() user_messages = [m for m in messages if isinstance(m.data, UserMessageData)] assert user_messages attachments = user_messages[-1].data.attachments assert attachments is not None and len(attachments) == 1 attachment = attachments[0] - assert attachment.type.value == "selection" + assert attachment.type == "selection" assert attachment.display_name == "selected-file.cs" assert attachment.file_path == file_path assert attachment.text == 'string Value = "SELECTION_SENTINEL";' @@ -822,7 +826,7 @@ async def test_should_list_sessions_with_context(self, ctx: E2ETestContext): our_session = None for _ in range(50): sessions = await ctx.client.list_sessions() - our_session = next((s for s in sessions if s.sessionId == session.session_id), None) + our_session = next((s for s in sessions if s.session_id == session.session_id), None) if our_session is not None: break await asyncio.sleep(0.1) @@ -854,9 +858,9 @@ async def test_should_get_session_metadata_by_id(self, ctx: E2ETestContext): break await asyncio.sleep(0.1) assert metadata is not None - assert metadata.sessionId == session.session_id - assert isinstance(metadata.startTime, str) and metadata.startTime - assert isinstance(metadata.modifiedTime, str) and metadata.modifiedTime + assert metadata.session_id == session.session_id + assert isinstance(metadata.start_time, datetime) + assert isinstance(metadata.modified_time, datetime) not_found = await ctx.client.get_session_metadata("non-existent-session-id") assert not_found is None @@ -1085,7 +1089,7 @@ async def test_should_send_with_mode_property(self, ctx: E2ETestContext): mode="plan", # type: ignore[arg-type] ) - messages = await session.get_messages() + messages = await session.get_events() user_messages = [m for m in messages if isinstance(m.data, UserMessageData)] assert user_messages last = user_messages[-1].data diff --git a/python/e2e/test_session_fs_e2e.py b/python/e2e/test_session_fs_e2e.py index 0afb565ef..9d00057ec 100644 --- a/python/e2e/test_session_fs_e2e.py +++ b/python/e2e/test_session_fs_e2e.py @@ -12,8 +12,12 @@ import pytest import pytest_asyncio -from copilot import CopilotClient, SessionFsConfig, define_tool -from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot import ( + CopilotClient, + RuntimeConnection, + SessionFsConfig, + define_tool, +) from copilot.generated.rpc import ( SessionFSReaddirWithTypesEntry, SessionFSReaddirWithTypesEntryType, @@ -45,13 +49,11 @@ @pytest_asyncio.fixture(scope="module", loop_scope="module") async def session_fs_client(ctx: E2ETestContext): client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, - session_fs=SESSION_FS_CONFIG, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, + session_fs=SESSION_FS_CONFIG, ) yield client try: @@ -117,13 +119,10 @@ async def test_should_load_session_data_from_fs_provider_on_resume( async def test_should_reject_setprovider_when_sessions_already_exist(self, ctx: E2ETestContext): client1 = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - use_stdio=False, - github_token=DEFAULT_GITHUB_TOKEN, - ) + connection=RuntimeConnection.for_tcp(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, ) session = None client2 = None @@ -132,14 +131,12 @@ async def test_should_reject_setprovider_when_sessions_already_exist(self, ctx: session = await client1.create_session( on_permission_request=PermissionHandler.approve_all, ) - actual_port = client1.actual_port + actual_port = client1.runtime_port assert actual_port is not None client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{actual_port}", - session_fs=SESSION_FS_CONFIG, - ) + connection=RuntimeConnection.for_uri(f"localhost:{actual_port}"), + session_fs=SESSION_FS_CONFIG, ) with pytest.raises(Exception): @@ -171,7 +168,7 @@ def get_big_string() -> str: "Call the get_big_string tool and reply with the word DONE only." ) - messages = await session.get_messages() + messages = await session.get_events() tool_result = find_tool_call_result(messages, "get_big_string") assert tool_result is not None assert f"{SESSION_STATE_PATH}/temp/" in tool_result diff --git a/python/e2e/test_session_fs_sqlite_e2e.py b/python/e2e/test_session_fs_sqlite_e2e.py index 38c15ae08..565c55336 100644 --- a/python/e2e/test_session_fs_sqlite_e2e.py +++ b/python/e2e/test_session_fs_sqlite_e2e.py @@ -12,8 +12,7 @@ import pytest import pytest_asyncio -from copilot import CopilotClient, SessionFsConfig -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection, SessionFsConfig from copilot.generated.rpc import ( SessionFSReaddirWithTypesEntry, SessionFSReaddirWithTypesEntryType, @@ -200,13 +199,11 @@ def factory(session): @pytest_asyncio.fixture(scope="module", loop_scope="module") async def sqlite_client(ctx: E2ETestContext): client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, - session_fs=SESSION_FS_CONFIG, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, + session_fs=SESSION_FS_CONFIG, ) yield client try: diff --git a/python/e2e/test_streaming_fidelity_e2e.py b/python/e2e/test_streaming_fidelity_e2e.py index e47fb9911..79b34fc91 100644 --- a/python/e2e/test_streaming_fidelity_e2e.py +++ b/python/e2e/test_streaming_fidelity_e2e.py @@ -4,8 +4,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness import E2ETestContext @@ -79,12 +78,10 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) new_client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) try: @@ -131,12 +128,10 @@ async def test_should_not_produce_deltas_after_session_resume_with_streaming_dis # Resume with streaming disabled new_client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) try: session2 = await new_client.resume_session( @@ -184,8 +179,8 @@ async def test_should_emit_streaming_deltas_with_reasoning_effort_configured( assistant_events = [e for e in events if e.type.value == "assistant.message"] assert len(assistant_events) >= 1, "Expected final assistant.message" - # Check session.start event (from get_messages) has reasoning_effort - all_msgs = await session.get_messages() + # Check session.start event (from get_events) has reasoning_effort + all_msgs = await session.get_events() start_event = next((e for e in all_msgs if isinstance(e.data, SessionStartData)), None) assert start_event is not None, "Expected session.start event" assert start_event.data.reasoning_effort == "high" diff --git a/python/e2e/test_subagent_hooks_e2e.py b/python/e2e/test_subagent_hooks_e2e.py index e5262a23c..1ca2a54c1 100644 --- a/python/e2e/test_subagent_hooks_e2e.py +++ b/python/e2e/test_subagent_hooks_e2e.py @@ -7,7 +7,7 @@ import pytest -from copilot.client import CopilotClient, SubprocessConfig +from copilot.client import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness import E2ETestContext @@ -50,12 +50,10 @@ async def on_post_tool_use(input_data, invocation): "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=env, - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=env, + github_token=github_token, ) session = await client.create_session( @@ -85,7 +83,7 @@ async def on_post_tool_use(input_data, invocation): assert len(view_pre) > 0, "preToolUse should fire for the sub-agent's 'view' tool call" assert len(view_post) > 0, "postToolUse should fire for the sub-agent's 'view' tool call" - # input.sessionId distinguishes parent from sub-agent + # input.session_id distinguishes parent from sub-agent assert view_pre[0]["sessionId"] != task_pre[0]["sessionId"], ( "Sub-agent tool hooks should have a different sessionId than parent tool hooks" ) diff --git a/python/e2e/test_suspend_e2e.py b/python/e2e/test_suspend_e2e.py index ec34bfc37..b0f74140c 100644 --- a/python/e2e/test_suspend_e2e.py +++ b/python/e2e/test_suspend_e2e.py @@ -14,9 +14,9 @@ import pytest -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot import CopilotClient, RuntimeConnection +from copilot.generated.rpc import PermissionDecisionUserNotAvailable +from copilot.session import PermissionHandler from copilot.tools import Tool, ToolInvocation, ToolResult from .testharness import E2ETestContext @@ -30,15 +30,17 @@ def _make_subprocess_client(ctx: E2ETestContext, *, use_stdio: bool = True) -> C github_token = ( "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) - return CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - use_stdio=use_stdio, - tcp_connection_token="py-tcp-shared-test-token", + if use_stdio: + connection = RuntimeConnection.for_stdio(path=ctx.cli_path) + else: + connection = RuntimeConnection.for_tcp( + path=ctx.cli_path, connection_token="py-tcp-shared-test-token" ) + return CopilotClient( + connection=connection, + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) @@ -99,11 +101,13 @@ async def test_should_allow_resume_and_continue_conversation_after_suspend( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" session_id: str first_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: session1 = await first_client.create_session( @@ -120,7 +124,9 @@ async def test_should_allow_resume_and_continue_conversation_after_suspend( await _safe_force_stop(first_client) resumed_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: session2 = await resumed_client.resume_session( @@ -172,9 +178,7 @@ def tool_handler(args): assert not tool_invoked finally: if not release_permission_handler.done(): - release_permission_handler.set_result( - PermissionRequestResult(kind="user-not-available") - ) + release_permission_handler.set_result(PermissionDecisionUserNotAvailable()) await _safe_disconnect(session) async def test_should_reject_pending_external_tool_when_suspending(self, ctx: E2ETestContext): diff --git a/python/e2e/test_telemetry_e2e.py b/python/e2e/test_telemetry_e2e.py index 6b1f7766c..f18a9fb88 100644 --- a/python/e2e/test_telemetry_e2e.py +++ b/python/e2e/test_telemetry_e2e.py @@ -22,9 +22,8 @@ import pytest -from copilot import CopilotClient +from copilot import CopilotClient, RuntimeConnection, TelemetryConfig from copilot._telemetry import get_trace_context, trace_context -from copilot.client import SubprocessConfig, TelemetryConfig from copilot.session import PermissionHandler from copilot.tools import Tool, ToolInvocation, ToolResult @@ -82,18 +81,16 @@ def echo(invocation: ToolInvocation) -> ToolResult: "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - telemetry=TelemetryConfig( - file_path=str(telemetry_path), - exporter_type="file", - source_name=source_name, - capture_content=True, - ), - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + telemetry=TelemetryConfig( + file_path=str(telemetry_path), + exporter_type="file", + source_name=source_name, + capture_content=True, + ), ) try: @@ -209,18 +206,6 @@ async def test_can_set_all_properties(self): assert cfg["capture_content"] is True -class TestSubprocessConfigTelemetry: - """Mirrors CopilotClientOptions_Telemetry_DefaultsToNull.""" - - async def test_telemetry_defaults_to_none(self): - config = SubprocessConfig() - assert config.telemetry is None - - # NOTE: CopilotClientOptions_Clone_CopiesTelemetry from the C# baseline has - # no Python equivalent: SubprocessConfig is a plain dataclass with no - # Clone() method, so there is nothing meaningful to test. - - class TestTelemetryHelpers: """Mirrors TelemetryHelpers_Restores_W3C_Trace_Context.""" diff --git a/python/e2e/test_tools_e2e.py b/python/e2e/test_tools_e2e.py index 4800d97c4..2f121b46d 100644 --- a/python/e2e/test_tools_e2e.py +++ b/python/e2e/test_tools_e2e.py @@ -6,7 +6,8 @@ from pydantic import BaseModel, Field from copilot import define_tool -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionApproveOnce, PermissionDecisionReject +from copilot.session import PermissionHandler, PermissionNoResult from copilot.tools import Tool, ToolInvocation, ToolResult from .testharness import E2ETestContext, get_final_assistant_message @@ -148,7 +149,7 @@ def safe_lookup(params: LookupParams, invocation: ToolInvocation) -> str: def tracking_handler(request, invocation): nonlocal did_run_permission_request did_run_permission_request = True - return PermissionRequestResult(kind="no-result") + return PermissionNoResult() session = await ctx.client.create_session( on_permission_request=tracking_handler, tools=[safe_lookup] @@ -191,7 +192,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: def on_permission_request(request, invocation): permission_requests.append(request) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session( on_permission_request=on_permission_request, tools=[encrypt_string] @@ -202,7 +203,7 @@ def on_permission_request(request, invocation): assert "HELLO" in assistant_message.data.content # Should have received a custom-tool permission request - custom_tool_requests = [r for r in permission_requests if r.kind.value == "custom-tool"] + custom_tool_requests = [r for r in permission_requests if r.kind == "custom-tool"] assert len(custom_tool_requests) > 0 assert custom_tool_requests[0].tool_name == "encrypt_string" @@ -219,7 +220,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: return params.input.upper() def on_permission_request(request, invocation): - return PermissionRequestResult(kind="reject") + return PermissionDecisionReject() session = await ctx.client.create_session( on_permission_request=on_permission_request, tools=[encrypt_string] diff --git a/python/e2e/test_ui_elicitation_multi_client_e2e.py b/python/e2e/test_ui_elicitation_multi_client_e2e.py index 97f989ac4..398b83ee8 100644 --- a/python/e2e/test_ui_elicitation_multi_client_e2e.py +++ b/python/e2e/test_ui_elicitation_multi_client_e2e.py @@ -1,6 +1,6 @@ """E2E UI Elicitation Tests (multi-client) -Mirrors nodejs/test/e2e/ui_elicitation.test.ts — multi-client scenarios. +Mirrors nodejs/test/e2e/ui_elicitation.test.ts — multi-client scenarios. Tests: - capabilities.changed fires when second client joins with elicitation handler @@ -16,8 +16,7 @@ import pytest import pytest_asyncio -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.session_events import CapabilitiesChangedData from copilot.session import ( ElicitationContext, @@ -32,7 +31,7 @@ # --------------------------------------------------------------------------- -# Multi-client context (TCP mode) — same pattern as test_multi_client.py +# Multi-client context (TCP mode) — same pattern as test_multi_client.py # --------------------------------------------------------------------------- @@ -63,14 +62,12 @@ async def setup(self): # Client 1 uses TCP mode so additional clients can connect self._client1 = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - working_directory=self.work_dir, - env=self._get_env(), - use_stdio=False, - github_token=github_token, - tcp_connection_token="py-tcp-shared-test-token", - ) + connection=RuntimeConnection.for_tcp( + path=self.cli_path, connection_token="py-tcp-shared-test-token" + ), + working_directory=self.work_dir, + env=self._get_env(), + github_token=github_token, ) # Trigger connection to obtain the TCP port @@ -79,13 +76,12 @@ async def setup(self): ) await init_session.disconnect() - self._actual_port = self._client1.actual_port + self._actual_port = self._client1.runtime_port assert self._actual_port is not None self._client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{self._actual_port}", - tcp_connection_token="py-tcp-shared-test-token", + connection=RuntimeConnection.for_uri( + f"localhost:{self._actual_port}", connection_token="py-tcp-shared-test-token" ) ) @@ -139,9 +135,8 @@ def make_external_client(self) -> CopilotClient: """Create a new external client connected to the same CLI server.""" assert self._actual_port is not None return CopilotClient( - ExternalServerConfig( - url=f"localhost:{self._actual_port}", - tcp_connection_token="py-tcp-shared-test-token", + connection=RuntimeConnection.for_uri( + f"localhost:{self._actual_port}", connection_token="py-tcp-shared-test-token" ) ) @@ -263,7 +258,7 @@ def on_event(event): unsubscribe = session1.on(on_event) - # Client 2 joins WITH elicitation handler — triggers capabilities.changed + # Client 2 joins WITH elicitation handler — triggers capabilities.changed async def handler( context: ElicitationContext, ) -> ElicitationResult: @@ -339,7 +334,7 @@ def on_disabled(event): unsub_disabled = session1.on(on_disabled) - # Force-stop client 3 — destroys the socket, triggering server-side cleanup + # Force-stop client 3 — destroys the socket, triggering server-side cleanup await client3.force_stop() await asyncio.wait_for(cap_disabled.wait(), timeout=15.0) diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index d67311598..e2267913e 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -4,16 +4,17 @@ Provides isolated directories and a replaying proxy for testing the SDK. """ +import asyncio import contextlib import os import re import shutil import tempfile +import time from pathlib import Path from typing import Any -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from .proxy import CapiProxy @@ -80,13 +81,13 @@ async def setup(self, cli_args: list[str] | None = None): # Create the shared client (like Node.js/Go do) self._client = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - cli_args=cli_args or [], - working_directory=self.work_dir, - env=self.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, - ) + connection=RuntimeConnection.for_stdio( + path=self.cli_path, + args=tuple(cli_args or []), + ), + working_directory=self.work_dir, + env=self.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, ) async def teardown(self, test_failed: bool = False): @@ -177,3 +178,16 @@ async def get_exchanges(self): if not self._proxy: raise RuntimeError("Proxy not started") return await self._proxy.get_exchanges() + + async def wait_for_exchanges( + self, minimum_count: int = 1, timeout: float = 120.0 + ) -> list[dict[str, Any]]: + """Wait until the proxy has captured at least the requested exchanges.""" + deadline = time.monotonic() + timeout + exchanges: list[dict[str, Any]] = [] + while time.monotonic() < deadline: + exchanges = await self.get_exchanges() + if len(exchanges) >= minimum_count: + return exchanges + await asyncio.sleep(0.1) + raise TimeoutError(f"Timed out waiting for {minimum_count} chat completion request(s)") diff --git a/python/e2e/testharness/helper.py b/python/e2e/testharness/helper.py index c603a8ec5..d64ee00b8 100644 --- a/python/e2e/testharness/helper.py +++ b/python/e2e/testharness/helper.py @@ -65,7 +65,7 @@ def on_event(event): async def _get_existing_final_response(session: CopilotSession, already_idle: bool = False): """Check existing messages for a final response.""" - messages = await session.get_messages() + messages = await session.get_events() # Find last user message final_user_message_index = -1 diff --git a/python/test_canvas.py b/python/test_canvas.py new file mode 100644 index 000000000..4c9ab223f --- /dev/null +++ b/python/test_canvas.py @@ -0,0 +1,249 @@ +"""Unit tests for the canvas SDK surface.""" + +from __future__ import annotations + +import threading +from typing import Any + +import pytest + +from copilot._jsonrpc import JsonRpcError +from copilot.canvas import ( + CanvasAction, + CanvasActionContext, + CanvasDeclaration, + CanvasError, + CanvasHandler, + CanvasOpenContext, + CanvasOpenResponse, + ExtensionInfo, + OpenCanvasInstance, + _action_context_from_params, + _lifecycle_context_from_params, + _open_context_from_params, +) +from copilot.client import CopilotClient + + +def test_canvas_declaration_serializes_camelcase_and_drops_optional(): + decl = CanvasDeclaration( + id="my-canvas", + display_name="My Canvas", + description="Does the thing", + ) + assert decl.to_dict() == { + "id": "my-canvas", + "displayName": "My Canvas", + "description": "Does the thing", + } + + +def test_canvas_declaration_serializes_input_schema_and_actions(): + action = CanvasAction( + name="refresh", + description="Refresh the canvas", + ) + decl = CanvasDeclaration( + id="c", + display_name="C", + description="D", + input_schema={"type": "object"}, + actions=[action], + ) + payload = decl.to_dict() + assert payload["inputSchema"] == {"type": "object"} + assert payload["actions"] == [action.to_dict()] + + +def test_extension_info_serializes(): + info = ExtensionInfo(source="github-app", name="my-ext") + assert info.to_dict() == {"source": "github-app", "name": "my-ext"} + + +def test_canvas_open_response_drops_none_fields(): + assert CanvasOpenResponse().to_dict() == {} + assert CanvasOpenResponse(url="https://x", status="ok").to_dict() == { + "url": "https://x", + "status": "ok", + } + + +def test_canvas_error_envelope_and_factories(): + err = CanvasError("oops", "something broke") + assert err.code == "oops" + assert err.message == "something broke" + assert err.to_envelope() == {"code": "oops", "message": "something broke"} + + no_handler = CanvasError.no_handler() + assert no_handler.code == "canvas_action_no_handler" + + unset = CanvasError.handler_unset() + assert unset.code == "canvas_handler_unset" + + +async def test_default_canvas_handler_on_action_raises_no_handler(): + class StubHandler(CanvasHandler): + async def on_open(self, ctx: CanvasOpenContext) -> CanvasOpenResponse: + return CanvasOpenResponse() + + handler = StubHandler() + ctx = CanvasActionContext( + session_id="s", + extension_id="e", + canvas_id="c", + instance_id="i", + action_name="any", + input=None, + ) + with pytest.raises(CanvasError) as excinfo: + await handler.on_action(ctx) + assert excinfo.value.code == "canvas_action_no_handler" + + +def test_context_helpers_parse_params(): + base = { + "sessionId": "s", + "extensionId": "e", + "canvasId": "c", + "instanceId": "i", + "input": {"foo": 1}, + "host": {"capabilities": {"canvases": True}}, + } + open_ctx = _open_context_from_params(base) + assert open_ctx.session_id == "s" + assert open_ctx.canvas_id == "c" + assert open_ctx.input == {"foo": 1} + assert open_ctx.host is not None and open_ctx.host.capabilities.canvases is True + + close_ctx = _lifecycle_context_from_params(base) + assert close_ctx.canvas_id == "c" + assert close_ctx.instance_id == "i" + + action_ctx = _action_context_from_params({**base, "actionName": "refresh"}) + assert action_ctx.action_name == "refresh" + + +class _StubSession: + """Minimal CopilotSession stand-in for the inbound dispatch tests.""" + + def __init__(self, handler: CanvasHandler | None) -> None: + self._handler = handler + self._open_canvases: list[OpenCanvasInstance] = [] + self._open_canvases_lock = threading.Lock() + + def _get_canvas_handler(self) -> CanvasHandler | None: + return self._handler + + def _set_open_canvases(self, instances: list[OpenCanvasInstance]) -> None: + with self._open_canvases_lock: + self._open_canvases = list(instances) + + @property + def open_canvases(self) -> list[OpenCanvasInstance]: + with self._open_canvases_lock: + return list(self._open_canvases) + + +def _make_client_with_session(session_id: str, session: Any) -> CopilotClient: + """Construct a CopilotClient skeleton sufficient for testing the inbound + canvas dispatch helpers without actually launching the CLI.""" + client = CopilotClient.__new__(CopilotClient) + client._sessions = {session_id: session} + client._sessions_lock = threading.Lock() + return client + + +async def test_handle_canvas_open_dispatches_to_handler(): + class Handler(CanvasHandler): + def __init__(self) -> None: + self.received: CanvasOpenContext | None = None + + async def on_open(self, ctx: CanvasOpenContext) -> CanvasOpenResponse: + self.received = ctx + return CanvasOpenResponse(url="https://canvas.example", title="Hi") + + async def on_action(self, ctx: CanvasActionContext) -> Any: + return {"echo": ctx.input} + + handler = Handler() + session = _StubSession(handler) + client = _make_client_with_session("sess-1", session) + + result = await client._handle_canvas_open( + { + "sessionId": "sess-1", + "extensionId": "ext", + "canvasId": "c", + "instanceId": "i", + "input": {"q": 1}, + } + ) + assert result == {"url": "https://canvas.example", "title": "Hi"} + assert handler.received is not None + assert handler.received.canvas_id == "c" + + +async def test_handle_canvas_open_raises_when_handler_unset(): + session = _StubSession(handler=None) + client = _make_client_with_session("sess-1", session) + + with pytest.raises(CanvasError) as excinfo: + await client._handle_canvas_open( + { + "sessionId": "sess-1", + "extensionId": "ext", + "canvasId": "c", + "instanceId": "i", + } + ) + assert excinfo.value.code == "canvas_handler_unset" + + +async def test_handle_canvas_action_returns_arbitrary_value(): + class Handler(CanvasHandler): + async def on_open(self, ctx: CanvasOpenContext) -> CanvasOpenResponse: + return CanvasOpenResponse() + + async def on_action(self, ctx: CanvasActionContext) -> Any: + return [1, 2, 3] + + client = _make_client_with_session("sess-1", _StubSession(Handler())) + result = await client._handle_canvas_action_invoke( + { + "sessionId": "sess-1", + "extensionId": "ext", + "canvasId": "c", + "instanceId": "i", + "actionName": "do", + } + ) + assert result == [1, 2, 3] + + +async def test_canvas_request_handler_translates_canvas_error(): + err = CanvasError("bad", "fail") + + async def coro(params: dict) -> Any: + raise err + + wrapped = CopilotClient._canvas_request_handler(coro) + with pytest.raises(JsonRpcError) as excinfo: + await wrapped({}) + assert excinfo.value.code == -32603 + assert excinfo.value.message == "fail" + assert excinfo.value.data == {"code": "bad", "message": "fail"} + + +def test_set_open_canvases_round_trip(): + from copilot.generated.rpc import CanvasInstanceAvailability + + inst = OpenCanvasInstance( + availability=CanvasInstanceAvailability.READY, + canvas_id="c", + extension_id="e", + instance_id="i", + reopen=False, + ) + session = _StubSession(handler=None) + session._set_open_canvases([inst]) + assert session.open_canvases == [inst] diff --git a/python/test_client.py b/python/test_client.py index 8add6975b..14320b3a2 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -8,25 +8,28 @@ import pytest -from copilot import CopilotClient, define_tool +from copilot import ( + CopilotClient, + RuntimeConnection, + StdioRuntimeConnection, + define_tool, +) from copilot.client import ( CloudSessionOptions, CloudSessionRepository, - ExternalServerConfig, ModelCapabilities, ModelInfo, ModelLimits, ModelSupports, - SubprocessConfig, ) -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session import PermissionHandler from e2e.testharness import CLI_PATH class TestPermissionHandlerOptional: @pytest.mark.asyncio async def test_create_session_allows_missing_permission_handler(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: session = await client.create_session() @@ -36,7 +39,7 @@ async def test_create_session_allows_missing_permission_handler(self): @pytest.mark.asyncio async def test_create_session_allows_none_permission_handler(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: session = await client.create_session(on_permission_request=None) @@ -44,29 +47,9 @@ async def test_create_session_allows_none_permission_handler(self): finally: await client.force_stop() - @pytest.mark.asyncio - async def test_v2_permission_adapter_rejects_no_result(self): - client = CopilotClient(SubprocessConfig(CLI_PATH)) - await client.start() - try: - session = await client.create_session( - on_permission_request=lambda request, invocation: PermissionRequestResult( - kind="no-result" - ) - ) - with pytest.raises(ValueError, match="protocol v2 server"): - await client._handle_permission_request_v2( - { - "sessionId": session.session_id, - "permissionRequest": {"kind": "write"}, - } - ) - finally: - await client.force_stop() - @pytest.mark.asyncio async def test_resume_session_allows_none_permission_handler(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: session = await client.create_session( @@ -81,7 +64,7 @@ async def test_resume_session_allows_none_permission_handler(self): class TestCreateSessionConfig: @pytest.mark.asyncio async def test_create_session_forwards_cloud_options(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: captured = {} @@ -117,47 +100,47 @@ async def mock_request(method, params): class TestURLParsing: def test_parse_port_only_url(self): - client = CopilotClient(ExternalServerConfig(url="8080")) - assert client._actual_port == 8080 + client = CopilotClient(connection=RuntimeConnection.for_uri("8080")) + assert client._runtime_port == 8080 assert client._actual_host == "localhost" assert client._is_external_server def test_parse_host_port_url(self): - client = CopilotClient(ExternalServerConfig(url="127.0.0.1:9000")) - assert client._actual_port == 9000 + client = CopilotClient(connection=RuntimeConnection.for_uri("127.0.0.1:9000")) + assert client._runtime_port == 9000 assert client._actual_host == "127.0.0.1" assert client._is_external_server def test_parse_http_url(self): - client = CopilotClient(ExternalServerConfig(url="http://localhost:7000")) - assert client._actual_port == 7000 + client = CopilotClient(connection=RuntimeConnection.for_uri("http://localhost:7000")) + assert client._runtime_port == 7000 assert client._actual_host == "localhost" assert client._is_external_server def test_parse_https_url(self): - client = CopilotClient(ExternalServerConfig(url="https://example.com:443")) - assert client._actual_port == 443 + client = CopilotClient(connection=RuntimeConnection.for_uri("https://example.com:443")) + assert client._runtime_port == 443 assert client._actual_host == "example.com" assert client._is_external_server def test_invalid_url_format(self): with pytest.raises(ValueError, match="Invalid cli_url format"): - CopilotClient(ExternalServerConfig(url="invalid-url")) + CopilotClient(connection=RuntimeConnection.for_uri("invalid-url")) def test_invalid_port_too_high(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient(ExternalServerConfig(url="localhost:99999")) + CopilotClient(connection=RuntimeConnection.for_uri("localhost:99999")) def test_invalid_port_zero(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient(ExternalServerConfig(url="localhost:0")) + CopilotClient(connection=RuntimeConnection.for_uri("localhost:0")) def test_invalid_port_negative(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient(ExternalServerConfig(url="localhost:-1")) + CopilotClient(connection=RuntimeConnection.for_uri("localhost:-1")) def test_is_external_server_true(self): - client = CopilotClient(ExternalServerConfig(url="localhost:8080")) + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:8080")) assert client._is_external_server @@ -165,129 +148,114 @@ class TestSessionFsConfig: def test_missing_initial_cwd(self): with pytest.raises(ValueError, match="session_fs.initial_working_directory is required"): CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - log_level="error", - session_fs={ - "initial_working_directory": "", - "session_state_path": "/session-state", - "conventions": "posix", - }, - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + log_level="error", + session_fs={ + "initial_working_directory": "", + "session_state_path": "/session-state", + "conventions": "posix", + }, ) def test_missing_session_state_path(self): with pytest.raises(ValueError, match="session_fs.session_state_path is required"): CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - log_level="error", - session_fs={ - "initial_working_directory": "/", - "session_state_path": "", - "conventions": "posix", - }, - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + log_level="error", + session_fs={ + "initial_working_directory": "/", + "session_state_path": "", + "conventions": "posix", + }, ) class TestAuthOptions: def test_accepts_github_token(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - github_token="gho_test_token", - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + github_token="gho_test_token", + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.github_token == "gho_test_token" + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.github_token == "gho_test_token" def test_default_use_logged_in_user_true_without_token(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, log_level="error")) - assert isinstance(client._config, SubprocessConfig) - assert client._config.use_logged_in_user is True + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), log_level="error" + ) + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.use_logged_in_user is True def test_default_use_logged_in_user_false_with_token(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - github_token="gho_test_token", - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + github_token="gho_test_token", + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.use_logged_in_user is False + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.use_logged_in_user is False def test_explicit_use_logged_in_user_true_with_token(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - github_token="gho_test_token", - use_logged_in_user=True, - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + github_token="gho_test_token", + use_logged_in_user=True, + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.use_logged_in_user is True + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.use_logged_in_user is True def test_explicit_use_logged_in_user_false_without_token(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - use_logged_in_user=False, - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + use_logged_in_user=False, + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.use_logged_in_user is False + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.use_logged_in_user is False class TestSessionIdleTimeoutSeconds: def test_accepts_session_idle_timeout_seconds(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - session_idle_timeout_seconds=600, - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + session_idle_timeout_seconds=600, + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.session_idle_timeout_seconds == 600 + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.session_idle_timeout_seconds == 600 def test_default_session_idle_timeout_seconds_is_none(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, log_level="error")) - assert isinstance(client._config, SubprocessConfig) - assert client._config.session_idle_timeout_seconds is None + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), log_level="error" + ) + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.session_idle_timeout_seconds is None class TestCopilotHome: def test_accepts_copilot_home(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - copilot_home="/custom/copilot/home", - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + base_directory="/custom/copilot/home", + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.copilot_home == "/custom/copilot/home" + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.base_directory == "/custom/copilot/home" def test_default_copilot_home_is_none(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), log_level="error" ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.copilot_home is None + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.base_directory is None class TestOverridesBuiltInTool: @pytest.mark.asyncio async def test_overrides_built_in_tool_sent_in_tool_definition(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -316,7 +284,7 @@ def grep(params) -> str: @pytest.mark.asyncio async def test_resume_session_sends_overrides_built_in_tool(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -353,7 +321,7 @@ def grep(params) -> str: class TestInstructionDirectories: @pytest.mark.asyncio async def test_create_session_sends_instruction_directories(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -381,7 +349,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_sends_instruction_directories(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -430,7 +398,7 @@ def handler(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=handler, ) await client.start() @@ -462,7 +430,7 @@ def handler(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=handler, ) await client.start() @@ -491,7 +459,7 @@ async def handler(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=handler, ) await client.start() @@ -522,7 +490,7 @@ def handler(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=handler, ) models = await client.list_models() @@ -533,7 +501,7 @@ def handler(): class TestSessionConfigForwarding: @pytest.mark.asyncio async def test_create_session_forwards_client_name(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -554,7 +522,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_client_name(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -584,7 +552,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_forwards_enable_session_telemetry(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -606,7 +574,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_enable_session_telemetry(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -635,7 +603,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_forwards_provider_headers(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -656,7 +624,7 @@ async def mock_request(method, params): "headers": {"Authorization": "Bearer provider-token"}, "model_id": "gpt-4o", "wire_model": "my-finetune-v3", - "max_input_tokens": 100_000, + "max_prompt_tokens": 100_000, "max_output_tokens": 4096, }, ) @@ -673,7 +641,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_provider_headers(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -699,7 +667,7 @@ async def mock_request(method, params): "headers": {"Authorization": "Bearer resume-token"}, "model_id": "gpt-4o", "wire_model": "my-finetune-v3", - "max_input_tokens": 100_000, + "max_prompt_tokens": 100_000, "max_output_tokens": 4096, }, ) @@ -716,7 +684,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_session_send_forwards_request_headers(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -748,7 +716,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_forwards_agent(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -771,7 +739,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_agent(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -801,7 +769,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_defaults_include_sub_agent_streaming_events_to_true(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -824,7 +792,7 @@ async def mock_request(method, params): async def test_create_session_preserves_explicit_false_include_sub_agent_streaming_events( self, ): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -846,7 +814,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_defaults_include_sub_agent_streaming_events_to_true(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -876,7 +844,7 @@ async def mock_request(method, params): async def test_resume_session_preserves_explicit_false_include_sub_agent_streaming_events( self, ): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -905,7 +873,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_continue_pending_work(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -934,7 +902,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_omits_continue_pending_work_by_default(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -962,7 +930,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_set_model_sends_correct_rpc(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -990,7 +958,7 @@ async def mock_request(method, params): class TestCopilotClientContextManager: @pytest.mark.asyncio async def test_aenter_calls_start_and_returns_self(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) with patch.object(client, "start", new_callable=AsyncMock) as mock_start: result = await client.__aenter__() mock_start.assert_awaited_once() @@ -998,7 +966,7 @@ async def test_aenter_calls_start_and_returns_self(self): @pytest.mark.asyncio async def test_aexit_calls_stop(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) with patch.object(client, "stop", new_callable=AsyncMock) as mock_stop: await client.__aexit__(None, None, None) mock_stop.assert_awaited_once() diff --git a/python/test_commands_and_elicitation.py b/python/test_commands_and_elicitation.py index 470e2f8f3..7f708c74f 100644 --- a/python/test_commands_and_elicitation.py +++ b/python/test_commands_and_elicitation.py @@ -10,8 +10,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import ( AutoModeSwitchRequest, AutoModeSwitchResponse, @@ -50,7 +49,7 @@ class TestCommands: @pytest.mark.asyncio async def test_forwards_commands_in_session_create_rpc(self): """Verifies that commands (name + description) are serialized in session.create payload.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -89,7 +88,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_forwards_commands_in_session_resume_rpc(self): """Verifies that commands are serialized in session.resume payload.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -127,7 +126,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_routes_command_execute_event_to_correct_handler(self): """Verifies the command dispatch works for command.execute events.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -198,7 +197,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_sends_error_when_command_handler_throws(self): """Verifies error is sent via RPC when a command handler raises.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -256,7 +255,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_sends_error_for_unknown_command(self): """Verifies error is sent via RPC for an unrecognized command.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -317,7 +316,7 @@ class TestUiElicitation: @pytest.mark.asyncio async def test_reads_capabilities_from_session_create_response(self): """Verifies capabilities are parsed from session.create response.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -341,7 +340,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_defaults_capabilities_when_not_injected(self): """Verifies capabilities default to empty when server returns none.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -358,7 +357,7 @@ async def test_defaults_capabilities_when_not_injected(self): @pytest.mark.asyncio async def test_elicitation_throws_when_capability_is_missing(self): """Verifies that UI methods throw when elicitation is not supported.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -385,7 +384,7 @@ async def test_elicitation_throws_when_capability_is_missing(self): @pytest.mark.asyncio async def test_confirm_throws_when_capability_is_missing(self): """Verifies confirm throws when elicitation is not supported.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -409,7 +408,7 @@ class TestOnElicitationContext: @pytest.mark.asyncio async def test_sends_request_elicitation_flag_when_handler_provided(self): """Verifies requestElicitation=true is sent when onElicitationContext is provided.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -441,7 +440,7 @@ async def elicitation_handler( @pytest.mark.asyncio async def test_does_not_send_request_elicitation_when_no_handler(self): """Verifies requestElicitation=false when no handler is provided.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -469,7 +468,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_sends_mode_callback_flags_when_handlers_provided(self): """Verifies mode callback flags are sent when handlers are provided.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -494,8 +493,8 @@ def auto_handler( session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - on_exit_plan_mode=exit_handler, - on_auto_mode_switch=auto_handler, + on_exit_plan_mode_request=exit_handler, + on_auto_mode_switch_request=auto_handler, ) assert session is not None @@ -508,7 +507,7 @@ def auto_handler( @pytest.mark.asyncio async def test_sends_mode_callback_flags_on_resume_when_handlers_provided(self): """Verifies mode callback flags are sent on session.resume.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -528,8 +527,8 @@ async def mock_request(method, params): await client.resume_session( session.session_id, on_permission_request=PermissionHandler.approve_all, - on_exit_plan_mode=lambda request, invocation: {"approved": True}, - on_auto_mode_switch=lambda request, invocation: "yes", + on_exit_plan_mode_request=lambda request, invocation: {"approved": True}, + on_auto_mode_switch_request=lambda request, invocation: "yes", ) payload = captured["session.resume"] @@ -541,7 +540,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_dispatches_mode_callback_requests_to_registered_handlers(self): """Verifies direct mode requests are dispatched to registered handlers.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -570,8 +569,8 @@ async def auto_handler( session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - on_exit_plan_mode=exit_handler, - on_auto_mode_switch=auto_handler, + on_exit_plan_mode_request=exit_handler, + on_auto_mode_switch_request=auto_handler, ) exit_result = await client._handle_exit_plan_mode_request( @@ -603,7 +602,7 @@ async def auto_handler( @pytest.mark.asyncio async def test_sends_cancel_when_elicitation_handler_throws(self): """Verifies auto-cancel when the elicitation handler raises.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -648,7 +647,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_dispatches_elicitation_requested_event_to_handler(self): """Verifies that an elicitation.requested event dispatches to the handler.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -709,7 +708,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_elicitation_handler_receives_full_schema(self): """Verifies that requestedSchema passes type, properties, and required to handler.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -785,7 +784,7 @@ class TestCapabilitiesChanged: @pytest.mark.asyncio async def test_capabilities_changed_event_updates_session(self): """Verifies that a capabilities.changed event updates session capabilities.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 25b5cc2bc..8950f839b 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -17,10 +17,8 @@ ElicitationCompletedAction, ElicitationRequestedMode, ElicitationRequestedSchema, - PermissionPromptRequest, - PermissionPromptRequestKind, - PermissionRequest, - PermissionRequestKind, + PermissionPromptRequestMemory, + PermissionRequestMemory, PermissionRequestMemoryAction, SessionEventType, SessionTaskCompleteData, @@ -149,13 +147,20 @@ def test_missing_optional_fields_remain_none_after_parsing(self): the schema default instead of ``None`` and broke ``from_dict(to_dict(x))`` round-trips for instances where the field was ``None``. """ + from copilot.generated.session_events import ( + _load_PermissionPromptRequest, + _load_PermissionRequest, + ) + # #1141: PermissionRequest.action defaults to None when missing. - request = PermissionRequest.from_dict({"kind": "memory", "fact": "remember this"}) + request = _load_PermissionRequest({"kind": "memory", "fact": "remember this"}) + assert isinstance(request, PermissionRequestMemory) assert request.action is None assert PermissionRequestMemoryAction.STORE.value == "store" # sanity # #1140: PermissionPromptRequest.action defaults to None when missing. - prompt_request = PermissionPromptRequest.from_dict({"kind": "memory"}) + prompt_request = _load_PermissionPromptRequest({"kind": "memory", "fact": "remember this"}) + assert isinstance(prompt_request, PermissionPromptRequestMemory) assert prompt_request.action is None # #1139: SessionTaskCompleteData.summary defaults to None when missing. @@ -175,14 +180,28 @@ def test_optional_fields_round_trip_none(self): task = SessionTaskCompleteData(success=None, summary=None) assert SessionTaskCompleteData.from_dict(task.to_dict()) == task - # #1140: PermissionPromptRequest round-trip with action=None. - prompt = PermissionPromptRequest(kind=PermissionPromptRequestKind.MEMORY) + # #1140: PermissionPromptRequestMemory round-trip with action=None. + prompt = PermissionPromptRequestMemory(fact="test-fact") assert prompt.action is None assert "action" not in prompt.to_dict() - assert PermissionPromptRequest.from_dict(prompt.to_dict()) == prompt + assert PermissionPromptRequestMemory.from_dict(prompt.to_dict()) == prompt - # #1141: PermissionRequest round-trip with action=None. - permission = PermissionRequest(kind=PermissionRequestKind.MEMORY) + # #1141: PermissionRequestMemory round-trip with action=None. + permission = PermissionRequestMemory(fact="test-fact") assert permission.action is None assert "action" not in permission.to_dict() - assert PermissionRequest.from_dict(permission.to_dict()) == permission + assert PermissionRequestMemory.from_dict(permission.to_dict()) == permission + + # PermissionRequest is now a discriminated union; the dispatch loader + # should round-trip via the correct variant class. + from copilot.generated.session_events import _load_PermissionRequest + + round_tripped = _load_PermissionRequest(permission.to_dict()) + assert isinstance(round_tripped, PermissionRequestMemory) + assert round_tripped == permission + # PermissionPromptRequest likewise. + from copilot.generated.session_events import _load_PermissionPromptRequest + + round_tripped_prompt = _load_PermissionPromptRequest(prompt.to_dict()) + assert isinstance(round_tripped_prompt, PermissionPromptRequestMemory) + assert round_tripped_prompt == prompt diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py index 5f484add0..5d003da42 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -7,7 +7,7 @@ from copilot.generated.rpc import ( CommandsApi, CommandsInvokeRequest, - SlashCommandInvocationResultKind, + SlashCommandTextResult, ) @@ -19,6 +19,6 @@ async def test_commands_invoke_deserializes_slash_command_result(): result = await api.invoke(CommandsInvokeRequest(name="help")) - assert result.kind is SlashCommandInvocationResultKind.TEXT + assert isinstance(result, SlashCommandTextResult) assert result.text == "hello" assert result.markdown is True diff --git a/python/test_telemetry.py b/python/test_telemetry.py index d10ffeb9f..6481fd525 100644 --- a/python/test_telemetry.py +++ b/python/test_telemetry.py @@ -5,7 +5,7 @@ from unittest.mock import patch from copilot._telemetry import get_trace_context, trace_context -from copilot.client import SubprocessConfig, TelemetryConfig +from copilot.client import TelemetryConfig class TestGetTraceContext: @@ -73,17 +73,6 @@ def test_telemetry_config_type(self): assert config["otlp_endpoint"] == "http://localhost:4318" assert config["capture_content"] is True - def test_telemetry_config_in_subprocess_config(self): - """TelemetryConfig can be used in SubprocessConfig.""" - config = SubprocessConfig( - telemetry={ - "otlp_endpoint": "http://localhost:4318", - "exporter_type": "otlp-http", - } - ) - assert config.telemetry is not None - assert config.telemetry["otlp_endpoint"] == "http://localhost:4318" - def test_telemetry_env_var_mapping(self): """TelemetryConfig fields map to expected environment variable names.""" config: TelemetryConfig = { diff --git a/rust/.gitignore b/rust/.gitignore index c17da7f58..03bbce707 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock.bak +bundled_cli_version.txt diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9c4790a6e..56e658ad1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -83,8 +83,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -325,18 +323,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - [[package]] name = "getrandom" version = "0.4.2" @@ -345,7 +331,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi 6.0.0", + "r-efi", "wasip2", "wasip3", ] @@ -376,7 +362,6 @@ dependencies = [ "ureq", "uuid", "zip", - "zstd", ] [[package]] @@ -536,16 +521,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -731,12 +706,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -1810,31 +1779,3 @@ dependencies = [ "log", "simd-adler32", ] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b2a2b4f54..1e02f267c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,21 +11,23 @@ homepage = "https://github.com/github/copilot-sdk" documentation = "https://docs.rs/github-copilot-sdk" readme = "README.md" license = "MIT" -exclude = [ - "RELEASING.md", - "rust-toolchain.toml", - ".rustfmt.toml", - ".rustfmt.nightly.toml", - "clippy.toml", - ".gitignore", +include = [ + "src/**/*", + "examples/**/*", + "tests/**/*", + "build.rs", + "Cargo.toml", + "README.md", + "LICENSE", + "bundled_cli_version.txt", ] [lib] name = "github_copilot_sdk" [features] -default = [] -embedded-cli = ["dep:sha2", "dep:zstd"] +default = ["bundled-cli"] +bundled-cli = ["dep:dirs", "dep:tar", "dep:flate2", "dep:zip"] derive = ["dep:schemars"] test-support = [] @@ -46,22 +48,25 @@ tokio = { version = "1", features = ["io-util", "sync", "rt", "process", "net", tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", default-features = false } tracing = "0.1" -dirs = "5" +dirs = { version = "5", optional = true } parking_lot = "0.12" regex = "1" -sha2 = { version = "0.10", optional = true } getrandom = "0.2" uuid = { version = "1", default-features = false, features = ["v4"] } -zstd = { version = "0.13", optional = true } + +[target.'cfg(windows)'.dependencies] +zip = { version = "2", default-features = false, features = ["deflate"], optional = true } + +[target.'cfg(not(windows))'.dependencies] +flate2 = { version = "1", optional = true } +tar = { version = "0.4", optional = true } [dev-dependencies] rusqlite = { version = "0.35", features = ["bundled"] } schemars = "1" serial_test = "3" tempfile = "3" -sha2 = "0.10" tokio = { version = "1", features = ["rt-multi-thread"] } -zstd = "0.13" [build-dependencies] flate2 = "1" @@ -69,4 +74,3 @@ sha2 = "0.10" tar = "0.4" ureq = { version = "2", default-features = false, features = ["tls"] } zip = { version = "2", default-features = false, features = ["deflate"] } -zstd = "0.13" diff --git a/rust/README.md b/rust/README.md index 78103e4df..f4d80fefd 100644 --- a/rust/README.md +++ b/rust/README.md @@ -18,7 +18,7 @@ use github_copilot_sdk::handler::ApproveAllHandler; # async fn example() -> Result<(), github_copilot_sdk::Error> { let client = Client::start(ClientOptions::default()).await?; let session = client.create_session( - SessionConfig::default().with_handler(Arc::new(ApproveAllHandler)), + SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler)), ).await?; let _message_id = session.send("Hello!").await?; session.disconnect().await?; @@ -50,10 +50,10 @@ The SDK manages the CLI process lifecycle: spawning, health-checking, and gracef let client = Client::start(options).await?; // Create a new session -let session = client.create_session(config.with_handler(handler)).await?; +let session = client.create_session(config.with_permission_handler(handler)).await?; // Resume an existing session -let session = client.resume_session(config.with_handler(handler)).await?; +let session = client.resume_session(config.with_permission_handler(handler)).await?; // Low-level RPC let result = client.call("method.name", Some(params)).await?; @@ -78,11 +78,11 @@ client.stop().await?; | `extra_args` | `Vec` | Extra CLI flags | | `transport` | `Transport` | `Stdio` (default), `Tcp { port }`, or `External { host, port }` | -With the default `CliProgram::Resolve`, `Client::start()` automatically resolves the binary via `github_copilot_sdk::resolve::copilot_binary()` — checking `COPILOT_CLI_PATH`, the [embedded CLI](#embedded-cli), and then the system PATH. Use `CliProgram::Path(path)` to skip resolution. +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`. ### Session -Created via `Client::create_session` or `Client::resume_session`. Owns an internal event loop that dispatches events to the `SessionHandler`. +Created via `Client::create_session` or `Client::resume_session`. Owns an internal event loop that dispatches CLI callbacks to the focused handler traits you install on `SessionConfig`, and broadcasts session events through `subscribe()`. ```rust,ignore use github_copilot_sdk::MessageOptions; @@ -101,7 +101,7 @@ let _id = session .await?; // Message history -let messages = session.get_messages().await?; +let messages = session.get_events().await?; // Abort the current agent turn session.abort().await?; @@ -176,74 +176,57 @@ New RPCs land in the namespace immediately as the schema regenerates; helpers are added on top only when an ergonomic story is worth the maintenance. -### SessionHandler +### Handler Traits -Implement this trait to control how a session responds to CLI events. Two styles are supported: +The SDK exposes five focused handler traits, one per CLI callback type. Implement only the traits you need and install each with the matching `SessionConfig` setter. Each trait has a single `async fn handle(...)` method: -**1. Per-event methods (recommended).** Override only the callbacks you care about; every method has a safe default (permission → deny, user input → none, external tool → "no handler", elicitation → cancel, exit plan → default). When no handler is installed on a session, the SDK uses `NoopHandler`, which leaves permission and external tool requests pending for manual resolution. This is the `serenity::EventHandler` pattern. +| Trait | Setter | Purpose | +| ----------------------- | --------------------------------- | --------------------------------------------- | +| `PermissionHandler` | `with_permission_handler(...)` | Approve/deny tool-use permission requests | +| `ElicitationHandler` | `with_elicitation_handler(...)` | Respond to structured elicitation prompts | +| `UserInputHandler` | `with_user_input_handler(...)` | Answer free-form / choice user-input prompts | +| `ExitPlanModeHandler` | `with_exit_plan_mode_handler(...)`| Respond when the agent exits plan mode | +| `AutoModeSwitchHandler` | `with_auto_mode_switch_handler(...)`| Respond to automatic mode-switch proposals | + +The CLI's `requestPermission` / `requestElicitation` / `requestUserInput` / etc. wire flags are derived automatically from which traits you've installed — clients that don't install a handler are silently skipped, letting another connected client handle the request. ```rust,ignore +use std::sync::Arc; use async_trait::async_trait; -use github_copilot_sdk::handler::{PermissionResult, SessionHandler}; +use github_copilot_sdk::handler::{PermissionHandler, PermissionResult}; use github_copilot_sdk::types::{PermissionRequestData, RequestId, SessionId}; -struct MyHandler; +struct MyPermissions; #[async_trait] -impl SessionHandler for MyHandler { - async fn on_permission_request( +impl PermissionHandler for MyPermissions { + async fn handle( &self, _sid: SessionId, _rid: RequestId, data: PermissionRequestData, ) -> PermissionResult { if data.extra.get("tool").and_then(|v| v.as_str()) == Some("view") { - PermissionResult::Approved + PermissionResult::approve_once() } else { - PermissionResult::Denied + PermissionResult::reject(None) } } - - async fn on_session_event(&self, sid: SessionId, event: github_copilot_sdk::types::SessionEvent) { - println!("[{sid}] {}", event.event_type); - } } + +let config = SessionConfig::default().with_permission_handler(Arc::new(MyPermissions)); ``` -**2. Single `on_event` method.** Override `on_event` directly and `match` on `HandlerEvent` — useful for logging middleware, custom routing, or when you want one exhaustive dispatch point. +A single type can implement multiple handler traits — share one `Arc` across the setters by cloning: ```rust,ignore -use github_copilot_sdk::handler::*; -use async_trait::async_trait; - -#[async_trait] -impl SessionHandler for MyRouter { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::SessionEvent { session_id, event } => { - println!("[{session_id}] {}", event.event_type); - HandlerResponse::Ok - } - HandlerEvent::PermissionRequest { .. } => { - HandlerResponse::Permission(PermissionResult::Approved) - } - HandlerEvent::UserInput { question, .. } => { - HandlerResponse::UserInput(Some(UserInputResponse { - answer: prompt_user(&question), - was_freeform: true, - })) - } - _ => HandlerResponse::Ok, - } - } -} +let h = Arc::new(MyHandler); +let config = SessionConfig::default() + .with_permission_handler(h.clone()) + .with_user_input_handler(h); ``` -The default `on_event` dispatches to the per-event methods, so overriding `on_event` short-circuits them entirely — pick one style per handler. - -Events are processed serially per session — blocking in a handler method pauses that session's event loop (which is correct, since the CLI is also waiting for the response). Other sessions are unaffected. - -> **Note:** Notification-triggered events (`PermissionRequest` via `permission.requested`, `ExternalTool` via `external_tool.requested`) are dispatched on spawned tasks and may run concurrently with the serial event loop. See the trait-level docs on `SessionHandler` for details. +The built-in `ApproveAllHandler` and `DenyAllHandler` implement `PermissionHandler` for the common cases. To observe streamed session events (assistant messages, tool calls, etc.), call `session.subscribe()` — see [Streaming](#streaming) below. ### SessionConfig @@ -254,10 +237,11 @@ let config = SessionConfig { content: Some("Always explain your reasoning.".into()), ..Default::default() }), - request_elicitation: Some(true), // enable elicitation provider ..Default::default() -}; -let session = client.create_session(config.with_handler(handler)).await?; +} +.with_elicitation_handler(Arc::new(my_elicitation_handler)) +.with_permission_handler(handler); +let session = client.create_session(config).await?; ``` ### Session Hooks @@ -300,7 +284,7 @@ impl SessionHooks for MyHooks { let session = client .create_session( config - .with_handler(handler) + .with_permission_handler(handler) .with_hooks(Arc::new(MyHooks)), ) .await?; @@ -337,22 +321,23 @@ impl SystemMessageTransform for MyTransform { let session = client .create_session( config - .with_handler(handler) - .with_transform(Arc::new(MyTransform)), + .with_permission_handler(handler) + .with_system_message_transform(Arc::new(MyTransform)), ) .await?; ``` ### Tool Registration -Define client-side tools as named types with `ToolHandler`, then route them with `ToolHandlerRouter`. Enable the `derive` feature for `schema_for::()` — it generates JSON Schema from Rust types via `schemars`. +Define client-side tools as named types implementing `ToolHandler` and attach +them to `Tool` declarations via `Tool::with_handler`, then install via +`SessionConfig::with_tools`. Enable the `derive` feature for `schema_for::()` +— it generates JSON Schema from Rust types via `schemars`. ```rust,ignore use std::sync::Arc; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{ - schema_for, tool_parameters, JsonSchema, ToolHandler, ToolHandlerRouter, -}; +use github_copilot_sdk::tool::{schema_for, JsonSchema, ToolHandler}; use github_copilot_sdk::{Error, SessionConfig, Tool, ToolInvocation, ToolResult}; use serde::Deserialize; use async_trait::async_trait; @@ -369,58 +354,48 @@ struct GetWeatherTool; #[async_trait] impl ToolHandler for GetWeatherTool { - fn tool(&self) -> Tool { - Tool { - name: "get_weather".to_string(), - namespaced_name: None, - description: "Get weather for a city".to_string(), - parameters: tool_parameters(schema_for::()), - instructions: None, - } - } - async fn call(&self, inv: ToolInvocation) -> Result { let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; Ok(ToolResult::Text(format!("Weather in {}: sunny", params.city))) } } -// Build a router that dispatches tool calls by name -let router = ToolHandlerRouter::new( - vec![Box::new(GetWeatherTool)], - Arc::new(ApproveAllHandler), -); +let tool = Tool::new("get_weather") + .with_description("Get weather for a city") + .with_parameters(schema_for::()) + .with_handler(Arc::new(GetWeatherTool)); -let config = SessionConfig { - tools: Some(router.tools()), - ..Default::default() -} -.with_handler(Arc::new(router)); +let config = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![tool]); let session = client.create_session(config).await?; ``` -Tools are named types (not closures) — visible in stack traces and navigable via "go to definition". The router implements `SessionHandler`, forwarding unrecognized tools and non-tool events to the inner handler. +Tools are named types (not closures) — visible in stack traces and navigable via "go to definition". The SDK registers each tool's handler under its `Tool::name` and surfaces the same `Tool` definitions to the CLI automatically. + +Tools without an attached handler (`Tool::with_handler` never called) are declaration-only: the SDK advertises them on the wire but doesn't dispatch invocations to anything. Useful when another connected client services the tool. -For trivial tools that don't need a named type, [`define_tool`](crate::tool::define_tool) collapses the definition to a single expression: +For trivial tools that don't need a named type, the `define_tool` helper function (available with the `derive` feature) collapses the definition to a single expression and returns a fully-formed `Tool` with handler attached: ```rust,ignore -use github_copilot_sdk::tool::{define_tool, JsonSchema, ToolHandlerRouter}; +use github_copilot_sdk::tool::{define_tool, JsonSchema}; use github_copilot_sdk::ToolResult; use serde::Deserialize; #[derive(Deserialize, JsonSchema)] struct GetWeatherParams { city: String } -let router = ToolHandlerRouter::new( - vec![define_tool( - "get_weather", - "Get weather for a city", - |_inv, params: GetWeatherParams| async move { - Ok(ToolResult::Text(format!("Sunny in {}", params.city))) - }, - )], - Arc::new(ApproveAllHandler), +let tool = define_tool( + "get_weather", + "Get weather for a city", + |_inv, params: GetWeatherParams| async move { + Ok(ToolResult::Text(format!("Sunny in {}", params.city))) + }, ); + +let config = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![tool]); ``` The closure receives the full [`ToolInvocation`](crate::types::ToolInvocation) alongside the deserialized parameters, so handlers that need `inv.session_id` or `inv.tool_call_id` for telemetry, streaming updates, or scoped lookups can use them directly. Use `_inv` when you don't need the metadata. @@ -429,13 +404,12 @@ Reach for the `ToolHandler` trait directly when you need shared state across mul ### Permission Policies -Set a permission policy directly on `SessionConfig` with the chainable builders. They wrap whatever handler you've installed (defaulting to `NoopHandler` if none) so only permission requests are intercepted; every other event flows through unchanged. +Set a permission policy directly on `SessionConfig` with the chainable builders. They install a synthesized `PermissionHandler` so only permission requests are intercepted; every other event flows through unchanged. ```rust,ignore let session = client .create_session( SessionConfig::default() - .with_handler(Arc::new(my_handler)) .approve_all_permissions(), // or .deny_all_permissions() // or .approve_permissions_if(|data| { @@ -445,54 +419,86 @@ let session = client .await?; ``` -> Call the policy method **after** `with_handler` — `with_handler` overwrites the handler field, so `approve_all_permissions().with_handler(...)` discards the wrap. +> The policy builders set the permission handler slot directly; they're equivalent to calling `with_permission_handler(...)` with the corresponding built-in (`ApproveAllHandler`, `DenyAllHandler`, or `permission::approve_if(...)`). -For composing a policy onto a handler outside the builder chain (e.g. when wrapping a `ToolHandlerRouter` you've built elsewhere), the `permission` module exposes the same primitives as free functions: +The `permission` module also exposes the policy primitives as standalone helpers for the rare case where you want to construct the handler value separately and install it via `with_permission_handler`: ```rust,ignore use github_copilot_sdk::permission; -let router = ToolHandlerRouter::new(tools, Arc::new(MyHandler)); -let handler = permission::approve_all(Arc::new(router)); -// or permission::deny_all(...) / permission::approve_if(..., predicate) +let handler = permission::approve_if(|data| { + data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") +}); +// or permission::approve_all() / permission::deny_all() -let session = client.create_session(config.with_handler(handler)).await?; +let session = client + .create_session(config.with_permission_handler(handler)) + .await?; ``` -### Capabilities & Elicitation +### Elicitation -The SDK negotiates capabilities with the CLI after session creation. Enable elicitation to let the agent present structured UI dialogs (forms, URL prompts) to the user. +To opt your client into receiving `elicitation.requested` broadcasts, install an `ElicitationHandler` on the session config. The wire flag `requestElicitation` is derived from the presence of the handler; clients without one are silently skipped, allowing other connected clients on the same CLI to handle the request. ```rust,ignore -let config = SessionConfig { - request_elicitation: Some(true), - ..Default::default() -}; +use async_trait::async_trait; +use github_copilot_sdk::handler::{ElicitationHandler, ElicitationResult}; +use github_copilot_sdk::types::{ElicitationRequest, RequestId, SessionId}; + +struct MyElicitation; + +#[async_trait] +impl ElicitationHandler for MyElicitation { + async fn handle( + &self, + _sid: SessionId, + _rid: RequestId, + _request: ElicitationRequest, + ) -> ElicitationResult { + ElicitationResult::cancel() + } +} + +let config = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_elicitation_handler(Arc::new(MyElicitation)); ``` -The handler receives `HandlerEvent::ElicitationRequest` with a message, optional JSON Schema for form fields, and an optional mode. Known modes include `Form` and `Url`, but the mode may be absent or an unknown future value. Return `HandlerResponse::Elicitation(result)`. +The handler receives a message, optional JSON Schema for form fields, and an optional mode. Known modes include `Form` and `Url`, but the mode may be absent or an unknown future value. ### User Input Requests -Some sessions ask the user free-form questions (or multiple-choice prompts) outside the elicitation flow. Implement `SessionHandler::on_user_input` and the SDK will forward `userInput.request` callbacks: +Some sessions ask the user free-form questions (or multiple-choice prompts) outside the elicitation flow. Install a `UserInputHandler` and the SDK will forward `userInput.request` callbacks: ```rust,ignore -async fn on_user_input( - &self, - _session_id: SessionId, - question: String, - choices: Option>, - _allow_freeform: Option, -) -> Option { - // Render `question` + `choices` to your UI, then: - Some(UserInputResponse { - answer: "Yes".to_string(), - was_freeform: false, - }) +use async_trait::async_trait; +use github_copilot_sdk::handler::{UserInputHandler, UserInputResponse}; +use github_copilot_sdk::types::SessionId; + +struct MyUserInput; + +#[async_trait] +impl UserInputHandler for MyUserInput { + async fn handle( + &self, + _sid: SessionId, + question: String, + _choices: Option>, + _allow_freeform: Option, + ) -> Option { + // Render `question` + `choices` to your UI, then: + Some(UserInputResponse { + answer: "Yes".to_string(), + was_freeform: false, + }) + } } + +let config = SessionConfig::default() + .with_user_input_handler(Arc::new(MyUserInput)); ``` -Return `None` to signal "no answer available" (the CLI falls back to its own prompt). Enable via `SessionConfig::request_user_input` (defaults to `Some(true)`). +Return `None` to signal "no answer available" (the CLI falls back to its own prompt). ### Slash Commands @@ -664,8 +670,9 @@ ergonomics the dynamically-typed SDKs don't. [`SessionConfig::with_session_fs_provider`]. The factory pattern doesn't cleanly express in Rust at the session-config call site — there is no `Session` value to thread in, and the SDK already prefers traits over - boxed closures for handler-shaped APIs (`SessionHandler`, `SessionHooks`, - `ToolHandler`). + boxed closures for handler-shaped APIs (`PermissionHandler`, `ToolHandler`, + `SessionHooks`, + `SystemMessageTransform`). ```rust,ignore use std::sync::Arc; @@ -682,7 +689,7 @@ let client = Client::start(options).await?; let session = client .create_session( SessionConfig::default() - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_session_fs_provider(Arc::new(MyProvider::new())), ) .await?; @@ -691,6 +698,15 @@ let session = client See [`examples/session_fs.rs`](examples/session_fs.rs) for a complete in-memory provider implementation. +- **Canvas action dispatch is a single trait method, not per-action closures.** + The Node SDK binds an optional `handler` closure on each entry of a canvas's + `actions[]`. The Rust SDK exposes + [`CanvasHandler::on_action`](crate::canvas::CanvasHandler::on_action) and expects the implementor to match on + `ctx.action_name`. Same reasoning as `SessionFsProvider`: per-callback + `Box` fields fight `Send + Sync + 'static` and skip exhaustiveness + checks, and the SDK prefers trait + default-impl methods for handler-shaped + extension points. + ### Rust-only API A handful of conveniences exist only on the Rust SDK as of 0.1.0. These @@ -705,9 +721,9 @@ none of them are scheduled for removal. identifier from an arbitrary `String` at compile time. Node/Python/Go use bare strings. - **Permission policy builders** — `permission::approve_all`, - `permission::deny_all`, and `permission::approve_if(handler, predicate)` - in `crate::permission` provide composable, no-handler-needed permission - shortcuts that wrap an existing `SessionHandler`. Other SDKs require a + `permission::deny_all`, and `permission::approve_if(predicate)` + in `crate::permission` provide composable, no-handler-needed + `PermissionHandler` shortcuts. Other SDKs require a full handler implementation for these patterns. - **`Client::from_streams`** — connect to a CLI server over arbitrary caller-supplied `AsyncRead` / `AsyncWrite`. Useful for testing, @@ -728,39 +744,61 @@ none of them are scheduled for removal. | `lib.rs` | `Client`, `ClientOptions`, `CliProgram`, `Transport`, `Error` | | `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` | `SessionHandler` trait, `HandlerEvent`/`HandlerResponse` enums, `ApproveAllHandler`, `DenyAllHandler`, `NoopHandler` | +| `handler.rs` | `PermissionHandler`, `ElicitationHandler`, `UserInputHandler`, `ExitPlanModeHandler`, `AutoModeSwitchHandler` traits; `ApproveAllHandler`, `DenyAllHandler` | | `hooks.rs` | `SessionHooks` trait, `HookEvent`/`HookOutput` enums, typed hook inputs/outputs | | `transforms.rs` | `SystemMessageTransform` trait, section-level system message customization | -| `tool.rs` | `ToolHandler` trait, `ToolHandlerRouter`, `schema_for::()` (with `derive` feature) | +| `tool.rs` | `ToolHandler` trait, `define_tool`, `schema_for::()` (with `derive` feature) | | `types.rs` | CLI protocol types (`SessionId`, `SessionEvent`, `SessionConfig`, `Tool`, etc.) | -| `resolve.rs` | Binary resolution (`copilot_binary`, `node_binary`, `extended_path`) | -| `embeddedcli.rs` | Embedded CLI extraction (`embedded-cli` feature) | +| `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 | | `jsonrpc.rs` | Internal Content-Length framed JSON-RPC transport | ## Embedded CLI -By default, `copilot_binary()` searches `COPILOT_CLI_PATH`, the system PATH, and common install locations. To **ship with a specific CLI version** embedded in the binary, set `COPILOT_CLI_VERSION` at build time: +The SDK bundles the Copilot CLI binary inside the consumer's compiled crate by default. No env var setup, no separate install — just `cargo build` and you get a self-contained binary. + +To opt out (e.g. for binary-size-sensitive consumers, or environments that provide the CLI via PATH), set `default-features = false`: -```bash -COPILOT_CLI_VERSION=1.0.15 cargo build +```toml +github-copilot-sdk = { version = "0.1", default-features = false } ``` ### How it works -1. **Build time:** The SDK's `build.rs` detects `COPILOT_CLI_VERSION`, downloads the platform-appropriate archive from the [`github/copilot-cli` GitHub Releases](https://github.com/github/copilot-cli/releases) (`copilot-{platform}.tar.gz` on macOS/Linux, `.zip` on Windows), verifies the archive's SHA-256 against the release's `SHA256SUMS.txt`, extracts the `copilot` binary, compresses it with zstd, and embeds via `include_bytes!()`. No extra steps or tools needed — just the env var. +1. **Pinned at publish time.** When the rust crate is published, a workflow step writes `bundled_cli_version.txt` (CLI version + per-platform SHA-256 hashes) into the crate from the in-effect `nodejs/package-lock.json` and the matching GitHub Release's `SHA256SUMS.txt`. This file is gitignored locally; it only exists in the published crate tarball. + +2. **Build time:** The SDK's `build.rs` resolves the version + per-platform SHA-256: + - `COPILOT_CLI_VERSION` env var (advanced override; fetches live `SHA256SUMS.txt`). + - Otherwise, `bundled_cli_version.txt` from the published crate. + - Otherwise (mono-repo contributor build), live read from `../nodejs/package-lock.json` + live fetch of `SHA256SUMS.txt`. + + It then downloads the platform-appropriate archive from the [`github/copilot-cli` GitHub Releases](https://github.com/github/copilot-cli/releases) (`copilot-{platform}.tar.gz` on macOS/Linux, `.zip` on Windows), verifies the SHA-256, extracts the `copilot` binary, compresses it with zstd, and embeds via `include_bytes!()`. + +3. **Runtime:** On the first call to `github_copilot_sdk::Client::start()`, the embedded archive is lazily extracted to the platform cache dir (`%LOCALAPPDATA%\github-copilot-sdk-{version}\` on Windows, `~/Library/Caches/github-copilot-sdk-{version}/` on macOS, `$XDG_CACHE_HOME/github-copilot-sdk-{version}/` (or `~/.cache/...`) on Linux). Subsequent runs reuse the extracted binary. -2. **Runtime:** On the first call to `github_copilot_sdk::resolve::copilot_binary()`, the embedded binary is lazily extracted to `~/.cache/github-copilot-sdk-{version}/copilot` (or `copilot.exe` on Windows), SHA-256 verified, and cached. Subsequent calls return the cached path. +### Overriding the extraction location -3. **Dev builds:** Without the env var, `build.rs` does nothing. The binary is resolved from PATH as usual — zero friction. +Use [`ClientOptions::with_bundled_cli_extract_dir`] when you need to place the extracted binary somewhere other than the platform cache dir (CI runners with ephemeral homes, sandboxes that disallow cache paths, etc.): + +```rust,ignore +use std::path::PathBuf; +use github_copilot_sdk::{Client, ClientOptions}; + +let options = ClientOptions::new() + .with_bundled_cli_extract_dir(PathBuf::from("/var/run/my-app/copilot")); +let client = Client::start(options).await?; +``` ### Resolution priority `copilot_binary()` checks these sources in order: -1. `COPILOT_CLI_PATH` environment variable -2. Embedded CLI (build-time, via `COPILOT_CLI_VERSION`) -3. System PATH + common install locations +1. Explicit `CliProgram::Path(path)` on `ClientOptions::program` +2. `COPILOT_CLI_PATH` environment variable +3. Embedded CLI (when the `bundled-cli` feature is enabled, which it is by default) + +There is no PATH scanning. If both 1+2 are unset and the SDK was built with `default-features = false`, `Client::start` returns `Error::BinaryNotFound`. ### Platforms @@ -768,26 +806,21 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64` ## Features -No features are enabled by default — the bare SDK resolves the CLI from `COPILOT_CLI_PATH` or the system PATH without pulling in additional feature-gated dependencies. - | Feature | Default | Description | | -------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `embedded-cli` | — | Build-time CLI embedding via `COPILOT_CLI_VERSION` (adds `sha2`, `zstd`). Enable when you need to ship a self-contained binary with a pinned CLI version. | +| `bundled-cli` | ✓ | Build-time CLI embedding. Pulls in `dirs`, `tar`+`flate2` (Linux/macOS), or `zip` (Windows). Disable via `default-features = false` to opt out (e.g. when shipping a smaller binary or when always supplying the CLI via `CliProgram::Path` / `COPILOT_CLI_PATH`). | | `derive` | — | `schema_for::()` for generating JSON Schema from Rust types (adds `schemars`). Enable when defining [tool parameters](#tool-registration). | ```toml # These examples use registry syntax for illustration; until the crate is # published, use a path or git dependency instead. -# Minimal — resolve CLI from PATH +# Default — bundles the Copilot CLI in your binary. github-copilot-sdk = "0.1" -# Ship a pinned CLI version in your binary -github-copilot-sdk = { version = "0.1", features = ["embedded-cli"] } +# Opt out of bundling — resolve CLI from COPILOT_CLI_PATH or system PATH instead. +github-copilot-sdk = { version = "0.1", default-features = false } -# Derive JSON Schema for tool parameters +# Derive JSON Schema for tool parameters (adds to default bundled-cli). github-copilot-sdk = { version = "0.1", features = ["derive"] } - -# Both -github-copilot-sdk = { version = "0.1", features = ["embedded-cli", "derive"] } ``` diff --git a/rust/build.rs b/rust/build.rs index 22463c9a9..c64dbff9b 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -9,75 +9,66 @@ fn main() { println!("cargo:rerun-if-env-changed=BUNDLED_CLI_CACHE_DIR"); println!("cargo::rustc-check-cfg=cfg(has_bundled_cli)"); - let Ok(version) = std::env::var("COPILOT_CLI_VERSION") else { + // The `bundled-cli` cargo feature gates bundling at the build-system level. + // When disabled (e.g. via `default-features = false`), runtime archive + // helpers (tar/flate2/zip) are not in the graph and no download happens. + if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_none() { return; - }; + } + + println!("cargo:rerun-if-changed=bundled_cli_version.txt"); + println!("cargo:rerun-if-changed=../nodejs/package-lock.json"); let Some(platform) = target_platform() else { - println!( - "cargo:warning=COPILOT_CLI_VERSION set but unsupported target platform, skipping CLI bundling" - ); + println!("cargo:warning=Unsupported target platform for Copilot CLI bundling — skipping"); return; }; let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is always set by cargo"); let out = Path::new(&out_dir); + // Resolve version + per-asset SHA-256 from one of three sources, in order: + // 1. `COPILOT_CLI_VERSION` env-var override (live SHA256SUMS.txt fetch) + // 2. `bundled_cli_version.txt` snapshot at the crate root (published-crate + // consumer; generated by the publish workflow) + // 3. Sibling `../nodejs/package-lock.json` (mono-repo contributor build; + // live SHA256SUMS.txt fetch) + let (version, expected_hash) = resolve_version_and_hash(platform.asset_name); + let base_url = format!("https://github.com/github/copilot-cli/releases/download/v{version}"); let cache_dir = std::env::var("BUNDLED_CLI_CACHE_DIR") .ok() .map(std::path::PathBuf::from); - // Download SHA256SUMS and find the expected hash for our platform's tarball. let asset_name = platform.asset_name; - println!("cargo:warning=Bundling GitHub Copilot CLI v{version} ({asset_name})"); - // Download checksums and find the expected hash for our platform's archive. - let checksums_url = format!("{base_url}/SHA256SUMS.txt"); - let checksums = download_with_retry(&checksums_url); - let checksums_text = - std::str::from_utf8(&checksums).expect("checksums file is not valid UTF-8"); - let expected_hash = find_sha256_for_asset(checksums_text, asset_name); - - // Use a versioned cache key since copilot asset names don't include the version. + // Versioned cache key since copilot asset names don't include the version. let cache_key = format!("v{version}-{asset_name}"); - // Download the archive (or read from cache) and verify integrity. + // Download the archive (or read from cache) and verify SHA-256. The raw + // archive is what gets embedded — extraction happens at runtime. Quiet on + // cache hit; logs `Downloading` + `Caching archive at` on cache miss. let archive = cached_download( &format!("{base_url}/{asset_name}"), &cache_key, &expected_hash, &cache_dir, ); - println!("cargo:warning=SHA-256 verified ({expected_hash})"); - - // Extract the binary from the archive. - let binary = extract_binary(&archive, platform.binary_name, platform.is_zip); - println!( - "cargo:warning=Extracted {} ({} bytes)", - platform.binary_name, - binary.len() - ); - // Compress and embed. - let hash = sha256(&binary); - let compressed = zstd::encode_all(&binary[..], 19).expect("zstd compression failed"); - println!( - "cargo:warning=Compressed to {} bytes ({:.1}%)", - compressed.len(), - compressed.len() as f64 / binary.len() as f64 * 100.0 - ); + // Sanity check: the runtime extraction path expects `binary_name` inside + // the archive. Fail the build now (with a clear message) rather than + // shipping a broken bundle if the upstream archive layout ever changes. + verify_binary_present_in_archive(&archive, platform.binary_name, asset_name); - std::fs::write(out.join("copilot_cli.zst"), &compressed) - .expect("failed to write copilot_cli.zst"); + std::fs::write(out.join("copilot_cli.archive"), &archive) + .expect("failed to write copilot_cli.archive"); - let hash_tokens: Vec = hash.iter().map(|b| format!("0x{b:02x}")).collect(); let generated = format!( r#"// Auto-generated by github-copilot-sdk build.rs. Do not edit. -pub(super) static CLI_BYTES: &[u8] = include_bytes!("copilot_cli.zst"); -pub(super) static CLI_HASH: [u8; 32] = [{}]; +pub(super) static CLI_ARCHIVE: &[u8] = include_bytes!("copilot_cli.archive"); pub(super) static CLI_VERSION: &str = "{version}"; +pub(super) static CLI_BINARY_NAME: &str = "{binary_name}"; "#, - hash_tokens.join(", ") + binary_name = platform.binary_name, ); std::fs::write(out.join("bundled_cli.rs"), generated).expect("failed to write bundled_cli.rs"); @@ -85,10 +76,113 @@ pub(super) static CLI_VERSION: &str = "{version}"; println!("cargo:rustc-cfg=has_bundled_cli"); } +/// Resolve the CLI version and the expected SHA-256 hash for the current +/// target's archive. Picks one of three sources in order. Panics with a clear +/// error if none are available. +fn resolve_version_and_hash(asset_name: &str) -> (String, String) { + // 1. Env-var override — fetches live SHA256SUMS for the overridden version. + if let Ok(version) = std::env::var("COPILOT_CLI_VERSION") { + let hash = fetch_live_sha256(&version, asset_name); + return (version, hash); + } + + // 2. Snapshot file at the crate root (published-crate consumer). + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); + let snapshot = Path::new(&manifest_dir).join("bundled_cli_version.txt"); + if snapshot.is_file() { + let contents = std::fs::read_to_string(&snapshot) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", snapshot.display())); + return parse_snapshot(&contents, asset_name) + .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); + } + + // 3. Mono-repo lockfile — read version, fetch live SHA256SUMS. + let lockfile = Path::new(&manifest_dir) + .join("..") + .join("nodejs") + .join("package-lock.json"); + if lockfile.is_file() { + let version = read_version_from_package_lock(&lockfile); + let hash = fetch_live_sha256(&version, asset_name); + return (version, hash); + } + + panic!( + "Could not resolve the Copilot CLI version to bundle.\n\ + Tried:\n\ + - COPILOT_CLI_VERSION env var (unset)\n\ + - {} (missing)\n\ + - {} (missing)\n\ + To opt out of bundling, set `default-features = false` on the github-copilot-sdk dependency.", + snapshot.display(), + lockfile.display(), + ); +} + +/// Parse the `bundled_cli_version.txt` snapshot file. Format is one +/// `key=value` per line. The first line is `version=X.Y.Z`; subsequent lines +/// map asset filename to hex SHA-256. Blank lines and lines starting with `#` +/// are skipped. +fn parse_snapshot(contents: &str, asset_name: &str) -> Result<(String, String), String> { + let mut version: Option = None; + let mut hash: Option = None; + for (line_no, raw) in contents.lines().enumerate() { + let line = raw.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let (key, value) = line + .split_once('=') + .ok_or_else(|| format!("line {}: expected `key=value`, got `{raw}`", line_no + 1))?; + match key.trim() { + "version" => version = Some(value.trim().to_string()), + k if k == asset_name => hash = Some(value.trim().to_string()), + _ => {} + } + } + let version = version.ok_or("missing `version=` line")?; + let hash = hash.ok_or_else(|| format!("missing hash for asset `{asset_name}`"))?; + Ok((version, hash)) +} + +/// Read the `@github/copilot` version from `nodejs/package-lock.json`. +fn read_version_from_package_lock(path: &Path) -> String { + let contents = std::fs::read_to_string(path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); + // Minimal JSON walk: find `"node_modules/@github/copilot"` object and + // its `"version"` field. Full JSON parsing keeps build.rs dep-light by + // using a regex; the file is generated by npm and we're matching an + // exact key path. + let key = "\"node_modules/@github/copilot\""; + let key_pos = contents + .find(key) + .unwrap_or_else(|| panic!("{} does not contain {key}", path.display())); + let after_key = &contents[key_pos + key.len()..]; + let version_key = "\"version\""; + let v_pos = after_key + .find(version_key) + .unwrap_or_else(|| panic!("no `version` field found near {key} in {}", path.display())); + let after_v = &after_key[v_pos + version_key.len()..]; + let q1 = after_v.find('"').expect("malformed version"); + let after_q1 = &after_v[q1 + 1..]; + let q2 = after_q1.find('"').expect("malformed version"); + after_q1[..q2].to_string() +} + +/// Fetch the live `SHA256SUMS.txt` for the given version from GitHub Releases +/// and pluck out the entry for `asset_name`. +fn fetch_live_sha256(version: &str, asset_name: &str) -> String { + let base_url = format!("https://github.com/github/copilot-cli/releases/download/v{version}"); + let checksums_url = format!("{base_url}/SHA256SUMS.txt"); + let checksums = download_with_retry(&checksums_url); + let checksums_text = + std::str::from_utf8(&checksums).expect("checksums file is not valid UTF-8"); + find_sha256_for_asset(checksums_text, asset_name) +} + struct Platform { asset_name: &'static str, binary_name: &'static str, - is_zip: bool, } fn target_platform() -> Option { @@ -99,32 +193,26 @@ fn target_platform() -> Option { ("macos", "aarch64") => Some(Platform { asset_name: "copilot-darwin-arm64.tar.gz", binary_name: "copilot", - is_zip: false, }), ("macos", "x86_64") => Some(Platform { asset_name: "copilot-darwin-x64.tar.gz", binary_name: "copilot", - is_zip: false, }), ("linux", "x86_64") => Some(Platform { asset_name: "copilot-linux-x64.tar.gz", binary_name: "copilot", - is_zip: false, }), ("linux", "aarch64") => Some(Platform { asset_name: "copilot-linux-arm64.tar.gz", binary_name: "copilot", - is_zip: false, }), ("windows", "x86_64") => Some(Platform { asset_name: "copilot-win32-x64.zip", binary_name: "copilot.exe", - is_zip: true, }), ("windows", "aarch64") => Some(Platform { asset_name: "copilot-win32-arm64.zip", binary_name: "copilot.exe", - is_zip: true, }), _ => None, } @@ -145,10 +233,7 @@ fn cached_download( if cached_path.is_file() { match std::fs::read(&cached_path) { Ok(data) if hex_sha256(&data) == expected_hash => { - println!( - "cargo:warning=Using cached archive: {}", - cached_path.display() - ); + // Silent cache hit — nothing to surface. return data; } Ok(_) => { @@ -165,6 +250,7 @@ fn cached_download( } } + println!("cargo:warning=Downloading {url}"); let data = download_with_retry(url); let actual_hash = hex_sha256(&data); if actual_hash != expected_hash { @@ -182,13 +268,12 @@ fn cached_download( ); } else { let cached_path = dir.join(cache_key); + println!("cargo:warning=Caching archive at {}", cached_path.display()); if let Err(e) = std::fs::write(&cached_path, &data) { println!( "cargo:warning=Failed to write cache file {}: {e}", cached_path.display() ); - } else { - println!("cargo:warning=Cached archive to: {}", cached_path.display()); } } } @@ -278,61 +363,63 @@ fn find_sha256_for_asset(sums: &str, asset_name: &str) -> String { panic!("SHA256SUMS.txt does not contain an entry for {asset_name}"); } -fn extract_binary(archive_bytes: &[u8], binary_name: &str, is_zip: bool) -> Vec { - if is_zip { - extract_from_zip(archive_bytes, binary_name) +fn sha256(data: &[u8]) -> [u8; 32] { + let mut hasher = sha2::Sha256::new(); + hasher.update(data); + hasher.finalize().into() +} + +/// Walks the downloaded archive at build time to confirm an entry matching +/// `binary_name` exists. Panics with a clear message if not — defends against +/// silent breakage if the upstream archive layout ever changes. +fn verify_binary_present_in_archive(archive: &[u8], binary_name: &str, asset_name: &str) { + let found = if asset_name.ends_with(".zip") { + archive_contains_zip_entry(archive, binary_name) } else { - extract_from_tarball(archive_bytes, binary_name) + archive_contains_tar_entry(archive, binary_name) + }; + if !found { + panic!( + "Copilot CLI archive `{asset_name}` does not contain an entry named `{binary_name}`. \ + The upstream archive 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." + ); } } -fn extract_from_tarball(tarball: &[u8], binary_name: &str) -> Vec { - let gz = flate2::read::GzDecoder::new(tarball); +fn archive_contains_tar_entry(targz: &[u8], binary_name: &str) -> bool { + let gz = flate2::read::GzDecoder::new(targz); let mut archive = tar::Archive::new(gz); - - for entry in archive.entries().expect("failed to read tarball entries") { - let mut entry = entry.expect("failed to read tarball entry"); - let path = entry - .path() - .expect("entry has no path") - .to_string_lossy() - .to_string(); - if path == binary_name || path.ends_with(&format!("/{binary_name}")) { - let mut bytes = Vec::new(); - entry - .read_to_end(&mut bytes) - .expect("failed to read binary from tarball"); - return bytes; + let Ok(entries) = archive.entries() else { + return false; + }; + for entry in entries.flatten() { + let Ok(path) = entry.path() else { + continue; + }; + let name = path.to_string_lossy(); + if name == binary_name || name.ends_with(&format!("/{binary_name}")) { + return true; } } - - panic!("'{binary_name}' not found in tarball"); + false } -fn extract_from_zip(zip_bytes: &[u8], binary_name: &str) -> Vec { - // Minimal zip extraction — find the binary by name. - // The Windows assets are .zip files with just copilot.exe at the root. +fn archive_contains_zip_entry(zip_bytes: &[u8], binary_name: &str) -> bool { let cursor = std::io::Cursor::new(zip_bytes); - let mut archive = zip::ZipArchive::new(cursor).expect("failed to read zip archive"); - + let Ok(mut archive) = zip::ZipArchive::new(cursor) else { + return false; + }; for i in 0..archive.len() { - let mut file = archive.by_index(i).expect("failed to read zip entry"); - let name = file.name().to_string(); + let Ok(entry) = archive.by_index(i) else { + continue; + }; + let name = entry.name(); if name == binary_name || name.ends_with(&format!("/{binary_name}")) { - let mut bytes = Vec::new(); - file.read_to_end(&mut bytes) - .expect("failed to read binary from zip"); - return bytes; + return true; } } - - panic!("'{binary_name}' not found in zip"); -} - -fn sha256(data: &[u8]) -> [u8; 32] { - let mut hasher = sha2::Sha256::new(); - hasher.update(data); - hasher.finalize().into() + false } fn hex_sha256(data: &[u8]) -> String { diff --git a/rust/examples/chat.rs b/rust/examples/chat.rs index 37293c6bc..6b361fdea 100644 --- a/rust/examples/chat.rs +++ b/rust/examples/chat.rs @@ -13,39 +13,29 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; -use github_copilot_sdk::handler::{ - HandlerEvent, HandlerResponse, PermissionResult, SessionHandler, UserInputResponse, -}; -use github_copilot_sdk::types::{MessageOptions, SessionConfig, SessionEvent}; +use github_copilot_sdk::handler::{ApproveAllHandler, UserInputHandler, UserInputResponse}; +use github_copilot_sdk::types::{MessageOptions, SessionConfig, SessionEvent, SessionId}; use github_copilot_sdk::{Client, ClientOptions}; -/// Handler that prints assistant message deltas as they stream in -/// and auto-approves permissions. -struct ChatHandler; +/// User input handler that prompts on stdin. +struct StdinUserInputHandler; #[async_trait] -impl SessionHandler for ChatHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::SessionEvent { event, .. } => { - print_event(&event); - HandlerResponse::Ok - } - HandlerEvent::PermissionRequest { .. } => { - HandlerResponse::Permission(PermissionResult::Approved) - } - HandlerEvent::UserInput { question, .. } => { - // Prompt the user on behalf of the agent. - print!("\n[agent asks] {question}\n> "); - io::stdout().flush().ok(); - let answer = read_line().unwrap_or_default(); - HandlerResponse::UserInput(Some(UserInputResponse { - answer, - was_freeform: true, - })) - } - _ => HandlerResponse::Ok, - } +impl UserInputHandler for StdinUserInputHandler { + async fn handle( + &self, + _session_id: SessionId, + question: String, + _choices: Option>, + _allow_freeform: Option, + ) -> Option { + print!("\n[agent asks] {question}\n> "); + io::stdout().flush().ok(); + let answer = read_line()?; + Some(UserInputResponse { + answer, + was_freeform: true, + }) } } @@ -91,9 +81,11 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { let client = Client::start(ClientOptions::default()).await?; let config = { - let mut cfg = SessionConfig::default(); + let mut cfg = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_user_input_handler(Arc::new(StdinUserInputHandler)); cfg.streaming = Some(true); - cfg.with_handler(Arc::new(ChatHandler)) + cfg }; let session = client.create_session(config).await?; @@ -102,6 +94,14 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { session.id() ); + // Spawn a task to print streamed assistant deltas as session events arrive. + let mut events = session.subscribe(); + tokio::spawn(async move { + while let Ok(event) = events.recv().await { + print_event(&event); + } + }); + loop { print!("> "); io::stdout().flush().ok(); @@ -117,6 +117,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } println!("\nGoodbye."); - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/rust/examples/hooks.rs b/rust/examples/hooks.rs index f3e7e1f24..7c5dbbeaf 100644 --- a/rust/examples/hooks.rs +++ b/rust/examples/hooks.rs @@ -103,7 +103,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { // hooks: true is set automatically when a hooks handler is provided. let config = SessionConfig::default() - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_hooks(Arc::new(AuditHooks)); let session = client.create_session(config).await?; @@ -128,6 +128,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { println!("\n{text}"); } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/rust/examples/lifecycle_observer.rs b/rust/examples/lifecycle_observer.rs index 612792073..8edb2cd38 100644 --- a/rust/examples/lifecycle_observer.rs +++ b/rust/examples/lifecycle_observer.rs @@ -39,7 +39,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { let client = Client::start(ClientOptions::default()).await?; - println!("[client] state: {:?}", client.state()); + println!("[client] started, pid: {:?}", client.pid()); // Wildcard lifecycle subscriber: see every session.lifecycle event, // counting deletions inline by filtering on event_type. @@ -63,9 +63,9 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } }); - let config = SessionConfig::default().with_handler(Arc::new(ApproveAllHandler)); + let config = SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; - println!("[client] state after create: {:?}", client.state()); + println!("[client] session created: {}", session.id()); // Per-session observer: see every assistant message, tool call, etc. // Subscribers fire alongside the constructor handler; they're great for @@ -97,13 +97,13 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { ) .await?; - session.destroy().await?; + session.disconnect().await?; // Synchronous shutdown — useful in panicking-cleanup paths or tests // where you don't have an async runtime available to await `stop()`. // For graceful shutdown in normal flow, prefer `client.stop().await`. client.force_stop(); - println!("[client] state after force_stop: {:?}", client.state()); + println!("[client] force-stopped"); // Stopping the client closes the broadcast senders, so the consumer // tasks observe `RecvError::Closed` and exit cleanly. diff --git a/rust/examples/session_fs.rs b/rust/examples/session_fs.rs index 0dbbb3414..924e6947f 100644 --- a/rust/examples/session_fs.rs +++ b/rust/examples/session_fs.rs @@ -125,7 +125,7 @@ async fn main() -> Result<(), Box> { let session = client .create_session( SessionConfig::default() - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_session_fs_provider(provider), ) .await?; diff --git a/rust/examples/tool_server.rs b/rust/examples/tool_server.rs index 55bacbbe6..93492d20c 100644 --- a/rust/examples/tool_server.rs +++ b/rust/examples/tool_server.rs @@ -30,9 +30,7 @@ use async_trait::async_trait; #[cfg(feature = "derive")] use github_copilot_sdk::handler::ApproveAllHandler; #[cfg(feature = "derive")] -use github_copilot_sdk::tool::{ - JsonSchema, ToolHandler, ToolHandlerRouter, schema_for, tool_parameters, -}; +use github_copilot_sdk::tool::{JsonSchema, ToolHandler, schema_for}; #[cfg(feature = "derive")] use github_copilot_sdk::types::{MessageOptions, SessionConfig, Tool, ToolInvocation, ToolResult}; #[cfg(feature = "derive")] @@ -59,14 +57,6 @@ struct GetWeatherTool; #[cfg(feature = "derive")] #[async_trait] impl ToolHandler for GetWeatherTool { - fn tool(&self) -> Tool { - let mut tool = Tool::default(); - tool.name = "get_weather".to_string(); - tool.description = "Get the current weather for a city.".to_string(); - tool.parameters = tool_parameters(schema_for::()); - tool - } - async fn call(&self, invocation: ToolInvocation) -> Result { let params: GetWeatherParams = serde_json::from_value(invocation.arguments)?; let unit = params.unit.as_deref().unwrap_or("celsius"); @@ -90,20 +80,6 @@ struct RollDiceTool; #[cfg(feature = "derive")] #[async_trait] impl ToolHandler for RollDiceTool { - fn tool(&self) -> Tool { - let mut tool = Tool::default(); - tool.name = "roll_dice".to_string(); - tool.description = "Roll one or more dice and return the total.".to_string(); - tool.parameters = tool_parameters(serde_json::json!({ - "type": "object", - "properties": { - "sides": { "type": "integer", "description": "Number of sides per die (default 6, max 1000)." }, - "count": { "type": "integer", "description": "Number of dice to roll (default 1, max 100)." } - } - })); - tool - } - async fn call(&self, invocation: ToolInvocation) -> Result { let sides = invocation .arguments @@ -145,20 +121,26 @@ impl ToolHandler for RollDiceTool { #[cfg(feature = "derive")] #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let router = ToolHandlerRouter::new( - vec![Box::new(GetWeatherTool), Box::new(RollDiceTool)], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); - let handler = Arc::new(router); - let client = Client::start(ClientOptions::default()).await?; - let config = { - let mut cfg = SessionConfig::default(); - cfg.tools = Some(tools); - cfg.with_handler(handler) - }; + let config = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![ + Tool::new("get_weather") + .with_description("Get the current weather for a city.") + .with_parameters(schema_for::()) + .with_handler(Arc::new(GetWeatherTool)), + Tool::new("roll_dice") + .with_description("Roll one or more dice and return the total.") + .with_parameters(serde_json::json!({ + "type": "object", + "properties": { + "sides": { "type": "integer", "description": "Number of sides per die (default 6, max 1000)." }, + "count": { "type": "integer", "description": "Number of dice to roll (default 1, max 100)." } + } + })) + .with_handler(Arc::new(RollDiceTool)), + ]); let session = client.create_session(config).await?; println!( @@ -182,6 +164,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { println!("{text}"); } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/rust/scripts/snapshot-bundled-cli-version.sh b/rust/scripts/snapshot-bundled-cli-version.sh new file mode 100644 index 000000000..b1eb1a2a1 --- /dev/null +++ b/rust/scripts/snapshot-bundled-cli-version.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# Snapshot the Copilot CLI version + per-platform SHA-256 hashes for the +# rust crate's bundled-CLI build.rs. Runs at SDK publish time, mirroring +# how .NET's _GenerateVersionProps BeforeTargets="Pack" target writes +# GitHub.Copilot.SDK.props before NuGet packing. +# +# Inputs: +# - ../nodejs/package-lock.json (sibling) - source of the pinned version. +# - https://github.com/github/copilot-cli/releases/v{version}/SHA256SUMS.txt - +# authoritative per-platform hashes. +# +# Output: +# - bundled_cli_version.txt (in the rust crate root). Gitignored. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUST_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +REPO_ROOT="$(cd "${RUST_DIR}/.." && pwd)" +LOCKFILE="${REPO_ROOT}/nodejs/package-lock.json" +OUTPUT="${RUST_DIR}/bundled_cli_version.txt" + +if [[ ! -f "${LOCKFILE}" ]]; then + echo "error: ${LOCKFILE} not found" >&2 + exit 1 +fi + +VERSION="$(node -e "console.log(require('${LOCKFILE}').packages['node_modules/@github/copilot'].version)")" +if [[ -z "${VERSION}" ]]; then + echo "error: could not read @github/copilot version from ${LOCKFILE}" >&2 + exit 1 +fi + +CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/download/v${VERSION}/SHA256SUMS.txt" +echo "Fetching ${CHECKSUMS_URL}" +SHA256SUMS="$(curl -fsSL --retry 3 --retry-delay 2 "${CHECKSUMS_URL}")" + +ASSETS=( + "copilot-darwin-arm64.tar.gz" + "copilot-darwin-x64.tar.gz" + "copilot-linux-arm64.tar.gz" + "copilot-linux-x64.tar.gz" + "copilot-win32-arm64.zip" + "copilot-win32-x64.zip" +) + +declare -A HASHES +for asset in "${ASSETS[@]}"; do + hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v a="${asset}" '$2 == a { print $1 }')" + if [[ -z "${hash}" ]]; then + echo "error: SHA256SUMS.txt missing entry for ${asset}" >&2 + exit 1 + fi + HASHES[$asset]="${hash}" +done + +{ + echo "# Auto-generated by rust/scripts/snapshot-bundled-cli-version.sh" + echo "# Do not edit. Regenerated by the publish workflow on every release." + echo "version=${VERSION}" + for asset in "${ASSETS[@]}"; do + echo "${asset}=${HASHES[$asset]}" + done +} > "${OUTPUT}" + +echo "Wrote ${OUTPUT} (version=${VERSION}, ${#ASSETS[@]} hashes)" \ No newline at end of file diff --git a/rust/src/canvas.rs b/rust/src/canvas.rs new file mode 100644 index 000000000..ba13742ff --- /dev/null +++ b/rust/src/canvas.rs @@ -0,0 +1,380 @@ +//! Canvas declarations, provider callbacks, and host-side canvas RPC types. + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use thiserror::Error; + +use crate::generated::api_types::CanvasAction; +use crate::types::SessionId; + +/// JSON Schema object used for canvas inputs and canvas-scoped tools. +pub type CanvasJsonSchema = serde_json::Map; + +/// Declarative metadata for a single canvas, sent over the wire on +/// `session.create` / `session.resume`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct CanvasDeclaration { + /// Canvas identifier, unique within the declaring connection. + pub id: String, + /// Human-readable name shown in host UI and canvas pickers. + pub display_name: String, + /// Short, single-sentence description shown to the agent in canvas catalogs. + pub description: String, + /// JSON Schema for the `input` payload accepted by `canvas.open`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input_schema: Option, + /// Agent-callable actions this canvas exposes. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub actions: Option>, +} + +impl CanvasDeclaration { + /// Construct a canvas declaration with the required fields set. + pub fn new( + id: impl Into, + display_name: impl Into, + description: impl Into, + ) -> Self { + Self { + id: id.into(), + display_name: display_name.into(), + description: description.into(), + input_schema: None, + actions: None, + } + } + + /// Set the description surfaced in discovery and agent context. + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = description.into(); + self + } +} + +/// Response returned from [`CanvasHandler::on_open`]. +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct CanvasOpenResponse { + /// URL the host should render. Optional for canvases with no visual surface. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Provider-supplied title shown in host chrome. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Provider-supplied status text shown in host chrome. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +/// Host capabilities passed to canvas provider callbacks. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasHostContext { + /// Host capability details. + #[serde(default)] + pub capabilities: CanvasHostCapabilities, +} + +/// Host capability details passed to canvas provider callbacks. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasHostCapabilities { + /// Whether the host supports canvas rendering. + #[serde(default)] + pub canvases: bool, +} + +/// Context handed to [`CanvasHandler::on_open`]. +#[derive(Debug, Clone)] +pub struct CanvasOpenContext { + /// Session that requested the canvas. + pub session_id: SessionId, + /// Owning provider identifier. + pub extension_id: String, + /// Canvas id from the declaring [`CanvasDeclaration`]. + pub canvas_id: String, + /// Stable instance id supplied by the runtime. + pub instance_id: String, + /// Validated input payload. + pub input: Value, + /// Host capabilities supplied by the runtime. + pub host: Option, +} + +/// Context handed to [`CanvasHandler::on_action`]. +#[derive(Debug, Clone)] +pub struct CanvasActionContext { + /// Session that invoked the action. + pub session_id: SessionId, + /// Owning provider identifier. + pub extension_id: String, + /// Canvas id targeted by the action. + pub canvas_id: String, + /// Instance id targeted by the action. + pub instance_id: String, + /// Action name from [`crate::generated::api_types::CanvasAction::name`]. + pub action_name: String, + /// Validated input payload. + pub input: Value, + /// Host capabilities supplied by the runtime. + pub host: Option, +} + +/// Context handed to a canvas's close lifecycle hook. +#[derive(Debug, Clone)] +pub struct CanvasLifecycleContext { + /// Session owning the canvas instance. + pub session_id: SessionId, + /// Owning provider identifier. + pub extension_id: String, + /// Canvas id from the declaring [`CanvasDeclaration`]. + pub canvas_id: String, + /// Instance id this lifecycle event applies to. + pub instance_id: String, + /// Host capabilities supplied by the runtime. + pub host: Option, +} + +/// Structured error returned from canvas handlers. +#[derive(Debug, Clone, Error, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[error("{code}: {message}")] +pub struct CanvasError { + /// Machine-readable error code. + pub code: String, + /// Human-readable message. + pub message: String, +} + +impl CanvasError { + /// Construct a new error envelope with the given code and message. + pub fn new(code: impl Into, message: impl Into) -> Self { + Self { + code: code.into(), + message: message.into(), + } + } + + /// Default error returned when a custom action has no handler. + pub fn no_handler() -> Self { + Self::new( + "canvas_action_no_handler", + "No handler implemented for this canvas action", + ) + } +} + +/// Result alias for canvas handler methods. +pub type CanvasResult = Result; + +/// Provider-side canvas lifecycle handler. +/// +/// A session installs a single [`CanvasHandler`] (via +/// [`SessionConfig::with_canvas_handler`](crate::types::SessionConfig::with_canvas_handler)). +/// The handler receives every inbound `canvas.open` / `canvas.close` / +/// `canvas.action.invoke` JSON-RPC request the runtime issues for this +/// session and decides — typically by inspecting [`CanvasOpenContext::canvas_id`] +/// — which application-side canvas should handle the call. +/// +/// The SDK does not maintain a per-canvas registry; multiplexing across +/// declared canvases is the implementor's responsibility. +#[async_trait] +pub trait CanvasHandler: Send + Sync { + /// Open a new canvas instance. + async fn on_open(&self, ctx: CanvasOpenContext) -> CanvasResult; + + /// Handle a non-lifecycle action declared by the canvas. + async fn on_action(&self, _ctx: CanvasActionContext) -> CanvasResult { + Err(CanvasError::no_handler()) + } + + /// Canvas was closed by the user or agent. + async fn on_close(&self, _ctx: CanvasLifecycleContext) -> CanvasResult<()> { + Ok(()) + } +} + +/// Common fields sent by direct `canvas.*` provider callbacks. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CanvasProviderRequestParams { + pub session_id: SessionId, + pub extension_id: String, + pub canvas_id: String, + pub instance_id: String, + #[serde(default)] + pub input: Value, + #[serde(default)] + pub host: Option, +} + +/// Wire-level params for `canvas.action.invoke`. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CanvasInvokeParams { + pub session_id: SessionId, + pub extension_id: String, + pub canvas_id: String, + pub instance_id: String, + pub action_name: String, + #[serde(default)] + pub input: Value, + #[serde(default)] + pub host: Option, +} + +impl CanvasProviderRequestParams { + pub(crate) fn into_open_context(self) -> CanvasOpenContext { + CanvasOpenContext { + session_id: self.session_id, + extension_id: self.extension_id, + canvas_id: self.canvas_id, + instance_id: self.instance_id, + input: self.input, + host: self.host, + } + } + + pub(crate) fn into_lifecycle_context(self) -> CanvasLifecycleContext { + CanvasLifecycleContext { + session_id: self.session_id, + extension_id: self.extension_id, + canvas_id: self.canvas_id, + instance_id: self.instance_id, + host: self.host, + } + } +} + +impl CanvasInvokeParams { + pub(crate) fn into_action_context(self) -> CanvasActionContext { + CanvasActionContext { + session_id: self.session_id, + extension_id: self.extension_id, + canvas_id: self.canvas_id, + instance_id: self.instance_id, + action_name: self.action_name, + input: self.input, + host: self.host, + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + struct EchoHandler; + + #[async_trait] + impl CanvasHandler for EchoHandler { + async fn on_open(&self, ctx: CanvasOpenContext) -> CanvasResult { + Ok(CanvasOpenResponse { + url: Some(format!("https://example.test/{}", ctx.canvas_id)), + title: Some("Echo".to_string()), + status: Some("ready".to_string()), + }) + } + + async fn on_action(&self, ctx: CanvasActionContext) -> CanvasResult { + Ok(json!({ "echoed": ctx.action_name, "input": ctx.input })) + } + } + + #[test] + fn declaration_serializes_camel_case_and_skips_none() { + let decl = CanvasDeclaration { + id: "counter".to_string(), + display_name: "Counter".to_string(), + description: "Count things".to_string(), + input_schema: None, + actions: Some(vec![CanvasAction { + name: "increment".to_string(), + description: Some("bump".to_string()), + input_schema: None, + }]), + }; + + let value = serde_json::to_value(&decl).unwrap(); + + assert_eq!(value["id"], "counter"); + assert_eq!(value["displayName"], "Counter"); + assert_eq!(value["description"], "Count things"); + assert_eq!(value["actions"][0]["name"], "increment"); + } + + #[tokio::test] + async fn handler_on_open_returns_response() { + let handler = EchoHandler; + let response = handler + .on_open(CanvasOpenContext { + session_id: SessionId::from("s1"), + extension_id: "project:echo".to_string(), + canvas_id: "echo".to_string(), + instance_id: "echo-1".to_string(), + input: json!({ "x": 1 }), + host: None, + }) + .await + .unwrap(); + + assert_eq!(response.url.as_deref(), Some("https://example.test/echo")); + assert_eq!(response.title.as_deref(), Some("Echo")); + assert_eq!(response.status.as_deref(), Some("ready")); + } + + #[tokio::test] + async fn handler_on_action_returns_value() { + let handler = EchoHandler; + let result = handler + .on_action(CanvasActionContext { + session_id: SessionId::from("s1"), + extension_id: "project:echo".to_string(), + canvas_id: "echo".to_string(), + instance_id: "inst-1".to_string(), + action_name: "shout".to_string(), + input: json!("hi"), + host: None, + }) + .await + .unwrap(); + + assert_eq!(result["echoed"], "shout"); + assert_eq!(result["input"], "hi"); + } + + #[tokio::test] + async fn default_on_action_returns_no_handler_error() { + struct OpenOnly; + #[async_trait] + impl CanvasHandler for OpenOnly { + async fn on_open(&self, _ctx: CanvasOpenContext) -> CanvasResult { + Ok(CanvasOpenResponse { + url: None, + title: None, + status: None, + }) + } + } + + let err = OpenOnly + .on_action(CanvasActionContext { + session_id: SessionId::from("s1"), + extension_id: "project:open-only".to_string(), + canvas_id: "x".to_string(), + instance_id: "x-1".to_string(), + action_name: "anything".to_string(), + input: Value::Null, + host: None, + }) + .await + .unwrap_err(); + + assert_eq!(err.code, "canvas_action_no_handler"); + } +} diff --git a/rust/src/embeddedcli.rs b/rust/src/embeddedcli.rs index d0e5ea9ff..e1eb147dc 100644 --- a/rust/src/embeddedcli.rs +++ b/rust/src/embeddedcli.rs @@ -1,17 +1,31 @@ -#[cfg(any(has_bundled_cli, test))] +//! Lazy runtime installer for the CLI binary that build.rs embedded in this +//! crate (gated on the `bundled-cli` cargo feature, which is in the default +//! feature set). +//! +//! build.rs downloads the platform's `copilot-{platform}.{tar.gz,zip}` +//! archive from GitHub Releases, SHA-256 verifies it against the version +//! pinned in `bundled_cli_version.txt`, and embeds the **raw archive bytes** +//! into the consumer's compiled artifact via `include_bytes!()`. Extraction +//! to a real on-disk path is deferred until the first call to +//! [`path`] / [`install_at`] — at which point the bytes are part of the +//! consumer's signed binary and trusted, so no further hashing is done. + +#[cfg(has_bundled_cli)] use std::fs; -#[cfg(any(has_bundled_cli, test))] -use std::io::{self, Read, Write}; -#[cfg(any(has_bundled_cli, test))] -use std::path::Path; -use std::path::PathBuf; +#[cfg(all(has_bundled_cli, not(windows)))] +use std::io::Read; +#[cfg(has_bundled_cli)] +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; use std::sync::OnceLock; #[cfg(has_bundled_cli)] use tracing::{info, warn}; -// When the SDK is built with COPILOT_CLI_VERSION set, build.rs generates -// bundled_cli.rs with the compressed binary bytes, hash, and version. +// When the `bundled-cli` cargo feature is enabled and the target platform is +// supported, build.rs generates `bundled_cli.rs` exposing the raw archive +// bytes plus the version + binary-name constants the runtime install path +// consumes. #[cfg(has_bundled_cli)] mod build_time { include!(concat!(env!("OUT_DIR"), "/bundled_cli.rs")); @@ -19,36 +33,27 @@ mod build_time { static INSTALLED_PATH: OnceLock> = OnceLock::new(); -/// Returns the bundled CLI version string, if one was embedded at build time. -pub fn bundled_version() -> Option<&'static str> { - #[cfg(has_bundled_cli)] - { - Some(build_time::CLI_VERSION) - } - #[cfg(not(has_bundled_cli))] - { - None - } -} - -/// Returns the path to the installed CLI binary, lazily extracting on first call. +/// Returns the path to the installed CLI binary, lazily extracting the +/// embedded archive on first call. /// -/// When the SDK was built with `COPILOT_CLI_VERSION` set, this extracts the -/// embedded binary to `~/.cache/github-copilot-sdk-{version}/copilot` (or -/// `copilot.exe` on Windows), verifies the SHA-256 hash, and returns the -/// path. Subsequent calls return the cached result. +/// On first call this extracts the embedded archive to +/// `/github-copilot-sdk-{version}/copilot[.exe]` and +/// returns the resulting path. The cache dir comes from +/// [`dirs::cache_dir()`] — `%LOCALAPPDATA%` on Windows, +/// `~/Library/Caches/` on macOS, `$XDG_CACHE_HOME` (or `~/.cache/`) on +/// Linux. Subsequent calls return the cached result. The extraction +/// is skipped when the target file already exists — the per-version +/// install directory and the assumption that the consumer's binary is +/// trusted mean no further hashing is needed. /// /// Returns `None` if no CLI was embedded at build time. -pub fn path() -> Option { +pub(crate) fn path() -> Option { INSTALLED_PATH .get_or_init(|| { #[cfg(has_bundled_cli)] { - match install( - build_time::CLI_BYTES, - build_time::CLI_HASH, - build_time::CLI_VERSION, - ) { + let dir = default_install_dir(build_time::CLI_VERSION); + match install(&dir, build_time::CLI_ARCHIVE) { Ok(path) => { info!(path = %path.display(), version = build_time::CLI_VERSION, "embedded CLI installed"); return Some(path); @@ -63,55 +68,67 @@ pub fn path() -> Option { .clone() } -#[cfg(has_bundled_cli)] -fn install( - compressed: &[u8], - expected_hash: [u8; 32], - version: &str, -) -> Result { - let verbose = std::env::var("COPILOT_CLI_INSTALL_VERBOSE").ok().as_deref() == Some("1"); +/// Install the embedded CLI binary into the given directory instead of the +/// default `/github-copilot-sdk-{version}/` location +/// (see [`path`] for the per-platform mapping). +/// +/// Idempotent: skips extraction if the target binary already exists. +/// Returns `None` when the SDK was built without a bundled CLI. +#[allow(dead_code)] // Used by resolve.rs when ClientOptions::bundled_cli_extract_dir is set. +pub(crate) fn install_at(extract_dir: &Path) -> Option { + #[cfg(has_bundled_cli)] + { + match install(extract_dir, build_time::CLI_ARCHIVE) { + Ok(path) => { + info!(path = %path.display(), version = build_time::CLI_VERSION, "embedded CLI installed"); + return Some(path); + } + Err(e) => { + warn!(error = %e, "embedded CLI installation failed"); + } + } + } + #[cfg(not(has_bundled_cli))] + { + let _ = extract_dir; + } + None +} +#[cfg(has_bundled_cli)] +fn default_install_dir(version: &str) -> PathBuf { let cache = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); - // Use a versioned directory so multiple versions can coexist, - // but keep the binary named `copilot` — the CLI checks argv[0] - // for this exact name. - let install_dir = if version.is_empty() { + if version.is_empty() { cache.join("github-copilot-sdk") } else { cache.join(format!("github-copilot-sdk-{}", sanitize_version(version))) - }; - fs::create_dir_all(&install_dir).map_err(EmbeddedCliError::CreateDir)?; + } +} - let binary_name = binary_name(); - let final_path = install_dir.join(&binary_name); +#[cfg(has_bundled_cli)] +fn install(install_dir: &Path, archive: &[u8]) -> Result { + let verbose = std::env::var("COPILOT_CLI_INSTALL_VERBOSE").ok().as_deref() == Some("1"); + + fs::create_dir_all(install_dir).map_err(EmbeddedCliError::CreateDir)?; + + let final_path = install_dir.join(build_time::CLI_BINARY_NAME); - // If the binary already exists and hash matches, skip extraction. + // Per-version install dir means a present file at this path is the + // binary we want — no need to hash-verify the bytes are unchanged. if final_path.is_file() { - let existing_hash = hash_file(&final_path)?; - if existing_hash == expected_hash { - if verbose { - eprintln!("embedded CLI already installed at {}", final_path.display()); - } - return Ok(final_path); - } if verbose { - eprintln!("embedded CLI hash mismatch, reinstalling"); + eprintln!("embedded CLI already installed at {}", final_path.display()); } + return Ok(final_path); } let start = std::time::Instant::now(); - let decompressed = decompress(compressed)?; - - let actual_hash = sha256(&decompressed); - if actual_hash != expected_hash { - return Err(EmbeddedCliError::HashMismatch); - } - - write_binary(&final_path, &decompressed)?; + let bytes = extract_binary(archive, build_time::CLI_BINARY_NAME)?; + write_binary(&final_path, &bytes)?; if verbose { eprintln!( - "embedded CLI installed at {} in {:?}", + "embedded CLI extracted to {} in {:?}", final_path.display(), start.elapsed() ); @@ -120,13 +137,39 @@ fn install( Ok(final_path) } -#[cfg(any(has_bundled_cli, test))] -fn binary_name() -> String { - if cfg!(target_os = "windows") { - "copilot.exe".to_string() - } else { - "copilot".to_string() +#[cfg(all(has_bundled_cli, not(windows)))] +fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> { + let gz = flate2::read::GzDecoder::new(archive); + let mut tar = tar::Archive::new(gz); + for entry in tar.entries().map_err(EmbeddedCliError::Archive)? { + let mut entry = entry.map_err(EmbeddedCliError::Archive)?; + let path = entry.path().map_err(EmbeddedCliError::Archive)?; + let name = path.to_string_lossy(); + if name == binary_name || name.ends_with(&format!("/{binary_name}")) { + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .map_err(EmbeddedCliError::Archive)?; + return Ok(bytes); + } } + Err(EmbeddedCliError::BinaryNotFoundInArchive) +} + +#[cfg(all(has_bundled_cli, 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(EmbeddedCliError::Zip)?; + for i in 0..zip.len() { + let mut entry = zip.by_index(i).map_err(EmbeddedCliError::Zip)?; + 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(EmbeddedCliError::Io)?; + return Ok(bytes); + } + } + Err(EmbeddedCliError::BinaryNotFoundInArchive) } #[cfg(has_bundled_cli)] @@ -140,41 +183,7 @@ fn sanitize_version(version: &str) -> String { .collect() } -#[cfg(any(has_bundled_cli, test))] -fn decompress(data: &[u8]) -> Result, EmbeddedCliError> { - let mut decoder = zstd::Decoder::new(data).map_err(EmbeddedCliError::Decompress)?; - let mut out = Vec::new(); - decoder - .read_to_end(&mut out) - .map_err(EmbeddedCliError::Decompress)?; - Ok(out) -} - -#[cfg(any(has_bundled_cli, test))] -fn sha256(data: &[u8]) -> [u8; 32] { - use sha2::Digest; - let mut hasher = sha2::Sha256::new(); - hasher.update(data); - hasher.finalize().into() -} - #[cfg(has_bundled_cli)] -fn hash_file(path: &Path) -> Result<[u8; 32], EmbeddedCliError> { - use sha2::Digest; - let mut file = fs::File::open(path).map_err(EmbeddedCliError::Io)?; - let mut hasher = sha2::Sha256::new(); - let mut buf = [0u8; 8192]; - loop { - let n = file.read(&mut buf).map_err(EmbeddedCliError::Io)?; - if n == 0 { - break; - } - hasher.update(&buf[..n]); - } - Ok(hasher.finalize().into()) -} - -#[cfg(any(has_bundled_cli, test))] fn write_binary(path: &Path, data: &[u8]) -> Result<(), EmbeddedCliError> { let mut file = fs::OpenOptions::new() .write(true) @@ -195,84 +204,24 @@ fn write_binary(path: &Path, data: &[u8]) -> Result<(), EmbeddedCliError> { Ok(()) } -#[cfg(any(has_bundled_cli, test))] +#[cfg(has_bundled_cli)] #[derive(Debug, thiserror::Error)] #[allow(dead_code)] enum EmbeddedCliError { #[error("failed to create install directory: {0}")] CreateDir(io::Error), - #[error("decompression failed: {0}")] - Decompress(io::Error), + #[cfg(not(windows))] + #[error("failed to read archive entry: {0}")] + Archive(io::Error), + + #[cfg(windows)] + #[error("failed to read zip archive: {0}")] + Zip(zip::result::ZipError), - #[error("SHA-256 hash of decompressed binary does not match expected hash")] - HashMismatch, + #[error("CLI binary not found in embedded archive")] + BinaryNotFoundInArchive, #[error("I/O error: {0}")] Io(io::Error), } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn install_extracts_to_cache_dir() { - let temp = tempfile::tempdir().expect("should create temp dir"); - let original = b"fake copilot binary"; - let hash = sha256(original); - let compressed = zstd::encode_all(&original[..], 3).expect("compression should succeed"); - - // Override cache dir via env for test isolation. - let path = install_to_dir(&temp, &compressed, hash); - let expected_name = binary_name(); - assert!(path.is_file()); - assert_eq!( - path.file_name().and_then(|s| s.to_str()), - Some(expected_name.as_str()) - ); - - let installed_content = fs::read(&path).expect("should read installed binary"); - assert_eq!(installed_content, original); - - // Second install should be idempotent (hash matches, skips extraction). - let path2 = install_to_dir(&temp, &compressed, hash); - assert_eq!(path, path2); - } - - #[test] - fn install_rejects_hash_mismatch() { - let temp = tempfile::tempdir().expect("should create temp dir"); - let original = b"fake copilot binary"; - let wrong_hash = [0u8; 32]; - let compressed = zstd::encode_all(&original[..], 3).expect("compression should succeed"); - - let result = install_to_dir_result(&temp, &compressed, wrong_hash); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("SHA-256"),); - } - - // Test helpers that install to a specific directory instead of the global cache. - fn install_to_dir(temp: &tempfile::TempDir, compressed: &[u8], hash: [u8; 32]) -> PathBuf { - install_to_dir_result(temp, compressed, hash).expect("install should succeed") - } - - fn install_to_dir_result( - temp: &tempfile::TempDir, - compressed: &[u8], - hash: [u8; 32], - ) -> Result { - let install_dir = temp.path().to_path_buf(); - fs::create_dir_all(&install_dir).expect("create dir"); - let binary_name = binary_name(); - let final_path = install_dir.join(&binary_name); - - let decompressed = decompress(compressed)?; - let actual_hash = sha256(&decompressed); - if actual_hash != hash { - return Err(EmbeddedCliError::HashMismatch); - } - write_binary(&final_path, &decompressed)?; - Ok(final_path) - } -} diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 0ef378f48..db57b293a 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -96,6 +96,16 @@ pub mod rpc_methods { pub const SESSION_AUTH_GETSTATUS: &str = "session.auth.getStatus"; /// `session.auth.setCredentials` pub const SESSION_AUTH_SETCREDENTIALS: &str = "session.auth.setCredentials"; + /// `session.canvas.list` + pub const SESSION_CANVAS_LIST: &str = "session.canvas.list"; + /// `session.canvas.listOpen` + pub const SESSION_CANVAS_LISTOPEN: &str = "session.canvas.listOpen"; + /// `session.canvas.open` + pub const SESSION_CANVAS_OPEN: &str = "session.canvas.open"; + /// `session.canvas.close` + pub const SESSION_CANVAS_CLOSE: &str = "session.canvas.close"; + /// `session.canvas.invokeAction` + pub const SESSION_CANVAS_INVOKEACTION: &str = "session.canvas.invokeAction"; /// `session.model.getCurrent` pub const SESSION_MODEL_GETCURRENT: &str = "session.model.getCurrent"; /// `session.model.switchTo` @@ -199,6 +209,18 @@ pub mod rpc_methods { pub const SESSION_MCP_REMOVEGITHUB: &str = "session.mcp.removeGitHub"; /// `session.mcp.oauth.login` pub const SESSION_MCP_OAUTH_LOGIN: &str = "session.mcp.oauth.login"; + /// `session.mcp.apps.readResource` + pub const SESSION_MCP_APPS_READRESOURCE: &str = "session.mcp.apps.readResource"; + /// `session.mcp.apps.listTools` + pub const SESSION_MCP_APPS_LISTTOOLS: &str = "session.mcp.apps.listTools"; + /// `session.mcp.apps.callTool` + pub const SESSION_MCP_APPS_CALLTOOL: &str = "session.mcp.apps.callTool"; + /// `session.mcp.apps.setHostContext` + pub const SESSION_MCP_APPS_SETHOSTCONTEXT: &str = "session.mcp.apps.setHostContext"; + /// `session.mcp.apps.getHostContext` + pub const SESSION_MCP_APPS_GETHOSTCONTEXT: &str = "session.mcp.apps.getHostContext"; + /// `session.mcp.apps.diagnose` + pub const SESSION_MCP_APPS_DIAGNOSE: &str = "session.mcp.apps.diagnose"; /// `session.plugins.list` pub const SESSION_PLUGINS_LIST: &str = "session.plugins.list"; /// `session.options.update` @@ -472,6 +494,13 @@ pub struct AgentInfo { /// Stable identifier for selection. For most agents this is the same as `name`; for plugin/builtin agents it may differ. Always populated; defaults to `name` when no distinct id was assigned. pub id: String, /// MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(default)] pub mcp_servers: HashMap, /// Preferred model id for this agent. When omitted, inherits the outer agent's model. @@ -850,6 +879,199 @@ pub struct ApiKeyAuthInfo { pub r#type: ApiKeyAuthInfoType, } +/// Canvas action that the agent or host can invoke. To discover the input schema for a particular action, call the list_canvas_capabilities tool. +/// +///
+/// +/// **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 CanvasAction { + /// Description of the action + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// JSON Schema for the action input + #[serde(skip_serializing_if = "Option::is_none")] + pub input_schema: Option, + /// Action name exposed by the canvas provider + pub name: String, +} + +/// Canvas close parameters. +/// +///
+/// +/// **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 CanvasCloseRequest { + /// Open canvas instance identifier + pub instance_id: String, +} + +/// Canvas action invocation parameters. +/// +///
+/// +/// **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 CanvasInvokeActionRequest { + /// Action name to invoke + pub action_name: String, + /// Action input + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + /// Open canvas instance identifier + pub instance_id: String, +} + +/// Canvas action invocation result. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasInvokeActionResult { + /// Provider-supplied action result + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +/// Canvas available in the current 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 DiscoveredCanvas { + /// Actions the agent or host may invoke on an open instance + #[serde(default)] + pub actions: Vec, + /// Provider-local canvas identifier + pub canvas_id: String, + /// Short, single-sentence description shown to the agent in canvas catalogs. + pub description: String, + /// Human-readable canvas name + pub display_name: String, + /// Owning provider identifier + pub extension_id: String, + /// Owning extension display name, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_name: Option, + /// JSON Schema for canvas open input + #[serde(skip_serializing_if = "Option::is_none")] + pub input_schema: Option, +} + +/// Declared canvases available in this 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 CanvasList { + /// Declared canvases available in this session + pub canvases: Vec, +} + +/// Open canvas instance snapshot. +/// +///
+/// +/// **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 OpenCanvasInstance { + /// Runtime-controlled routing state for an open canvas instance. + pub availability: CanvasInstanceAvailability, + /// Provider-local canvas identifier + pub canvas_id: String, + /// Owning provider identifier + pub extension_id: String, + /// Owning extension display name, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_name: Option, + /// Input supplied when the instance was opened + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + /// Stable caller-supplied canvas instance identifier + pub instance_id: String, + /// Whether this snapshot came from an idempotent reopen + pub reopen: bool, + /// Provider-supplied status text + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + /// Rendered title + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// URL for web-rendered canvases + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// Live open-canvas snapshot. +/// +///
+/// +/// **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 CanvasListOpenResult { + /// Currently open canvas instances + pub open_canvases: Vec, +} + +/// Canvas open parameters. +/// +///
+/// +/// **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 CanvasOpenRequest { + /// Provider-local canvas identifier + pub canvas_id: String, + /// Owning provider identifier. Optional when the canvasId is unique across providers; required to disambiguate when multiple providers register the same canvasId. + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_id: Option, + /// Canvas open input + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + /// Caller-supplied stable instance identifier + pub instance_id: String, +} + /// Optional unstructured input hint /// ///
@@ -1102,7 +1324,7 @@ pub struct ConnectRemoteSessionParams { /// Optional connection token presented by the SDK client during the handshake. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ConnectRequest { +pub(crate) struct ConnectRequest { /// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, @@ -1111,7 +1333,7 @@ pub struct ConnectRequest { /// Handshake result reporting the server's protocol version and package version on success. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ConnectResult { +pub(crate) struct ConnectResult { /// Always true on success pub ok: bool, /// Server protocol version number @@ -1169,7 +1391,7 @@ pub struct DiscoveredMcpServer { pub name: String, /// Configuration source: user, workspace, plugin, or builtin pub source: McpServerSource, - /// Server transport type: stdio, http, sse, or memory + /// Server transport type: stdio, http, sse (deprecated), or memory #[serde(skip_serializing_if = "Option::is_none")] pub r#type: Option, } @@ -1428,6 +1650,9 @@ pub struct ExternalToolTextResultForLlmBinaryResultsForLlm { /// Human-readable description of the binary data #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// Optional metadata from the producing tool. + #[serde(default)] + pub metadata: HashMap, /// MIME type of the binary data pub mime_type: String, /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. @@ -2125,6 +2350,289 @@ pub struct LspInitializeRequest { pub working_directory: Option, } +/// MCP server, tool name, and arguments to invoke from an MCP App view. +/// +///
+/// +/// **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 McpAppsCallToolRequest { + /// Tool arguments + #[serde(default)] + pub arguments: HashMap, + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + pub origin_server_name: String, + /// MCP server hosting the tool + pub server_name: String, + /// MCP tool name + pub tool_name: String, +} + +/// Capability negotiation snapshot +/// +///
+/// +/// **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 McpAppsDiagnoseCapability { + /// Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers + pub advertised: bool, + /// Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on + pub feature_flag_enabled: bool, + /// Whether the session has the `mcp-apps` capability + pub session_has_mcp_apps: bool, +} + +/// MCP server to diagnose MCP Apps wiring for. +/// +///
+/// +/// **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 McpAppsDiagnoseRequest { + /// MCP server to probe + pub server_name: String, +} + +/// What the server returned for this 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 McpAppsDiagnoseServer { + /// Whether the named server is currently connected + pub connected: bool, + /// Up to 5 tool names with `_meta.ui` for quick inspection + pub sample_tool_names: Vec, + /// Total tools returned by the server's tools/list + pub tool_count: f64, + /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set) + pub tools_with_ui_meta: f64, +} + +/// Diagnostic snapshot of MCP Apps wiring for the named server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpAppsDiagnoseResult { + /// Capability negotiation snapshot + pub capability: McpAppsDiagnoseCapability, + /// What the server returned for this session + pub server: McpAppsDiagnoseServer, +} + +/// Current host context +/// +///
+/// +/// **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 McpAppsHostContextDetails { + /// Display modes the host supports + #[serde(default)] + pub available_display_modes: Vec, + /// Current display mode (SEP-1865) + #[serde(skip_serializing_if = "Option::is_none")] + pub display_mode: Option, + /// BCP-47 locale, e.g. 'en-US' + #[serde(skip_serializing_if = "Option::is_none")] + pub locale: Option, + /// Platform type for responsive design + #[serde(skip_serializing_if = "Option::is_none")] + pub platform: Option, + /// UI theme preference per SEP-1865 + #[serde(skip_serializing_if = "Option::is_none")] + pub theme: Option, + /// IANA timezone, e.g. 'America/New_York' + #[serde(skip_serializing_if = "Option::is_none")] + pub time_zone: Option, + /// Host application identifier + #[serde(skip_serializing_if = "Option::is_none")] + pub user_agent: Option, +} + +/// Current host context advertised to MCP App guests. +/// +///
+/// +/// **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 McpAppsHostContext { + /// Current host context + pub context: McpAppsHostContextDetails, +} + +/// MCP server to list app-callable tools for. +/// +///
+/// +/// **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 McpAppsListToolsRequest { + /// **Required.** Server whose ui:// view issued the request. Per SEP-1865 ('callable by the app from this server only'), the call is rejected when this differs from `serverName`, and rejected outright when missing. + pub origin_server_name: String, + /// MCP server hosting the app + pub server_name: String, +} + +/// App-callable tools from the named MCP server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpAppsListToolsResult { + /// App-callable tools from the server + pub tools: Vec>, +} + +/// MCP server and resource URI to fetch. +/// +///
+/// +/// **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 McpAppsReadResourceRequest { + /// Name of the MCP server hosting the resource + pub server_name: String, + /// Resource URI (typically ui://...) + pub uri: String, +} + +/// Schema for the `McpAppsResourceContent` type. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpAppsResourceContent { + /// Resource-level metadata (CSP, permissions, etc.) + #[serde(rename = "_meta", default)] + pub meta: HashMap, + /// Base64-encoded binary content + #[serde(skip_serializing_if = "Option::is_none")] + pub blob: Option, + /// MIME type of the content + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + /// Text content (e.g. HTML) + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + /// The resource URI (typically ui://...) + pub uri: String, +} + +/// Resource contents returned by the MCP server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpAppsReadResourceResult { + /// Resource contents returned by the server + pub contents: Vec, +} + +/// Host context advertised to MCP App guests +/// +///
+/// +/// **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 McpAppsSetHostContextDetails { + /// Display modes the host supports + #[serde(default)] + pub available_display_modes: Vec, + /// Current display mode (SEP-1865) + #[serde(skip_serializing_if = "Option::is_none")] + pub display_mode: Option, + /// BCP-47 locale, e.g. 'en-US' + #[serde(skip_serializing_if = "Option::is_none")] + pub locale: Option, + /// Platform type for responsive design + #[serde(skip_serializing_if = "Option::is_none")] + pub platform: Option, + /// UI theme preference per SEP-1865 + #[serde(skip_serializing_if = "Option::is_none")] + pub theme: Option, + /// IANA timezone, e.g. 'America/New_York' + #[serde(skip_serializing_if = "Option::is_none")] + pub time_zone: Option, + /// Host application identifier + #[serde(skip_serializing_if = "Option::is_none")] + pub user_agent: Option, +} + +/// Host context to advertise to MCP App guests. +/// +///
+/// +/// **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 McpAppsSetHostContextRequest { + /// Host context advertised to MCP App guests + pub context: McpAppsSetHostContextDetails, +} + /// The requestId previously passed to executeSampling that should be cancelled. /// ///
@@ -2287,18 +2795,6 @@ pub struct McpExecuteSamplingParams { pub server_name: String, } -/// MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct McpExecuteSamplingResult {} - /// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, and the callback success-page copy. /// ///
@@ -2763,6 +3259,24 @@ pub struct MetadataSnapshotRemoteMetadata { pub task_type: Option, } +/// Long context tier pricing (available for models with extended context windows) +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelBillingTokenPricesLongContext { + /// AI Credits cost per billing batch of cached tokens + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_price: Option, + /// Maximum context window tokens for the long context tier + #[serde(skip_serializing_if = "Option::is_none")] + pub context_max: Option, + /// AI Credits cost per billing batch of input tokens + #[serde(skip_serializing_if = "Option::is_none")] + pub input_price: Option, + /// AI Credits cost per billing batch of output tokens + #[serde(skip_serializing_if = "Option::is_none")] + pub output_price: Option, +} + /// Token-level pricing information for this model #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2770,15 +3284,21 @@ pub struct ModelBillingTokenPrices { /// Number of tokens per standard billing batch #[serde(skip_serializing_if = "Option::is_none")] pub batch_size: Option, - /// Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + /// AI Credits cost per billing batch of cached tokens + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_price: Option, + /// Maximum context window tokens for the default tier + #[serde(skip_serializing_if = "Option::is_none")] + pub context_max: Option, + /// AI Credits cost per billing batch of input tokens #[serde(skip_serializing_if = "Option::is_none")] - pub cache_price: Option, - /// Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + pub input_price: Option, + /// Long context tier pricing (available for models with extended context windows) #[serde(skip_serializing_if = "Option::is_none")] - pub input_price: Option, - /// Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + pub long_context: Option, + /// AI Credits cost per billing batch of output tokens #[serde(skip_serializing_if = "Option::is_none")] - pub output_price: Option, + pub output_price: Option, } /// Billing information @@ -5160,8 +5680,9 @@ pub struct SendRequest { #[serde(skip_serializing_if = "Option::is_none")] pub required_tool: Option, /// Optional provenance tag copied to the resulting user.message event. Supported values are `system`, `command-*`, and `schedule-*`. + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, + pub(crate) source: Option, /// W3C Trace Context traceparent header for distributed tracing of this agent turn #[serde(skip_serializing_if = "Option::is_none")] pub traceparent: Option, @@ -6397,6 +6918,13 @@ pub struct SessionsSetAdditionalPluginsResult {} #[serde(rename_all = "camelCase")] pub struct SessionUpdateOptionsParams { /// Additional content-exclusion policies to merge into the session's policy set. Opaque shape; see `ContentExclusionApiResponse` in the runtime. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(default)] pub additional_content_exclusion_policies: Vec, /// Runtime context discriminator (e.g., `cli`, `actions`). @@ -6475,6 +7003,13 @@ pub struct SessionUpdateOptionsParams { #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Custom model-provider configuration (BYOK). Opaque shape; see `ProviderConfig` in the runtime. + /// + ///
+ /// + /// **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 provider: Option, /// Reasoning effort for the selected model (model-defined enum). @@ -6484,6 +7019,13 @@ pub struct SessionUpdateOptionsParams { #[serde(skip_serializing_if = "Option::is_none")] pub running_in_interactive_mode: Option, /// Sandbox configuration shape; opaque to SDK consumers. See `SandboxConfig` in the runtime. + /// + ///
+ /// + /// **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 sandbox_config: Option, /// Shell init profile (`None` or `NonInteractive`). @@ -8557,12 +9099,120 @@ pub struct SessionSuspendParams { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionSendResult { - /// Unique identifier assigned to the message - pub message_id: String, +pub struct SessionSendResult { + /// Unique identifier assigned to the message + pub message_id: String, +} + +/// Result of aborting the current 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 SessionAbortResult { + /// Error message if the abort failed + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Whether the abort completed successfully + pub success: bool, +} + +/// 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 SessionAuthGetStatusParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Authentication status and account metadata for the session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionAuthGetStatusResult { + /// Authentication type + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_type: Option, + /// Copilot plan tier (e.g., individual_pro, business) + #[serde(skip_serializing_if = "Option::is_none")] + pub copilot_plan: Option, + /// Authentication host URL + #[serde(skip_serializing_if = "Option::is_none")] + pub host: Option, + /// Whether the session has resolved authentication + pub is_authenticated: bool, + /// Authenticated login/username, if available + #[serde(skip_serializing_if = "Option::is_none")] + pub login: Option, + /// Human-readable authentication status description + #[serde(skip_serializing_if = "Option::is_none")] + pub status_message: Option, +} + +/// Indicates whether the credential update succeeded. +/// +///
+/// +/// **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 SessionAuthSetCredentialsResult { + /// Whether the operation succeeded + pub success: bool, +} + +/// 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 SessionCanvasListParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Declared canvases available in this 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 SessionCanvasListResult { + /// Declared canvases available in this session + pub canvases: Vec, } -/// Result of aborting the current turn +/// Identifies the target session. /// ///
/// @@ -8572,15 +9222,12 @@ pub struct SessionSendResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionAbortResult { - /// Error message if the abort failed - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - /// Whether the abort completed successfully - pub success: bool, +pub struct SessionCanvasListOpenParams { + /// Target session identifier + pub session_id: SessionId, } -/// Identifies the target session. +/// Live open-canvas snapshot. /// ///
/// @@ -8590,12 +9237,12 @@ pub struct SessionAbortResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionAuthGetStatusParams { - /// Target session identifier - pub session_id: SessionId, +pub struct SessionCanvasListOpenResult { + /// Currently open canvas instances + pub open_canvases: Vec, } -/// Authentication status and account metadata for the session. +/// Open canvas instance snapshot. /// ///
/// @@ -8605,27 +9252,35 @@ pub struct SessionAuthGetStatusParams { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionAuthGetStatusResult { - /// Authentication type +pub struct SessionCanvasOpenResult { + /// Runtime-controlled routing state for an open canvas instance. + pub availability: CanvasInstanceAvailability, + /// Provider-local canvas identifier + pub canvas_id: String, + /// Owning provider identifier + pub extension_id: String, + /// Owning extension display name, when available #[serde(skip_serializing_if = "Option::is_none")] - pub auth_type: Option, - /// Copilot plan tier (e.g., individual_pro, business) + pub extension_name: Option, + /// Input supplied when the instance was opened #[serde(skip_serializing_if = "Option::is_none")] - pub copilot_plan: Option, - /// Authentication host URL + pub input: Option, + /// Stable caller-supplied canvas instance identifier + pub instance_id: String, + /// Whether this snapshot came from an idempotent reopen + pub reopen: bool, + /// Provider-supplied status text #[serde(skip_serializing_if = "Option::is_none")] - pub host: Option, - /// Whether the session has resolved authentication - pub is_authenticated: bool, - /// Authenticated login/username, if available + pub status: Option, + /// Rendered title #[serde(skip_serializing_if = "Option::is_none")] - pub login: Option, - /// Human-readable authentication status description + pub title: Option, + /// URL for web-rendered canvases #[serde(skip_serializing_if = "Option::is_none")] - pub status_message: Option, + pub url: Option, } -/// Indicates whether the credential update succeeded. +/// Canvas action invocation result. /// ///
/// @@ -8635,9 +9290,10 @@ pub struct SessionAuthGetStatusResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionAuthSetCredentialsResult { - /// Whether the operation succeeded - pub success: bool, +pub struct SessionCanvasInvokeActionResult { + /// Provider-supplied action result + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, } /// Identifies the target session. @@ -9656,6 +10312,83 @@ pub struct SessionMcpOauthLoginResult { pub authorization_url: Option, } +/// Resource contents returned by the MCP server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpAppsReadResourceResult { + /// Resource contents returned by the server + pub contents: Vec, +} + +/// App-callable tools from the named MCP server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpAppsListToolsResult { + /// App-callable tools from the server + pub tools: 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 SessionMcpAppsGetHostContextParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Current host context advertised to MCP App guests. +/// +///
+/// +/// **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 SessionMcpAppsGetHostContextResult { + /// Current host context + pub context: McpAppsHostContextDetails, +} + +/// Diagnostic snapshot of MCP Apps wiring for the named server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpAppsDiagnoseResult { + /// Capability negotiation snapshot + pub capability: McpAppsDiagnoseCapability, + /// What the server returned for this session + pub server: McpAppsDiagnoseServer, +} + /// Identifies the target session. /// ///
@@ -10991,6 +11724,36 @@ pub struct SessionFsSqliteExistsParams { pub session_id: SessionId, } +/// MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+pub type McpExecuteSamplingResult = HashMap; + +/// The form values submitted by the user (present when action is 'accept') +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+pub type UIElicitationResponseContent = HashMap; + +/// Standard MCP CallToolResult +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+pub type SessionMcpAppsCallToolResult = HashMap; + /// Where the agent definition was loaded from /// ///
@@ -11070,6 +11833,28 @@ pub enum AuthInfoType { Unknown, } +/// Runtime-controlled routing state for an open canvas instance. +/// +///
+/// +/// **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 CanvasInstanceAvailability { + /// The owning provider is currently connected and routing calls will be dispatched normally. + #[serde(rename = "ready")] + Ready, + /// The owning provider is not currently connected. Routing calls fail with canvas_provider_unavailable until the agent re-issues open_canvas (which rehydrates via a fresh canvas.open) or the provider reconnects. + #[serde(rename = "stale")] + Stale, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion) /// ///
@@ -11170,7 +11955,7 @@ pub enum CopilotApiTokenAuthInfoType { CopilotApiToken, } -/// Server transport type: stdio, http, sse, or memory +/// Server transport type: stdio, http, sse (deprecated), or memory #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum DiscoveredMcpServerType { /// Server communicates over stdio with a local child process. @@ -11179,7 +11964,7 @@ pub enum DiscoveredMcpServerType { /// Server communicates over streamable HTTP. #[serde(rename = "http")] Http, - /// Server communicates over Server-Sent Events. + /// Server communicates over Server-Sent Events (deprecated). #[serde(rename = "sse")] Sse, /// Server is backed by an in-memory runtime implementation. @@ -11523,6 +12308,200 @@ pub enum SessionLogLevel { Unknown, } +/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. +/// +///
+/// +/// **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 McpAppsHostContextDetailsAvailableDisplayMode { + /// Rendered inline within the host conversation surface + #[serde(rename = "inline")] + Inline, + /// Rendered as a fullscreen overlay + #[serde(rename = "fullscreen")] + Fullscreen, + /// Rendered as a picture-in-picture floating panel + #[serde(rename = "pip")] + Pip, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Current display mode (SEP-1865) +/// +///
+/// +/// **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 McpAppsHostContextDetailsDisplayMode { + /// Rendered inline within the host conversation surface + #[serde(rename = "inline")] + Inline, + /// Rendered as a fullscreen overlay + #[serde(rename = "fullscreen")] + Fullscreen, + /// Rendered as a picture-in-picture floating panel + #[serde(rename = "pip")] + Pip, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Platform type for responsive design +/// +///
+/// +/// **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 McpAppsHostContextDetailsPlatform { + /// Host runs in a web browser + #[serde(rename = "web")] + Web, + /// Host runs as a desktop application + #[serde(rename = "desktop")] + Desktop, + /// Host runs on a mobile device + #[serde(rename = "mobile")] + Mobile, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// UI theme preference per SEP-1865 +/// +///
+/// +/// **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 McpAppsHostContextDetailsTheme { + /// Light UI theme + #[serde(rename = "light")] + Light, + /// Dark UI theme + #[serde(rename = "dark")] + Dark, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. +/// +///
+/// +/// **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 McpAppsSetHostContextDetailsAvailableDisplayMode { + /// Rendered inline within the host conversation surface + #[serde(rename = "inline")] + Inline, + /// Rendered as a fullscreen overlay + #[serde(rename = "fullscreen")] + Fullscreen, + /// Rendered as a picture-in-picture floating panel + #[serde(rename = "pip")] + Pip, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Current display mode (SEP-1865) +/// +///
+/// +/// **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 McpAppsSetHostContextDetailsDisplayMode { + /// Rendered inline within the host conversation surface + #[serde(rename = "inline")] + Inline, + /// Rendered as a fullscreen overlay + #[serde(rename = "fullscreen")] + Fullscreen, + /// Rendered as a picture-in-picture floating panel + #[serde(rename = "pip")] + Pip, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Platform type for responsive design +/// +///
+/// +/// **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 McpAppsSetHostContextDetailsPlatform { + /// Host runs in a web browser + #[serde(rename = "web")] + Web, + /// Host runs as a desktop application + #[serde(rename = "desktop")] + Desktop, + /// Host runs on a mobile device + #[serde(rename = "mobile")] + Mobile, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// UI theme preference per SEP-1865 +/// +///
+/// +/// **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 McpAppsSetHostContextDetailsTheme { + /// Light UI theme + #[serde(rename = "light")] + Light, + /// Dark UI theme + #[serde(rename = "dark")] + Dark, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Outcome of the sampling inference. 'success' produced a response; 'failure' encountered an error (including agent-side rejection by content filter or criteria); 'cancelled' the caller cancelled this execution via cancelSamplingExecution. /// ///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index b5599e09a..45011cf82 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -107,7 +107,7 @@ impl<'a> ClientRpc<'a> { /// # Returns /// /// Handshake result reporting the server's protocol version and package version on success. - pub async fn connect(&self, params: ConnectRequest) -> Result { + pub(crate) async fn connect(&self, params: ConnectRequest) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client @@ -1137,6 +1137,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.canvas.*` sub-namespace. + pub fn canvas(&self) -> SessionRpcCanvas<'a> { + SessionRpcCanvas { + session: self.session, + } + } + /// `session.commands.*` sub-namespace. pub fn commands(&self) -> SessionRpcCommands<'a> { SessionRpcCommands { @@ -1664,6 +1671,153 @@ impl<'a> SessionRpcAuth<'a> { } } +/// `session.canvas.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcCanvas<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcCanvas<'a> { + /// Lists canvases declared for the session. + /// + /// Wire method: `session.canvas.list`. + /// + /// # Returns + /// + /// Declared canvases available in this 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 list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CANVAS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists currently open canvas instances for the live session. + /// + /// Wire method: `session.canvas.listOpen`. + /// + /// # Returns + /// + /// Live open-canvas snapshot. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list_open(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CANVAS_LISTOPEN, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Opens or focuses a canvas instance. + /// + /// Wire method: `session.canvas.open`. + /// + /// # Parameters + /// + /// * `params` - Canvas open parameters. + /// + /// # Returns + /// + /// Open canvas instance snapshot. + /// + ///
+ /// + /// **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 open(&self, params: CanvasOpenRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CANVAS_OPEN, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Closes an open canvas instance. + /// + /// Wire method: `session.canvas.close`. + /// + /// # Parameters + /// + /// * `params` - Canvas close parameters. + /// + ///
+ /// + /// **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 close(&self, params: CanvasCloseRequest) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CANVAS_CLOSE, Some(wire_params)) + .await?; + Ok(()) + } + + /// Invokes an action on an open canvas instance. + /// + /// Wire method: `session.canvas.invokeAction`. + /// + /// # Parameters + /// + /// * `params` - Canvas action invocation parameters. + /// + /// # Returns + /// + /// Canvas action invocation result. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn invoke_action( + &self, + params: CanvasInvokeActionRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CANVAS_INVOKEACTION, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.commands.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcCommands<'a> { @@ -2444,6 +2598,13 @@ pub struct SessionRpcMcp<'a> { } impl<'a> SessionRpcMcp<'a> { + /// `session.mcp.apps.*` sub-namespace. + pub fn apps(&self) -> SessionRpcMcpApps<'a> { + SessionRpcMcpApps { + session: self.session, + } + } + /// `session.mcp.oauth.*` sub-namespace. pub fn oauth(&self) -> SessionRpcMcpOauth<'a> { SessionRpcMcpOauth { @@ -2677,6 +2838,209 @@ impl<'a> SessionRpcMcp<'a> { } } +/// `session.mcp.apps.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcpApps<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcpApps<'a> { + /// Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. + /// + /// Wire method: `session.mcp.apps.readResource`. + /// + /// # Parameters + /// + /// * `params` - MCP server and resource URI to fetch. + /// + /// # Returns + /// + /// Resource contents returned by the MCP server. + /// + ///
+ /// + /// **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_resource( + &self, + params: McpAppsReadResourceRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_APPS_READRESOURCE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. + /// + /// Wire method: `session.mcp.apps.listTools`. + /// + /// # Parameters + /// + /// * `params` - MCP server to list app-callable tools for. + /// + /// # Returns + /// + /// App-callable tools from the named MCP server. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list_tools( + &self, + params: McpAppsListToolsRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_APPS_LISTTOOLS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. + /// + /// Wire method: `session.mcp.apps.callTool`. + /// + /// # Parameters + /// + /// * `params` - MCP server, tool name, and arguments to invoke from an MCP App view. + /// + /// # Returns + /// + /// Standard MCP CallToolResult + /// + ///
+ /// + /// **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 call_tool( + &self, + params: McpAppsCallToolRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_APPS_CALLTOOL, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. + /// + /// Wire method: `session.mcp.apps.setHostContext`. + /// + /// # Parameters + /// + /// * `params` - Host context to advertise to MCP App guests. + /// + ///
+ /// + /// **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 set_host_context( + &self, + params: McpAppsSetHostContextRequest, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_APPS_SETHOSTCONTEXT, + Some(wire_params), + ) + .await?; + Ok(()) + } + + /// Read the current host context advertised to MCP App guests. + /// + /// Wire method: `session.mcp.apps.getHostContext`. + /// + /// # Returns + /// + /// Current host context advertised to MCP App guests. + /// + ///
+ /// + /// **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_host_context(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_APPS_GETHOSTCONTEXT, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. + /// + /// Wire method: `session.mcp.apps.diagnose`. + /// + /// # Parameters + /// + /// * `params` - MCP server to diagnose MCP Apps wiring for. + /// + /// # Returns + /// + /// Diagnostic snapshot of MCP Apps wiring for the named server. + /// + ///
+ /// + /// **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 diagnose( + &self, + params: McpAppsDiagnoseRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MCP_APPS_DIAGNOSE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.mcp.oauth.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcMcpOauth<'a> { diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 1f6334466..6605ecbde 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -171,6 +171,12 @@ pub enum SessionEventType { SessionMcpServerStatusChanged, #[serde(rename = "session.extensions_loaded")] SessionExtensionsLoaded, + #[serde(rename = "session.canvas.opened")] + SessionCanvasOpened, + #[serde(rename = "session.canvas.registry_changed")] + SessionCanvasRegistryChanged, + #[serde(rename = "mcp_app.tool_call_complete")] + McpAppToolCallComplete, /// Unknown event type for forward compatibility. #[default] #[serde(other)] @@ -345,6 +351,12 @@ pub enum SessionEventData { SessionMcpServerStatusChanged(SessionMcpServerStatusChangedData), #[serde(rename = "session.extensions_loaded")] SessionExtensionsLoaded(SessionExtensionsLoadedData), + #[serde(rename = "session.canvas.opened")] + SessionCanvasOpened(SessionCanvasOpenedData), + #[serde(rename = "session.canvas.registry_changed")] + SessionCanvasRegistryChanged(SessionCanvasRegistryChangedData), + #[serde(rename = "mcp_app.tool_call_complete")] + McpAppToolCallComplete(McpAppToolCallCompleteData), } /// A session event with typed data payload. @@ -499,6 +511,9 @@ pub struct SessionErrorData { /// GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs #[serde(skip_serializing_if = "Option::is_none")] pub provider_call_id: Option, + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + #[serde(skip_serializing_if = "Option::is_none")] + pub service_request_id: Option, /// Error stack trace, when available #[serde(skip_serializing_if = "Option::is_none")] pub stack: Option, @@ -589,6 +604,9 @@ pub struct SessionModelChangeData { /// Reason the change happened, when not user-initiated. Currently `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path. UI clients can use this to render contextual copy. #[serde(skip_serializing_if = "Option::is_none")] pub cause: Option, + /// Context tier after the model change; null explicitly clears a previously selected tier + #[serde(skip_serializing_if = "Option::is_none")] + pub context_tier: Option, /// Newly selected model identifier pub new_model: String, /// Model that was previously selected, if any @@ -723,9 +741,23 @@ pub struct ShutdownCodeChanges { #[serde(rename_all = "camelCase")] pub struct ShutdownModelMetricRequests { /// Cumulative cost multiplier for requests to this model + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub cost: Option, /// Total number of API requests made to this model + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub count: Option, } @@ -765,6 +797,13 @@ pub struct ShutdownModelMetric { #[serde(default)] pub token_details: HashMap, /// Accumulated nano-AI units cost for this model + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub total_nano_aiu: Option, /// Token usage breakdown @@ -815,11 +854,19 @@ pub struct SessionShutdownData { /// Cumulative time spent in API calls during the session, in milliseconds pub total_api_duration_ms: i64, /// Session-wide accumulated nano-AI units cost + /// + ///
+ /// + /// **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 total_nano_aiu: Option, /// Total number of premium API requests used during the session + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub total_premium_requests: Option, + pub(crate) total_premium_requests: Option, } /// Session event "session.context_changed". Updated working directory and git context after the change @@ -907,7 +954,7 @@ pub struct CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail { /// Per-request cost and usage data from the CAPI copilot_usage response field #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CompactionCompleteCompactionTokensUsedCopilotUsage { +pub(crate) struct CompactionCompleteCompactionTokensUsedCopilotUsage { /// Itemized token usage breakdown pub token_details: Vec, /// Total cost in nano-AI units for this request @@ -925,8 +972,9 @@ pub struct CompactionCompleteCompactionTokensUsed { #[serde(skip_serializing_if = "Option::is_none")] pub cache_write_tokens: Option, /// Per-request cost and usage data from the CAPI copilot_usage response field + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub copilot_usage: Option, + pub(crate) copilot_usage: Option, /// Duration of the compaction LLM call in milliseconds #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, @@ -978,6 +1026,9 @@ pub struct SessionCompactionCompleteData { /// GitHub request tracing ID (x-github-request-id header) for the compaction LLM call #[serde(skip_serializing_if = "Option::is_none")] pub request_id: Option, + /// Copilot service request ID (x-copilot-service-request-id header) for the compaction LLM call + #[serde(skip_serializing_if = "Option::is_none")] + pub service_request_id: Option, /// Whether compaction completed successfully pub success: bool, /// LLM-generated summary of the compacted conversation history @@ -1126,9 +1177,23 @@ pub struct AssistantMessageToolRequest { #[serde(rename_all = "camelCase")] pub struct AssistantMessageData { /// Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(default)] pub anthropic_advisor_blocks: Vec, /// Anthropic advisor model ID used for this response, for timeline display on replay + /// + ///
+ /// + /// **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 anthropic_advisor_model: Option, /// The assistant's text response content @@ -1164,6 +1229,9 @@ pub struct AssistantMessageData { /// GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs #[serde(skip_serializing_if = "Option::is_none")] pub request_id: Option, + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + #[serde(skip_serializing_if = "Option::is_none")] + pub service_request_id: Option, /// Tool invocations requested by the assistant in this message #[serde(default)] pub tool_requests: Vec, @@ -1223,7 +1291,7 @@ pub struct AssistantUsageCopilotUsageTokenDetail { /// Per-request cost and usage data from the CAPI copilot_usage response field #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AssistantUsageCopilotUsage { +pub(crate) struct AssistantUsageCopilotUsage { /// Itemized token usage breakdown pub token_details: Vec, /// Total cost in nano-AI units for this request @@ -1233,24 +1301,32 @@ pub struct AssistantUsageCopilotUsage { /// Schema for the `AssistantUsageQuotaSnapshot` type. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AssistantUsageQuotaSnapshot { +pub(crate) struct AssistantUsageQuotaSnapshot { /// Total requests allowed by the entitlement - pub entitlement_requests: i64, + #[doc(hidden)] + pub(crate) entitlement_requests: i64, /// Whether the user has an unlimited usage entitlement - pub is_unlimited_entitlement: bool, + #[doc(hidden)] + pub(crate) is_unlimited_entitlement: bool, /// Number of additional usage requests made this period - pub overage: f64, + #[doc(hidden)] + pub(crate) overage: f64, /// Whether additional usage is allowed when quota is exhausted - pub overage_allowed_with_exhausted_quota: bool, + #[doc(hidden)] + pub(crate) overage_allowed_with_exhausted_quota: bool, /// Percentage of quota remaining (0 to 100) - pub remaining_percentage: f64, + #[doc(hidden)] + pub(crate) remaining_percentage: f64, /// Date when the quota resets + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub reset_date: Option, + pub(crate) reset_date: Option, /// Whether usage is still permitted after quota exhaustion - pub usage_allowed_with_exhausted_quota: bool, + #[doc(hidden)] + pub(crate) usage_allowed_with_exhausted_quota: bool, /// Number of requests already consumed - pub used_requests: i64, + #[doc(hidden)] + pub(crate) used_requests: i64, } /// Session event "assistant.usage". LLM API call usage metrics including tokens, costs, quotas, and billing information @@ -1270,9 +1346,17 @@ pub struct AssistantUsageData { #[serde(skip_serializing_if = "Option::is_none")] pub cache_write_tokens: Option, /// Per-request cost and usage data from the CAPI copilot_usage response field + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub copilot_usage: Option, + pub(crate) copilot_usage: Option, /// Model multiplier cost for billing purposes + /// + ///
+ /// + /// **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 cost: Option, /// Duration of the API call in milliseconds @@ -1301,14 +1385,18 @@ pub struct AssistantUsageData { #[serde(skip_serializing_if = "Option::is_none")] pub provider_call_id: Option, /// Per-quota resource usage snapshots, keyed by quota identifier + #[doc(hidden)] #[serde(default)] - pub quota_snapshots: HashMap, + pub(crate) quota_snapshots: HashMap, /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, /// Number of output tokens used for reasoning (e.g., chain-of-thought) #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_tokens: Option, + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + #[serde(skip_serializing_if = "Option::is_none")] + pub service_request_id: Option, /// Time to first token in milliseconds. Only available for streaming requests #[serde(skip_serializing_if = "Option::is_none")] pub time_to_first_token_ms: Option, @@ -1336,6 +1424,9 @@ pub struct ModelCallFailureData { /// GitHub request tracing ID (x-github-request-id header) for server-side log correlation #[serde(skip_serializing_if = "Option::is_none")] pub provider_call_id: Option, + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation + #[serde(skip_serializing_if = "Option::is_none")] + pub service_request_id: Option, /// Where the failed model call originated pub source: ModelCallFailureSource, /// HTTP status code from the failed request @@ -1552,6 +1643,102 @@ pub struct ToolExecutionCompleteContentResource { pub r#type: ToolExecutionCompleteContentResourceType, } +/// Schema for the `ToolExecutionCompleteUIResourceMetaUICsp` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResourceMetaUICsp { + #[serde(default)] + pub base_uri_domains: Vec, + #[serde(default)] + pub connect_domains: Vec, + #[serde(default)] + pub frame_domains: Vec, + #[serde(default)] + pub resource_domains: Vec, +} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsCamera` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResourceMetaUIPermissionsCamera {} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite {} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation {} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone {} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissions` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResourceMetaUIPermissions { + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsCamera` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub camera: Option, + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsClipboardWrite` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub clipboard_write: Option, + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub geolocation: Option, + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub microphone: Option, +} + +/// Schema for the `ToolExecutionCompleteUIResourceMetaUI` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResourceMetaUI { + /// Schema for the `ToolExecutionCompleteUIResourceMetaUICsp` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub csp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub domain: Option, + /// Schema for the `ToolExecutionCompleteUIResourceMetaUIPermissions` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub permissions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prefers_border: Option, +} + +/// Resource-level UI metadata (CSP, permissions, visual preferences) +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResourceMeta { + /// Schema for the `ToolExecutionCompleteUIResourceMetaUI` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub ui: Option, +} + +/// MCP Apps UI resource content for rendering in a sandboxed iframe +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteUIResource { + /// Resource-level UI metadata (CSP, permissions, visual preferences) + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// Base64-encoded HTML content + #[serde(skip_serializing_if = "Option::is_none")] + pub blob: Option, + /// MIME type of the content + pub mime_type: String, + /// HTML content as a string + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + /// The ui:// URI of the resource + pub uri: String, +} + /// Tool execution result on success #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1564,6 +1751,44 @@ pub struct ToolExecutionCompleteResult { /// Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. #[serde(skip_serializing_if = "Option::is_none")] pub detailed_content: Option, + /// MCP Apps UI resource content for rendering in a sandboxed iframe + #[serde(skip_serializing_if = "Option::is_none")] + pub ui_resource: Option, +} + +/// Schema for the `ToolExecutionCompleteToolDescriptionMetaUI` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteToolDescriptionMetaUI { + /// URI of the UI resource + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_uri: Option, + /// Who can access this tool + #[serde(default)] + pub visibility: Vec, +} + +/// MCP Apps metadata for UI resource association +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteToolDescriptionMeta { + /// Schema for the `ToolExecutionCompleteToolDescriptionMetaUI` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub ui: Option, +} + +/// Tool definition metadata, present for MCP tools with MCP Apps support +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionCompleteToolDescription { + /// MCP Apps metadata for UI resource association + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// Tool description + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Tool name + pub name: String, } /// Session event "tool.execution_complete". Tool execution completion results including success status, detailed output, and error information @@ -1597,6 +1822,9 @@ pub struct ToolExecutionCompleteData { pub success: bool, /// Unique identifier for the completed tool call pub tool_call_id: String, + /// Tool definition metadata, present for MCP tools with MCP Apps support + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_description: Option, /// Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) #[serde(default)] pub tool_telemetry: HashMap, @@ -1627,6 +1855,12 @@ 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), personal-claude (~/.claude/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise 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) + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger: Option, } /// Session event "subagent.started". Sub-agent startup details including parent tool call and agent information @@ -2699,9 +2933,15 @@ pub struct CommandsChangedData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CapabilitiesChangedUI { + /// Whether canvas rendering is now supported + #[serde(skip_serializing_if = "Option::is_none")] + pub canvases: Option, /// Whether elicitation is now supported #[serde(skip_serializing_if = "Option::is_none")] pub elicitation: Option, + /// Whether MCP Apps (SEP-1865) UI passthrough is now supported + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp_apps: Option, } /// Session event "capabilities.changed". Session capability change notification @@ -2833,11 +3073,20 @@ pub struct McpServersLoadedServer { pub error: Option, /// Server name (config key) pub name: String, + /// Name of the plugin that supplied the effective MCP server config, only when source is plugin + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin_name: Option, + /// Version of the plugin that supplied the effective MCP server config, only when source is plugin + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin_version: Option, /// Configuration source: user, workspace, plugin, or builtin #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// Connection status: connected, failed, needs-auth, pending, disabled, or not_configured pub status: McpServerStatus, + /// Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server) + #[serde(skip_serializing_if = "Option::is_none")] + pub transport: Option, } /// Session event "session.mcp_servers_loaded". @@ -2852,6 +3101,9 @@ pub struct SessionMcpServersLoadedData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMcpServerStatusChangedData { + /// Error message if the server entered a failed state + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, /// Name of the MCP server whose status changed pub server_name: String, /// Connection status: connected, failed, needs-auth, pending, disabled, or not_configured @@ -2880,6 +3132,137 @@ pub struct SessionExtensionsLoadedData { pub extensions: Vec, } +/// Session event "session.canvas.opened". +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCanvasOpenedData { + /// Runtime-controlled routing state for the instance. "ready" when the provider connection is live; "stale" when the provider has gone away and the instance is awaiting rebinding. + pub availability: CanvasOpenedAvailability, + /// Provider-local canvas identifier + pub canvas_id: String, + /// Owning provider identifier + pub extension_id: String, + /// Owning extension display name, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_name: Option, + /// Input supplied when the instance was opened + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + /// Stable caller-supplied canvas instance identifier + pub instance_id: String, + /// Whether this notification represents an idempotent reopen + pub reopen: bool, + /// Provider-supplied status text + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + /// Rendered title + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// URL for web-rendered canvases + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// Schema for the `CanvasRegistryChangedCanvasAction` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasRegistryChangedCanvasAction { + /// Action description + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// JSON Schema for action input + #[serde(default)] + pub input_schema: HashMap, + /// Action name + pub name: String, +} + +/// Schema for the `CanvasRegistryChangedCanvas` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CanvasRegistryChangedCanvas { + /// Actions the agent or host may invoke + #[serde(default)] + pub actions: Vec, + /// Provider-local canvas identifier + pub canvas_id: String, + /// Short, single-sentence description shown to the agent in canvas catalogs. + pub description: String, + /// Human-readable canvas name + pub display_name: String, + /// Owning provider identifier + pub extension_id: String, + /// Owning extension display name, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_name: Option, + /// JSON Schema for canvas open input + #[serde(default)] + pub input_schema: HashMap, +} + +/// Session event "session.canvas.registry_changed". +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCanvasRegistryChangedData { + /// Canvas declarations currently available + pub canvases: Vec, +} + +/// Set when the underlying tools/call threw an error before returning a CallToolResult +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpAppToolCallCompleteError { + /// Human-readable error message + pub message: String, +} + +/// Schema for the `McpAppToolCallCompleteToolMetaUI` type. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpAppToolCallCompleteToolMetaUI { + /// `ui://` URI declared by the tool's `_meta.ui.resourceUri` + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_uri: Option, + /// Tool visibility per SEP-1865 (typically a subset of `["model","app"]`) + #[serde(default)] + pub visibility: Vec, +} + +/// The tool's `_meta.ui` block at the time of the call, so consumers can decide whether to forward the result to the model without re-listing tools. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpAppToolCallCompleteToolMeta { + /// Schema for the `McpAppToolCallCompleteToolMetaUI` type. + #[serde(skip_serializing_if = "Option::is_none")] + pub ui: Option, +} + +/// Session event "mcp_app.tool_call_complete". MCP App view called a tool on a connected MCP server (SEP-1865) +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpAppToolCallCompleteData { + /// Arguments passed to the tool by the app view, if any + #[serde(default)] + pub arguments: HashMap, + /// Wall-clock duration of the underlying tools/call in milliseconds + pub duration_ms: f64, + /// Set when the underlying tools/call threw an error before returning a CallToolResult + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Standard MCP CallToolResult returned by the server. Present whether or not the call set isError. + #[serde(default)] + pub result: HashMap, + /// Name of the MCP server hosting the tool + pub server_name: String, + /// True when the call completed without throwing AND the MCP CallToolResult did not set isError + pub success: bool, + /// The tool's `_meta.ui` block at the time of the call, so consumers can decide whether to forward the result to the model without re-listing tools. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_meta: Option, + /// MCP tool name that was invoked + pub tool_name: String, +} + /// Hosting platform type of the repository (github or ado) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum WorkingDirectoryContextHostType { @@ -2913,6 +3296,20 @@ pub enum ReasoningSummary { Unknown, } +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SessionModelChangeDataContextTier { + /// Default context tier with standard context window size. + #[serde(rename = "default")] + Default, + /// Extended context tier with a larger context window. + #[serde(rename = "long_context")] + LongContext, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The session mode the agent is operating in #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SessionMode { @@ -3170,6 +3567,39 @@ pub enum ToolExecutionCompleteContent { Resource(ToolExecutionCompleteContentResource), } +/// Allowed values for the `ToolExecutionCompleteToolDescriptionMetaUIVisibility` enumeration. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ToolExecutionCompleteToolDescriptionMetaUIVisibility { + /// Tool is callable by the model (LLM tool surface) + #[serde(rename = "model")] + Model, + /// Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool + #[serde(rename = "app")] + App, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// 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) +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SkillInvokedTrigger { + /// Skill invocation requested explicitly by the user, such as via a slash command or UI affordance. + #[serde(rename = "user-invoked")] + UserInvoked, + /// Skill invocation requested by the agent. + #[serde(rename = "agent-invoked")] + AgentInvoked, + /// Skill content loaded as part of another context, such as a configured custom agent or subagent. + #[serde(rename = "context-load")] + ContextLoad, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Message role: "system" for system prompts, "developer" for developer-injected instructions #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SystemMessageRole { @@ -3767,6 +4197,27 @@ pub enum McpServerStatus { Unknown, } +/// Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server) +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpServerTransport { + /// Server communicates over stdio with a local child process. + #[serde(rename = "stdio")] + Stdio, + /// Server communicates over streamable HTTP. + #[serde(rename = "http")] + Http, + /// Server communicates over Server-Sent Events (deprecated). + #[serde(rename = "sse")] + Sse, + /// Server is backed by an in-memory runtime implementation. + #[serde(rename = "memory")] + Memory, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Discovery source #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ExtensionsLoadedExtensionSource { @@ -3802,3 +4253,18 @@ pub enum ExtensionsLoadedExtensionStatus { #[serde(other)] Unknown, } + +/// Runtime-controlled routing state for the instance. "ready" when the provider connection is live; "stale" when the provider has gone away and the instance is awaiting rebinding. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CanvasOpenedAvailability { + /// Provider connection is live; actions can be invoked. + #[serde(rename = "ready")] + Ready, + /// Provider has gone away; the instance is awaiting rebinding. + #[serde(rename = "stale")] + Stale, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 565b09d56..dadd1706f 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -1,159 +1,81 @@ -//! Event handler traits for session lifecycle. +//! Optional session-callback traits. //! -//! The [`SessionHandler`](crate::handler::SessionHandler) trait is the primary extension point — implement -//! [`on_event`](crate::handler::SessionHandler::on_event) to control how sessions respond to -//! CLI events, permission requests, tool calls, and user input prompts. +//! Each callback the CLI may dispatch (permission requests, elicitation +//! prompts, user-input questions, exit-plan-mode prompts, +//! auto-mode-switch prompts) has its own focused trait with a single +//! `handle` method. +//! +//! Handlers are **optional**: install only the ones the application cares +//! about. The SDK derives the corresponding wire flag on +//! `session.create` / `session.resume` from the presence of each handler, +//! so the runtime does not emit broadcasts this client would never +//! respond to. +//! +//! Tool dispatch uses its own per-tool registry built from +//! [`Tool::with_handler`](crate::types::Tool::with_handler) on entries passed to +//! [`SessionConfig::with_tools`](crate::types::SessionConfig::with_tools). use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use crate::generated::api_types::{ + PermissionDecision, PermissionDecisionApproveOnce, PermissionDecisionReject, + PermissionDecisionUserNotAvailable, +}; use crate::types::{ ElicitationRequest, ElicitationResult, ExitPlanModeData, PermissionRequestData, RequestId, - SessionEvent, SessionId, ToolInvocation, ToolResult, + SessionId, }; -/// Events dispatched by the SDK session event loop to the handler. +/// Decision returned by a [`PermissionHandler`]. /// -/// The handler returns a [`HandlerResponse`] indicating how the SDK should -/// respond to the CLI. For fire-and-forget events (`SessionEvent`), the -/// response is ignored. -#[non_exhaustive] -#[derive(Debug)] -pub enum HandlerEvent { - /// Informational session event from the timeline (e.g. assistant.message_delta, - /// session.idle, tool.execution_start). Fire-and-forget — return `HandlerResponse::Ok`. - SessionEvent { - /// The session that emitted this event. - session_id: SessionId, - /// The event payload. - event: SessionEvent, - }, - - /// The CLI requests permission for an action. Return `HandlerResponse::Permission(..)`. - PermissionRequest { - /// The requesting session. - session_id: SessionId, - /// Unique ID to correlate the response. - request_id: RequestId, - /// Permission request payload. - data: PermissionRequestData, - }, - - /// The CLI requests user input. Return `HandlerResponse::UserInput(..)`. - /// The handler may block (e.g. awaiting a UI dialog) — this is expected. - UserInput { - /// The requesting session. - session_id: SessionId, - /// The question text to present. - question: String, - /// Optional multiple-choice options. - choices: Option>, - /// Whether free-form text input is allowed. - allow_freeform: Option, - }, - - /// The CLI requests execution of a client-defined tool. - /// Return `HandlerResponse::ToolResult(..)`. - ExternalTool { - /// The tool call to execute. - invocation: ToolInvocation, - }, +/// Either a concrete wire-level [`PermissionDecision`] (approve, reject, +/// approve-for-session, approve-permanently, user-not-available, …) or +/// [`PermissionResult::NoResult`], which tells the SDK to suppress its +/// response so another connected client can answer instead. +#[derive(Debug, Clone)] +pub enum PermissionResult { + /// Send a permission decision on the wire. + Decision(PermissionDecision), + /// Decline to respond to this request, allowing another connected + /// client to answer instead. The SDK suppresses the response. + NoResult, +} - /// The CLI broadcasts an elicitation request for the provider to handle. - /// Return `HandlerResponse::Elicitation(..)`. - ElicitationRequest { - /// The requesting session. - session_id: SessionId, - /// Unique ID to correlate the response. - request_id: RequestId, - /// The elicitation request payload. - request: ElicitationRequest, - }, +impl PermissionResult { + /// Approve this single request. + pub fn approve_once() -> Self { + Self::Decision(PermissionDecision::ApproveOnce( + PermissionDecisionApproveOnce::default(), + )) + } - /// The CLI requests exiting plan mode. Return `HandlerResponse::ExitPlanMode(..)`. - ExitPlanMode { - /// The requesting session. - session_id: SessionId, - /// Plan mode exit payload. - data: ExitPlanModeData, - }, + /// Reject the request, optionally forwarding feedback to the LLM. + pub fn reject(feedback: impl Into>) -> Self { + Self::Decision(PermissionDecision::Reject(PermissionDecisionReject { + feedback: feedback.into(), + ..Default::default() + })) + } - /// The CLI asks whether to switch to auto model when an eligible rate - /// limit is hit. Return [`HandlerResponse::AutoModeSwitch`]. - AutoModeSwitch { - /// The requesting session. - session_id: SessionId, - /// The specific rate-limit error code that triggered the request, - /// if known (e.g. `user_weekly_rate_limited`, `user_global_rate_limited`). - error_code: Option, - /// Seconds until the rate limit resets, when known. - retry_after_seconds: Option, - }, -} + /// Deny because no user is available to confirm. + pub fn user_not_available() -> Self { + Self::Decision(PermissionDecision::UserNotAvailable( + PermissionDecisionUserNotAvailable::default(), + )) + } -/// Response from the handler back to the SDK, used to construct the -/// JSON-RPC reply sent to the CLI. -#[non_exhaustive] -#[derive(Debug)] -pub enum HandlerResponse { - /// No response needed (used for fire-and-forget `SessionEvent`s). - Ok, - /// Do not send a response. The consumer will resolve the pending request out-of-band. - NoResult, - /// Permission decision. - Permission(PermissionResult), - /// User input response (or `None` to signal no input available). - UserInput(Option), - /// Result of a tool execution. - ToolResult(ToolResult), - /// Elicitation result (accept/decline/cancel with optional form data). - Elicitation(ElicitationResult), - /// Exit plan mode decision. - ExitPlanMode(ExitPlanModeResult), - /// Auto-mode-switch decision. - AutoModeSwitch(AutoModeSwitchResponse), + /// Decline to respond, allowing another connected client to answer + /// instead. + pub fn no_result() -> Self { + Self::NoResult + } } -/// Result of a permission request. -/// -/// `#[non_exhaustive]` so future variants can be added without a major -/// version bump. Match arms must include a `_` fallback. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum PermissionResult { - /// Permission granted. - Approved, - /// Permission denied. - Denied, - /// Defer the response. The handler will resolve this request itself - /// later — typically after a UI prompt — by calling - /// `session.permissions.handlePendingPermissionRequest` directly. The - /// SDK will not send a response for this request. - /// - /// **Notification path only** (`permission.requested`). On the direct - /// RPC path (`permission.request`), `Deferred` falls back to - /// [`Approved`](Self::Approved) because that path must return a value - /// to satisfy the JSON-RPC reply contract. - Deferred, - /// Provide the full response payload. The SDK passes the value as-is - /// in the `result` field of `handlePendingPermissionRequest` - /// (notification path) or as the JSON-RPC `result` directly (direct - /// RPC path). - /// - /// Use this for response shapes beyond `{ "kind": "approve-once" }` - /// or `{ "kind": "reject" }` — for example, "approve and remember" - /// with allowlist data. - Custom(serde_json::Value), - /// No user is available to respond — for example, headless agents - /// without an interactive session. Sent as - /// `{ "kind": "user-not-available" }`. - UserNotAvailable, - /// The handler has no result to provide and the CLI should fall back - /// to another permission responder or its default policy. On the - /// notification path, the SDK will not send a pending permission response. - /// Distinct from [`Deferred`](Self::Deferred), where the handler takes - /// responsibility for resolving the request later out-of-band. - NoResult, +impl From for PermissionResult { + fn from(value: PermissionDecision) -> Self { + Self::Decision(value) + } } /// Response to a user input request. @@ -189,488 +111,159 @@ impl Default for ExitPlanModeResult { } } -/// Response to a [`HandlerEvent::AutoModeSwitch`] request. -/// -/// Wire serialization matches the CLI's `autoModeSwitch.request` response -/// schema: `"yes"`, `"yes_always"`, or `"no"`. +/// Response to an auto-mode-switch request. #[non_exhaustive] #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AutoModeSwitchResponse { /// Approve the auto-mode switch for this rate-limit cycle only. Yes, - /// Approve and remember — auto-accept future auto-mode switches in this - /// session without prompting. + /// Approve and remember -- auto-accept future auto-mode switches in + /// this session without prompting. YesAlways, - /// Decline the auto-mode switch. The session stays on the current model - /// and surfaces the rate-limit error. + /// Decline the auto-mode switch. The session stays on the current + /// model and surfaces the rate-limit error. No, } -/// Callback trait for session events. -/// -/// Implement this trait to control how a session responds to CLI events, -/// permission requests, tool calls, user input prompts, elicitations, and -/// plan-mode exits. There are two styles of implementation — pick whichever -/// fits your use case: -/// -/// 1. **Per-event methods (recommended for most handlers).** Override the -/// specific `on_*` methods you care about; every method has a safe -/// default so you only write what you need. This is the pattern used by -/// [`serenity::EventHandler`][serenity], `lapin`, and most Rust SDKs -/// that dispatch broker/client callbacks. -/// 2. **Single [`on_event`](Self::on_event) method.** Override this one -/// method and `match` on [`HandlerEvent`] yourself. Useful for logging -/// middleware, custom routing, or when you want an exhaustiveness check -/// across all variants. -/// -/// When you override [`on_event`](Self::on_event) directly, the per-event methods are not -/// called — your implementation is entirely responsible for dispatch. The -/// default [`on_event`](Self::on_event) fans out to the per-event methods. -/// -/// [serenity]: https://docs.rs/serenity/latest/serenity/client/trait.EventHandler.html -/// -/// # Default behavior -/// -/// - Permission requests → **denied**. -/// - User input → `None` (no answer available). -/// - External tool calls → failure result with "no handler registered". -/// - Elicitation → `"cancel"`. -/// - Exit plan mode → [`ExitPlanModeResult::default`]. -/// - Auto-mode-switch → [`AutoModeSwitchResponse::No`] (decline by default; the -/// session stays on its current model and surfaces the rate-limit error). -/// - Session events → ignored (fire-and-forget). +/// Handler for `permission.requested` broadcasts. /// -/// # Concurrency -/// -/// **Request-triggered events** (`UserInput`, `ExternalTool` via `tool.call`, -/// `ExitPlanMode`, `PermissionRequest` via `permission.request`) are awaited -/// inline in the event loop and therefore processed **serially** per session. -/// Blocking here pauses that session's event loop — which is correct, since -/// the CLI is also blocked waiting for the response. -/// -/// **Notification-triggered events** (`PermissionRequest` via -/// `permission.requested`, `ExternalTool` via `external_tool.requested`) are -/// dispatched on spawned tasks and may run **concurrently** with each other -/// and with the serial event loop. Implementations must be safe for -/// concurrent invocation. -/// -/// # Example -/// -/// ```no_run -/// use async_trait::async_trait; -/// use github_copilot_sdk::handler::{PermissionResult, SessionHandler}; -/// use github_copilot_sdk::types::{PermissionRequestData, RequestId, SessionId}; -/// -/// struct ApproveReadsOnly; -/// -/// #[async_trait] -/// impl SessionHandler for ApproveReadsOnly { -/// async fn on_permission_request( -/// &self, -/// _sid: SessionId, -/// _rid: RequestId, -/// data: PermissionRequestData, -/// ) -> PermissionResult { -/// match data.extra.get("tool").and_then(|v| v.as_str()) { -/// Some("view") | Some("ls") | Some("grep") => PermissionResult::Approved, -/// _ => PermissionResult::Denied, -/// } -/// } -/// } -/// ``` +/// Install via +/// [`SessionConfig::with_permission_handler`](crate::types::SessionConfig::with_permission_handler) +/// (or the matching method on [`ResumeSessionConfig`](crate::types::ResumeSessionConfig)). +/// When no permission handler is supplied, the SDK sends +/// `requestPermission: false` on the wire and the runtime short-circuits +/// permission prompts for this client. #[async_trait] -pub trait SessionHandler: Send + Sync + 'static { - /// Handle an event from the session. - /// - /// The default implementation destructures `event` and calls the - /// matching per-event method (e.g. [`on_permission_request`](Self::on_permission_request) - /// for [`HandlerEvent::PermissionRequest`]). Override this method only - /// if you want a single dispatch point with exhaustive matching — most - /// handlers should override the per-event methods instead. - /// - /// See the [trait-level docs](SessionHandler#concurrency) for details on - /// which events may be dispatched concurrently. - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::SessionEvent { session_id, event } => { - self.on_session_event(session_id, event).await; - HandlerResponse::Ok - } - HandlerEvent::PermissionRequest { - session_id, - request_id, - data, - } => HandlerResponse::Permission( - self.on_permission_request(session_id, request_id, data) - .await, - ), - HandlerEvent::UserInput { - session_id, - question, - choices, - allow_freeform, - } => HandlerResponse::UserInput( - self.on_user_input(session_id, question, choices, allow_freeform) - .await, - ), - HandlerEvent::ExternalTool { invocation } => { - HandlerResponse::ToolResult(self.on_external_tool(invocation).await) - } - HandlerEvent::ElicitationRequest { - session_id, - request_id, - request, - } => HandlerResponse::Elicitation( - self.on_elicitation(session_id, request_id, request).await, - ), - HandlerEvent::ExitPlanMode { session_id, data } => { - HandlerResponse::ExitPlanMode(self.on_exit_plan_mode(session_id, data).await) - } - HandlerEvent::AutoModeSwitch { - session_id, - error_code, - retry_after_seconds, - } => HandlerResponse::AutoModeSwitch( - self.on_auto_mode_switch(session_id, error_code, retry_after_seconds) - .await, - ), - } - } - - /// Informational timeline event (assistant messages, tool execution - /// markers, session idle, etc.). Fire-and-forget — the return value is - /// ignored. - /// - /// Default: do nothing. - async fn on_session_event(&self, _session_id: SessionId, _event: SessionEvent) {} - - /// The CLI is asking whether the agent may perform a privileged action. - /// - /// Default: [`PermissionResult::Denied`]. The default-deny posture - /// matches the CLI's safety model; override to implement your own - /// policy (see the [`permission`](crate::permission) module for common - /// wrappers like `approve_all` / `approve_if`). - async fn on_permission_request( +pub trait PermissionHandler: Send + Sync + 'static { + /// Resolve a permission request. + async fn handle( &self, - _session_id: SessionId, - _request_id: RequestId, - _data: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::Denied - } + session_id: SessionId, + request_id: RequestId, + data: PermissionRequestData, + ) -> PermissionResult; +} - /// The CLI is asking the user a question (optionally with a list of - /// choices). - /// - /// Default: `None` — the CLI interprets this as "no answer available" - /// and falls back to its own prompt behavior. - async fn on_user_input( +/// Handler for `elicitation.requested` broadcasts. +/// +/// When unset, `requestElicitation: false` goes on the wire. +#[async_trait] +pub trait ElicitationHandler: Send + Sync + 'static { + /// Respond to an elicitation prompt (form, URL confirm, etc.). + async fn handle( &self, - _session_id: SessionId, - _question: String, - _choices: Option>, - _allow_freeform: Option, - ) -> Option { - None - } - - /// The CLI wants to invoke a client-defined ("external") tool. - /// - /// Default: a failure [`ToolResult`] indicating no tool handler is - /// registered. Typical implementations route to a - /// [`ToolHandlerRouter`](crate::tool::ToolHandlerRouter) which - /// dispatches to tools registered via - /// [`define_tool`](crate::tool::define_tool) or custom - /// [`ToolHandler`](crate::tool::ToolHandler) impls. - async fn on_external_tool(&self, invocation: ToolInvocation) -> ToolResult { - let msg = format!("No handler registered for tool '{}'", invocation.tool_name); - ToolResult::Expanded(crate::types::ToolResultExpanded { - text_result_for_llm: msg.clone(), - result_type: "failure".to_string(), - binary_results_for_llm: None, - session_log: None, - error: Some(msg), - tool_telemetry: None, - }) - } + session_id: SessionId, + request_id: RequestId, + request: ElicitationRequest, + ) -> ElicitationResult; +} - /// The CLI is requesting an elicitation (structured form / URL prompt). - /// - /// Default: cancel. - async fn on_elicitation( +/// Handler for `user_input.requested` events from the `ask_user` tool. +/// +/// When unset, `requestUserInput: false` goes on the wire and the +/// `ask_user` tool is disabled for the session. +#[async_trait] +pub trait UserInputHandler: Send + Sync + 'static { + /// Answer a question on behalf of the user. Return `None` to signal + /// "no answer available". + async fn handle( &self, - _session_id: SessionId, - _request_id: RequestId, - _request: ElicitationRequest, - ) -> ElicitationResult { - ElicitationResult { - action: "cancel".to_string(), - content: None, - } - } + session_id: SessionId, + question: String, + choices: Option>, + allow_freeform: Option, + ) -> Option; +} - /// The CLI is asking the user whether to exit plan mode. - /// - /// Default: [`ExitPlanModeResult::default`] (approved with no action). - async fn on_exit_plan_mode( - &self, - _session_id: SessionId, - _data: ExitPlanModeData, - ) -> ExitPlanModeResult { - ExitPlanModeResult::default() - } +/// Handler for `exit_plan_mode.requested` events. When unset, +/// `requestExitPlanMode: false` goes on the wire. +#[async_trait] +pub trait ExitPlanModeHandler: Send + Sync + 'static { + /// Decide whether to leave plan mode. + async fn handle(&self, session_id: SessionId, data: ExitPlanModeData) -> ExitPlanModeResult; +} - /// The CLI is asking whether to switch to auto model after an eligible - /// rate limit. - /// - /// `retry_after_seconds`, when present, is the number of seconds until the - /// rate limit resets. Handlers can use it to render a humanized reset time - /// alongside the prompt. - /// - /// Default: [`AutoModeSwitchResponse::No`] — decline. Override only if - /// your application surfaces a UX for the rate-limit-recovery prompt. - async fn on_auto_mode_switch( +/// Handler for `auto_mode_switch.requested` events. When unset, +/// `requestAutoModeSwitch: false` goes on the wire. +#[async_trait] +pub trait AutoModeSwitchHandler: Send + Sync + 'static { + /// Decide whether to fall back to the auto model after an eligible + /// rate-limit error. `retry_after_seconds`, when present, is the + /// number of seconds until the rate limit resets. + async fn handle( &self, - _session_id: SessionId, - _error_code: Option, - _retry_after_seconds: Option, - ) -> AutoModeSwitchResponse { - AutoModeSwitchResponse::No - } + session_id: SessionId, + error_code: Option, + retry_after_seconds: Option, + ) -> AutoModeSwitchResponse; } -/// A [`SessionHandler`] that auto-approves all permissions and ignores all events. -/// -/// Useful for CLI tools, scripts, and tests that don't need interactive -/// permission prompts or custom tool handling. +/// A [`PermissionHandler`] that approves every request. Useful for CLI +/// tools, scripts, and tests that don't need interactive permission +/// prompts. #[derive(Debug, Clone)] pub struct ApproveAllHandler; #[async_trait] -impl SessionHandler for ApproveAllHandler { - async fn on_permission_request( +impl PermissionHandler for ApproveAllHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, _data: PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } } -/// A [`SessionHandler`] that denies all permission requests and otherwise -/// relies on the trait's default fallback responses for every other event -/// (e.g. tool invocations return "unhandled", elicitations cancel, plan-mode -/// prompts decline). Use this when a session should never wait for manual -/// permission approval. +/// A [`PermissionHandler`] that denies every request. #[derive(Debug, Clone)] pub struct DenyAllHandler; #[async_trait] -impl SessionHandler for DenyAllHandler { - // All defaults are already safe: permissions deny, everything else is a - // sensible fallback. We just reuse them here for clarity. -} - -/// A [`SessionHandler`] that leaves permission requests and external tool calls pending. -/// -/// This is the default used when no handler is set on -/// [`SessionConfig::handler`](crate::types::SessionConfig::handler). It lets consumers -/// observe `permission.requested` and `external_tool.requested` events and later resolve -/// them with the corresponding pending-request RPC methods. -#[derive(Debug, Clone)] -pub struct NoopHandler; - -#[async_trait] -impl SessionHandler for NoopHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::SessionEvent { .. } => HandlerResponse::Ok, - HandlerEvent::PermissionRequest { .. } => { - HandlerResponse::Permission(PermissionResult::NoResult) - } - HandlerEvent::UserInput { .. } => HandlerResponse::UserInput(None), - HandlerEvent::ExternalTool { .. } => HandlerResponse::NoResult, - HandlerEvent::ElicitationRequest { .. } => { - HandlerResponse::Elicitation(ElicitationResult { - action: "cancel".to_string(), - content: None, - }) - } - HandlerEvent::ExitPlanMode { .. } => { - HandlerResponse::ExitPlanMode(ExitPlanModeResult::default()) - } - HandlerEvent::AutoModeSwitch { .. } => { - HandlerResponse::AutoModeSwitch(AutoModeSwitchResponse::No) - } - } +impl PermissionHandler for DenyAllHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _data: PermissionRequestData, + ) -> PermissionResult { + PermissionResult::reject(None) } } #[cfg(test)] mod tests { - use serde_json::Value; - use super::*; - use crate::types::{PermissionRequestData, RequestId, SessionId}; - - fn perm_data() -> PermissionRequestData { - PermissionRequestData::default() - } - - // A handler that overrides only `on_permission_request` (per-method style). - struct ApproveViaPerMethod; - - #[async_trait] - impl SessionHandler for ApproveViaPerMethod { - async fn on_permission_request( - &self, - _: SessionId, - _: RequestId, - _: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::Approved - } - } - - // A handler that overrides `on_event` directly (legacy / routing style). - struct ApproveViaOnEvent; - - #[async_trait] - impl SessionHandler for ApproveViaOnEvent { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::PermissionRequest { .. } => { - HandlerResponse::Permission(PermissionResult::Approved) - } - _ => HandlerResponse::Ok, - } - } - } - - #[tokio::test] - async fn per_method_override_dispatches_via_default_on_event() { - let h = ApproveViaPerMethod; - let resp = h - .on_event(HandlerEvent::PermissionRequest { - session_id: SessionId::from("s1".to_string()), - request_id: RequestId::new("r1"), - data: perm_data(), - }) - .await; - assert!(matches!( - resp, - HandlerResponse::Permission(PermissionResult::Approved) - )); - } #[tokio::test] - async fn on_event_override_short_circuits_per_method_defaults() { - let h = ApproveViaOnEvent; - let resp = h - .on_event(HandlerEvent::PermissionRequest { - session_id: SessionId::from("s1".to_string()), - request_id: RequestId::new("r1"), - data: perm_data(), - }) + async fn approve_all_handler_returns_approved() { + let result = ApproveAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData::default(), + ) .await; assert!(matches!( - resp, - HandlerResponse::Permission(PermissionResult::Approved) + result, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) )); } #[tokio::test] - async fn deny_all_handler_uses_default_permission_deny() { - let h = DenyAllHandler; - let resp = h - .on_event(HandlerEvent::PermissionRequest { - session_id: SessionId::from("s1".to_string()), - request_id: RequestId::new("r1"), - data: perm_data(), - }) + async fn deny_all_handler_returns_denied() { + let result = DenyAllHandler + .handle( + SessionId::from("s1"), + RequestId::new("1"), + PermissionRequestData::default(), + ) .await; assert!(matches!( - resp, - HandlerResponse::Permission(PermissionResult::Denied) + result, + PermissionResult::Decision(PermissionDecision::Reject(_)) )); } - - #[tokio::test] - async fn default_on_external_tool_returns_failure() { - let h = DenyAllHandler; - let resp = h - .on_event(HandlerEvent::ExternalTool { - invocation: crate::types::ToolInvocation { - session_id: SessionId::from("s1".to_string()), - tool_call_id: "tc1".to_string(), - tool_name: "missing".to_string(), - arguments: Value::Null, - traceparent: None, - tracestate: None, - }, - }) - .await; - match resp { - HandlerResponse::ToolResult(crate::types::ToolResult::Expanded(exp)) => { - assert_eq!(exp.result_type, "failure"); - assert!(exp.text_result_for_llm.contains("missing")); - assert_eq!(exp.error.as_deref(), Some(exp.text_result_for_llm.as_str())); - } - other => panic!("unexpected response: {other:?}"), - } - } - - #[tokio::test] - async fn noop_handler_leaves_permission_and_external_tool_pending() { - let h = NoopHandler; - let permission = h - .on_event(HandlerEvent::PermissionRequest { - session_id: SessionId::from("s1".to_string()), - request_id: RequestId::new("r1"), - data: perm_data(), - }) - .await; - assert!(matches!( - permission, - HandlerResponse::Permission(PermissionResult::NoResult) - )); - - let tool = h - .on_event(HandlerEvent::ExternalTool { - invocation: crate::types::ToolInvocation { - session_id: SessionId::from("s1".to_string()), - tool_call_id: "tc1".to_string(), - tool_name: "manual".to_string(), - arguments: Value::Null, - traceparent: None, - tracestate: None, - }, - }) - .await; - assert!(matches!(tool, HandlerResponse::NoResult)); - } - - #[tokio::test] - async fn default_on_elicitation_returns_cancel() { - let h = DenyAllHandler; - let resp = h - .on_event(HandlerEvent::ElicitationRequest { - session_id: SessionId::from("s1".to_string()), - request_id: RequestId::new("r1"), - request: crate::types::ElicitationRequest { - message: "test".to_string(), - requested_schema: None, - mode: Some(crate::types::ElicitationMode::Form), - elicitation_source: None, - url: None, - }, - }) - .await; - match resp { - HandlerResponse::Elicitation(r) => assert_eq!(r.action, "cancel"), - other => panic!("unexpected response: {other:?}"), - } - } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 464c599a3..787697e2e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,17 +3,19 @@ #![deny(rustdoc::broken_intra_doc_links)] #![cfg_attr(test, allow(clippy::unwrap_used))] +/// Canvas declarations, provider callbacks, and host-side canvas RPC types. +pub mod canvas; /// Bundled CLI binary extraction and caching. -pub mod embeddedcli; +pub(crate) mod embeddedcli; /// Event handler traits for session lifecycle. pub mod handler; /// Lifecycle hook callbacks (pre/post tool use, prompt submission, session start/end). pub mod hooks; mod jsonrpc; -/// Permission-policy helpers that wrap an existing [`handler::SessionHandler`]. +/// Permission-policy helpers that produce a [`handler::PermissionHandler`]. pub mod permission; /// GitHub Copilot CLI binary resolution (env var, embedded, PATH search). -pub mod resolve; +pub(crate) mod resolve; mod router; /// Session management — create, resume, send messages, and interact with the agent. pub mod session; @@ -30,6 +32,7 @@ pub mod trace_context; pub mod transforms; /// Protocol types shared between the SDK and the GitHub Copilot CLI. pub mod types; +mod wire; /// Auto-generated protocol types from Copilot JSON Schemas. pub mod generated; @@ -68,7 +71,7 @@ pub use sdk_protocol_version::{SDK_PROTOCOL_VERSION, get_sdk_protocol_version}; pub use subscription::{EventSubscription, Lagged, LifecycleSubscription, RecvError}; /// Minimum protocol version this SDK can communicate with. -const MIN_PROTOCOL_VERSION: u32 = 2; +const MIN_PROTOCOL_VERSION: u32 = 3; /// Errors returned by the SDK. #[derive(Debug, thiserror::Error)] @@ -289,6 +292,10 @@ pub enum Transport { Tcp { /// Port to listen on (0 for OS-assigned). port: u16, + /// Optional connection token. When `None` and the SDK is spawning + /// the CLI, the SDK auto-generates a 128-bit hex token so the + /// loopback listener is safe by default. + connection_token: Option, }, /// Connect to an already-running CLI server (no process spawning). External { @@ -296,6 +303,9 @@ pub enum Transport { host: String, /// Port of the running server. port: u16, + /// Optional connection token. Required when the external server + /// was started with a token, ignored otherwise. + connection_token: Option, }, } @@ -318,12 +328,13 @@ impl From for CliProgram { /// Options for starting a [`Client`]. /// -/// When `program` is [`CliProgram::Resolve`] (the default), -/// [`Client::start`] automatically resolves the binary via -/// [`resolve::copilot_binary()`] — checking `COPILOT_CLI_PATH`, the -/// embedded CLI, and then the system PATH and common install locations. +/// When `program` is [`CliProgram::Resolve`] (the default), [`Client::start`] +/// uses the bundled Copilot CLI that was embedded at build time (via the +/// default `bundled-cli` cargo feature). /// -/// Set `program` to [`CliProgram::Path`] to use an explicit binary. +/// Set `program` to [`CliProgram::Path`] to use an explicit binary instead. +/// This is the required path if you've opted out of bundling via +/// `default-features = false`. #[non_exhaustive] pub struct ClientOptions { /// How to locate the CLI binary. @@ -350,7 +361,8 @@ pub struct ClientOptions { /// [`Self::github_token`] is set, in which case false). pub use_logged_in_user: Option, /// Log level passed to the CLI server via `--log-level`. When `None`, - /// the SDK uses [`LogLevel::Info`]. + /// the SDK does not pass `--log-level` to the runtime at all and the + /// CLI uses its built-in default. pub log_level: Option, /// Server-wide idle timeout for sessions, in seconds. When set to a /// positive value, the SDK passes `--session-idle-timeout ` to @@ -392,23 +404,27 @@ pub struct ClientOptions { /// auth, telemetry buffers). When set, exported as `COPILOT_HOME` to /// the spawned CLI process. Useful for sandboxing test runs or /// running multiple isolated SDK instances side-by-side. - pub copilot_home: Option, - /// Optional connection token for TCP transport. Sent to the CLI in - /// the `connect` handshake and exported as `COPILOT_CONNECTION_TOKEN` - /// to spawned CLI processes. Required when the CLI server was started - /// with a token, ignored otherwise. - /// - /// When the SDK spawns its own CLI in TCP mode and this is left - /// `None`, a UUID is generated automatically so the loopback listener - /// is safe by default. Combining with [`Transport::Stdio`] is invalid - /// and surfaces as an error from [`Client::start`]. - pub tcp_connection_token: Option, + pub base_directory: Option, /// Enable remote session support (Mission Control integration). /// When `true`, the SDK passes `--remote` to the spawned CLI process so /// sessions in a GitHub repository working directory are accessible from /// GitHub web and mobile. Ignored when connecting to an external server /// via [`Transport::External`]. - pub remote: bool, + pub enable_remote_sessions: bool, + /// Override the directory where the bundled CLI binary is extracted on + /// first use. + /// + /// When `None` (the default), the SDK extracts the embedded CLI to + /// `/github-copilot-sdk-{version}/copilot[.exe]`, + /// where the cache dir is [`dirs::cache_dir()`] — + /// `%LOCALAPPDATA%` on Windows, `~/Library/Caches/` on macOS, + /// `$XDG_CACHE_HOME` (or `~/.cache/`) on Linux. Use this knob to + /// redirect the extraction (e.g. to a session-scoped temp directory in + /// CI runners) without changing the global cache layout. + /// + /// Ignored when the SDK was built without a bundled CLI (i.e. with + /// `default-features = false` to disable the `bundled-cli` feature). + pub bundled_cli_extract_dir: Option, } impl std::fmt::Debug for ClientOptions { @@ -441,12 +457,9 @@ impl std::fmt::Debug for ClientOptions { &self.on_get_trace_context.as_ref().map(|_| ""), ) .field("telemetry", &self.telemetry) - .field("copilot_home", &self.copilot_home) - .field( - "tcp_connection_token", - &self.tcp_connection_token.as_ref().map(|_| ""), - ) - .field("remote", &self.remote) + .field("base_directory", &self.base_directory) + .field("enable_remote_sessions", &self.enable_remote_sessions) + .field("bundled_cli_extract_dir", &self.bundled_cli_extract_dir) .finish() } } @@ -475,7 +488,7 @@ pub enum LogLevel { Error, /// Warnings and errors. Warning, - /// Default. Info and above. + /// Info and above. Info, /// Debug, info, warnings, errors. Debug, @@ -651,9 +664,9 @@ impl Default for ClientOptions { session_fs: None, on_get_trace_context: None, telemetry: None, - copilot_home: None, - tcp_connection_token: None, - remote: false, + base_directory: None, + enable_remote_sessions: false, + bundled_cli_extract_dir: None, } } } @@ -799,23 +812,22 @@ impl ClientOptions { /// Override the directory where the CLI persists its state. Set as /// `COPILOT_HOME` on the spawned CLI process. - pub fn with_copilot_home(mut self, home: impl Into) -> Self { - self.copilot_home = Some(home.into()); + pub fn with_base_directory(mut self, dir: impl Into) -> Self { + self.base_directory = Some(dir.into()); self } - /// Set the connection token for TCP transport. Sent in the `connect` - /// handshake and exported as `COPILOT_CONNECTION_TOKEN` to spawned - /// CLI processes. - pub fn with_tcp_connection_token(mut self, token: impl Into) -> Self { - self.tcp_connection_token = Some(token.into()); + /// Enable remote session support (Mission Control). Passes `--remote` + /// to the spawned CLI process. + pub fn with_enable_remote_sessions(mut self, enabled: bool) -> Self { + self.enable_remote_sessions = enabled; self } - /// Enable remote session support (Mission Control). Passes `--remote` - /// to the spawned CLI process. - pub fn with_remote(mut self, enabled: bool) -> Self { - self.remote = enabled; + /// Override the directory where the bundled CLI binary is extracted on + /// first use. See [`Self::bundled_cli_extract_dir`]. + pub fn with_bundled_cli_extract_dir(mut self, dir: impl Into) -> Self { + self.bundled_cli_extract_dir = Some(dir.into()); self } } @@ -929,39 +941,42 @@ impl Client { )); } } - // Validate token + transport combination. Stdio cannot use a - // connection token; auto-generate a UUID when the SDK spawns - // its own CLI in TCP mode and no explicit token was set. - if let Some(token) = &options.tcp_connection_token { - if token.is_empty() { - return Err(Error::InvalidConfig( - "tcp_connection_token must be a non-empty string".to_string(), - )); + // Validate token shape. Stdio variants no longer carry a token + // (enforced by the type). For Tcp/External, empty-string is + // rejected eagerly. + match &options.transport { + Transport::Tcp { + connection_token: Some(t), + .. } - if matches!(options.transport, Transport::Stdio) { + | Transport::External { + connection_token: Some(t), + .. + } if t.is_empty() => { return Err(Error::InvalidConfig( - "tcp_connection_token cannot be used with Transport::Stdio".to_string(), + "connection_token must be a non-empty string".to_string(), )); } + _ => {} } - let effective_connection_token: Option = match &options.transport { + // Capture (and where needed, auto-generate) the token actually sent + // to the server. For Tcp, the SDK auto-generates one when the + // caller leaves it unset so the loopback listener is safe by + // default. + let mut options = options; + let effective_connection_token: Option = match &mut options.transport { Transport::Stdio => None, - Transport::Tcp { .. } => Some( - options - .tcp_connection_token - .clone() - .unwrap_or_else(generate_connection_token), + Transport::Tcp { + connection_token, .. + } => Some( + connection_token + .get_or_insert_with(generate_connection_token) + .clone(), ), - Transport::External { .. } => options.tcp_connection_token.clone(), + Transport::External { + connection_token, .. + } => connection_token.clone(), }; - let mut options = options; - if matches!(options.transport, Transport::Tcp { .. }) - && options.tcp_connection_token.is_none() - { - // Auto-generated tokens flow to the spawned CLI via env, so - // make the field reflect what we'll actually send. - options.tcp_connection_token = effective_connection_token.clone(); - } let session_fs_config = options.session_fs.clone(); let session_fs_sqlite_declared = session_fs_config .as_ref() @@ -973,7 +988,9 @@ impl Client { path.clone() } CliProgram::Resolve => { - let resolved = resolve::copilot_binary()?; + let resolved = resolve::copilot_binary_with_extract_dir( + options.bundled_cli_extract_dir.as_deref(), + )?; info!(path = %resolved.display(), "resolved copilot CLI"); #[cfg(windows)] { @@ -993,7 +1010,11 @@ impl Client { }; let client = match options.transport { - Transport::External { ref host, port } => { + Transport::External { + ref host, + port, + connection_token: _, + } => { info!(host = %host, port = %port, "connecting to external CLI server"); let connect_start = Instant::now(); let stream = TcpStream::connect((host.as_str(), port)).await?; @@ -1016,7 +1037,10 @@ impl Client { effective_connection_token.clone(), )? } - Transport::Tcp { port } => { + Transport::Tcp { + port, + connection_token: _, + } => { let (mut child, actual_port) = Self::spawn_tcp(&program, &options, port).await?; let connect_start = Instant::now(); let stream = TcpStream::connect(("127.0.0.1", actual_port)).await?; @@ -1280,10 +1304,14 @@ impl Client { ); } } - if let Some(home) = &options.copilot_home { - command.env("COPILOT_HOME", home); + if let Some(dir) = &options.base_directory { + command.env("COPILOT_HOME", dir); } - if let Some(token) = &options.tcp_connection_token { + if let Transport::Tcp { + connection_token: Some(token), + .. + } = &options.transport + { command.env("COPILOT_CONNECTION_TOKEN", token); } for (key, value) in &options.env { @@ -1342,25 +1370,26 @@ impl Client { } fn remote_args(options: &ClientOptions) -> Vec { - if options.remote { + if options.enable_remote_sessions { vec!["--remote".to_string()] } else { Vec::new() } } + fn log_level_args(options: &ClientOptions) -> Vec<&'static str> { + match options.log_level { + Some(level) => vec!["--log-level", level.as_str()], + None => Vec::new(), + } + } + fn spawn_stdio(program: &Path, options: &ClientOptions) -> Result { info!(cwd = ?options.working_directory, program = %program.display(), "spawning copilot CLI (stdio)"); let mut command = Self::build_command(program, options); - let log_level = options.log_level.unwrap_or(LogLevel::Info); command - .args([ - "--server", - "--stdio", - "--no-auto-update", - "--log-level", - log_level.as_str(), - ]) + .args(["--server", "--stdio", "--no-auto-update"]) + .args(Self::log_level_args(options)) .args(Self::auth_args(options)) .args(Self::session_idle_timeout_args(options)) .args(Self::remote_args(options)) @@ -1382,16 +1411,9 @@ impl Client { ) -> Result<(Child, u16), Error> { info!(cwd = ?options.working_directory, program = %program.display(), port = %port, "spawning copilot CLI (tcp)"); let mut command = Self::build_command(program, options); - let log_level = options.log_level.unwrap_or(LogLevel::Info); command - .args([ - "--server", - "--port", - &port.to_string(), - "--no-auto-update", - "--log-level", - log_level.as_str(), - ]) + .args(["--server", "--port", &port.to_string(), "--no-auto-update"]) + .args(Self::log_level_args(options)) .args(Self::auth_args(options)) .args(Self::session_idle_timeout_args(options)) .args(Self::remote_args(options)) @@ -1586,8 +1608,8 @@ impl Client { /// /// # Handshake sequence /// - /// 1. Sends the `connect` JSON-RPC method, forwarding - /// [`ClientOptions::tcp_connection_token`] (or the auto-generated + /// 1. Sends the `connect` JSON-RPC method, forwarding the + /// [`Transport`]'s `connection_token` (or the auto-generated /// token for SDK-spawned TCP servers) as the `token` param. This /// is the canonical handshake used by all SDK languages and is /// what the CLI uses to enforce loopback authentication when @@ -1652,7 +1674,7 @@ impl Client { /// Send the `connect` JSON-RPC handshake. Returns the server's /// reported protocol version, or `None` if the server omits it. - /// Forwards [`ClientOptions::tcp_connection_token`] (or the + /// Forwards the [`Transport`]'s `connection_token` (or the /// auto-generated token for SDK-spawned TCP servers) as the `token` /// param. Server-side, the token is required when the server was /// started with `COPILOT_CONNECTION_TOKEN`. @@ -1986,16 +2008,6 @@ impl Client { pub fn subscribe_lifecycle(&self) -> LifecycleSubscription { LifecycleSubscription::new(self.inner.lifecycle_tx.subscribe()) } - - /// Return the current [`ConnectionState`]. - /// - /// The state advances to [`Connected`](ConnectionState::Connected) once - /// [`Client::start`] / [`Client::from_streams`] returns successfully and - /// drops to [`Disconnected`](ConnectionState::Disconnected) after - /// [`stop`](Self::stop) or [`force_stop`](Self::force_stop). - pub fn state(&self) -> ConnectionState { - *self.inner.state.lock() - } } impl Drop for ClientInner { @@ -2055,7 +2067,7 @@ mod tests { .with_use_logged_in_user(false) .with_log_level(LogLevel::Debug) .with_session_idle_timeout_seconds(120) - .with_remote(true); + .with_enable_remote_sessions(true); assert!(matches!(opts.program, CliProgram::Path(_))); assert_eq!(opts.prefix_args, vec![std::ffi::OsString::from("node")]); assert_eq!(opts.working_directory, PathBuf::from("/tmp")); @@ -2072,7 +2084,7 @@ mod tests { assert_eq!(opts.use_logged_in_user, Some(false)); assert!(matches!(opts.log_level, Some(LogLevel::Debug))); assert_eq!(opts.session_idle_timeout_seconds, Some(120)); - assert!(opts.remote); + assert!(opts.enable_remote_sessions); } #[test] @@ -2275,7 +2287,7 @@ mod tests { #[test] fn build_command_sets_copilot_home_env_when_configured() { - let opts = ClientOptions::new().with_copilot_home(PathBuf::from("/custom/copilot")); + let opts = ClientOptions::new().with_base_directory(PathBuf::from("/custom/copilot")); let cmd = Client::build_command(Path::new("/bin/echo"), &opts); assert_eq!( env_value(&cmd, "COPILOT_HOME"), @@ -2289,7 +2301,10 @@ mod tests { #[test] fn build_command_sets_connection_token_env_when_configured() { - let opts = ClientOptions::new().with_tcp_connection_token("secret-token"); + let opts = ClientOptions::new().with_transport(Transport::Tcp { + port: 0, + connection_token: Some("secret-token".to_string()), + }); let cmd = Client::build_command(Path::new("/bin/echo"), &opts); assert_eq!( env_value(&cmd, "COPILOT_CONNECTION_TOKEN"), @@ -2302,26 +2317,25 @@ mod tests { } #[tokio::test] - async fn start_rejects_token_with_stdio_transport() { + async fn start_rejects_empty_connection_token() { let opts = ClientOptions::new() - .with_tcp_connection_token("token-123") + .with_transport(Transport::Tcp { + port: 0, + connection_token: Some(String::new()), + }) .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); let err = Client::start(opts).await.unwrap_err(); assert!(matches!(err, Error::InvalidConfig(_)), "got {err:?}"); - let Error::InvalidConfig(msg) = err else { - unreachable!() - }; - assert!( - msg.contains("Stdio"), - "error should explain the stdio incompatibility: {msg}" - ); } #[tokio::test] - async fn start_rejects_empty_connection_token() { + async fn start_rejects_empty_external_connection_token() { let opts = ClientOptions::new() - .with_tcp_connection_token("") - .with_transport(Transport::Tcp { port: 0 }) + .with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port: 1, + connection_token: Some(String::new()), + }) .with_program(CliProgram::Path(PathBuf::from("/bin/echo"))); let err = Client::start(opts).await.unwrap_err(); assert!(matches!(err, Error::InvalidConfig(_)), "got {err:?}"); @@ -2397,12 +2411,28 @@ mod tests { #[test] fn remote_args_emit_flag_when_enabled() { let opts = ClientOptions { - remote: true, + enable_remote_sessions: true, ..Default::default() }; assert_eq!(Client::remote_args(&opts), vec!["--remote".to_string()]); } + #[test] + fn log_level_args_omitted_when_unset() { + let opts = ClientOptions::default(); + assert!(opts.log_level.is_none()); + assert!( + Client::log_level_args(&opts).is_empty(), + "with no caller-supplied log_level the SDK must not pass --log-level" + ); + } + + #[test] + fn log_level_args_emit_flag_when_set() { + let opts = ClientOptions::default().with_log_level(LogLevel::Debug); + assert_eq!(Client::log_level_args(&opts), vec!["--log-level", "debug"]); + } + #[test] fn log_level_str_round_trips() { for level in [ diff --git a/rust/src/permission.rs b/rust/src/permission.rs index 364cb3c91..2ddd773a3 100644 --- a/rust/src/permission.rs +++ b/rust/src/permission.rs @@ -1,106 +1,124 @@ -//! Permission-policy helpers that compose with an existing -//! [`SessionHandler`](crate::handler::SessionHandler). +//! Permission policy primitives that produce a [`PermissionHandler`](crate::handler::PermissionHandler). //! -//! These wrap an inner handler and override **only** permission requests, -//! forwarding every other event (tool calls, user input, elicitation, -//! session events) to the inner handler. Use them when you have a custom -//! tool handler — typically a [`ToolHandlerRouter`](crate::tool::ToolHandlerRouter) — -//! but want a one-line policy for permission prompts. +//! Compose these into a session via the builder methods +//! [`SessionConfig::approve_all_permissions`](crate::types::SessionConfig::approve_all_permissions), +//! [`deny_all_permissions`](crate::types::SessionConfig::deny_all_permissions), +//! and [`approve_permissions_if`](crate::types::SessionConfig::approve_permissions_if). +//! The same primitives are also available as standalone functions that +//! return an `Arc` you can install via +//! [`SessionConfig::with_permission_handler`](crate::types::SessionConfig::with_permission_handler). //! -//! For a full handler that approves or denies everything, see +//! For a one-shot approve / deny without composition, see //! [`ApproveAllHandler`](crate::handler::ApproveAllHandler) and //! [`DenyAllHandler`](crate::handler::DenyAllHandler). -//! -//! # Example -//! -//! ```rust,no_run -//! # use std::sync::Arc; -//! # use github_copilot_sdk::handler::ApproveAllHandler; -//! # use github_copilot_sdk::permission; -//! # use github_copilot_sdk::tool::ToolHandlerRouter; -//! let router = ToolHandlerRouter::new(vec![], Arc::new(ApproveAllHandler)); -//! // Inherit the router's tool dispatch but auto-approve all permission prompts: -//! let handler = permission::approve_all(Arc::new(router)); -//! ``` use std::sync::Arc; use async_trait::async_trait; -use crate::handler::{HandlerEvent, HandlerResponse, PermissionResult, SessionHandler}; -use crate::types::PermissionRequestData; +use crate::handler::{PermissionHandler, PermissionResult}; +use crate::types::{PermissionRequestData, RequestId, SessionId}; -/// Wrap `inner` so that every [`HandlerEvent::PermissionRequest`] is -/// auto-approved. All other events are forwarded to `inner`. -pub fn approve_all(inner: Arc) -> Arc { - Arc::new(PermissionOverrideHandler { - inner, +/// Return a [`PermissionHandler`] that approves every request. +pub fn approve_all() -> Arc { + Arc::new(PolicyHandler { policy: Policy::ApproveAll, }) } -/// Wrap `inner` so that every [`HandlerEvent::PermissionRequest`] is -/// auto-denied. All other events are forwarded to `inner`. -pub fn deny_all(inner: Arc) -> Arc { - Arc::new(PermissionOverrideHandler { - inner, +/// Return a [`PermissionHandler`] that denies every request. +pub fn deny_all() -> Arc { + Arc::new(PolicyHandler { policy: Policy::DenyAll, }) } -/// Wrap `inner` with a closure-based policy: `predicate` is called for each -/// permission request; `true` approves, `false` denies. All other events -/// are forwarded to `inner`. +/// Return a [`PermissionHandler`] that consults a predicate for each +/// request. `true` approves, `false` denies. /// /// ```rust,no_run -/// # use std::sync::Arc; -/// # use github_copilot_sdk::handler::ApproveAllHandler; /// # use github_copilot_sdk::permission; -/// let inner = Arc::new(ApproveAllHandler); -/// let handler = permission::approve_if(inner, |data| { -/// // Inspect data.extra (the raw JSON payload) for custom policy. +/// let handler = permission::approve_if(|data| { /// data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") /// }); /// # let _ = handler; /// ``` -pub fn approve_if(inner: Arc, predicate: F) -> Arc +pub fn approve_if(predicate: F) -> Arc where F: Fn(&PermissionRequestData) -> bool + Send + Sync + 'static, { - Arc::new(PermissionOverrideHandler { - inner, + Arc::new(PolicyHandler { policy: Policy::Predicate(Arc::new(predicate)), }) } -enum Policy { +/// Internal policy enum used by both the standalone helpers and the +/// `SessionConfig` policy builders. +/// +/// Stored as `pub(crate)` on `SessionConfig::permission_policy` so that +/// the order of `with_permission_handler(...)` and the policy builders +/// does not matter -- the policy is applied at `Client::create_session` +/// time. +#[derive(Clone)] +pub(crate) enum Policy { ApproveAll, DenyAll, Predicate(Arc bool + Send + Sync>), } -struct PermissionOverrideHandler { - inner: Arc, +impl std::fmt::Debug for Policy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ApproveAll => f.write_str("Policy::ApproveAll"), + Self::DenyAll => f.write_str("Policy::DenyAll"), + Self::Predicate(_) => f.write_str("Policy::Predicate()"), + } + } +} + +/// Resolve the effective permission handler for a session, given the +/// caller-supplied handler and policy. Called by `Client::create_session` +/// and `Client::resume_session`. +/// +/// Semantics: +/// - When `policy` is `Some`, the policy entirely replaces the handler +/// for permission decisions. (Caller-supplied handler, if any, is +/// discarded -- the policy is what answers permission requests.) +/// - When `policy` is `None` and `handler` is `Some`, the handler stands. +/// - When both are `None`, returns `None` (no handler -- the SDK sends +/// `requestPermission: false`). +pub(crate) fn resolve_handler( + handler: Option>, + policy: Option, +) -> Option> { + match (handler, policy) { + (_, Some(policy)) => Some(Arc::new(PolicyHandler { policy })), + (Some(h), None) => Some(h), + (None, None) => None, + } +} + +struct PolicyHandler { policy: Policy, } #[async_trait] -impl SessionHandler for PermissionOverrideHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::PermissionRequest { ref data, .. } => { - let approved = match &self.policy { - Policy::ApproveAll => true, - Policy::DenyAll => false, - Policy::Predicate(f) => f(data), - }; - HandlerResponse::Permission(if approved { - PermissionResult::Approved - } else { - PermissionResult::Denied - }) - } - other => self.inner.on_event(other).await, +impl PermissionHandler for PolicyHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + data: PermissionRequestData, + ) -> PermissionResult { + let approved = match &self.policy { + Policy::ApproveAll => true, + Policy::DenyAll => false, + Policy::Predicate(f) => f(&data), + }; + if approved { + PermissionResult::approve_once() + } else { + PermissionResult::reject(None) } } } @@ -108,61 +126,94 @@ impl SessionHandler for PermissionOverrideHandler { #[cfg(test)] mod tests { use super::*; - use crate::handler::ApproveAllHandler; - use crate::types::{RequestId, SessionId}; - - fn request() -> HandlerEvent { - HandlerEvent::PermissionRequest { - session_id: SessionId::from("s1"), - request_id: RequestId::new("1"), - data: PermissionRequestData { - extra: serde_json::json!({"tool": "shell"}), - ..Default::default() - }, + + fn data() -> PermissionRequestData { + PermissionRequestData { + extra: serde_json::json!({ "tool": "shell" }), + ..Default::default() } } #[tokio::test] - async fn approve_all_approves_permission_requests() { - let h = approve_all(Arc::new(ApproveAllHandler)); - match h.on_event(request()).await { - HandlerResponse::Permission(PermissionResult::Approved) => {} - other => panic!("expected Approved, got {other:?}"), - } + async fn approve_all_approves() { + let h = approve_all(); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision(crate::types::PermissionDecision::ApproveOnce(_)) + )); } #[tokio::test] - async fn deny_all_denies_permission_requests() { - let h = deny_all(Arc::new(ApproveAllHandler)); - match h.on_event(request()).await { - HandlerResponse::Permission(PermissionResult::Denied) => {} - other => panic!("expected Denied, got {other:?}"), - } + async fn deny_all_denies() { + let h = deny_all(); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision(crate::types::PermissionDecision::Reject(_)) + )); } #[tokio::test] async fn approve_if_consults_predicate() { - let h = approve_if(Arc::new(ApproveAllHandler), |data| { - data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") - }); - match h.on_event(request()).await { - HandlerResponse::Permission(PermissionResult::Denied) => {} - other => panic!("expected Denied for shell, got {other:?}"), + let h = approve_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")); + assert!(matches!( + h.handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision(crate::types::PermissionDecision::Reject(_)) + )); + } + + #[tokio::test] + async fn resolve_handler_policy_wins() { + struct AlwaysApprove; + #[async_trait] + impl PermissionHandler for AlwaysApprove { + async fn handle( + &self, + _: SessionId, + _: RequestId, + _: PermissionRequestData, + ) -> PermissionResult { + PermissionResult::approve_once() + } } + let resolved = + resolve_handler(Some(Arc::new(AlwaysApprove)), Some(Policy::DenyAll)).unwrap(); + // Policy wins -- the AlwaysApprove handler is discarded. + assert!(matches!( + resolved + .handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision(crate::types::PermissionDecision::Reject(_)) + )); } #[tokio::test] - async fn non_permission_events_forward_to_inner() { - let h = deny_all(Arc::new(ApproveAllHandler)); - let event = HandlerEvent::UserInput { - session_id: SessionId::from("s1"), - question: "continue?".to_string(), - choices: None, - allow_freeform: None, - }; - match h.on_event(event).await { - HandlerResponse::UserInput(None) => {} - other => panic!("expected UserInput forwarded, got {other:?}"), + async fn resolve_handler_with_only_handler() { + struct H; + #[async_trait] + impl PermissionHandler for H { + async fn handle( + &self, + _: SessionId, + _: RequestId, + _: PermissionRequestData, + ) -> PermissionResult { + PermissionResult::approve_once() + } } + let resolved = resolve_handler(Some(Arc::new(H)), None).unwrap(); + assert!(matches!( + resolved + .handle(SessionId::from("s"), RequestId::new("1"), data()) + .await, + PermissionResult::Decision(crate::types::PermissionDecision::ApproveOnce(_)) + )); + } + + #[test] + fn resolve_handler_with_neither_returns_none() { + assert!(resolve_handler(None, None).is_none()); } } diff --git a/rust/src/resolve.rs b/rust/src/resolve.rs index 8521a4b55..7a1b29a04 100644 --- a/rust/src/resolve.rs +++ b/rust/src/resolve.rs @@ -1,677 +1,57 @@ -use std::collections::HashSet; +//! Internal resolution of the GitHub Copilot CLI binary. +//! +//! Resolution order (matches the .NET and TypeScript SDKs): +//! +//! 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 (gated on the +//! default `bundled-cli` cargo feature). +//! +//! There is no PATH scanning and no walking of standard install locations. +//! If you've opted out of bundling (via `default-features = false`) and +//! neither `CliProgram::Path` nor `COPILOT_CLI_PATH` is set, +//! [`Client::start`](crate::Client::start) returns +//! [`Error::BinaryNotFound`](crate::Error::BinaryNotFound). + use std::env; -use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use serde::Serialize; use tracing::warn; use crate::Error; -/// How the copilot binary was resolved. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum BinarySource { - /// Extracted from the build-time embedded binary. - Bundled, - /// Set via `COPILOT_CLI_PATH` environment variable. - EnvOverride, - /// Found on PATH or standard search locations. - Local, -} - -/// Find the `copilot` CLI binary on the system. -/// -/// Checks `COPILOT_CLI_PATH` env var first, then searches PATH and common -/// install locations (homebrew, nvm, nodenv, fnm, volta, cargo, etc.). -/// Use `COPILOT_CLI_NAME` to override the binary name (default: `copilot`). -pub fn copilot_binary() -> Result { - copilot_binary_with_source().map(|(path, _)| path) -} - -/// Like [`copilot_binary`] but also reports how the binary was resolved. -pub fn copilot_binary_with_source() -> Result<(PathBuf, BinarySource), Error> { +/// Resolve the CLI binary, optionally overriding the directory the bundled +/// CLI is extracted to. Called by `Client::start` to thread +/// `ClientOptions::bundled_cli_extract_dir` through to +/// `embeddedcli::install_at`. +pub(crate) fn copilot_binary_with_extract_dir( + extract_dir: Option<&Path>, +) -> Result { if let Ok(value) = env::var("COPILOT_CLI_PATH") { - let candidate = PathBuf::from(value); + let candidate = PathBuf::from(&value); if candidate.is_file() { - return Ok((candidate, BinarySource::EnvOverride)); - } - if candidate.is_dir() - && let Some(found) = find_copilot_in_dir(&candidate) - { - return Ok((found, BinarySource::EnvOverride)); - } - warn!(path = %candidate.display(), "COPILOT_CLI_PATH set but not usable"); - } - - if let Some(path) = crate::embeddedcli::path() { - return Ok((path, BinarySource::Bundled)); - } - - for dir in standard_search_paths() { - if let Some(found) = find_copilot_in_dir(&dir) { - return Ok((found, BinarySource::Local)); + return Ok(candidate); } + warn!( + path = %candidate.display(), + "COPILOT_CLI_PATH is set but does not point to a file; falling back to bundled CLI" + ); } - Err(Error::BinaryNotFound { - name: "copilot", - hint: "ensure the GitHub Copilot CLI is installed and on PATH, or set COPILOT_CLI_PATH. use COPILOT_CLI_NAME to override the binary name (default: copilot)", - }) -} - -/// Find the `copilot` CLI binary using only the current PATH entries. -/// -/// This is intentionally narrower than [`copilot_binary`]: it does not honor -/// override env vars and does not search inferred install locations. -pub fn copilot_binary_on_path() -> Result { - if let Some(found) = find_executable_in_path( - env::var_os("PATH").as_deref(), - &literal_copilot_executable_names(), - ) { - return Ok(found); + let bundled = match extract_dir { + Some(dir) => crate::embeddedcli::install_at(dir), + None => crate::embeddedcli::path(), + }; + if let Some(path) = bundled { + return Ok(path); } Err(Error::BinaryNotFound { name: "copilot", - hint: "ensure the `copilot` command is installed and available on PATH", + hint: "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 \ + feature enabled, set COPILOT_CLI_PATH, or supply an explicit path via \ + `CliProgram::Path(...)` on `ClientOptions::program`.", }) } - -/// Build an extended `PATH` by prepending `extra` dirs to the standard -/// search paths (current PATH + common install locations). -pub fn extended_path(extra: &[PathBuf]) -> Option { - let mut paths = SearchPaths::new(); - for p in extra { - paths.push(p.clone()); - } - paths.append_standard(); - if paths.is_empty() { - return None; - } - env::join_paths(paths).ok() -} - -fn copilot_executable_names() -> Vec { - let base = env::var("COPILOT_CLI_NAME").unwrap_or_else(|_| "copilot".to_string()); - executable_names_for_base(&base) -} - -fn literal_copilot_executable_names() -> Vec { - executable_names_for_base("copilot") -} - -fn executable_names_for_base(base: &str) -> Vec { - #[cfg(target_os = "windows")] - { - vec![ - format!("{}.exe", base), - format!("{}.cmd", base), - format!("{}.bat", base), - ] - } - #[cfg(not(target_os = "windows"))] - { - vec![base.to_string()] - } -} - -fn find_executable(dir: &Path, names: &[impl AsRef]) -> Option { - if dir.as_os_str().is_empty() { - return None; - } - names - .iter() - .map(|n| dir.join(n.as_ref())) - .find(|c| c.is_file()) -} - -fn find_copilot_in_dir(dir: &Path) -> Option { - find_executable(dir, &copilot_executable_names()) -} - -fn find_executable_in_path( - path_env: Option<&OsStr>, - names: &[impl AsRef], -) -> Option { - let path_env = path_env?; - for dir in env::split_paths(path_env) { - if let Some(found) = find_executable(&dir, names) { - return Some(found); - } - } - None -} - -/// Ordered, deduplicated collection of directory paths to search for binaries. -/// -/// Paths are stored in insertion order. Duplicates and empty paths are -/// silently dropped on `push`. Implements `Iterator` so it can be passed -/// directly to `env::join_paths` or used in a `for` loop. -struct SearchPaths { - seen: HashSet, - paths: Vec, -} - -impl SearchPaths { - fn new() -> Self { - Self { - seen: HashSet::new(), - paths: Vec::new(), - } - } - - /// Add a path if it hasn't been seen before. Empty paths are ignored. - fn push(&mut self, path: PathBuf) { - if !path.as_os_str().is_empty() && self.seen.insert(path.clone()) { - self.paths.push(path); - } - } - - fn is_empty(&self) -> bool { - self.paths.is_empty() - } - - /// Append the standard search paths: current PATH, home-relative dirs, - /// version manager paths (nvm, nodenv, fnm), and platform-specific dirs. - fn append_standard(&mut self) { - if let Some(existing) = env::var_os("PATH") { - for p in env::split_paths(&existing) { - self.push(p); - } - } - - if let Some(home) = dirs::home_dir() { - self.push(home.join(".local/bin")); - self.push(home.join(".cargo/bin")); - self.push(home.join(".bun/bin")); - self.push(home.join(".npm-global/bin")); - self.push(home.join(".yarn/bin")); - self.push(home.join(".volta/bin")); - self.push(home.join(".asdf/shims")); - self.push(home.join("bin")); - } - - // Platform-specific standard dirs come before version-manager paths - // so that the system-installed node (e.g. /opt/homebrew/bin/node) - // takes precedence over arbitrary old versions found under - // ~/.nvm/versions, ~/.nodenv/versions, etc. - #[cfg(target_os = "macos")] - { - self.push(PathBuf::from("/opt/homebrew/bin")); - self.push(PathBuf::from("/usr/local/bin")); - self.push(PathBuf::from("/usr/bin")); - self.push(PathBuf::from("/bin")); - self.push(PathBuf::from("/usr/sbin")); - self.push(PathBuf::from("/sbin")); - } - - #[cfg(target_os = "linux")] - { - self.push(PathBuf::from("/usr/local/bin")); - self.push(PathBuf::from("/usr/bin")); - self.push(PathBuf::from("/bin")); - self.push(PathBuf::from("/snap/bin")); - } - - #[cfg(target_os = "windows")] - { - if let Some(appdata) = env::var_os("APPDATA") { - self.push(PathBuf::from(appdata).join("npm")); - } - if let Some(local) = env::var_os("LOCALAPPDATA") { - let local = PathBuf::from(local); - self.push(local.join("Programs")); - // User-scope winget install of Git for Windows. - self.push(local.join("Programs").join("Git").join("cmd")); - self.push(local.join("Programs").join("Git").join("bin")); - } - // Git for Windows standard machine-scope install locations. - for env_var in ["ProgramFiles", "ProgramW6432", "ProgramFiles(x86)"] { - if let Some(program_files) = env::var_os(env_var) { - let program_files = PathBuf::from(program_files); - self.push(program_files.join("Git").join("cmd")); - self.push(program_files.join("Git").join("bin")); - } - } - } - - // Version manager paths are a fallback for binary discovery — - // they enumerate every installed version, so an arbitrary old - // node/copilot can appear first if filesystem ordering is unlucky. - for p in collect_nvm_paths() { - self.push(p); - } - for p in collect_nodenv_paths() { - self.push(p); - } - for p in collect_fnm_paths() { - self.push(p); - } - } -} - -impl IntoIterator for SearchPaths { - type IntoIter = std::vec::IntoIter; - type Item = PathBuf; - - fn into_iter(self) -> Self::IntoIter { - self.paths.into_iter() - } -} - -/// Collect standard search paths for binary resolution. -fn standard_search_paths() -> SearchPaths { - let mut paths = SearchPaths::new(); - paths.append_standard(); - paths -} - -fn collect_nvm_paths() -> Vec { - let mut paths = Vec::new(); - let nvm_dir = env::var_os("NVM_DIR") - .map(PathBuf::from) - .or_else(|| dirs::home_dir().map(|home| home.join(".nvm"))); - let Some(nvm_dir) = nvm_dir else { - return paths; - }; - let versions_dir = nvm_dir.join("versions").join("node"); - let entries = match std::fs::read_dir(&versions_dir) { - Ok(entries) => entries, - Err(_) => return paths, - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - paths.push(path.join("bin")); - } - } - paths -} - -fn collect_nodenv_paths() -> Vec { - let mut paths = Vec::new(); - let root = env::var_os("NODENV_ROOT") - .map(PathBuf::from) - .or_else(|| dirs::home_dir().map(|home| home.join(".nodenv"))); - let Some(root) = root else { - return paths; - }; - let versions_dir = root.join("versions"); - let entries = match std::fs::read_dir(&versions_dir) { - Ok(entries) => entries, - Err(_) => return paths, - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - paths.push(path.join("bin")); - } - } - paths -} - -fn fnm_root_candidates_from( - fnm_dir: Option, - xdg_data_home: Option, - home: Option, -) -> Vec { - let mut roots = SearchPaths::new(); - - if let Some(fnm_dir) = fnm_dir.filter(|path| !path.as_os_str().is_empty()) { - roots.push(fnm_dir); - } - - if let Some(xdg_data_home) = xdg_data_home.filter(|path| !path.as_os_str().is_empty()) { - roots.push(xdg_data_home.join("fnm")); - } - - if let Some(home) = home { - roots.push(home.join(".local").join("share").join("fnm")); - roots.push(home.join(".fnm")); - } - - roots.paths -} - -fn collect_fnm_paths() -> Vec { - let roots = fnm_root_candidates_from( - env::var_os("FNM_DIR").map(PathBuf::from), - env::var_os("XDG_DATA_HOME").map(PathBuf::from), - dirs::home_dir(), - ); - - let mut paths = SearchPaths::new(); - for root in &roots { - paths.push(root.join("aliases").join("default").join("bin")); - - let versions_dir = root.join("node-versions"); - let entries = match std::fs::read_dir(&versions_dir) { - Ok(entries) => entries, - Err(_) => continue, - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - paths.push(path.join("installation").join("bin")); - } - } - } - - paths.paths -} - -#[cfg(test)] -mod tests { - use std::path::{Path, PathBuf}; - use std::{env, fs}; - - use serial_test::serial; - use tempfile::tempdir; - - use super::{ - copilot_binary_on_path, find_executable_in_path, fnm_root_candidates_from, - literal_copilot_executable_names, - }; - - #[test] - fn fnm_root_candidates_include_xdg_and_legacy_locations() { - let home = PathBuf::from("/tmp/copilot-home"); - - let roots = fnm_root_candidates_from(None, None, Some(home.clone())); - - assert_eq!( - roots, - vec![ - home.join(".local").join("share").join("fnm"), - home.join(".fnm"), - ] - ); - } - - #[test] - fn fnm_root_candidates_prefer_explicit_locations_first() { - let home = PathBuf::from("/tmp/copilot-home"); - let explicit_fnm_dir = PathBuf::from("/tmp/custom-fnm"); - let xdg_data_home = PathBuf::from("/tmp/xdg-data"); - - let roots = fnm_root_candidates_from( - Some(explicit_fnm_dir.clone()), - Some(xdg_data_home.clone()), - Some(home.clone()), - ); - - assert_eq!( - roots, - vec![ - explicit_fnm_dir, - xdg_data_home.join("fnm"), - home.join(".local").join("share").join("fnm"), - home.join(".fnm"), - ] - ); - } - - #[test] - fn fnm_root_candidates_ignore_empty_xdg_data_home() { - let home = PathBuf::from("/tmp/copilot-home"); - - let roots = fnm_root_candidates_from(None, Some(PathBuf::new()), Some(home.clone())); - - assert_eq!( - roots, - vec![ - home.join(".local").join("share").join("fnm"), - home.join(".fnm"), - ] - ); - assert!(!roots.iter().any(|path| path == &PathBuf::from("fnm"))); - } - - #[test] - fn fnm_root_produces_expected_bin_paths() { - let temp_dir = tempdir().expect("should create temp dir"); - let root = temp_dir.path().join("fnm-root"); - let alias_bin = root.join("aliases").join("default").join("bin"); - let version_bin = root - .join("node-versions") - .join("v22.18.0") - .join("installation") - .join("bin"); - - fs::create_dir_all(&alias_bin).expect("should create fnm alias bin"); - fs::create_dir_all(&version_bin).expect("should create fnm version bin"); - - let roots = fnm_root_candidates_from(Some(root.clone()), None, None); - assert_eq!(roots, vec![root.clone()]); - - // Verify the expected bin paths exist under the root structure - assert!(alias_bin.is_dir()); - assert!(version_bin.is_dir()); - } - - #[test] - fn find_copilot_in_path_finds_binary_in_path_entries() { - let temp_dir = tempdir().expect("should create temp dir"); - let bin_dir = temp_dir.path().join("bin"); - fs::create_dir_all(&bin_dir).expect("should create bin dir"); - - let executable_name = literal_copilot_executable_names() - .into_iter() - .next() - .expect("should provide a copilot executable name"); - let executable_path = bin_dir.join(&executable_name); - fs::write(&executable_path, "#!/bin/sh\n").expect("should create fake binary"); - - let path_env = - env::join_paths([Path::new("/missing"), bin_dir.as_path()]).expect("should build PATH"); - - assert_eq!( - find_executable_in_path( - Some(path_env.as_os_str()), - &literal_copilot_executable_names() - ), - Some(executable_path) - ); - } - - #[test] - fn find_copilot_in_path_ignores_missing_entries() { - let path_env = env::join_paths([Path::new("/missing-one"), Path::new("/missing-two")]) - .expect("should build PATH"); - - assert_eq!( - find_executable_in_path( - Some(path_env.as_os_str()), - &literal_copilot_executable_names() - ), - None - ); - } - - #[test] - #[serial] - #[cfg(target_os = "macos")] - fn platform_dirs_precede_version_manager_dirs() { - let temp = tempdir().expect("should create temp dir"); - let fake_home = temp.path().join("home"); - - // Create fake nvm version dirs so collect_nvm_paths() returns entries. - let nvm_dir = fake_home.join(".nvm"); - let nvm_version_bin = nvm_dir - .join("versions") - .join("node") - .join("v18.0.0") - .join("bin"); - fs::create_dir_all(&nvm_version_bin).expect("should create nvm version bin"); - - // Create fake nodenv version dirs. - let nodenv_root = fake_home.join(".nodenv"); - let nodenv_version_bin = nodenv_root.join("versions").join("20.0.0").join("bin"); - fs::create_dir_all(&nodenv_version_bin).expect("should create nodenv version bin"); - - // Create fake fnm version dirs. - let fnm_root = fake_home.join(".local").join("share").join("fnm"); - let fnm_version_bin = fnm_root - .join("node-versions") - .join("v22.0.0") - .join("installation") - .join("bin"); - fs::create_dir_all(&fnm_version_bin).expect("should create fnm version bin"); - - // Save env vars. - let prev_path = env::var_os("PATH"); - let prev_home = env::var_os("HOME"); - let prev_nvm_dir = env::var_os("NVM_DIR"); - let prev_nodenv_root = env::var_os("NODENV_ROOT"); - let prev_fnm_dir = env::var_os("FNM_DIR"); - let prev_xdg_data_home = env::var_os("XDG_DATA_HOME"); - - // Set env: empty PATH so only append_standard() dirs appear, - // HOME to our fake home, and explicit version-manager roots. - // Safety: test-only, single-threaded via #[serial]. - unsafe { - env::set_var("PATH", ""); - env::set_var("HOME", &fake_home); - env::set_var("NVM_DIR", &nvm_dir); - env::set_var("NODENV_ROOT", &nodenv_root); - env::remove_var("FNM_DIR"); - env::remove_var("XDG_DATA_HOME"); - } - - let paths: Vec = super::standard_search_paths().into_iter().collect(); - - // Restore env vars. - // Safety: test-only, single-threaded via #[serial]. - unsafe { - match prev_path { - Some(v) => env::set_var("PATH", v), - None => env::remove_var("PATH"), - } - match prev_home { - Some(v) => env::set_var("HOME", v), - None => env::remove_var("HOME"), - } - match prev_nvm_dir { - Some(v) => env::set_var("NVM_DIR", v), - None => env::remove_var("NVM_DIR"), - } - match prev_nodenv_root { - Some(v) => env::set_var("NODENV_ROOT", v), - None => env::remove_var("NODENV_ROOT"), - } - match prev_fnm_dir { - Some(v) => env::set_var("FNM_DIR", v), - None => env::remove_var("FNM_DIR"), - } - match prev_xdg_data_home { - Some(v) => env::set_var("XDG_DATA_HOME", v), - None => env::remove_var("XDG_DATA_HOME"), - } - } - - let platform_dirs: Vec = vec![ - PathBuf::from("/opt/homebrew/bin"), - PathBuf::from("/usr/local/bin"), - PathBuf::from("/usr/bin"), - PathBuf::from("/bin"), - PathBuf::from("/usr/sbin"), - PathBuf::from("/sbin"), - ]; - - // Find the last platform dir index and the first version-manager dir index. - let last_platform_idx = platform_dirs - .iter() - .filter_map(|d| paths.iter().position(|p| p == d)) - .max() - .expect("at least one platform dir should be present"); - - let version_manager_prefixes = [ - nvm_version_bin.parent().unwrap().parent().unwrap(), // .nvm/versions/node - nodenv_version_bin.parent().unwrap().parent().unwrap(), // .nodenv/versions - fnm_version_bin - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap(), // .local/share/fnm - ]; - - let first_version_mgr_idx = paths - .iter() - .position(|p| { - version_manager_prefixes - .iter() - .any(|prefix| p.starts_with(prefix)) - }) - .expect("at least one version-manager dir should be present"); - - assert!( - last_platform_idx < first_version_mgr_idx, - "Platform dirs (last at index {last_platform_idx}) must precede \ - version-manager dirs (first at index {first_version_mgr_idx}).\n\ - Full path list: {paths:#?}" - ); - } - - #[test] - #[serial] - fn find_executable_in_path_can_ignore_copilot_name_override() { - let temp_dir = tempdir().expect("should create temp dir"); - let bin_dir = temp_dir.path().join("bin"); - fs::create_dir_all(&bin_dir).expect("should create bin dir"); - - let path_executable_name = literal_copilot_executable_names() - .into_iter() - .next() - .expect("should provide a literal copilot executable name"); - #[cfg(target_os = "windows")] - let overridden_executable_name = "my-copilot.exe"; - - #[cfg(not(target_os = "windows"))] - let overridden_executable_name = "my-copilot"; - - let path_executable_path = bin_dir.join(&path_executable_name); - let overridden_executable_path = bin_dir.join(overridden_executable_name); - - fs::write(&path_executable_path, "#!/bin/sh\n").expect("should create literal fake binary"); - fs::write(&overridden_executable_path, "#!/bin/sh\n") - .expect("should create overridden fake binary"); - - let path_env = - env::join_paths([Path::new("/missing"), bin_dir.as_path()]).expect("should build PATH"); - - let previous_path = env::var_os("PATH"); - let previous_copilot_cli_name = env::var_os("COPILOT_CLI_NAME"); - // Safety: test-only, single-threaded via #[serial]. - unsafe { - env::set_var("PATH", &path_env); - env::set_var("COPILOT_CLI_NAME", "my-copilot"); - } - - let resolved_path = copilot_binary_on_path(); - - // Safety: test-only, single-threaded via #[serial]. - unsafe { - if let Some(previous_path) = previous_path { - env::set_var("PATH", previous_path); - } else { - env::remove_var("PATH"); - } - - if let Some(previous_copilot_cli_name) = previous_copilot_cli_name { - env::set_var("COPILOT_CLI_NAME", previous_copilot_cli_name); - } else { - env::remove_var("COPILOT_CLI_NAME"); - } - } - - assert_eq!( - resolved_path.expect("should find the literal copilot binary on PATH"), - path_executable_path - ); - } -} diff --git a/rust/src/session.rs b/rust/src/session.rs index d533dbc44..f216b866b 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -4,23 +4,22 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use parking_lot::Mutex as ParkingLotMutex; +use serde::de::DeserializeOwned; use serde_json::Value; use tokio::sync::oneshot; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tracing::{Instrument, warn}; -use crate::generated::api_types::{ - LogRequest, ModelSwitchToRequest, PermissionDecision, PermissionDecisionApproveOnce, - PermissionDecisionApproveOnceKind, PermissionDecisionReject, PermissionDecisionRejectKind, -}; +use crate::canvas::{CanvasHandler, CanvasInvokeParams, CanvasProviderRequestParams}; +use crate::generated::api_types::{LogRequest, ModelSwitchToRequest, OpenCanvasInstance}; use crate::generated::session_events::{ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, SessionErrorData, SessionEventType, }; use crate::handler::{ - AutoModeSwitchResponse, HandlerEvent, HandlerResponse, PermissionResult, SessionHandler, - UserInputResponse, + AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler, + PermissionHandler, PermissionResult, UserInputHandler, UserInputResponse, }; use crate::hooks::SessionHooks; use crate::session_fs::SessionFsProvider; @@ -28,14 +27,31 @@ use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ CommandContext, CommandDefinition, CommandHandler, CreateSessionResult, ElicitationRequest, - ElicitationResult, ExitPlanModeData, GetMessagesResponse, InputOptions, MessageOptions, - PermissionRequestData, RequestId, ResumeSessionConfig, SectionOverride, SessionCapabilities, - SessionConfig, SessionEvent, SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation, - ToolResult, ToolResultExpanded, ToolResultResponse, TraceContext, - ensure_attachment_display_names, + ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions, + PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, SectionOverride, + SessionCapabilities, SessionConfig, SessionEvent, SessionId, SetModelOptions, + SystemMessageConfig, ToolInvocation, ToolResult, ToolResultExpanded, TraceContext, + UiInputOptions, ensure_attachment_display_names, }; use crate::{Client, Error, JsonRpcResponse, SessionError, SessionEventNotification, error_codes}; +/// Bundle of the per-session callbacks the SDK dispatches to. Built from a +/// [`SessionConfig`] / [`ResumeSessionConfig`] at +/// [`Client::create_session`] / [`Client::resume_session`] time. Each +/// field is `None` (or an empty map for tools) when the caller didn't +/// install a handler -- in that case the SDK skips dispatch for that +/// event type. The wire flags on `session.create` / `session.resume` +/// are derived from these fields. +#[derive(Clone)] +pub(crate) struct SessionHandlers { + pub permission: Option>, + pub elicitation: Option>, + pub user_input: Option>, + pub exit_plan_mode: Option>, + pub auto_mode_switch: Option>, + pub tools: Arc>>, +} + /// Shared state between a [`Session`] and its event loop, used by [`Session::send_and_wait`]. struct IdleWaiter { tx: oneshot::Sender, Error>>, @@ -106,9 +122,10 @@ impl Drop for PendingSessionRegistration { /// A session on a GitHub Copilot CLI server. /// /// Created via [`Client::create_session`] or [`Client::resume_session`]. -/// Owns an internal event loop that dispatches events to the [`SessionHandler`]. +/// Owns an internal event loop that dispatches events to the per-callback +/// handlers installed on the session config. /// -/// Protocol methods (`send`, `get_messages`, `abort`, etc.) automatically +/// Protocol methods (`send`, `get_events`, `abort`, etc.) automatically /// inject the session ID into RPC params. /// /// Call [`destroy`](Self::destroy) for graceful cleanup (RPC + local). If dropped @@ -148,6 +165,8 @@ pub struct Session { idle_waiter: Arc>>, /// Capabilities negotiated with the CLI, updated on `capabilities.changed` events. capabilities: Arc>, + /// Canvas instances currently known to be open for this session. + open_canvases: Arc>>, /// Broadcast channel for runtime event subscribers — see [`Session::subscribe`]. event_tx: tokio::sync::broadcast::Sender, } @@ -181,6 +200,12 @@ impl Session { self.capabilities.read().clone() } + /// Open canvas instances reported by the most recent `session.resume` + /// response or surfaced by inbound `canvas.opened` events. + pub fn open_canvases(&self) -> Vec { + self.open_canvases.read().clone() + } + /// Returns a [`CancellationToken`] that fires when this session shuts /// down (via [`Session::stop_event_loop`], [`Session::destroy`], or /// [`Drop`]). @@ -220,10 +245,10 @@ impl Session { /// /// **Observe-only.** Subscribers receive a clone of every /// [`SessionEvent`] but cannot influence permission decisions, tool - /// results, or anything else that requires returning a - /// [`HandlerResponse`]. Those remain - /// the responsibility of the [`SessionHandler`] passed via - /// [`SessionConfig::handler`](crate::types::SessionConfig::handler). + /// results, or anything else that requires returning a value. Those + /// remain the responsibility of the per-callback handlers passed via + /// [`SessionConfig`]'s `with_*_handler` + /// builder methods. /// /// The returned handle implements both an inherent /// [`recv`](crate::subscription::EventSubscription::recv) method and @@ -446,8 +471,8 @@ impl Session { } } - /// Retrieve the session's message history. - pub async fn get_messages(&self) -> Result, Error> { + /// Retrieve the session's timeline events. + pub async fn get_events(&self) -> Result, Error> { let result = self .client .call( @@ -459,6 +484,12 @@ impl Session { Ok(response.events) } + /// Deprecated alias for [`get_events`](Self::get_events). + #[deprecated(since = "0.1.0", note = "Use `get_events()` instead")] + pub async fn get_messages(&self) -> Result, Error> { + self.get_events().await + } + /// Abort the current agent turn. /// /// # Cancel safety @@ -519,11 +550,11 @@ impl Session { Ok(()) } - /// Alias for [`disconnect`](Self::disconnect). - /// - /// Named after the `session.destroy` wire RPC. Prefer `disconnect` in - /// new code — the wire-level "destroy" is misleading because on-disk - /// state is preserved. + /// Deprecated alias for [`disconnect`](Self::disconnect). The + /// underlying wire RPC happens to be named `session.destroy`, but it + /// only severs the connection — on-disk session state is preserved. + /// Prefer `disconnect` in new code. + #[deprecated(since = "0.1.0", note = "Use `disconnect()` instead")] pub async fn destroy(&self) -> Result<(), Error> { self.disconnect().await } @@ -689,11 +720,11 @@ impl<'a> SessionUi<'a> { /// Ask the user for free-form text input. /// /// Returns the input string on accept, or `None` on decline/cancel. - /// Use [`InputOptions`] to set validation constraints and field metadata. + /// Use [`UiInputOptions`] to set validation constraints and field metadata. pub async fn input( &self, message: &str, - options: Option<&InputOptions<'_>>, + options: Option<&UiInputOptions<'_>>, ) -> Result, Error> { self.session.assert_elicitation()?; let mut field = serde_json::json!({ "type": "string" }); @@ -739,35 +770,57 @@ impl Client { /// Sends `session.create`, registers the session on the router, /// and spawns an internal event loop that dispatches to the handler. /// - /// All callbacks (event handler, hooks, transform) are configured - /// via [`SessionConfig`] using [`with_handler`](SessionConfig::with_handler), - /// [`with_hooks`](SessionConfig::with_hooks), and - /// [`with_transform`](SessionConfig::with_transform). + /// All callbacks (per-event handlers, tool handlers, hooks, transform) + /// are configured via [`SessionConfig`] using its `with_*_handler` / + /// `with_tools` / `with_hooks` / `with_system_message_transform` builder + /// methods. /// /// If [`hooks_handler`](SessionConfig::hooks_handler) is set, the /// wire-level `hooks` flag is automatically enabled. /// - /// If [`transform`](SessionConfig::transform) is set, the SDK injects + /// If [`system_message_transform`](SessionConfig::system_message_transform) is set, the SDK injects /// `action: "transform"` sections into the [`SystemMessageConfig`] wire /// format and handles `systemMessage.transform` RPC callbacks during /// the session. /// - /// If [`handler`](SessionConfig::handler) is `None`, the session uses - /// [`NoopHandler`](crate::handler::NoopHandler) — permission requests and - /// external tool calls are left pending for the consumer to resolve. + /// Each per-event handler is independently optional. If a handler is + /// not installed, the SDK signals the runtime not to emit the matching + /// broadcast (and silently skips dispatch if one arrives anyway). pub async fn create_session(&self, mut config: SessionConfig) -> Result { let total_start = Instant::now(); - let handler = config - .handler - .take() - .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler)); - let hooks = config.hooks_handler.take(); - let transforms = config.transform.take(); - let tools_count = config.tools.as_ref().map_or(0, Vec::len); - let commands_count = config.commands.as_ref().map_or(0, Vec::len); + let session_id = config + .session_id + .clone() + .unwrap_or_else(|| SessionId::from(uuid::Uuid::new_v4().to_string())); + config.session_id = Some(session_id.clone()); + if config.hooks_handler.is_some() && config.hooks.is_none() { + config.hooks = Some(true); + } + if let Some(transforms) = config.system_message_transform.clone() { + inject_transform_sections(&mut config, transforms.as_ref()); + } + let (wire, mut runtime) = config.into_wire(session_id.clone())?; + + let permission_handler = crate::permission::resolve_handler( + runtime.permission_handler.take(), + runtime.permission_policy.take(), + ); + let handlers = SessionHandlers { + permission: permission_handler, + elicitation: runtime.elicitation_handler.take(), + user_input: runtime.user_input_handler.take(), + exit_plan_mode: runtime.exit_plan_mode_handler.take(), + auto_mode_switch: runtime.auto_mode_switch_handler.take(), + tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)), + }; + let hooks = runtime.hooks_handler.take(); + let transforms = runtime.system_message_transform.take(); + let tools_count = wire.tools.as_ref().map_or(0, Vec::len); + let commands_count = runtime.commands.as_ref().map_or(0, Vec::len); let has_hooks = hooks.is_some(); - let command_handlers = build_command_handler_map(config.commands.as_deref()); - let session_fs_provider = config.session_fs_provider.take(); + let command_handlers = build_command_handler_map(runtime.commands.as_deref()); + let canvas_handler = runtime.canvas_handler.take(); + let session_fs_provider = runtime.session_fs_provider.take(); if self.inner.session_fs_configured && session_fs_provider.is_none() { return Err(Error::Session(SessionError::SessionFsProviderRequired)); } @@ -782,18 +835,7 @@ impl Client { )); } - if hooks.is_some() && config.hooks.is_none() { - config.hooks = Some(true); - } - if let Some(ref transforms) = transforms { - inject_transform_sections(&mut config, transforms.as_ref()); - } - let session_id = config - .session_id - .clone() - .unwrap_or_else(|| SessionId::from(uuid::Uuid::new_v4().to_string())); - config.session_id = Some(session_id.clone()); - let mut params = serde_json::to_value(&config)?; + let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; inject_trace_context(&mut params, &trace_ctx); @@ -806,10 +848,11 @@ impl Client { let event_loop = spawn_event_loop( session_id.clone(), self.clone(), - handler, + handlers, hooks, transforms, command_handlers, + canvas_handler, session_fs_provider, channels, idle_waiter.clone(), @@ -872,6 +915,7 @@ impl Client { shutdown, idle_waiter, capabilities, + open_canvases: Arc::new(parking_lot::RwLock::new(Vec::new())), event_tx, }) } @@ -888,17 +932,35 @@ impl Client { /// fields are unset. pub async fn resume_session(&self, mut config: ResumeSessionConfig) -> Result { let total_start = Instant::now(); - let handler = config - .handler - .take() - .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler)); - let hooks = config.hooks_handler.take(); - let transforms = config.transform.take(); - let tools_count = config.tools.as_ref().map_or(0, Vec::len); - let commands_count = config.commands.as_ref().map_or(0, Vec::len); + let session_id = config.session_id.clone(); + if config.hooks_handler.is_some() && config.hooks.is_none() { + config.hooks = Some(true); + } + if let Some(transforms) = config.system_message_transform.clone() { + inject_transform_sections_resume(&mut config, transforms.as_ref()); + } + let (wire, mut runtime) = config.into_wire()?; + + let permission_handler = crate::permission::resolve_handler( + runtime.permission_handler.take(), + runtime.permission_policy.take(), + ); + let handlers = SessionHandlers { + permission: permission_handler, + elicitation: runtime.elicitation_handler.take(), + user_input: runtime.user_input_handler.take(), + exit_plan_mode: runtime.exit_plan_mode_handler.take(), + auto_mode_switch: runtime.auto_mode_switch_handler.take(), + tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)), + }; + let hooks = runtime.hooks_handler.take(); + let transforms = runtime.system_message_transform.take(); + let tools_count = wire.tools.as_ref().map_or(0, Vec::len); + let commands_count = runtime.commands.as_ref().map_or(0, Vec::len); let has_hooks = hooks.is_some(); - let command_handlers = build_command_handler_map(config.commands.as_deref()); - let session_fs_provider = config.session_fs_provider.take(); + let command_handlers = build_command_handler_map(runtime.commands.as_deref()); + let canvas_handler = runtime.canvas_handler.take(); + let session_fs_provider = runtime.session_fs_provider.take(); if self.inner.session_fs_configured && session_fs_provider.is_none() { return Err(Error::Session(SessionError::SessionFsProviderRequired)); } @@ -913,14 +975,7 @@ impl Client { )); } - if hooks.is_some() && config.hooks.is_none() { - config.hooks = Some(true); - } - if let Some(ref transforms) = transforms { - inject_transform_sections_resume(&mut config, transforms.as_ref()); - } - let session_id = config.session_id.clone(); - let mut params = serde_json::to_value(&config)?; + let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; inject_trace_context(&mut params, &trace_ctx); @@ -933,10 +988,11 @@ impl Client { let event_loop = spawn_event_loop( session_id.clone(), self.clone(), - handler, + handlers, hooks, transforms, command_handlers, + canvas_handler, session_fs_provider, channels, idle_waiter.clone(), @@ -969,12 +1025,17 @@ impl Client { "Client::resume_session session resume request completed successfully" ); - // The CLI may reassign the session ID on resume. - let cli_session_id: SessionId = result - .get("sessionId") - .and_then(|v| v.as_str()) - .unwrap_or(&session_id) - .into(); + let resume_result: ResumeSessionResult = match serde_json::from_value(result) { + Ok(result) => result, + Err(error) => { + registration.cleanup(event_loop).await; + return Err(error.into()); + } + }; + let cli_session_id = resume_result + .session_id + .clone() + .unwrap_or_else(|| session_id.clone()); if cli_session_id != session_id { registration.cleanup(event_loop).await; return Err(Error::Session(SessionError::SessionIdMismatch { @@ -983,19 +1044,6 @@ impl Client { })); } - let resume_capabilities: Option = result - .get("capabilities") - .and_then(|v| { - serde_json::from_value(v.clone()) - .map_err(|e| warn!(error = %e, "failed to deserialize capabilities from resume response")) - .ok() - }); - let remote_url = result - .get("remoteUrl") - .or_else(|| result.get("remote_url")) - .and_then(|value| value.as_str()) - .map(ToString::to_string); - // Reload skills after resume (best-effort). let skills_reload_start = Instant::now(); if let Err(e) = self @@ -1019,7 +1067,10 @@ impl Client { ); } - *capabilities.write() = resume_capabilities.unwrap_or_default(); + *capabilities.write() = resume_result.capabilities.unwrap_or_default(); + let open_canvases = Arc::new(parking_lot::RwLock::new( + resume_result.open_canvases.unwrap_or_default(), + )); tracing::debug!( elapsed_ms = total_start.elapsed().as_millis(), @@ -1030,13 +1081,14 @@ impl Client { Ok(Session { id: session_id, cwd: self.cwd().clone(), - workspace_path: None, - remote_url, + workspace_path: resume_result.workspace_path, + remote_url: resume_result.remote_url, client: self.clone(), event_loop: ParkingLotMutex::new(Some(event_loop)), shutdown, idle_waiter, capabilities, + open_canvases, event_tx, }) } @@ -1060,10 +1112,11 @@ fn build_command_handler_map(commands: Option<&[CommandDefinition]>) -> Arc, + handlers: SessionHandlers, hooks: Option>, transforms: Option>, command_handlers: Arc, + canvas_handler: Option>, session_fs_provider: Option>, channels: crate::router::SessionChannels, idle_waiter: Arc>>, @@ -1094,13 +1147,19 @@ fn spawn_event_loop( _ = shutdown.cancelled() => break, Some(notification) = notifications.recv() => { handle_notification( - &session_id, &client, &handler, &command_handlers, notification, &idle_waiter, &capabilities, &event_tx, + &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &event_tx, ).await; } Some(request) = requests.recv() => { - handle_request( - &session_id, &client, &handler, hooks.as_deref(), transforms.as_deref(), session_fs_provider.as_ref(), request, - ).await; + let ctx = RequestDispatchContext { + client: &client, + handlers: &handlers, + hooks: hooks.as_deref(), + transforms: transforms.as_deref(), + canvas_handler: canvas_handler.as_ref(), + session_fs_provider: session_fs_provider.as_ref(), + }; + handle_request(&session_id, ctx, request).await; } else => break, } @@ -1124,67 +1183,16 @@ fn extract_request_id(data: &Value) -> Option { .map(RequestId::new) } -fn pending_permission_result_kind(response: &HandlerResponse) -> &'static str { - match response { - HandlerResponse::Permission(PermissionResult::Approved) => "approve-once", - HandlerResponse::Permission(PermissionResult::Denied) => "reject", - // Fallback to "user-not-available" for UserNotAvailable, Deferred (when - // forced through this path), Custom (handled separately upstream), and - // any non-permission/no-result HandlerResponse that gets here defensively. - _ => "user-not-available", - } -} - -fn permission_request_response(response: &HandlerResponse) -> PermissionDecision { - match response { - HandlerResponse::Permission(PermissionResult::Approved) => { - PermissionDecision::ApproveOnce(PermissionDecisionApproveOnce { - kind: PermissionDecisionApproveOnceKind::ApproveOnce, - }) - } - _ => PermissionDecision::Reject(PermissionDecisionReject { - kind: PermissionDecisionRejectKind::Reject, - feedback: None, - }), - } -} - -/// Map a handler response into the `result` payload for the notification -/// path (`session.permissions.handlePendingPermissionRequest`). -/// -/// Returns `None` when the SDK must not respond. -fn notification_permission_payload(response: &HandlerResponse) -> Option { - match response { - HandlerResponse::Permission(PermissionResult::Deferred | PermissionResult::NoResult) => { - None - } - HandlerResponse::Permission(PermissionResult::Custom(value)) => Some(value.clone()), - _ => Some(serde_json::json!({ - "kind": pending_permission_result_kind(response), - })), - } -} - -/// Map a handler response into the JSON-RPC `result` payload for the -/// direct-RPC path (`permission.request`). +/// Map a [`PermissionResult`] to the `result` payload sent back to the +/// server via `session.permissions.handlePendingPermissionRequest`. /// -/// Always returns a value. [`PermissionResult::Deferred`] is treated as -/// [`PermissionResult::Approved`] here because the JSON-RPC contract -/// requires a reply — see the variant's doc comment. -fn direct_permission_payload(response: &HandlerResponse) -> Value { - match response { - HandlerResponse::Permission(PermissionResult::Custom(value)) => value.clone(), - HandlerResponse::Permission(PermissionResult::Deferred) => serde_json::to_value( - permission_request_response(&HandlerResponse::Permission(PermissionResult::Approved)), - ) - .expect("serializing direct permission response should succeed"), - HandlerResponse::Permission( - PermissionResult::NoResult | PermissionResult::UserNotAvailable, - ) => serde_json::json!({ - "kind": pending_permission_result_kind(response), - }), - _ => serde_json::to_value(permission_request_response(response)) - .expect("serializing direct permission response should succeed"), +/// Returns `None` when the SDK must not send a response. +fn notification_permission_payload(result: &PermissionResult) -> Option { + match result { + PermissionResult::NoResult => None, + PermissionResult::Decision(decision) => Some( + serde_json::to_value(decision).expect("serializing permission decision should succeed"), + ), } } @@ -1200,33 +1208,12 @@ fn tool_failure_result(message: impl Into) -> ToolResult { }) } -fn notification_tool_payload(response: HandlerResponse) -> Option { - match response { - HandlerResponse::ToolResult(result) => { - Some(serde_json::to_value(result).unwrap_or(Value::Null)) - } - HandlerResponse::NoResult => None, - _ => Some( - serde_json::to_value(tool_failure_result("Unexpected handler response")) - .unwrap_or(Value::Null), - ), - } -} - -fn direct_tool_result(response: HandlerResponse) -> ToolResult { - match response { - HandlerResponse::ToolResult(result) => result, - HandlerResponse::NoResult => tool_failure_result("No tool handler available"), - _ => tool_failure_result("Unexpected handler response"), - } -} - /// Process a notification from the CLI's broadcast channel. #[allow(clippy::too_many_arguments)] async fn handle_notification( session_id: &SessionId, client: &Client, - handler: &Arc, + handlers: &SessionHandlers, command_handlers: &Arc, notification: SessionEventNotification, idle_waiter: &Arc>>, @@ -1303,14 +1290,6 @@ async fn handle_notification( // before any consumer subscribes. let _ = event_tx.send(event.clone()); - // Fire-and-forget dispatch for the general event. - handler - .on_event(HandlerEvent::SessionEvent { - session_id: session_id.clone(), - event, - }) - .await; - // Update capabilities when the CLI reports changes. The CLI sends // the full updated capabilities object — replace wholesale so removals // and new subfields are handled correctly. @@ -1335,8 +1314,25 @@ async fn handle_notification( let Some(request_id) = extract_request_id(¬ification.event.data) else { return; }; + // Honor the runtime's `resolvedByHook` signal — when the + // server has already resolved the permission via a hook, + // clients must not send a second response. + if notification + .event + .data + .get("resolvedByHook") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + { + return; + } + // Multi-client safety: if this client has no permission + // handler installed, don't respond — another client on the + // same CLI may handle it. + let Some(permission_handler) = handlers.permission.clone() else { + return; + }; let client = client.clone(); - let handler = handler.clone(); let sid = session_id.clone(); let data: PermissionRequestData = serde_json::from_value(notification.event.data.clone()).unwrap_or_else(|_| { @@ -1354,22 +1350,19 @@ async fn handle_notification( tokio::spawn( async move { let handler_start = Instant::now(); - let response = handler - .on_event(HandlerEvent::PermissionRequest { - session_id: sid.clone(), - request_id: request_id.clone(), - data, - }) + let result = permission_handler + .handle(sid.clone(), request_id.clone(), data) .await; tracing::debug!( elapsed_ms = handler_start.elapsed().as_millis(), session_id = %sid, request_id = %request_id, - "SessionHandler::on_permission_request dispatch" + "PermissionHandler::handle dispatch" ); - let Some(result_value) = notification_permission_payload(&response) else { - // Handler returned Deferred — it will call - // handlePendingPermissionRequest itself. + let Some(result_value) = notification_permission_payload(&result) else { + // Handler returned Deferred / NoResult — it will + // call handlePendingPermissionRequest itself (or + // leave the request unanswered). return; }; let rpc_start = Instant::now(); @@ -1434,8 +1427,18 @@ async fn handle_notification( return; } }; + // Multi-client safety: look up a handler for the requested + // tool name. If this client has no handler installed for that + // tool, don't respond — another connected client may have one. + let tool_handler = if data.tool_name.is_empty() { + None + } else { + handlers.tools.get(&data.tool_name).cloned() + }; + let Some(tool_handler) = tool_handler else { + return; + }; let client = client.clone(); - let handler = handler.clone(); let sid = session_id.clone(); let span = tracing::error_span!( "external_tool_handler", @@ -1444,12 +1447,12 @@ async fn handle_notification( ); tokio::spawn( async move { - if data.tool_call_id.is_empty() || data.tool_name.is_empty() { - let error_msg = if data.tool_call_id.is_empty() { - "Missing toolCallId" - } else { - "Missing toolName" - }; + // `tool_name.is_empty()` would have produced a `None` + // lookup in `handlers.tools` and short-circuited at the + // outer guard above, so only the tool_call_id check is + // reachable here. + if data.tool_call_id.is_empty() { + let error_msg = "Missing toolCallId"; let rpc_start = Instant::now(); let _ = client .call( @@ -1482,9 +1485,10 @@ async fn handle_notification( tracestate: data.tracestate, }; let handler_start = Instant::now(); - let response = handler - .on_event(HandlerEvent::ExternalTool { invocation }) - .await; + let tool_result = match tool_handler.call(invocation).await { + Ok(r) => r, + Err(e) => tool_failure_result(e.to_string()), + }; tracing::debug!( elapsed_ms = handler_start.elapsed().as_millis(), session_id = %sid, @@ -1493,9 +1497,7 @@ async fn handle_notification( tool_name = %tool_name, "ToolHandler::call dispatch" ); - let Some(result_value) = notification_tool_payload(response) else { - return; - }; + let result_value = serde_json::to_value(tool_result).unwrap_or(Value::Null); let rpc_start = Instant::now(); let _ = client .call( @@ -1522,7 +1524,7 @@ async fn handle_notification( SessionEventType::UserInputRequested => { // Notification-only signal for observers (UI, telemetry). // The CLI follows up with a `userInput.request` JSON-RPC call - // that drives `HandlerEvent::UserInput` dispatch — handling + // that drives the `UserInputHandler` dispatch — handling // the notification here too would double-fire the handler // and produce duplicate prompts on the consumer side. See // github/github-app#4249. @@ -1531,6 +1533,12 @@ async fn handle_notification( let Some(request_id) = extract_request_id(¬ification.event.data) else { return; }; + // Multi-client safety: if this client has no elicitation + // handler installed, don't respond — another client on the + // same CLI may handle it. + let Some(elicitation_handler) = handlers.elicitation.clone() else { + return; + }; let elicitation_data: ElicitationRequestedData = match serde_json::from_value(notification.event.data.clone()) { Ok(d) => d, @@ -1557,7 +1565,6 @@ async fn handle_notification( url: elicitation_data.url, }; let client = client.clone(); - let handler = handler.clone(); let sid = session_id.clone(); let span = tracing::error_span!( "elicitation_request_handler", @@ -1581,26 +1588,22 @@ async fn handle_notification( ); async move { let handler_start = Instant::now(); - let response = handler - .on_event(HandlerEvent::ElicitationRequest { - session_id: sid.clone(), - request_id: request_id.clone(), - request, - }) + let response = elicitation_handler + .handle(sid.clone(), request_id.clone(), request) .await; tracing::debug!( elapsed_ms = handler_start.elapsed().as_millis(), session_id = %sid, request_id = %request_id, - "SessionHandler::on_elicitation dispatch" + "ElicitationHandler::handle dispatch" ); response } .instrument(span) }); let result = match handler_task.await { - Ok(HandlerResponse::Elicitation(r)) => r, - _ => cancel.clone(), + Ok(r) => r, + Err(_) => cancel.clone(), }; let rpc_start = Instant::now(); if let Err(e) = client @@ -1704,17 +1707,28 @@ async fn handle_notification( } } +struct RequestDispatchContext<'a> { + client: &'a Client, + handlers: &'a SessionHandlers, + hooks: Option<&'a dyn SessionHooks>, + transforms: Option<&'a dyn SystemMessageTransform>, + canvas_handler: Option<&'a Arc>, + session_fs_provider: Option<&'a Arc>, +} + /// Process a JSON-RPC request from the CLI. async fn handle_request( session_id: &SessionId, - client: &Client, - handler: &Arc, - hooks: Option<&dyn SessionHooks>, - transforms: Option<&dyn SystemMessageTransform>, - session_fs_provider: Option<&Arc>, + ctx: RequestDispatchContext<'_>, request: crate::JsonRpcRequest, ) { let sid = session_id.clone(); + let client = ctx.client; + let handlers = ctx.handlers; + let hooks = ctx.hooks; + let transforms = ctx.transforms; + let canvas_handler = ctx.canvas_handler; + let session_fs_provider = ctx.session_fs_provider; if request.method.starts_with("sessionFs.") { crate::session_fs_dispatch::dispatch(client, session_fs_provider, request).await; @@ -1722,6 +1736,38 @@ async fn handle_request( } match request.method.as_str() { + "canvas.open" => { + let Some(params) = + parse_request_params::(client, request.id, &request) + .await + else { + return; + }; + let result = dispatch_canvas_open(canvas_handler, params).await; + send_canvas_dispatch_response(client, request.id, result).await; + } + + "canvas.close" => { + let Some(params) = + parse_request_params::(client, request.id, &request) + .await + else { + return; + }; + let result = dispatch_canvas_close(canvas_handler, params).await; + send_canvas_dispatch_response(client, request.id, result).await; + } + + "canvas.action.invoke" => { + let Some(params) = + parse_request_params::(client, request.id, &request).await + else { + return; + }; + let result = dispatch_canvas_action(canvas_handler, params).await; + send_canvas_dispatch_response(client, request.id, result).await; + } + "hooks.invoke" => { let params = request.params.as_ref(); let hook_type = params @@ -1754,49 +1800,6 @@ async fn handle_request( let _ = client.send_response(&rpc_response).await; } - "tool.call" => { - let invocation: ToolInvocation = match request - .params - .as_ref() - .and_then(|p| serde_json::from_value::(p.clone()).ok()) - { - Some(inv) => inv, - None => { - let _ = send_error_response( - client, - request.id, - error_codes::INVALID_PARAMS, - "invalid tool.call params", - ) - .await; - return; - } - }; - let tool_call_id = invocation.tool_call_id.clone(); - let tool_name = invocation.tool_name.clone(); - let handler_start = Instant::now(); - let response = handler - .on_event(HandlerEvent::ExternalTool { invocation }) - .await; - tracing::debug!( - elapsed_ms = handler_start.elapsed().as_millis(), - session_id = %sid, - tool_call_id = %tool_call_id, - tool_name = %tool_name, - "ToolHandler::call dispatch" - ); - let tool_result = direct_tool_result(response); - let rpc_response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: request.id, - result: Some(serde_json::json!(ToolResultResponse { - result: tool_result - })), - error: None, - }; - let _ = client.send_response(&rpc_response).await; - } - "userInput.request" => { let params = request.params.as_ref(); let Some(question) = params @@ -1831,29 +1834,28 @@ async fn handle_request( .and_then(|v| v.as_bool()); let handler_start = Instant::now(); - let response = handler - .on_event(HandlerEvent::UserInput { - session_id: sid.clone(), - question, - choices, - allow_freeform, - }) - .await; + let response = if let Some(user_input_handler) = handlers.user_input.as_ref() { + user_input_handler + .handle(sid.clone(), question, choices, allow_freeform) + .await + } else { + None + }; tracing::debug!( elapsed_ms = handler_start.elapsed().as_millis(), session_id = %sid, - "SessionHandler::on_user_input dispatch" + "UserInputHandler::handle dispatch" ); let rpc_result = match response { - HandlerResponse::UserInput(Some(UserInputResponse { + Some(UserInputResponse { answer, was_freeform, - })) => serde_json::json!({ + }) => serde_json::json!({ "answer": answer, "wasFreeform": was_freeform, }), - _ => serde_json::json!({ "noResponse": true }), + None => serde_json::json!({ "noResponse": true }), }; let rpc_response = JsonRpcResponse { jsonrpc: "2.0".to_string(), @@ -1878,17 +1880,11 @@ async fn handle_request( } }; - let response = handler - .on_event(HandlerEvent::ExitPlanMode { - session_id: sid, - data, - }) - .await; - - let rpc_result = match response { - HandlerResponse::ExitPlanMode(result) => serde_json::to_value(result) - .expect("ExitPlanModeResult serialization cannot fail"), - _ => serde_json::json!({ "approved": true }), + let rpc_result = if let Some(exit_plan_handler) = handlers.exit_plan_mode.as_ref() { + let result = exit_plan_handler.handle(sid, data).await; + serde_json::to_value(result).expect("ExitPlanModeResult serialization cannot fail") + } else { + serde_json::json!({ "approved": true }) }; let rpc_response = JsonRpcResponse { jsonrpc: "2.0".to_string(), @@ -1912,17 +1908,12 @@ async fn handle_request( .and_then(|p| p.get("retryAfterSeconds")) .and_then(|v| v.as_f64()); - let response = handler - .on_event(HandlerEvent::AutoModeSwitch { - session_id: sid, - error_code, - retry_after_seconds, - }) - .await; - - let answer = match response { - HandlerResponse::AutoModeSwitch(answer) => answer, - _ => AutoModeSwitchResponse::No, + let answer = if let Some(auto_mode_handler) = handlers.auto_mode_switch.as_ref() { + auto_mode_handler + .handle(sid, error_code, retry_after_seconds) + .await + } else { + AutoModeSwitchResponse::No }; let rpc_response = JsonRpcResponse { jsonrpc: "2.0".to_string(), @@ -1933,64 +1924,6 @@ async fn handle_request( let _ = client.send_response(&rpc_response).await; } - "permission.request" => { - let Some(request_id) = request - .params - .as_ref() - .and_then(|p| p.get("requestId")) - .and_then(|v| v.as_str()) - .filter(|s| !s.is_empty()) - else { - warn!("permission.request missing 'requestId' field"); - let rpc_response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: request.id, - result: None, - error: Some(crate::JsonRpcError { - code: error_codes::INVALID_PARAMS, - message: "missing required field: requestId".to_string(), - data: None, - }), - }; - let _ = client.send_response(&rpc_response).await; - return; - }; - let request_id = RequestId::new(request_id); - let raw_params = request - .params - .as_ref() - .cloned() - .unwrap_or(Value::Object(serde_json::Map::new())); - let data: PermissionRequestData = - serde_json::from_value(raw_params.clone()).unwrap_or(PermissionRequestData { - kind: None, - tool_call_id: None, - extra: raw_params, - }); - - let handler_start = Instant::now(); - let response = handler - .on_event(HandlerEvent::PermissionRequest { - session_id: sid.clone(), - request_id: request_id.clone(), - data, - }) - .await; - tracing::debug!( - elapsed_ms = handler_start.elapsed().as_millis(), - session_id = %sid, - request_id = %request_id, - "SessionHandler::on_permission_request dispatch" - ); - let rpc_response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: request.id, - result: Some(direct_permission_payload(&response)), - error: None, - }; - let _ = client.send_response(&rpc_response).await; - } - "systemMessage.transform" => { let params = request.params.as_ref(); let sections: HashMap = @@ -2067,6 +2000,112 @@ async fn handle_request( } } +async fn parse_request_params( + client: &Client, + id: u64, + request: &crate::JsonRpcRequest, +) -> Option +where + T: DeserializeOwned, +{ + let params = request + .params + .as_ref() + .cloned() + .unwrap_or(Value::Object(serde_json::Map::new())); + match serde_json::from_value(params) { + Ok(params) => Some(params), + Err(error) => { + let _ = send_error_response( + client, + id, + error_codes::INVALID_PARAMS, + &format!("invalid params: {error}"), + ) + .await; + None + } + } +} + +async fn send_canvas_dispatch_response( + client: &Client, + id: u64, + result: crate::canvas::CanvasResult, +) { + let response = match result { + Ok(value) => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: Some(value), + error: None, + }, + Err(error) => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id, + result: None, + error: Some(crate::JsonRpcError { + code: error_codes::INTERNAL_ERROR, + message: error.message.clone(), + data: Some(serde_json::json!({ + "code": error.code, + "message": error.message, + })), + }), + }, + }; + if let Err(error) = client.send_response(&response).await { + warn!( + request_id = id, + error = %error, + "failed to send canvas provider response" + ); + } +} + +fn canvas_handler_or_err( + handler: Option<&Arc>, +) -> crate::canvas::CanvasResult<&Arc> { + handler.ok_or_else(|| { + crate::canvas::CanvasError::new( + "canvas_handler_unset", + "No CanvasHandler installed on this session; \ + call SessionConfig::with_canvas_handler before creating the session.", + ) + }) +} + +async fn dispatch_canvas_open( + handler: Option<&Arc>, + params: CanvasProviderRequestParams, +) -> crate::canvas::CanvasResult { + let handler = canvas_handler_or_err(handler)?; + let response = handler.on_open(params.into_open_context()).await?; + serde_json::to_value(response).map_err(|error| { + crate::canvas::CanvasError::new( + "canvas_open_response_serialization_failed", + format!("failed to serialize canvas.open response: {error}"), + ) + }) +} + +async fn dispatch_canvas_close( + handler: Option<&Arc>, + params: CanvasProviderRequestParams, +) -> crate::canvas::CanvasResult { + let handler = canvas_handler_or_err(handler)?; + handler.on_close(params.into_lifecycle_context()).await?; + Ok(Value::Null) +} + +async fn dispatch_canvas_action( + handler: Option<&Arc>, + params: CanvasInvokeParams, +) -> crate::canvas::CanvasResult { + let handler = canvas_handler_or_err(handler)?; + handler.on_action(params.into_action_context()).await +} + async fn send_error_response( client: &Client, id: u64, @@ -2120,130 +2159,31 @@ fn inject_transform_sections_resume( mod tests { use serde_json::json; - use super::{ - direct_permission_payload, notification_permission_payload, pending_permission_result_kind, - permission_request_response, - }; - use crate::handler::{HandlerResponse, PermissionResult}; - - #[test] - fn pending_permission_requests_use_decision_kinds() { - assert_eq!( - pending_permission_result_kind(&HandlerResponse::Permission( - PermissionResult::Approved, - )), - "approve-once" - ); - assert_eq!( - pending_permission_result_kind(&HandlerResponse::Permission(PermissionResult::Denied)), - "reject" - ); - assert_eq!( - pending_permission_result_kind(&HandlerResponse::Ok), - "user-not-available" - ); - } + use super::notification_permission_payload; + use crate::handler::PermissionResult; #[test] - fn direct_permission_requests_use_decision_response_kinds() { - assert_eq!( - serde_json::to_value(permission_request_response(&HandlerResponse::Permission( - PermissionResult::Approved - ),)) - .expect("serializing approved permission response should succeed"), - json!({ "kind": "approve-once" }) - ); - assert_eq!( - serde_json::to_value(permission_request_response(&HandlerResponse::Permission( - PermissionResult::Denied - ),)) - .expect("serializing denied permission response should succeed"), - json!({ "kind": "reject" }) - ); - assert_eq!( - serde_json::to_value(permission_request_response(&HandlerResponse::Ok)) - .expect("serializing fallback permission response should succeed"), - json!({ "kind": "reject" }) - ); + fn notification_payload_suppresses_no_result() { + assert!(notification_permission_payload(&PermissionResult::NoResult).is_none()); } #[test] - fn notification_payload_handles_non_responses_and_custom() { - // Deferred/NoResult -> no payload, SDK must not respond. - assert!( - notification_permission_payload(&HandlerResponse::Permission( - PermissionResult::Deferred, - )) - .is_none() - ); - assert!( - notification_permission_payload(&HandlerResponse::Permission( - PermissionResult::NoResult, - )) - .is_none() - ); - - // Custom → handler-supplied value passed through verbatim. - let custom = json!({ - "kind": "approve-and-remember", - "allowlist": ["ls", "grep"], - }); + fn notification_payload_serializes_decisions() { assert_eq!( - notification_permission_payload(&HandlerResponse::Permission( - PermissionResult::Custom(custom.clone()), - )), - Some(custom) - ); - - // Approved/Denied → existing kind-only shape. - assert_eq!( - notification_permission_payload(&HandlerResponse::Permission( - PermissionResult::Approved, - )), + notification_permission_payload(&PermissionResult::approve_once()), Some(json!({ "kind": "approve-once" })) ); assert_eq!( - notification_permission_payload( - &HandlerResponse::Permission(PermissionResult::Denied,) - ), + notification_permission_payload(&PermissionResult::reject(None)), Some(json!({ "kind": "reject" })) ); - } - - #[test] - fn direct_payload_handles_deferred_and_custom() { - // Custom → handler-supplied value passed through verbatim. - let custom = json!({ - "kind": "approve-and-remember", - "allowlist": ["ls", "grep"], - }); - assert_eq!( - direct_permission_payload(&HandlerResponse::Permission(PermissionResult::Custom( - custom.clone(), - ))), - custom - ); - - // Deferred → falls back to Approved because the direct RPC must reply. - assert_eq!( - direct_permission_payload(&HandlerResponse::Permission(PermissionResult::Deferred)), - json!({ "kind": "approve-once" }) - ); - - // NoResult -> direct RPC cannot be left pending, so report no user. - assert_eq!( - direct_permission_payload(&HandlerResponse::Permission(PermissionResult::NoResult)), - json!({ "kind": "user-not-available" }) - ); - - // Approved/Denied → existing kind-only shape. assert_eq!( - direct_permission_payload(&HandlerResponse::Permission(PermissionResult::Approved)), - json!({ "kind": "approve-once" }) + notification_permission_payload(&PermissionResult::reject(Some("bad".to_string()))), + Some(json!({ "kind": "reject", "feedback": "bad" })) ); assert_eq!( - direct_permission_payload(&HandlerResponse::Permission(PermissionResult::Denied)), - json!({ "kind": "reject" }) + notification_permission_payload(&PermissionResult::user_not_available()), + Some(json!({ "kind": "user-not-available" })) ); } } diff --git a/rust/src/subscription.rs b/rust/src/subscription.rs index 52c15b2eb..69886a195 100644 --- a/rust/src/subscription.rs +++ b/rust/src/subscription.rs @@ -4,10 +4,10 @@ //! [`Client::subscribe_lifecycle`](crate::Client::subscribe_lifecycle). //! //! Each subscription is an opt-in **observer** of events that are also -//! delivered to the [`SessionHandler`](crate::handler::SessionHandler). -//! Subscribers receive a clone of every event but cannot influence -//! permission decisions, tool results, or anything else that requires -//! returning a [`HandlerResponse`](crate::handler::HandlerResponse). +//! delivered to the per-event handlers installed on the session config +//! (see [`crate::handler`]). Subscribers receive a clone of every event but +//! cannot influence permission decisions, tool results, or any other event +//! whose handler return value affects the runtime. //! //! # Async iteration //! diff --git a/rust/src/tool.rs b/rust/src/tool.rs index 3342f4b9f..b9b44bc0a 100644 --- a/rust/src/tool.rs +++ b/rust/src/tool.rs @@ -1,14 +1,19 @@ //! Typed tool definition framework. //! -//! Provides the [`ToolHandler`](crate::tool::ToolHandler) trait for implementing tools as named types, -//! and [`ToolHandlerRouter`](crate::tool::ToolHandlerRouter) for automatic dispatch of tool calls within a -//! [`SessionHandler`](crate::handler::SessionHandler). +//! Provides the [`ToolHandler`](crate::tool::ToolHandler) trait for +//! implementing tools as named types. Attach a handler to a +//! [`Tool`](crate::types::Tool) via +//! [`Tool::with_handler`](crate::types::Tool::with_handler), then install +//! the resulting tools on a session via +//! [`SessionConfig::with_tools`](crate::types::SessionConfig::with_tools). +//! The SDK builds an internal name-keyed registry from the handlers and +//! dispatches to the matching handler when the CLI broadcasts +//! `external_tool.requested`. //! //! Enable the `derive` feature for `schema_for`, which generates JSON //! Schema from Rust types via `schemars`. use std::collections::HashMap; -use std::sync::Arc; use async_trait::async_trait; /// Re-export of [`schemars::JsonSchema`] for deriving tool parameter schemas. @@ -16,11 +21,9 @@ use async_trait::async_trait; pub use schemars::JsonSchema; use crate::Error; -use crate::handler::{PermissionResult, SessionHandler, UserInputResponse}; -use crate::types::{ - ElicitationRequest, ElicitationResult, PermissionRequestData, RequestId, SessionEvent, - SessionId, Tool, ToolBinaryResult, ToolInvocation, ToolResult, ToolResultExpanded, -}; +#[cfg(any(feature = "derive", test))] +use crate::types::Tool; +use crate::types::{ToolBinaryResult, ToolInvocation, ToolResult, ToolResultExpanded}; /// Generate a JSON Schema [`Value`](serde_json::Value) from a Rust type. /// @@ -54,11 +57,13 @@ pub fn schema_for() -> serde_json::Value { } /// Convert a JSON Schema [`Value`](serde_json::Value) into the -/// [`Tool::parameters`] map shape expected by the protocol. +/// [`Tool::parameters`](crate::types::Tool::parameters) map shape +/// expected by the protocol. /// /// Panics if the input is not a JSON object — tool parameter schemas /// are always top-level objects (`{"type": "object", ...}`). Pair with -/// [`schema_for`] or a `serde_json::json!(...)` literal. +/// `schema_for` (available with the `derive` feature) or a +/// `serde_json::json!(...)` literal. /// /// Use [`try_tool_parameters`] when the schema comes from dynamic input and /// should return a recoverable error instead of panicking. @@ -172,64 +177,65 @@ pub fn convert_mcp_call_tool_result(value: &serde_json::Value) -> Option, /// } /// -/// struct GetWeatherTool; +/// struct GetWeather; /// /// #[async_trait] -/// impl ToolHandler for GetWeatherTool { -/// fn tool(&self) -> Tool { -/// Tool { -/// name: "get_weather".to_string(), -/// namespaced_name: None, -/// description: "Get weather for a city".to_string(), -/// parameters: tool_parameters(schema_for::()), -/// instructions: None, -/// ..Default::default() -/// } -/// } -/// +/// impl ToolHandler for GetWeather { /// async fn call(&self, inv: ToolInvocation) -> Result { /// let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; /// Ok(ToolResult::Text(format!("Weather in {}: sunny", params.city))) /// } /// } +/// +/// // Build the Tool declaration with the handler attached: +/// let tool = Tool::new("get_weather") +/// .with_description("Get weather for a city") +/// .with_parameters(schema_for::()) +/// .with_handler(Arc::new(GetWeather)); /// ``` #[async_trait] -pub trait ToolHandler: Send + Sync { - /// The tool definition sent to the CLI during session creation. - fn tool(&self) -> Tool; - +pub trait ToolHandler: Send + Sync + 'static { /// Handle a tool invocation from the agent. async fn call(&self, invocation: ToolInvocation) -> Result; } -/// Define a tool from an async function (or closure) that takes a typed, +/// Define a [`Tool`] from an async function (or closure) that takes a typed, /// `JsonSchema`-derived parameter struct. /// -/// The returned `Box` plugs directly into -/// [`ToolHandlerRouter::new`]. JSON Schema for the parameter type is generated -/// via [`schema_for`] at construction time. +/// The returned [`Tool`] carries an attached handler ready to install on a +/// session via [`SessionConfig::with_tools`](crate::types::SessionConfig::with_tools). +/// JSON Schema for the parameter type is generated via [`schema_for`] at +/// construction time. /// /// The handler bound (`Fn(ToolInvocation, P) -> Fut + Send + Sync + 'static`) /// accepts both bare `async fn` items and closures — the same shape as @@ -260,8 +266,6 @@ pub trait ToolHandler: Send + Sync { /// inv: ToolInvocation, /// params: GetWeatherParams, /// ) -> Result { -/// // `inv.session_id` and `inv.tool_call_id` are available for telemetry, -/// // streaming updates, scoping DB lookups, etc. /// let _ = inv.session_id; /// Ok(ToolResult::Text(format!("Sunny in {}", params.city))) /// } @@ -287,36 +291,24 @@ pub fn define_tool( name: impl Into, description: impl Into, handler: F, -) -> Box +) -> Tool where P: schemars::JsonSchema + serde::de::DeserializeOwned + Send + 'static, F: Fn(ToolInvocation, P) -> Fut + Send + Sync + 'static, Fut: std::future::Future> + Send + 'static, { - struct FnTool { - name: String, - description: String, - parameters: HashMap, + struct FnHandler { handler: F, _marker: std::marker::PhantomData, } #[async_trait] - impl ToolHandler for FnTool + impl ToolHandler for FnHandler where P: schemars::JsonSchema + serde::de::DeserializeOwned + Send + 'static, F: Fn(ToolInvocation, P) -> Fut + Send + Sync + 'static, Fut: std::future::Future> + Send + 'static, { - fn tool(&self) -> Tool { - Tool { - name: self.name.clone(), - description: self.description.clone(), - parameters: self.parameters.clone(), - ..Default::default() - } - } - async fn call(&self, mut invocation: ToolInvocation) -> Result { let arguments = std::mem::take(&mut invocation.arguments); let params: P = serde_json::from_value(arguments)?; @@ -324,153 +316,71 @@ where } } - Box::new(FnTool { + Tool { name: name.into(), description: description.into(), parameters: tool_parameters(schema_for::

()), + ..Default::default() + } + .with_handler(std::sync::Arc::new(FnHandler { handler, _marker: std::marker::PhantomData, - }) + })) } -/// A [`SessionHandler`] that dispatches tool calls to registered -/// [`ToolHandler`] implementations by name. +/// Define a declaration-only [`Tool`] with a JSON Schema derived from `P`. /// -/// For tool calls matching a registered handler, the handler is invoked -/// directly. All other events (permissions, user input, unrecognized tools) -/// are forwarded to the inner handler. +/// Equivalent to [`define_tool`] but produces a [`Tool`] with no attached +/// handler — useful when another connected client services this tool, or +/// when you only need to advertise the schema for capability negotiation. /// /// # Example /// /// ```rust,no_run -/// use std::sync::Arc; -/// use github_copilot_sdk::handler::ApproveAllHandler; -/// use github_copilot_sdk::tool::ToolHandlerRouter; +/// use github_copilot_sdk::tool::{define_tool_declaration, JsonSchema}; +/// use serde::Deserialize; /// -/// let router = ToolHandlerRouter::new( -/// vec![/* Box::new(MyTool), ... */], -/// Arc::new(ApproveAllHandler), -/// ); +/// #[derive(Deserialize, JsonSchema)] +/// struct Params { query: String } /// -/// // Use router.tools() in SessionConfig -/// // Use Arc::new(router) as the session handler +/// let declared = define_tool_declaration::( +/// "legacy_thing", +/// "Handled by another connected client", +/// ); +/// # let _ = declared; /// ``` -pub struct ToolHandlerRouter { - handlers: HashMap>, - inner: Arc, -} - -impl std::fmt::Debug for ToolHandlerRouter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut tools: Vec<_> = self.handlers.keys().collect(); - tools.sort(); - f.debug_struct("ToolHandlerRouter") - .field("tool_count", &self.handlers.len()) - .field("tools", &tools) - .finish() - } -} - -impl ToolHandlerRouter { - /// Create a router from tool handler impls and a fallback handler. - /// - /// Call [`tools()`](Self::tools) to get the tool definitions for - /// [`SessionConfig::tools`](crate::SessionConfig::tools). - pub fn new(tools: Vec>, inner: Arc) -> Self { - let mut handlers = HashMap::new(); - for tool in tools { - handlers.insert(tool.tool().name.clone(), tool); - } - Self { handlers, inner } - } - - /// Tool definitions for [`SessionConfig::tools`](crate::SessionConfig::tools). - pub fn tools(&self) -> Vec { - self.handlers.values().map(|h| h.tool()).collect() - } -} - -#[async_trait] -impl SessionHandler for ToolHandlerRouter { - async fn on_external_tool(&self, invocation: ToolInvocation) -> ToolResult { - let Some(handler) = self.handlers.get(&invocation.tool_name) else { - return self.inner.on_external_tool(invocation).await; - }; - match handler.call(invocation).await { - Ok(result) => result, - Err(e) => { - let msg = e.to_string(); - ToolResult::Expanded(ToolResultExpanded { - text_result_for_llm: msg.clone(), - result_type: "failure".to_string(), - binary_results_for_llm: None, - session_log: None, - error: Some(msg), - tool_telemetry: None, - }) - } - } - } - - async fn on_session_event(&self, session_id: SessionId, event: SessionEvent) { - self.inner.on_session_event(session_id, event).await - } - - async fn on_permission_request( - &self, - session_id: SessionId, - request_id: RequestId, - data: PermissionRequestData, - ) -> PermissionResult { - self.inner - .on_permission_request(session_id, request_id, data) - .await - } - - async fn on_user_input( - &self, - session_id: SessionId, - question: String, - choices: Option>, - allow_freeform: Option, - ) -> Option { - self.inner - .on_user_input(session_id, question, choices, allow_freeform) - .await - } - - async fn on_elicitation( - &self, - session_id: SessionId, - request_id: RequestId, - request: ElicitationRequest, - ) -> ElicitationResult { - self.inner - .on_elicitation(session_id, request_id, request) - .await +#[cfg(feature = "derive")] +pub fn define_tool_declaration

(name: impl Into, description: impl Into) -> Tool +where + P: schemars::JsonSchema, +{ + Tool { + name: name.into(), + description: description.into(), + parameters: tool_parameters(schema_for::

()), + ..Default::default() } } #[cfg(test)] mod tests { use super::*; - use crate::types::{PermissionRequestData, RequestId, SessionId}; + use crate::types::SessionId; struct EchoTool; - #[async_trait] - impl ToolHandler for EchoTool { - fn tool(&self) -> Tool { - Tool { - name: "echo".to_string(), - namespaced_name: None, - description: "Echo the input".to_string(), - parameters: tool_parameters(serde_json::json!({"type": "object"})), - instructions: None, - ..Default::default() - } + fn echo_tool() -> Tool { + Tool { + name: "echo".to_string(), + description: "Echo the input".to_string(), + parameters: tool_parameters(serde_json::json!({"type": "object"})), + ..Default::default() } + .with_handler(std::sync::Arc::new(EchoTool)) + } + #[async_trait] + impl ToolHandler for EchoTool { async fn call(&self, inv: ToolInvocation) -> Result { Ok(ToolResult::Text(inv.arguments.to_string())) } @@ -478,11 +388,11 @@ mod tests { #[test] fn tool_handler_returns_tool_definition() { - let tool = EchoTool; - let def = tool.tool(); + let def = echo_tool(); assert_eq!(def.name, "echo"); assert_eq!(def.description, "Echo the input"); assert!(def.parameters.contains_key("type")); + assert!(def.handler.is_some()); } #[test] @@ -685,11 +595,11 @@ mod tests { }, ); - let def = tool.tool(); - assert_eq!(def.name, "weather"); - assert_eq!(def.description, "Get the weather for a city"); - assert_eq!(def.parameters["type"], "object"); - assert!(def.parameters["properties"]["city"].is_object()); + assert_eq!(tool.name, "weather"); + assert_eq!(tool.description, "Get the weather for a city"); + assert_eq!(tool.parameters["type"], "object"); + assert!(tool.parameters["properties"]["city"].is_object()); + let handler = tool.handler.as_ref().expect("define_tool attaches handler"); let inv = ToolInvocation { session_id: SessionId::from("s1"), @@ -699,239 +609,12 @@ mod tests { traceparent: None, tracestate: None, }; - match tool.call(inv).await.unwrap() { + match handler.call(inv).await.unwrap() { ToolResult::Text(s) => assert_eq!(s, "sunny in Seattle"), _ => panic!("expected Text result"), } } - #[tokio::test] - async fn router_dispatches_to_correct_handler() { - struct ToolA; - #[async_trait] - impl ToolHandler for ToolA { - fn tool(&self) -> Tool { - Tool { - name: "tool_a".to_string(), - namespaced_name: None, - description: "A".to_string(), - parameters: HashMap::new(), - instructions: None, - ..Default::default() - } - } - - async fn call(&self, _inv: ToolInvocation) -> Result { - Ok(ToolResult::Text("a_result".to_string())) - } - } - - struct ToolB; - #[async_trait] - impl ToolHandler for ToolB { - fn tool(&self) -> Tool { - Tool { - name: "tool_b".to_string(), - namespaced_name: None, - description: "B".to_string(), - parameters: HashMap::new(), - instructions: None, - ..Default::default() - } - } - - async fn call(&self, _inv: ToolInvocation) -> Result { - Ok(ToolResult::Text("b_result".to_string())) - } - } - - let router = ToolHandlerRouter::new( - vec![Box::new(ToolA), Box::new(ToolB)], - Arc::new(crate::handler::ApproveAllHandler), - ); - - let tools = router.tools(); - assert_eq!(tools.len(), 2); - - let response = router - .on_external_tool(ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "tool_b".to_string(), - arguments: serde_json::json!({}), - traceparent: None, - tracestate: None, - }) - .await; - match response { - ToolResult::Text(s) => assert_eq!(s, "b_result"), - _ => panic!("expected ToolResult::Text"), - } - } - - #[tokio::test] - async fn router_falls_through_for_unknown_tool() { - use std::sync::atomic::{AtomicBool, Ordering}; - - struct FallbackHandler { - called: AtomicBool, - } - #[async_trait] - impl SessionHandler for FallbackHandler { - async fn on_external_tool(&self, _inv: ToolInvocation) -> ToolResult { - self.called.store(true, Ordering::Relaxed); - ToolResult::Text("fallback".to_string()) - } - } - - let fallback = Arc::new(FallbackHandler { - called: AtomicBool::new(false), - }); - let router = ToolHandlerRouter::new(vec![], fallback.clone()); - - let response = router - .on_external_tool(ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "unknown".to_string(), - arguments: serde_json::json!({}), - traceparent: None, - tracestate: None, - }) - .await; - assert!(fallback.called.load(Ordering::Relaxed)); - match response { - ToolResult::Text(s) => assert_eq!(s, "fallback"), - _ => panic!("expected fallback result"), - } - } - - #[tokio::test] - async fn router_returns_failure_on_handler_error() { - struct FailTool; - #[async_trait] - impl ToolHandler for FailTool { - fn tool(&self) -> Tool { - Tool { - name: "bad_tool".to_string(), - namespaced_name: None, - description: "Always fails".to_string(), - parameters: HashMap::new(), - instructions: None, - ..Default::default() - } - } - - async fn call(&self, _inv: ToolInvocation) -> Result { - Err(Error::Rpc { - code: -1, - message: "intentional failure".to_string(), - }) - } - } - - let router = ToolHandlerRouter::new( - vec![Box::new(FailTool)], - Arc::new(crate::handler::ApproveAllHandler), - ); - - let response = router - .on_external_tool(ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "bad_tool".to_string(), - arguments: serde_json::json!({}), - traceparent: None, - tracestate: None, - }) - .await; - match response { - ToolResult::Expanded(exp) => { - assert_eq!(exp.result_type, "failure"); - assert!(exp.error.unwrap().contains("intentional failure")); - } - _ => panic!("expected expanded failure result"), - } - } - - #[tokio::test] - async fn router_forwards_non_tool_events() { - struct PermHandler; - #[async_trait] - impl SessionHandler for PermHandler { - async fn on_permission_request( - &self, - _session_id: SessionId, - _request_id: RequestId, - _data: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::Denied - } - } - - let router = ToolHandlerRouter::new(vec![], Arc::new(PermHandler)); - - let response = router - .on_permission_request( - SessionId::from("s1"), - RequestId::new("r1"), - PermissionRequestData { - extra: serde_json::json!({}), - ..Default::default() - }, - ) - .await; - assert!(matches!(response, PermissionResult::Denied)); - } - - #[tokio::test] - async fn router_default_on_event_dispatches_via_per_event_methods() { - // Regression: callers using the legacy on_event entry point should - // still get correct dispatch through the inherited default impl. - use crate::handler::{HandlerEvent, HandlerResponse}; - - struct OkTool; - #[async_trait] - impl ToolHandler for OkTool { - fn tool(&self) -> Tool { - Tool { - name: "ok_tool".to_string(), - namespaced_name: None, - description: "ok".to_string(), - parameters: HashMap::new(), - instructions: None, - ..Default::default() - } - } - - async fn call(&self, _inv: ToolInvocation) -> Result { - Ok(ToolResult::Text("ok".to_string())) - } - } - - let router = ToolHandlerRouter::new( - vec![Box::new(OkTool)], - Arc::new(crate::handler::ApproveAllHandler), - ); - - let response = router - .on_event(HandlerEvent::ExternalTool { - invocation: ToolInvocation { - session_id: SessionId::from("s1"), - tool_call_id: "tc1".to_string(), - tool_name: "ok_tool".to_string(), - arguments: serde_json::json!({}), - traceparent: None, - tracestate: None, - }, - }) - .await; - match response { - HandlerResponse::ToolResult(ToolResult::Text(s)) => assert_eq!(s, "ok"), - _ => panic!("expected ToolResult via default on_event"), - } - } - // Tests requiring `schemars` (the `derive` feature). #[cfg(feature = "derive")] mod derive_tests { @@ -965,19 +648,18 @@ mod tests { struct GetWeatherTool; - #[async_trait] - impl ToolHandler for GetWeatherTool { - fn tool(&self) -> Tool { - Tool { - name: "get_weather".to_string(), - namespaced_name: None, - description: "Get weather for a city".to_string(), - parameters: tool_parameters(schema_for::()), - instructions: None, - ..Default::default() - } + fn get_weather_tool() -> Tool { + Tool { + name: "get_weather".to_string(), + description: "Get weather for a city".to_string(), + parameters: tool_parameters(schema_for::()), + ..Default::default() } + .with_handler(std::sync::Arc::new(GetWeatherTool)) + } + #[async_trait] + impl ToolHandler for GetWeatherTool { async fn call(&self, inv: ToolInvocation) -> Result { let params: GetWeatherParams = serde_json::from_value(inv.arguments)?; Ok(ToolResult::Text(format!( @@ -990,12 +672,12 @@ mod tests { #[test] fn tool_handler_with_schema_for() { - let tool = GetWeatherTool; - let def = tool.tool(); + let def = get_weather_tool(); assert_eq!(def.name, "get_weather"); let schema = serde_json::to_value(&def.parameters).expect("serialize tool parameters"); assert_eq!(schema["type"], "object"); assert!(schema["properties"]["city"].is_object()); + assert!(def.handler.is_some()); } #[tokio::test] @@ -1034,18 +716,14 @@ mod tests { } #[tokio::test] - async fn router_with_schema_for_tools() { - let router = ToolHandlerRouter::new( - vec![Box::new(GetWeatherTool)], - Arc::new(crate::handler::ApproveAllHandler), - ); - - let tools = router.tools(); - assert_eq!(tools.len(), 1); - assert_eq!(tools[0].name, "get_weather"); + async fn schema_for_derived_tool_round_trips_through_call() { + let tool = GetWeatherTool; - let response = router - .on_external_tool(ToolInvocation { + // Calling the tool with matching arguments returns the + // expected typed result. (Per-name dispatch is the SDK's + // concern; here we exercise just the handler contract.) + let result = tool + .call(ToolInvocation { session_id: SessionId::from("s1"), tool_call_id: "tc1".to_string(), tool_name: "get_weather".to_string(), @@ -1053,8 +731,9 @@ mod tests { traceparent: None, tracestate: None, }) - .await; - match response { + .await + .expect("ToolHandler::call should succeed for matching args"); + match result { ToolResult::Text(s) => assert!(s.contains("Portland")), _ => panic!("expected ToolResult::Text"), } diff --git a/rust/src/types.rs b/rust/src/types.rs index 94e694b8d..d841096c5 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -12,7 +12,12 @@ use std::time::Duration; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::handler::SessionHandler; +use crate::canvas::{CanvasDeclaration, CanvasHandler}; +use crate::generated::api_types::OpenCanvasInstance; +use crate::handler::{ + AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, PermissionHandler, + UserInputHandler, +}; use crate::hooks::SessionHooks; pub use crate::session_fs::{ DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig, @@ -22,17 +27,12 @@ pub use crate::session_fs::{ pub use crate::trace_context::{TraceContext, TraceContextProvider}; use crate::transforms::SystemMessageTransform; -/// Lifecycle state of a [`Client`](crate::Client) connection to the CLI. -/// -/// The state advances from `Connecting` → `Connected` during construction, -/// transitions to `Disconnected` after [`Client::stop`](crate::Client::stop) or -/// [`Client::force_stop`](crate::Client::force_stop), and lands in -/// `Error` if startup fails or the underlying transport tears down -/// unexpectedly. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] +/// Lifecycle state of a [`Client`](crate::Client) connection. Internal — +/// not part of the public API. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[allow(dead_code)] #[non_exhaustive] -pub enum ConnectionState { +pub(crate) enum ConnectionState { /// No CLI process is attached or the process has exited cleanly. Disconnected, /// The client is starting up (spawning the CLI, negotiating protocol). @@ -298,7 +298,14 @@ impl PartialEq<&str> for RequestId { /// (rather than using the schema-generated form) so it can carry runtime /// hints — `overrides_built_in_tool`, `skip_permission` — that don't appear /// in the wire schema but are honored by the CLI. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +/// +/// A `Tool` may optionally carry a [`handler`](Self::handler): an +/// `Arc` that implements the tool's runtime behavior. +/// When present, the SDK dispatches matching `external_tool.requested` +/// broadcasts to it automatically. When absent (`None`), the tool is +/// declaration-only — another connected client must service incoming +/// invocations. +#[derive(Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct Tool { @@ -327,6 +334,19 @@ pub struct Tool { /// access control. #[serde(default, skip_serializing_if = "is_false")] pub skip_permission: bool, + /// Optional runtime implementation. When `Some`, the SDK dispatches + /// matching `external_tool.requested` broadcasts to this handler. + /// When `None`, the tool is declaration-only. + /// + /// Skipped during serialization — the handler is runtime behavior, + /// not part of the wire representation. + /// + /// Crate-private to enforce builder semantics: external callers must + /// install a handler through [`Tool::with_handler`] and inspect via + /// [`Tool::handler`], so an already-attached handler cannot be + /// silently overwritten by direct field assignment. + #[serde(skip)] + pub(crate) handler: Option>, } #[inline] @@ -382,16 +402,19 @@ impl Tool { /// Set the JSON Schema for the tool's input parameters. /// - /// Accepts anything that converts into a JSON object, including a - /// `serde_json::Value` produced by `json!({...})`. Non-object values - /// are stored as an empty parameter map; callers that need direct - /// control over the field can construct a `HashMap` - /// and assign it to [`Tool::parameters`] via [`Default::default`]. + /// Accepts a JSON Schema as a `serde_json::Value`, typically built with + /// `serde_json::json!({...})` or returned by `schema_for` (available + /// with the `derive` feature). Tool parameter schemas are always + /// top-level JSON objects (`{"type": "object", ...}`). + /// + /// # Panics + /// + /// Panics if `parameters` is not a JSON object. Use + /// [`crate::tool::try_tool_parameters`] and assign to + /// [`Tool::parameters`] directly when the schema comes from dynamic + /// input and should produce a recoverable error instead. pub fn with_parameters(mut self, parameters: Value) -> Self { - self.parameters = parameters - .as_object() - .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) - .unwrap_or_default(); + self.parameters = crate::tool::tool_parameters(parameters); self } @@ -410,6 +433,40 @@ impl Tool { self.skip_permission = skip; self } + + /// Attach a runtime implementation. The SDK will dispatch matching + /// `external_tool.requested` broadcasts to `handler` for this tool's + /// name. Without a handler the tool is declaration-only. + pub fn with_handler(mut self, handler: Arc) -> Self { + self.handler = Some(handler); + self + } + + /// Returns the attached runtime handler, if any. + /// + /// Read-only inspection — to install or replace a handler, use + /// [`Tool::with_handler`]. + pub fn handler(&self) -> Option<&Arc> { + self.handler.as_ref() + } +} + +impl std::fmt::Debug for Tool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tool") + .field("name", &self.name) + .field("namespaced_name", &self.namespaced_name) + .field("description", &self.description) + .field("instructions", &self.instructions) + .field("parameters", &self.parameters) + .field("overrides_built_in_tool", &self.overrides_built_in_tool) + .field("skip_permission", &self.skip_permission) + .field( + "handler", + &self.handler.as_ref().map(|_| "").unwrap_or("None"), + ) + .finish() + } } /// Context passed to a [`CommandHandler`] when a registered slash command @@ -718,6 +775,26 @@ impl CloudSessionOptions { } } +/// Stable extension identity for session participants that provide canvases. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ExtensionInfo { + /// Extension namespace/source, e.g. `"github-app"`. + pub source: String, + /// Stable provider name within the source namespace. + pub name: String, +} + +impl ExtensionInfo { + /// Create stable extension identity metadata. + pub fn new(source: impl Into, name: impl Into) -> Self { + Self { + source: source.into(), + name: name.into(), + } + } +} + /// Configuration for a single MCP server. /// /// MCP (Model Context Protocol) servers expose external tools to the @@ -736,7 +813,7 @@ impl CloudSessionOptions { /// servers.insert( /// "playwright".to_string(), /// McpServerConfig::Stdio(McpStdioServerConfig { -/// tools: vec!["*".to_string()], +/// tools: Some(vec!["*".to_string()]), /// command: "npx".to_string(), /// args: vec!["-y".to_string(), "@playwright/mcp".to_string()], /// ..Default::default() @@ -745,7 +822,7 @@ impl CloudSessionOptions { /// servers.insert( /// "weather".to_string(), /// McpServerConfig::Http(McpHttpServerConfig { -/// tools: vec!["forecast".to_string()], +/// tools: Some(vec!["forecast".to_string()]), /// url: "https://example.com/mcp".to_string(), /// ..Default::default() /// }), @@ -772,9 +849,13 @@ pub enum McpServerConfig { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpStdioServerConfig { - /// Tools to expose from this server. `["*"]` exposes all; `[]` exposes none. - #[serde(default)] - pub tools: Vec, + /// Tools to expose from this server. + /// + /// - `None` (field omitted on the wire) — expose **all** tools. + /// - `Some(vec![])` — expose **no** tools. + /// - `Some(vec!["a", ...])` — expose only the listed tools. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tools: Option>, /// Optional timeout in milliseconds for tool calls to this server. #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, @@ -798,9 +879,13 @@ pub struct McpStdioServerConfig { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpHttpServerConfig { - /// Tools to expose from this server. `["*"]` exposes all; `[]` exposes none. - #[serde(default)] - pub tools: Vec, + /// Tools to expose from this server. + /// + /// - `None` (field omitted on the wire) — expose **all** tools. + /// - `Some(vec![])` — expose **no** tools. + /// - `Some(vec!["a", ...])` — expose only the listed tools. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tools: Option>, /// Optional timeout in milliseconds for tool calls to this server. #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, @@ -957,14 +1042,6 @@ pub struct AzureProviderOptions { pub api_version: Option, } -/// Wire default for [`SessionConfig::env_value_mode`] / -/// [`ResumeSessionConfig::env_value_mode`]. The runtime understands -/// `"direct"` (literal values) or `"indirect"` (env-var lookup); the SDK -/// only ever sends `"direct"`. -fn default_env_value_mode() -> String { - "direct".into() -} - /// Configuration for creating a new session via the `session.create` RPC. /// /// All fields are optional — the CLI applies sensible defaults. @@ -1007,114 +1084,80 @@ fn default_env_value_mode() -> String { /// # Field naming across SDKs /// /// Rust field names are snake_case (`available_tools`, `system_message`); -/// they round-trip to the camelCase wire protocol via `#[serde(rename_all = -/// "camelCase")]`. When porting code from the TypeScript, Go, Python, or -/// .NET SDKs — or reading the raw JSON-RPC traces — fields appear as -/// `availableTools`, `systemMessage`, etc. -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +/// the wire protocol uses camelCase (`availableTools`, `systemMessage`). +/// The mapping happens inside `SessionConfig::into_wire` (crate-private), +/// which builds a separate `SessionCreateWire` payload. This config +/// struct is no longer itself serializable — the trait-object handler +/// fields (e.g. [`permission_handler`](Self::permission_handler)) could +/// never round-trip through serde, so the only legitimate serialization +/// path is now `into_wire`. When porting code from the TypeScript, Go, +/// Python, or .NET SDKs — or reading the raw JSON-RPC traces — fields +/// appear as `availableTools`, `systemMessage`, etc. +#[derive(Clone)] #[non_exhaustive] pub struct SessionConfig { /// Custom session ID. When unset, the CLI generates one. - #[serde(skip_serializing_if = "Option::is_none")] pub session_id: Option, /// Model to use (e.g. `"gpt-4"`, `"claude-sonnet-4"`). - #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Application name sent as `User-Agent` context. - #[serde(skip_serializing_if = "Option::is_none")] pub client_name: Option, /// Reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`). - #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, /// Enable streaming token deltas via `assistant.message_delta` events. - #[serde(skip_serializing_if = "Option::is_none")] pub streaming: Option, /// Custom system message configuration. - #[serde(skip_serializing_if = "Option::is_none")] pub system_message: Option, /// Client-defined tool declarations to expose to the agent. - #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, + /// Canvas declarations this connection provides to the runtime. + pub canvases: Option>, + /// Provider-side canvas lifecycle handler. The SDK routes inbound + /// `canvas.open` / `canvas.close` / `canvas.action.invoke` requests to + /// this handler. Use [`with_canvas_handler`](Self::with_canvas_handler) + /// to install one. + pub canvas_handler: Option>, + /// Request canvas renderer tools for this connection. + pub request_canvas_renderer: Option, + /// Request extension tools and dispatch for this connection. + pub request_extensions: Option, + /// Stable extension identity for canvas/tool providers on this connection. + pub extension_info: Option, /// Allowlist of built-in tool names the agent may use. - #[serde(skip_serializing_if = "Option::is_none")] pub available_tools: Option>, /// Blocklist of built-in tool names the agent must not use. - #[serde(skip_serializing_if = "Option::is_none")] pub excluded_tools: Option>, /// MCP server configurations passed through to the CLI. - #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, - /// Wire-format hint for MCP `env` map values. The runtime understands - /// `"direct"` (literal values) and `"indirect"` (env-var lookup); the - /// SDK only ever sends `"direct"` and consumers don't have a knob. - #[serde(default = "default_env_value_mode", skip_deserializing)] - pub(crate) env_value_mode: String, /// When true, the CLI runs config discovery (MCP config files, skills, plugins). - #[serde(skip_serializing_if = "Option::is_none")] pub enable_config_discovery: Option, - /// Enable the `ask_user` tool for interactive user input. Defaults to - /// `Some(true)` via [`SessionConfig::default`]. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_user_input: Option, - /// Enable `permission.request` JSON-RPC calls from the CLI. Defaults - /// to `Some(true)` via [`SessionConfig::default`]; the default - /// [`NoopHandler`](crate::handler::NoopHandler) leaves requests pending - /// for the consumer to resolve. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_permission: Option, - /// Enable `exitPlanMode.request` JSON-RPC calls for plan approval. - /// Defaults to `Some(true)` via [`SessionConfig::default`]. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_exit_plan_mode: Option, - /// Enable `autoModeSwitch.request` JSON-RPC calls. When `true`, the CLI - /// asks the handler whether to switch to auto model when an eligible - /// rate limit is hit. Defaults to `Some(true)` via - /// [`SessionConfig::default`]. Without this flag, the CLI surfaces the - /// rate-limit error directly without offering the auto-mode switch. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_auto_mode_switch: Option, - /// Advertise elicitation provider capability. When true, the CLI sends - /// `elicitation.requested` events that the handler can respond to. - /// Defaults to `Some(true)` via [`SessionConfig::default`]. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_elicitation: Option, /// Skill directory paths passed through to the GitHub Copilot CLI. - #[serde(skip_serializing_if = "Option::is_none")] pub skill_directories: Option>, /// Additional directories to search for custom instruction files. /// Forwarded to the CLI; not the same as [`skill_directories`](Self::skill_directories). - #[serde(skip_serializing_if = "Option::is_none")] pub instruction_directories: Option>, /// Skill names to disable. Skills in this set will not be available /// even if found in skill directories. - #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, /// Enable session hooks. When `true`, the CLI sends `hooks.invoke` /// RPC requests at key lifecycle points (pre/post tool use, prompt /// submission, session start/end, errors). - #[serde(skip_serializing_if = "Option::is_none")] pub hooks: Option, /// Custom agents (sub-agents) configured for this session. - #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents: Option>, /// Configures the built-in default agent. Use `excluded_tools` to /// hide tools from the default agent while keeping them available /// to custom sub-agents that reference them in their `tools` list. - #[serde(skip_serializing_if = "Option::is_none")] pub default_agent: Option, /// Name of the custom agent to activate when the session starts. /// Must match the `name` of one of the agents in [`Self::custom_agents`]. - #[serde(skip_serializing_if = "Option::is_none")] pub agent: Option, /// Configures infinite sessions: persistent workspace + automatic /// context-window compaction. Enabled by default on the CLI. - #[serde(skip_serializing_if = "Option::is_none")] pub infinite_sessions: Option, /// Custom model provider (BYOK). When set, the session routes /// requests through this provider instead of the default Copilot /// routing. - #[serde(skip_serializing_if = "Option::is_none")] pub provider: Option, /// Enables or disables internal session telemetry for this session. /// @@ -1123,71 +1166,74 @@ pub struct SessionConfig { /// When a custom [`provider`](Self::provider) is configured, session /// telemetry is always disabled regardless of this setting. This is /// independent of [`ClientOptions::telemetry`](crate::ClientOptions::telemetry). - #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_telemetry: Option, /// Per-property overrides for model capabilities, deep-merged over /// runtime defaults. - #[serde(skip_serializing_if = "Option::is_none")] pub model_capabilities: Option, /// Override the default configuration directory location. When set, /// the session uses this directory for storing config and state. - #[serde(skip_serializing_if = "Option::is_none")] pub config_dir: Option, /// Working directory for the session. Tool operations resolve /// relative paths against this directory. - #[serde(skip_serializing_if = "Option::is_none")] pub working_directory: Option, /// Per-session GitHub token. Distinct from /// [`ClientOptions::github_token`](crate::ClientOptions::github_token), /// which authenticates the CLI process itself; this token determines /// the GitHub identity used for content exclusion, model routing, and /// quota checks for *this session*. - #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")] pub github_token: Option, /// Per-session remote behavior control: /// - `Off` — local only, no remote export (default) /// - `Export` — export session events to GitHub without /// enabling remote steering /// - `On` — export to GitHub AND enable remote steering - #[serde(skip_serializing_if = "Option::is_none")] pub remote_session: Option, /// Creates a remote session in the cloud instead of a local session. /// The optional repository is associated with the cloud session. - #[serde(skip_serializing_if = "Option::is_none")] pub cloud: Option, /// Forward sub-agent streaming events to this connection. When false, /// only non-streaming sub-agent events and `subagent.*` lifecycle events /// are delivered. Defaults to true on the CLI. - #[serde(skip_serializing_if = "Option::is_none")] pub include_sub_agent_streaming_events: Option, /// Slash commands registered for this session. When the CLI has a TUI, /// each command appears as `/name` for the user to invoke and the /// associated [`CommandHandler`] is called when executed. - #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)] pub commands: Option>, /// Custom session filesystem provider for this session. Required when /// the [`Client`](crate::Client) was started with /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs) set. /// See [`SessionFsProvider`]. - #[serde(skip)] pub session_fs_provider: Option>, - /// Session-level event handler. The default is - /// [`NoopHandler`](crate::handler::NoopHandler) — permission requests - /// and external tool calls are left pending for the consumer to resolve. - /// Use [`with_handler`](Self::with_handler) to install a custom handler. - #[serde(skip)] - pub handler: Option>, + /// Optional permission-request handler. When `None`, the SDK sends + /// `requestPermission: false` on the wire so the runtime does not + /// emit `permission.requested` broadcasts to this client. + pub permission_handler: Option>, + /// Optional elicitation-request handler. When `None`, + /// `requestElicitation: false` goes on the wire. + pub elicitation_handler: Option>, + /// Optional user-input handler. When `None`, + /// `requestUserInput: false` goes on the wire and the `ask_user` + /// tool is disabled. + pub user_input_handler: Option>, + /// Optional exit-plan-mode handler. When `None`, + /// `requestExitPlanMode: false` goes on the wire. + pub exit_plan_mode_handler: Option>, + /// Optional auto-mode-switch handler. When `None`, + /// `requestAutoModeSwitch: false` goes on the wire. + pub auto_mode_switch_handler: Option>, /// Session lifecycle hook handler (pre/post tool use, session /// start/end, etc.). When set, the SDK auto-enables the wire-level /// `hooks` flag. Use [`with_hooks`](Self::with_hooks) to install one. - #[serde(skip)] pub hooks_handler: Option>, + /// Permission policy applied to the handler. Stored separately from + /// `permission_handler` so the order of `with_permission_handler` and + /// `approve_all_permissions` (and friends) is irrelevant. + pub(crate) permission_policy: Option, /// System-message transform. When set, the SDK injects the matching /// `action: "transform"` sections into the system message and routes /// `systemMessage.transform` RPC callbacks to it during the session. - /// Use [`with_transform`](Self::with_transform) to install one. - #[serde(skip)] - pub transform: Option>, + /// Use [`with_system_message_transform`](Self::with_system_message_transform) to install one. + pub system_message_transform: Option>, } impl std::fmt::Debug for SessionConfig { @@ -1200,15 +1246,18 @@ impl std::fmt::Debug for SessionConfig { .field("streaming", &self.streaming) .field("system_message", &self.system_message) .field("tools", &self.tools) + .field("canvases", &self.canvases) + .field( + "canvas_handler", + &self.canvas_handler.as_ref().map(|_| ""), + ) + .field("request_canvas_renderer", &self.request_canvas_renderer) + .field("request_extensions", &self.request_extensions) + .field("extension_info", &self.extension_info) .field("available_tools", &self.available_tools) .field("excluded_tools", &self.excluded_tools) .field("mcp_servers", &self.mcp_servers) .field("enable_config_discovery", &self.enable_config_discovery) - .field("request_user_input", &self.request_user_input) - .field("request_permission", &self.request_permission) - .field("request_exit_plan_mode", &self.request_exit_plan_mode) - .field("request_auto_mode_switch", &self.request_auto_mode_switch) - .field("request_elicitation", &self.request_elicitation) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("disabled_skills", &self.disabled_skills) @@ -1237,22 +1286,44 @@ impl std::fmt::Debug for SessionConfig { "session_fs_provider", &self.session_fs_provider.as_ref().map(|_| ""), ) - .field("handler", &self.handler.as_ref().map(|_| "")) + .field( + "permission_handler", + &self.permission_handler.as_ref().map(|_| ""), + ) + .field( + "elicitation_handler", + &self.elicitation_handler.as_ref().map(|_| ""), + ) + .field( + "user_input_handler", + &self.user_input_handler.as_ref().map(|_| ""), + ) + .field( + "exit_plan_mode_handler", + &self.exit_plan_mode_handler.as_ref().map(|_| ""), + ) + .field( + "auto_mode_switch_handler", + &self.auto_mode_switch_handler.as_ref().map(|_| ""), + ) .field( "hooks_handler", &self.hooks_handler.as_ref().map(|_| ""), ) - .field("transform", &self.transform.as_ref().map(|_| "")) + .field( + "system_message_transform", + &self.system_message_transform.as_ref().map(|_| ""), + ) .finish() } } impl Default for SessionConfig { - /// Permission and elicitation flows are enabled by default. When no handler - /// is provided, the SDK installs `NoopHandler`, so permission and external - /// tool requests remain pending until the consumer responds out-of-band. - /// Callers that want the wire surface fully disabled set these explicitly - /// to `Some(false)`. + /// All wire-level "request" flags and handler fields start unset. + /// Install a [`PermissionHandler`] via + /// [`with_permission_handler`](Self::with_permission_handler) and + /// the SDK derives `requestPermission: true` on the wire at + /// [`Client::create_session`](crate::Client::create_session) time. fn default() -> Self { Self { session_id: None, @@ -1262,16 +1333,15 @@ impl Default for SessionConfig { streaming: None, system_message: None, tools: None, + canvases: None, + canvas_handler: None, + request_canvas_renderer: None, + request_extensions: None, + extension_info: None, available_tools: None, excluded_tools: None, mcp_servers: None, - env_value_mode: default_env_value_mode(), enable_config_discovery: None, - request_user_input: Some(true), - request_permission: Some(true), - request_exit_plan_mode: Some(true), - request_auto_mode_switch: Some(true), - request_elicitation: Some(true), skill_directories: None, instruction_directories: None, disabled_skills: None, @@ -1291,17 +1361,181 @@ impl Default for SessionConfig { include_sub_agent_streaming_events: None, commands: None, session_fs_provider: None, - handler: None, + permission_handler: None, + elicitation_handler: None, + user_input_handler: None, + exit_plan_mode_handler: None, + auto_mode_switch_handler: None, hooks_handler: None, - transform: None, + permission_policy: None, + system_message_transform: None, } } } +/// Runtime-only bundle drained out of a [`SessionConfig`] or +/// [`ResumeSessionConfig`] by [`SessionConfig::into_wire`] / +/// [`ResumeSessionConfig::into_wire`]. Holds the trait-object handlers, +/// session-fs provider, and slash commands so the wire payload struct +/// stays a pure data shape. +pub(crate) struct SessionConfigRuntime { + pub permission_handler: Option>, + pub permission_policy: Option, + pub elicitation_handler: Option>, + pub user_input_handler: Option>, + pub exit_plan_mode_handler: Option>, + pub auto_mode_switch_handler: Option>, + pub hooks_handler: Option>, + pub system_message_transform: Option>, + pub tool_handlers: HashMap>, + pub canvas_handler: Option>, + pub session_fs_provider: Option>, + pub commands: Option>, +} + impl SessionConfig { - /// Install a custom [`SessionHandler`] for this session. - pub fn with_handler(mut self, handler: Arc) -> Self { - self.handler = Some(handler); + /// Consume this config to produce the [`SessionCreateWire`] payload + /// for `session.create` and a [`SessionConfigRuntime`] bundle holding + /// the runtime-only fields (handlers, transforms, providers). + /// + /// Wire-format flags are derived from handler presence and the policy + /// field; runtime fields are moved out into the returned runtime so + /// the deep `Vec` / `HashMap` clones the previous + /// `&self`-based shape required are eliminated, and the order of + /// reading-vs-moving is enforced at compile time. + /// + /// [`SessionCreateWire`]: crate::wire::SessionCreateWire + pub(crate) fn into_wire( + mut self, + session_id: SessionId, + ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> { + let permission_active = + self.permission_handler.is_some() || self.permission_policy.is_some(); + let request_user_input = self.user_input_handler.is_some(); + let request_exit_plan_mode = self.exit_plan_mode_handler.is_some(); + let request_auto_mode_switch = self.auto_mode_switch_handler.is_some(); + let request_elicitation = self.elicitation_handler.is_some(); + let hooks_flag = self.hooks_handler.is_some(); + + let mut tool_handlers: HashMap> = HashMap::new(); + if let Some(tools) = self.tools.as_mut() { + for tool in tools.iter_mut() { + if let Some(handler) = tool.handler.take() + && tool_handlers.insert(tool.name.clone(), handler).is_some() + { + return Err(crate::Error::InvalidConfig(format!( + "duplicate tool handler registered for name {:?}", + tool.name + ))); + } + } + } + + let wire_commands = self.commands.as_ref().map(|cmds| { + cmds.iter() + .map(|c| crate::wire::CommandWireDefinition { + name: c.name.clone(), + description: c.description.clone(), + }) + .collect() + }); + let wire_canvases = self.canvases.clone(); + let canvas_handler = self.canvas_handler.clone(); + + let wire = crate::wire::SessionCreateWire { + session_id, + model: self.model, + client_name: self.client_name, + reasoning_effort: self.reasoning_effort, + streaming: self.streaming, + system_message: self.system_message, + tools: self.tools, + canvases: wire_canvases, + request_canvas_renderer: self.request_canvas_renderer, + request_extensions: self.request_extensions, + extension_info: self.extension_info, + available_tools: self.available_tools, + excluded_tools: self.excluded_tools, + mcp_servers: self.mcp_servers, + env_value_mode: "direct", + enable_config_discovery: self.enable_config_discovery, + request_user_input, + request_permission: permission_active, + request_exit_plan_mode, + request_auto_mode_switch, + request_elicitation, + hooks: hooks_flag, + skill_directories: self.skill_directories, + instruction_directories: self.instruction_directories, + disabled_skills: self.disabled_skills, + custom_agents: self.custom_agents, + default_agent: self.default_agent, + agent: self.agent, + infinite_sessions: self.infinite_sessions, + provider: self.provider, + enable_session_telemetry: self.enable_session_telemetry, + model_capabilities: self.model_capabilities, + config_dir: self.config_dir, + working_directory: self.working_directory, + github_token: self.github_token, + remote_session: self.remote_session, + cloud: self.cloud, + include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, + commands: wire_commands, + }; + + let runtime = SessionConfigRuntime { + permission_handler: self.permission_handler, + permission_policy: self.permission_policy, + elicitation_handler: self.elicitation_handler, + user_input_handler: self.user_input_handler, + exit_plan_mode_handler: self.exit_plan_mode_handler, + auto_mode_switch_handler: self.auto_mode_switch_handler, + hooks_handler: self.hooks_handler, + system_message_transform: self.system_message_transform, + tool_handlers, + canvas_handler, + session_fs_provider: self.session_fs_provider, + commands: self.commands, + }; + + Ok((wire, runtime)) + } + + /// Install a [`PermissionHandler`] for this session. When omitted, the + /// SDK sends `requestPermission: false` on the wire and the runtime + /// short-circuits permission prompts for this client. + pub fn with_permission_handler(mut self, handler: Arc) -> Self { + self.permission_handler = Some(handler); + self + } + + /// Install an [`ElicitationHandler`]. When omitted, the SDK sends + /// `requestElicitation: false` on the wire. + pub fn with_elicitation_handler(mut self, handler: Arc) -> Self { + self.elicitation_handler = Some(handler); + self + } + + /// Install a [`UserInputHandler`]. Required for the `ask_user` tool + /// to be enabled. + pub fn with_user_input_handler(mut self, handler: Arc) -> Self { + self.user_input_handler = Some(handler); + self + } + + /// Install an [`ExitPlanModeHandler`]. + pub fn with_exit_plan_mode_handler(mut self, handler: Arc) -> Self { + self.exit_plan_mode_handler = Some(handler); + self + } + + /// Install an [`AutoModeSwitchHandler`]. + pub fn with_auto_mode_switch_handler( + mut self, + handler: Arc, + ) -> Self { + self.auto_mode_switch_handler = Some(handler); self } @@ -1332,59 +1566,40 @@ impl SessionConfig { /// Install a [`SystemMessageTransform`]. The SDK injects the matching /// `action: "transform"` sections into the system message and routes /// `systemMessage.transform` RPC callbacks to it during the session. - pub fn with_transform(mut self, transform: Arc) -> Self { - self.transform = Some(transform); + pub fn with_system_message_transform( + mut self, + transform: Arc, + ) -> Self { + self.system_message_transform = Some(transform); self } - /// Wrap the configured handler so every permission request is - /// auto-approved. Forwards every non-permission event to the inner - /// handler unchanged. - /// - /// If no handler has been installed via [`with_handler`](Self::with_handler), - /// wraps a [`NoopHandler`](crate::handler::NoopHandler), so declaration-only - /// tools remain pending for manual resolution. - /// - /// Order-independent: `with_handler(...).approve_all_permissions()` and - /// `approve_all_permissions().with_handler(...)` are NOT equivalent — - /// the second form discards the wrap because `with_handler` overwrites - /// the handler field. Always call `approve_all_permissions` *after* - /// `with_handler`. + /// Auto-approve every permission request on this session. Stored as a + /// policy that's applied at + /// [`Client::create_session`](crate::Client::create_session) time, so + /// order with [`with_permission_handler`](Self::with_permission_handler) + /// is irrelevant. pub fn approve_all_permissions(mut self) -> Self { - let inner = self - .handler - .take() - .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler)); - self.handler = Some(crate::permission::approve_all(inner)); + self.permission_policy = Some(crate::permission::Policy::ApproveAll); self } - /// Wrap the configured handler so every permission request is - /// auto-denied. See [`approve_all_permissions`](Self::approve_all_permissions) - /// for ordering and default-handler semantics. + /// Auto-deny every permission request on this session. See + /// [`approve_all_permissions`](Self::approve_all_permissions). pub fn deny_all_permissions(mut self) -> Self { - let inner = self - .handler - .take() - .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler)); - self.handler = Some(crate::permission::deny_all(inner)); + self.permission_policy = Some(crate::permission::Policy::DenyAll); self } - /// Wrap the configured handler with a closure-based permission policy: - /// `predicate` is called for each permission request; `true` approves, - /// `false` denies. See + /// Apply a closure-based permission policy: `predicate` returns `true` + /// to approve, `false` to deny. See /// [`approve_all_permissions`](Self::approve_all_permissions) for - /// ordering and default-handler semantics. + /// ordering semantics. pub fn approve_permissions_if(mut self, predicate: F) -> Self where F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static, { - let inner = self - .handler - .take() - .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler)); - self.handler = Some(crate::permission::approve_if(inner, predicate)); + self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate))); self } @@ -1430,6 +1645,39 @@ impl SessionConfig { self } + /// Set canvas declarations for this connection. The runtime advertises + /// these to the agent; install a [`CanvasHandler`] via + /// [`with_canvas_handler`](Self::with_canvas_handler) to receive the + /// resulting provider callbacks. + pub fn with_canvases>(mut self, canvases: I) -> Self { + self.canvases = Some(canvases.into_iter().collect()); + self + } + + /// Install the provider-side [`CanvasHandler`] for this session. + pub fn with_canvas_handler(mut self, handler: Arc) -> Self { + self.canvas_handler = Some(handler); + self + } + + /// Request host canvas renderer tools for this connection. + pub fn with_request_canvas_renderer(mut self, request: bool) -> Self { + self.request_canvas_renderer = Some(request); + self + } + + /// Request extension tools and dispatch for this connection. + pub fn with_request_extensions(mut self, request: bool) -> Self { + self.request_extensions = Some(request); + self + } + + /// Set stable extension identity metadata for this connection. + pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self { + self.extension_info = Some(extension_info); + self + } + /// Set the allowlist of built-in tool names the agent may use. pub fn with_available_tools(mut self, tools: I) -> Self where @@ -1462,36 +1710,6 @@ impl SessionConfig { self } - /// Enable the `ask_user` tool. Defaults to `Some(true)` via [`Self::default`]. - pub fn with_request_user_input(mut self, enable: bool) -> Self { - self.request_user_input = Some(enable); - self - } - - /// Enable `permission.request` JSON-RPC calls. Defaults to `Some(true)`. - pub fn with_request_permission(mut self, enable: bool) -> Self { - self.request_permission = Some(enable); - self - } - - /// Enable `exitPlanMode.request` JSON-RPC calls. Defaults to `Some(true)`. - pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self { - self.request_exit_plan_mode = Some(enable); - self - } - - /// Enable `autoModeSwitch.request` JSON-RPC calls. Defaults to `Some(true)`. - pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self { - self.request_auto_mode_switch = Some(enable); - self - } - - /// Advertise elicitation provider capability. Defaults to `Some(true)`. - pub fn with_request_elicitation(mut self, enable: bool) -> Self { - self.request_elicitation = Some(enable); - self - } - /// Set skill directory paths passed through to the CLI. pub fn with_skill_directories(mut self, paths: I) -> Self where @@ -1625,89 +1843,64 @@ impl SessionConfig { /// /// See [`SessionConfig`] for the construction patterns (chained `with_*` /// builder vs. direct field assignment for `Option` pass-through) and -/// the note on snake_case vs. camelCase field naming. -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +/// the note on snake_case vs. camelCase field naming. This config is not +/// itself serializable — call `ResumeSessionConfig::into_wire` +/// (crate-private) to produce the wire payload. +#[derive(Clone)] #[non_exhaustive] pub struct ResumeSessionConfig { /// ID of the session to resume. pub session_id: SessionId, /// Application name sent as User-Agent context. - #[serde(skip_serializing_if = "Option::is_none")] pub client_name: Option, /// Desired reasoning effort to apply after resuming the session. - #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, /// Enable streaming token deltas. - #[serde(skip_serializing_if = "Option::is_none")] pub streaming: Option, /// Re-supply the system message so the agent retains workspace context /// across CLI process restarts. - #[serde(skip_serializing_if = "Option::is_none")] pub system_message: Option, /// Client-defined tool declarations to re-supply on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, + /// Canvas declarations this connection provides to the runtime. + pub canvases: Option>, + /// Provider-side canvas lifecycle handler. See + /// [`SessionConfig::canvas_handler`]. + pub canvas_handler: Option>, + /// Open canvas instances the caller knows were open before this resume. + pub open_canvases: Option>, + /// Request canvas renderer tools for this connection. + pub request_canvas_renderer: Option, + /// Request extension tools and dispatch for this connection. + pub request_extensions: Option, + /// Stable extension identity for canvas/tool providers on this connection. + pub extension_info: Option, /// Allowlist of tool names the agent may use. - #[serde(skip_serializing_if = "Option::is_none")] pub available_tools: Option>, /// Blocklist of built-in tool names. - #[serde(skip_serializing_if = "Option::is_none")] pub excluded_tools: Option>, /// Re-supply MCP servers so they remain available after app restart. - #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, - /// See [`SessionConfig::env_value_mode`]. Always `"direct"` on the wire. - #[serde(default = "default_env_value_mode", skip_deserializing)] - pub(crate) env_value_mode: String, /// Enable config discovery on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub enable_config_discovery: Option, - /// Enable the ask_user tool. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_user_input: Option, - /// Enable permission request RPCs. When no handler is set, permission requests - /// remain pending until the consumer responds out-of-band. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_permission: Option, - /// Enable exit-plan-mode request RPCs. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_exit_plan_mode: Option, - /// Enable auto-mode-switch request RPCs on resume. Defaults to - /// `Some(true)` via [`ResumeSessionConfig::new`]. See - /// [`SessionConfig::request_auto_mode_switch`] for details. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_auto_mode_switch: Option, - /// Advertise elicitation provider capability on resume. - #[serde(skip_serializing_if = "Option::is_none")] - pub request_elicitation: Option, /// Skill directory paths passed through to the GitHub Copilot CLI on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub skill_directories: Option>, /// Additional directories to search for custom instruction files on /// resume. Forwarded to the CLI; not the same as [`skill_directories`](Self::skill_directories). - #[serde(skip_serializing_if = "Option::is_none")] pub instruction_directories: Option>, /// Skill names to disable on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, /// Enable session hooks on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub hooks: Option, /// Custom agents to re-supply on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents: Option>, /// Configures the built-in default agent on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub default_agent: Option, /// Name of the custom agent to activate. - #[serde(skip_serializing_if = "Option::is_none")] pub agent: Option, /// Re-supply infinite session configuration on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub infinite_sessions: Option, /// Re-supply BYOK provider configuration on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub provider: Option, /// Enables or disables internal session telemetry for this session. /// @@ -1716,43 +1909,33 @@ pub struct ResumeSessionConfig { /// When a custom [`provider`](Self::provider) is configured, session /// telemetry is always disabled regardless of this setting. This is /// independent of [`ClientOptions::telemetry`](crate::ClientOptions::telemetry). - #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_telemetry: Option, /// Per-property model capability overrides on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub model_capabilities: Option, /// Override the default configuration directory location on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub config_dir: Option, /// Per-session working directory on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub working_directory: Option, /// Per-session GitHub token on resume. See /// [`SessionConfig::github_token`]. - #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")] pub github_token: Option, /// Per-session remote behavior control on resume. See /// [`SessionConfig::remote_session`]. - #[serde(skip_serializing_if = "Option::is_none")] pub remote_session: Option, /// Forward sub-agent streaming events to this connection on resume. - #[serde(skip_serializing_if = "Option::is_none")] pub include_sub_agent_streaming_events: Option, /// Slash commands registered for this session on resume. See /// [`SessionConfig::commands`] — commands are not persisted server-side, /// so the resume payload re-supplies the registration. - #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)] pub commands: Option>, /// Custom session filesystem provider. Required on resume when the /// [`Client`](crate::Client) was started with /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs). /// See [`SessionConfig::session_fs_provider`]. - #[serde(skip)] pub session_fs_provider: Option>, /// Force-fail resume if the session does not exist on disk, instead of - /// silently starting a new session. - #[serde(skip_serializing_if = "Option::is_none")] - pub disable_resume: Option, + /// silently starting a new session. Wire field name stays `disableResume`. + pub suppress_resume_event: Option, /// When `true`, instructs the runtime to continue any tool calls or /// permission requests that were pending when the previous connection /// was dropped. Use this together with [`Client::force_stop`] to hand @@ -1760,17 +1943,28 @@ pub struct ResumeSessionConfig { /// work. /// /// [`Client::force_stop`]: crate::Client::force_stop - #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, - /// Session-level event handler. See [`SessionConfig::handler`]. - #[serde(skip)] - pub handler: Option>, + /// Optional permission-request handler. See + /// [`SessionConfig::permission_handler`]. + pub permission_handler: Option>, + /// Optional elicitation handler. See + /// [`SessionConfig::elicitation_handler`]. + pub elicitation_handler: Option>, + /// Optional user-input handler. See + /// [`SessionConfig::user_input_handler`]. + pub user_input_handler: Option>, + /// Optional exit-plan-mode handler. See + /// [`SessionConfig::exit_plan_mode_handler`]. + pub exit_plan_mode_handler: Option>, + /// Optional auto-mode-switch handler. See + /// [`SessionConfig::auto_mode_switch_handler`]. + pub auto_mode_switch_handler: Option>, /// Session hook handler. See [`SessionConfig::hooks_handler`]. - #[serde(skip)] pub hooks_handler: Option>, - /// System-message transform. See [`SessionConfig::transform`]. - #[serde(skip)] - pub transform: Option>, + /// Permission policy. See `SessionConfig::permission_policy`. + pub(crate) permission_policy: Option, + /// System-message transform. See [`SessionConfig::system_message_transform`]. + pub system_message_transform: Option>, } impl std::fmt::Debug for ResumeSessionConfig { @@ -1782,15 +1976,19 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("streaming", &self.streaming) .field("system_message", &self.system_message) .field("tools", &self.tools) + .field("canvases", &self.canvases) + .field( + "canvas_handler", + &self.canvas_handler.as_ref().map(|_| ""), + ) + .field("open_canvases", &self.open_canvases) + .field("request_canvas_renderer", &self.request_canvas_renderer) + .field("request_extensions", &self.request_extensions) + .field("extension_info", &self.extension_info) .field("available_tools", &self.available_tools) .field("excluded_tools", &self.excluded_tools) .field("mcp_servers", &self.mcp_servers) .field("enable_config_discovery", &self.enable_config_discovery) - .field("request_user_input", &self.request_user_input) - .field("request_permission", &self.request_permission) - .field("request_exit_plan_mode", &self.request_exit_plan_mode) - .field("request_auto_mode_switch", &self.request_auto_mode_switch) - .field("request_elicitation", &self.request_elicitation) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("disabled_skills", &self.disabled_skills) @@ -1818,19 +2016,145 @@ impl std::fmt::Debug for ResumeSessionConfig { "session_fs_provider", &self.session_fs_provider.as_ref().map(|_| ""), ) - .field("handler", &self.handler.as_ref().map(|_| "")) + .field( + "permission_handler", + &self.permission_handler.as_ref().map(|_| ""), + ) + .field( + "elicitation_handler", + &self.elicitation_handler.as_ref().map(|_| ""), + ) + .field( + "user_input_handler", + &self.user_input_handler.as_ref().map(|_| ""), + ) + .field( + "exit_plan_mode_handler", + &self.exit_plan_mode_handler.as_ref().map(|_| ""), + ) + .field( + "auto_mode_switch_handler", + &self.auto_mode_switch_handler.as_ref().map(|_| ""), + ) .field( "hooks_handler", &self.hooks_handler.as_ref().map(|_| ""), ) - .field("transform", &self.transform.as_ref().map(|_| "")) - .field("disable_resume", &self.disable_resume) + .field( + "system_message_transform", + &self.system_message_transform.as_ref().map(|_| ""), + ) + .field("suppress_resume_event", &self.suppress_resume_event) .field("continue_pending_work", &self.continue_pending_work) .finish() } } impl ResumeSessionConfig { + /// Consume this config to produce the [`SessionResumeWire`] payload + /// for `session.resume` and a [`SessionConfigRuntime`] bundle holding + /// the runtime-only fields (handlers, transforms, providers). + /// + /// See [`SessionConfig::into_wire`] for the design rationale. + /// + /// [`SessionResumeWire`]: crate::wire::SessionResumeWire + pub(crate) fn into_wire( + mut self, + ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> { + let permission_active = + self.permission_handler.is_some() || self.permission_policy.is_some(); + let request_user_input = self.user_input_handler.is_some(); + let request_exit_plan_mode = self.exit_plan_mode_handler.is_some(); + let request_auto_mode_switch = self.auto_mode_switch_handler.is_some(); + let request_elicitation = self.elicitation_handler.is_some(); + let hooks_flag = self.hooks_handler.is_some(); + + let mut tool_handlers: HashMap> = HashMap::new(); + if let Some(tools) = self.tools.as_mut() { + for tool in tools.iter_mut() { + if let Some(handler) = tool.handler.take() + && tool_handlers.insert(tool.name.clone(), handler).is_some() + { + return Err(crate::Error::InvalidConfig(format!( + "duplicate tool handler registered for name {:?}", + tool.name + ))); + } + } + } + + let wire_commands = self.commands.as_ref().map(|cmds| { + cmds.iter() + .map(|c| crate::wire::CommandWireDefinition { + name: c.name.clone(), + description: c.description.clone(), + }) + .collect() + }); + let wire_canvases = self.canvases.clone(); + let canvas_handler = self.canvas_handler.clone(); + + let wire = crate::wire::SessionResumeWire { + session_id: self.session_id, + client_name: self.client_name, + reasoning_effort: self.reasoning_effort, + streaming: self.streaming, + system_message: self.system_message, + tools: self.tools, + canvases: wire_canvases, + open_canvases: self.open_canvases, + request_canvas_renderer: self.request_canvas_renderer, + request_extensions: self.request_extensions, + extension_info: self.extension_info, + available_tools: self.available_tools, + excluded_tools: self.excluded_tools, + mcp_servers: self.mcp_servers, + env_value_mode: "direct", + enable_config_discovery: self.enable_config_discovery, + request_user_input, + request_permission: permission_active, + request_exit_plan_mode, + request_auto_mode_switch, + request_elicitation, + hooks: hooks_flag, + skill_directories: self.skill_directories, + instruction_directories: self.instruction_directories, + disabled_skills: self.disabled_skills, + custom_agents: self.custom_agents, + default_agent: self.default_agent, + agent: self.agent, + infinite_sessions: self.infinite_sessions, + provider: self.provider, + enable_session_telemetry: self.enable_session_telemetry, + model_capabilities: self.model_capabilities, + config_dir: self.config_dir, + working_directory: self.working_directory, + github_token: self.github_token, + remote_session: self.remote_session, + include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, + commands: wire_commands, + suppress_resume_event: self.suppress_resume_event, + continue_pending_work: self.continue_pending_work, + }; + + let runtime = SessionConfigRuntime { + permission_handler: self.permission_handler, + permission_policy: self.permission_policy, + elicitation_handler: self.elicitation_handler, + user_input_handler: self.user_input_handler, + exit_plan_mode_handler: self.exit_plan_mode_handler, + auto_mode_switch_handler: self.auto_mode_switch_handler, + hooks_handler: self.hooks_handler, + system_message_transform: self.system_message_transform, + tool_handlers, + canvas_handler, + session_fs_provider: self.session_fs_provider, + commands: self.commands, + }; + + Ok((wire, runtime)) + } + /// Construct a `ResumeSessionConfig` with the given session ID and all /// other fields left unset. Combine with `.with_*` builders or struct /// update syntax (`..ResumeSessionConfig::new(id)`) to populate the @@ -1843,16 +2167,16 @@ impl ResumeSessionConfig { streaming: None, system_message: None, tools: None, + canvases: None, + canvas_handler: None, + open_canvases: None, + request_canvas_renderer: None, + request_extensions: None, + extension_info: None, available_tools: None, excluded_tools: None, mcp_servers: None, - env_value_mode: default_env_value_mode(), enable_config_discovery: None, - request_user_input: Some(true), - request_permission: Some(true), - request_exit_plan_mode: Some(true), - request_auto_mode_switch: Some(true), - request_elicitation: Some(true), skill_directories: None, instruction_directories: None, disabled_skills: None, @@ -1871,17 +2195,49 @@ impl ResumeSessionConfig { include_sub_agent_streaming_events: None, commands: None, session_fs_provider: None, - disable_resume: None, + suppress_resume_event: None, continue_pending_work: None, - handler: None, + permission_handler: None, + elicitation_handler: None, + user_input_handler: None, + exit_plan_mode_handler: None, + auto_mode_switch_handler: None, hooks_handler: None, - transform: None, + permission_policy: None, + system_message_transform: None, } } - /// Install a custom [`SessionHandler`] for this session. - pub fn with_handler(mut self, handler: Arc) -> Self { - self.handler = Some(handler); + /// Install a [`PermissionHandler`] for the resumed session. + pub fn with_permission_handler(mut self, handler: Arc) -> Self { + self.permission_handler = Some(handler); + self + } + + /// Install an [`ElicitationHandler`] for the resumed session. + pub fn with_elicitation_handler(mut self, handler: Arc) -> Self { + self.elicitation_handler = Some(handler); + self + } + + /// Install a [`UserInputHandler`] for the resumed session. + pub fn with_user_input_handler(mut self, handler: Arc) -> Self { + self.user_input_handler = Some(handler); + 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); + self + } + + /// Install an [`AutoModeSwitchHandler`] for the resumed session. + pub fn with_auto_mode_switch_handler( + mut self, + handler: Arc, + ) -> Self { + self.auto_mode_switch_handler = Some(handler); self } @@ -1893,8 +2249,11 @@ impl ResumeSessionConfig { } /// Install a [`SystemMessageTransform`]. - pub fn with_transform(mut self, transform: Arc) -> Self { - self.transform = Some(transform); + pub fn with_system_message_transform( + mut self, + transform: Arc, + ) -> Self { + self.system_message_transform = Some(transform); self } @@ -1913,41 +2272,27 @@ impl ResumeSessionConfig { self } - /// Wrap the configured handler so every permission request is - /// auto-approved. See - /// [`SessionConfig::approve_all_permissions`] for semantics. + /// Auto-approve every permission request on the resumed session. See + /// [`SessionConfig::approve_all_permissions`]. pub fn approve_all_permissions(mut self) -> Self { - let inner = self - .handler - .take() - .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler)); - self.handler = Some(crate::permission::approve_all(inner)); + self.permission_policy = Some(crate::permission::Policy::ApproveAll); self } - /// Wrap the configured handler so every permission request is - /// auto-denied. See - /// [`SessionConfig::deny_all_permissions`] for semantics. + /// Auto-deny every permission request on the resumed session. See + /// [`SessionConfig::deny_all_permissions`]. pub fn deny_all_permissions(mut self) -> Self { - let inner = self - .handler - .take() - .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler)); - self.handler = Some(crate::permission::deny_all(inner)); + self.permission_policy = Some(crate::permission::Policy::DenyAll); self } - /// Wrap the configured handler with a predicate-based permission policy. - /// See [`SessionConfig::approve_permissions_if`] for semantics. + /// Apply a closure-based permission policy on the resumed session. + /// See [`SessionConfig::approve_permissions_if`]. pub fn approve_permissions_if(mut self, predicate: F) -> Self where F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static, { - let inner = self - .handler - .take() - .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler)); - self.handler = Some(crate::permission::approve_if(inner, predicate)); + self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate))); self } @@ -1982,6 +2327,45 @@ impl ResumeSessionConfig { self } + /// Re-supply canvas declarations on resume. + pub fn with_canvases>(mut self, canvases: I) -> Self { + self.canvases = Some(canvases.into_iter().collect()); + self + } + + /// Install the provider-side [`CanvasHandler`] for the resumed session. + pub fn with_canvas_handler(mut self, handler: Arc) -> Self { + self.canvas_handler = Some(handler); + self + } + + /// Seed open canvas instances that were visible before resuming. + pub fn with_open_canvases>( + mut self, + open_canvases: I, + ) -> Self { + self.open_canvases = Some(open_canvases.into_iter().collect()); + self + } + + /// Request host canvas renderer tools for this connection on resume. + pub fn with_request_canvas_renderer(mut self, request: bool) -> Self { + self.request_canvas_renderer = Some(request); + self + } + + /// Request extension tools and dispatch for this connection on resume. + pub fn with_request_extensions(mut self, request: bool) -> Self { + self.request_extensions = Some(request); + self + } + + /// Set stable extension identity metadata for this connection on resume. + pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self { + self.extension_info = Some(extension_info); + self + } + /// Set the allowlist of tool names the agent may use. pub fn with_available_tools(mut self, tools: I) -> Self where @@ -2014,36 +2398,6 @@ impl ResumeSessionConfig { self } - /// Enable the `ask_user` tool. Defaults to `Some(true)` via [`Self::new`]. - pub fn with_request_user_input(mut self, enable: bool) -> Self { - self.request_user_input = Some(enable); - self - } - - /// Enable `permission.request` JSON-RPC calls. Defaults to `Some(true)`. - pub fn with_request_permission(mut self, enable: bool) -> Self { - self.request_permission = Some(enable); - self - } - - /// Enable `exitPlanMode.request` JSON-RPC calls. Defaults to `Some(true)`. - pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self { - self.request_exit_plan_mode = Some(enable); - self - } - - /// Enable `autoModeSwitch.request` JSON-RPC calls. Defaults to `Some(true)`. - pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self { - self.request_auto_mode_switch = Some(enable); - self - } - - /// Advertise elicitation provider capability on resume. Defaults to `Some(true)`. - pub fn with_request_elicitation(mut self, enable: bool) -> Self { - self.request_elicitation = Some(enable); - self - } - /// Set skill directory paths passed through to the CLI on resume. pub fn with_skill_directories(mut self, paths: I) -> Self where @@ -2163,8 +2517,8 @@ impl ResumeSessionConfig { /// Force-fail resume if the session does not exist on disk, instead /// of silently starting a new session. - pub fn with_disable_resume(mut self, disable: bool) -> Self { - self.disable_resume = Some(disable); + pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self { + self.suppress_resume_event = Some(suppress); self } @@ -2227,7 +2581,7 @@ impl SystemMessageConfig { } } -/// An override operation for a single system prompt section. +/// An override operation for a single system message section. /// /// Used within [`SystemMessageConfig::sections`] when `mode` is `"customize"`. /// The `action` field determines the operation: `"replace"`, `"remove"`, @@ -2260,6 +2614,31 @@ pub struct CreateSessionResult { pub capabilities: Option, } +/// Response from `session.resume`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ResumeSessionResult { + /// The CLI-assigned session ID. Older runtimes may omit this on resume. + #[serde(default)] + pub session_id: Option, + /// Workspace directory for the session (infinite sessions). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_path: Option, + /// Remote session URL, if the session is running remotely. + #[serde(default, alias = "remote_url")] + pub remote_url: Option, + /// Capabilities negotiated with the CLI for this session. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub capabilities: Option, + /// Canvas instances already open when the session was resumed. + #[serde( + default, + alias = "openCanvasInstances", + skip_serializing_if = "Option::is_none" + )] + pub open_canvases: Option>, +} + /// Severity level for [`Session::log`](crate::session::Session::log) messages. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -3050,9 +3429,10 @@ pub enum ElicitationMode { /// An incoming elicitation request from the CLI (provider side). /// -/// Received via `elicitation.requested` session event when the session was -/// created with `request_elicitation: true`. The provider should render a -/// form or dialog and return an [`ElicitationResult`]. +/// Received via `elicitation.requested` session event when the session has +/// an [`ElicitationHandler`] installed. +/// The provider should render a form or dialog and return an +/// [`ElicitationResult`]. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ElicitationRequest { @@ -3091,11 +3471,14 @@ pub struct UiCapabilities { /// Whether the host supports interactive elicitation dialogs. #[serde(skip_serializing_if = "Option::is_none")] pub elicitation: Option, + /// Host-specific canvas capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub canvases: Option, } /// Options for the [`SessionUi::input`](crate::session::SessionUi::input) convenience method. #[derive(Debug, Clone, Default)] -pub struct InputOptions<'a> { +pub struct UiInputOptions<'a> { /// Title label for the input field. pub title: Option<&'a str>, /// Descriptive text shown below the field. @@ -3142,7 +3525,8 @@ impl InputFormat { /// `pub use types::*` surfaces them alongside hand-written SDK types. pub use crate::generated::api_types::{ Model, ModelBilling, ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision, - ModelCapabilitiesSupports, ModelList, ModelPolicy, + ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision, + PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable, }; /// Permission categories the CLI may request approval for. @@ -3180,8 +3564,7 @@ pub enum PermissionRequestKind { /// /// Used for both the `permission.request` RPC call (which expects a response) /// and `permission.requested` notifications (fire-and-forget). Contains the -/// full params object. Note that `requestId` is also available as a separate -/// field on `HandlerEvent::PermissionRequest`. +/// full params object. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PermissionRequestData { @@ -3241,7 +3624,7 @@ mod tests { use super::{ Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange, - ConnectionState, CustomAgentConfig, DeliveryMode, GitHubReferenceType, + ConnectionState, CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig, ProviderConfig, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse, ensure_attachment_display_names, @@ -3296,9 +3679,9 @@ mod tests { } #[test] - fn tool_with_parameters_handles_non_object_value() { - let tool = Tool::new("noop").with_parameters(json!(null)); - assert!(tool.parameters.is_empty()); + #[should_panic(expected = "tool parameter schema must be a JSON object")] + fn tool_with_parameters_panics_on_non_object_value() { + let _ = Tool::new("noop").with_parameters(json!(null)); } #[test] @@ -3360,23 +3743,108 @@ mod tests { } #[test] - fn session_config_default_enables_permission_flow_flags() { + fn session_config_default_wire_flags_off_without_handlers() { let cfg = SessionConfig::default(); - assert_eq!(cfg.request_user_input, Some(true)); - assert_eq!(cfg.request_permission, Some(true)); - assert_eq!(cfg.request_elicitation, Some(true)); - assert_eq!(cfg.request_exit_plan_mode, Some(true)); - assert_eq!(cfg.request_auto_mode_switch, Some(true)); + // Wire flags are derived from handler presence at create_session + // time, not stored on the config. With no handlers installed, every + // request_* flag should serialize as false. + let (wire, _runtime) = cfg + .into_wire(SessionId::from("default-flags")) + .expect("default config has no duplicate handlers"); + assert!(!wire.request_user_input); + assert!(!wire.request_permission); + assert!(!wire.request_elicitation); + assert!(!wire.request_exit_plan_mode); + assert!(!wire.request_auto_mode_switch); + assert!(!wire.hooks); + } + + #[test] + fn resume_session_config_new_wire_flags_off_without_handlers() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags")); + let (wire, _runtime) = cfg + .into_wire() + .expect("default resume config has no duplicate handlers"); + assert!(!wire.request_user_input); + assert!(!wire.request_permission); + assert!(!wire.request_elicitation); + assert!(!wire.request_exit_plan_mode); + assert!(!wire.request_auto_mode_switch); + assert!(!wire.hooks); } #[test] - fn resume_session_config_new_enables_permission_flow_flags() { - let cfg = ResumeSessionConfig::new(SessionId::from("test-id")); - assert_eq!(cfg.request_user_input, Some(true)); - assert_eq!(cfg.request_permission, Some(true)); - assert_eq!(cfg.request_elicitation, Some(true)); - assert_eq!(cfg.request_exit_plan_mode, Some(true)); - assert_eq!(cfg.request_auto_mode_switch, Some(true)); + #[allow(clippy::field_reassign_with_default)] + fn session_config_into_wire_serializes_bucket_b_fields() { + use std::path::PathBuf; + + use super::{CloudSessionOptions, CloudSessionRepository}; + + let mut cfg = SessionConfig::default(); + cfg.config_dir = Some(PathBuf::from("/tmp/cfg")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(false); + cfg.enable_session_telemetry = Some(false); + cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export); + cfg.cloud = Some(CloudSessionOptions::with_repository( + CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), + )); + + let (wire, _runtime) = cfg + .into_wire(SessionId::from("custom-id")) + .expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["sessionId"], "custom-id"); + assert_eq!(wire_json["configDir"], "/tmp/cfg"); + assert_eq!(wire_json["workingDirectory"], "/tmp/work"); + assert_eq!(wire_json["gitHubToken"], "ghs_secret"); + assert_eq!(wire_json["includeSubAgentStreamingEvents"], false); + assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["remoteSession"], "export"); + assert_eq!(wire_json["cloud"]["repository"]["owner"], "github"); + assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk"); + assert_eq!(wire_json["cloud"]["repository"]["branch"], "main"); + + // Unset fields are omitted on the wire. + let (empty_wire, _) = SessionConfig::default() + .into_wire(SessionId::from("empty")) + .expect("default has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("gitHubToken").is_none()); + assert!(empty_json.get("enableSessionTelemetry").is_none()); + assert!(empty_json.get("remoteSession").is_none()); + assert!(empty_json.get("cloud").is_none()); + } + + #[test] + fn resume_session_config_into_wire_serializes_bucket_b_fields() { + use std::path::PathBuf; + + let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.config_dir = Some(PathBuf::from("/tmp/cfg")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(true); + cfg.enable_session_telemetry = Some(false); + cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On); + + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let wire_json = serde_json::to_value(&wire).unwrap(); + assert_eq!(wire_json["sessionId"], "sess-1"); + assert_eq!(wire_json["workingDirectory"], "/tmp/work"); + assert_eq!(wire_json["configDir"], "/tmp/cfg"); + assert_eq!(wire_json["gitHubToken"], "ghs_secret"); + assert_eq!(wire_json["includeSubAgentStreamingEvents"], true); + assert_eq!(wire_json["enableSessionTelemetry"], false); + assert_eq!(wire_json["remoteSession"], "on"); + + // Unset remote_session is omitted on the wire. + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("default resume has no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("remoteSession").is_none()); } #[test] @@ -3394,9 +3862,6 @@ mod tests { .with_excluded_tools(["dangerous"]) .with_mcp_servers(HashMap::new()) .with_enable_config_discovery(true) - .with_request_user_input(false) - .with_request_exit_plan_mode(false) - .with_request_auto_mode_switch(false) .with_skill_directories([PathBuf::from("/tmp/skills")]) .with_disabled_skills(["broken-skill"]) .with_agent("researcher") @@ -3404,7 +3869,8 @@ mod tests { .with_working_directory(PathBuf::from("/tmp/work")) .with_github_token("ghp_test") .with_enable_session_telemetry(false) - .with_include_sub_agent_streaming_events(false); + .with_include_sub_agent_streaming_events(false) + .with_extension_info(ExtensionInfo::new("github-app", "counter")); assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1")); assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4")); @@ -3422,10 +3888,6 @@ mod tests { ); assert!(cfg.mcp_servers.is_some()); assert_eq!(cfg.enable_config_discovery, Some(true)); - assert_eq!(cfg.request_user_input, Some(false)); // overrode default - assert_eq!(cfg.request_permission, Some(true)); // default preserved - assert_eq!(cfg.request_exit_plan_mode, Some(false)); - assert_eq!(cfg.request_auto_mode_switch, Some(false)); assert_eq!( cfg.skill_directories.as_deref(), Some(&[PathBuf::from("/tmp/skills")][..]) @@ -3440,6 +3902,10 @@ mod tests { assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); assert_eq!(cfg.enable_session_telemetry, Some(false)); assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); + assert_eq!( + cfg.extension_info, + Some(ExtensionInfo::new("github-app", "counter")) + ); } #[test] @@ -3454,9 +3920,6 @@ mod tests { .with_excluded_tools(["dangerous"]) .with_mcp_servers(HashMap::new()) .with_enable_config_discovery(true) - .with_request_user_input(false) - .with_request_exit_plan_mode(false) - .with_request_auto_mode_switch(false) .with_skill_directories([PathBuf::from("/tmp/skills")]) .with_disabled_skills(["broken-skill"]) .with_agent("researcher") @@ -3465,8 +3928,9 @@ mod tests { .with_github_token("ghp_test") .with_enable_session_telemetry(false) .with_include_sub_agent_streaming_events(true) - .with_disable_resume(true) - .with_continue_pending_work(true); + .with_suppress_resume_event(true) + .with_continue_pending_work(true) + .with_extension_info(ExtensionInfo::new("github-app", "counter")); assert_eq!(cfg.session_id.as_str(), "sess-2"); assert_eq!(cfg.client_name.as_deref(), Some("test-app")); @@ -3482,10 +3946,6 @@ mod tests { ); assert!(cfg.mcp_servers.is_some()); assert_eq!(cfg.enable_config_discovery, Some(true)); - assert_eq!(cfg.request_user_input, Some(false)); // overrode default - assert_eq!(cfg.request_permission, Some(true)); // default preserved - assert_eq!(cfg.request_exit_plan_mode, Some(false)); - assert_eq!(cfg.request_auto_mode_switch, Some(false)); assert_eq!( cfg.skill_directories.as_deref(), Some(&[PathBuf::from("/tmp/skills")][..]) @@ -3500,8 +3960,12 @@ mod tests { assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); assert_eq!(cfg.enable_session_telemetry, Some(false)); assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); - assert_eq!(cfg.disable_resume, Some(true)); + assert_eq!(cfg.suppress_resume_event, Some(true)); assert_eq!(cfg.continue_pending_work, Some(true)); + assert_eq!( + cfg.extension_info, + Some(ExtensionInfo::new("github-app", "counter")) + ); } /// `continue_pending_work` must serialize to wire as `continuePendingWork` @@ -3511,13 +3975,29 @@ mod tests { fn resume_session_config_serializes_continue_pending_work_to_camel_case() { let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true); - let wire = serde_json::to_value(&cfg).unwrap(); - assert_eq!(wire["continuePendingWork"], true); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["continuePendingWork"], true); // Unset case — skip_serializing_if must omit the field. - let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")); - let wire = serde_json::to_value(&cfg).unwrap(); - assert!(wire.get("continuePendingWork").is_none()); + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("continuePendingWork").is_none()); + } + + /// The Rust field is `suppress_resume_event`, but the wire field stays + /// `disableResume` to preserve compatibility with the runtime and other + /// SDKs. + #[test] + fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() { + let cfg = + ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["disableResume"], true); + assert!(json.get("suppressResumeEvent").is_none()); } /// `instruction_directories` must serialize to wire as @@ -3526,16 +4006,21 @@ mod tests { fn session_config_serializes_instruction_directories_to_camel_case() { let cfg = SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]); - let wire = serde_json::to_value(&cfg).unwrap(); + let (wire, _) = cfg + .into_wire(SessionId::from("instr-on")) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); assert_eq!( - wire["instructionDirectories"], + json["instructionDirectories"], serde_json::json!(["/tmp/instr"]) ); // Unset case — skip_serializing_if must omit the field. - let cfg = SessionConfig::default(); - let wire = serde_json::to_value(&cfg).unwrap(); - assert!(wire.get("instructionDirectories").is_none()); + let (wire, _) = SessionConfig::default() + .into_wire(SessionId::from("instr-off")) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("instructionDirectories").is_none()); } /// Same check on the resume path. Forwarded to the CLI on @@ -3544,15 +4029,18 @@ mod tests { fn resume_session_config_serializes_instruction_directories_to_camel_case() { let cfg = ResumeSessionConfig::new(SessionId::from("sess-1")) .with_instruction_directories([PathBuf::from("/tmp/instr")]); - let wire = serde_json::to_value(&cfg).unwrap(); + let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); assert_eq!( - wire["instructionDirectories"], + json["instructionDirectories"], serde_json::json!(["/tmp/instr"]) ); - let cfg = ResumeSessionConfig::new(SessionId::from("sess-2")); - let wire = serde_json::to_value(&cfg).unwrap(); - assert!(wire.get("instructionDirectories").is_none()); + let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2")) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("instructionDirectories").is_none()); } #[test] @@ -3677,11 +4165,10 @@ mod tests { } #[test] - fn connection_state_error_serializes_to_match_go() { - let json = serde_json::to_string(&ConnectionState::Error).unwrap(); - assert_eq!(json, "\"error\""); - let parsed: ConnectionState = serde_json::from_str("\"error\"").unwrap(); - assert_eq!(parsed, ConnectionState::Error); + fn connection_state_distinguishes_variants() { + // ConnectionState is now an internal type; verify we can construct + // and compare the variants used by the lifecycle code paths. + assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected); } /// `agentId` is the sub-agent attribution field added in copilot-sdk @@ -3741,19 +4228,14 @@ mod tests { } #[test] - fn connection_state_other_variants_serialize_as_lowercase() { - assert_eq!( - serde_json::to_string(&ConnectionState::Disconnected).unwrap(), - "\"disconnected\"" - ); - assert_eq!( - serde_json::to_string(&ConnectionState::Connecting).unwrap(), - "\"connecting\"" - ); - assert_eq!( - serde_json::to_string(&ConnectionState::Connected).unwrap(), - "\"connected\"" - ); + fn connection_state_variants_compile() { + // Defensive smoke test: all variants must be constructable from + // within the crate. (The enum was demoted from pub to pub(crate) + // in Phase D; this test guards against accidental removal.) + let _ = ConnectionState::Disconnected; + let _ = ConnectionState::Connecting; + let _ = ConnectionState::Connected; + let _ = ConnectionState::Error; } #[test] @@ -3900,88 +4382,164 @@ mod tests { mod permission_builder_tests { use std::sync::Arc; - use crate::handler::{ - ApproveAllHandler, HandlerEvent, HandlerResponse, PermissionResult, SessionHandler, - }; + use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; + use crate::permission; use crate::types::{ - PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, SessionId, + PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, + SessionId, }; - fn permission_event() -> HandlerEvent { - HandlerEvent::PermissionRequest { - session_id: SessionId::from("s1"), - request_id: RequestId::new("1"), - data: PermissionRequestData { - extra: serde_json::json!({"tool": "shell"}), - ..Default::default() - }, + fn data() -> PermissionRequestData { + PermissionRequestData { + extra: serde_json::json!({"tool": "shell"}), + ..Default::default() } } - async fn dispatch(handler: &Arc) -> HandlerResponse { - handler.on_event(permission_event()).await + /// Apply the same policy-resolution logic that `Client::create_session` + /// uses, so tests exercise the effective handler. + fn resolve_create(mut cfg: SessionConfig) -> Option> { + permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) + } + + fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option> { + permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take()) + } + + async fn dispatch(handler: &Arc) -> PermissionResult { + handler + .handle(SessionId::from("s1"), RequestId::new("1"), data()) + .await } #[tokio::test] - async fn session_config_approve_all_wraps_existing_handler() { + async fn approve_all_with_handler_present_approves() { let cfg = SessionConfig::default() - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .approve_all_permissions(); - let handler = cfg.handler.expect("handler should be set"); - match dispatch(&handler).await { - HandlerResponse::Permission(PermissionResult::Approved) => {} - other => panic!("expected Approved, got {other:?}"), - } + let h = resolve_create(cfg).expect("policy + handler yields handler"); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } #[tokio::test] - async fn session_config_approve_all_defaults_to_noop_inner() { - // Without with_handler, the wrap defaults to NoopHandler. The - // approve-all wrap intercepts permission events, so they're still - // approved -- the inner handler is consulted only for other events. + async fn approve_all_standalone_produces_handler() { let cfg = SessionConfig::default().approve_all_permissions(); - let handler = cfg.handler.expect("handler should be set"); - match dispatch(&handler).await { - HandlerResponse::Permission(PermissionResult::Approved) => {} - other => panic!("expected Approved, got {other:?}"), - } + let h = resolve_create(cfg).expect("policy alone yields handler"); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } + /// Phase I: order between with_permission_handler and the policy + /// builder must not matter. #[tokio::test] - async fn session_config_deny_all_denies() { - let cfg = SessionConfig::default() - .with_handler(Arc::new(ApproveAllHandler)) + async fn approve_all_is_order_independent() { + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let b = SessionConfig::default() + .approve_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); + } + + #[tokio::test] + async fn deny_all_is_order_independent() { + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) .deny_all_permissions(); - let handler = cfg.handler.expect("handler should be set"); - match dispatch(&handler).await { - HandlerResponse::Permission(PermissionResult::Denied) => {} - other => panic!("expected Denied, got {other:?}"), - } + let b = SessionConfig::default() + .deny_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); } #[tokio::test] - async fn session_config_approve_permissions_if_consults_predicate() { - let cfg = SessionConfig::default() - .with_handler(Arc::new(ApproveAllHandler)) - .approve_permissions_if(|data| { - data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") - }); - let handler = cfg.handler.expect("handler should be set"); - match dispatch(&handler).await { - HandlerResponse::Permission(PermissionResult::Denied) => {} - other => panic!("expected Denied for shell, got {other:?}"), - } + async fn approve_permissions_if_consults_predicate() { + let cfg = SessionConfig::default().approve_permissions_if(|d| { + d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") + }); + let h = resolve_create(cfg).unwrap(); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); + } + + #[tokio::test] + async fn approve_permissions_if_is_order_independent() { + let predicate = |d: &PermissionRequestData| { + d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") + }; + let a = SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_permissions_if(predicate); + let b = SessionConfig::default() + .approve_permissions_if(predicate) + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_create(a).unwrap(); + let hb = resolve_create(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); } #[tokio::test] - async fn resume_session_config_approve_all_wraps_existing_handler() { + async fn resume_session_config_approve_all_works() { let cfg = ResumeSessionConfig::new(SessionId::from("s1")) - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .approve_all_permissions(); - let handler = cfg.handler.expect("handler should be set"); - match dispatch(&handler).await { - HandlerResponse::Permission(PermissionResult::Approved) => {} - other => panic!("expected Approved, got {other:?}"), - } + let h = resolve_resume(cfg).unwrap(); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); + } + + #[tokio::test] + async fn resume_session_config_approve_all_is_order_independent() { + let a = ResumeSessionConfig::new(SessionId::from("s1")) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .approve_all_permissions(); + let b = ResumeSessionConfig::new(SessionId::from("s1")) + .approve_all_permissions() + .with_permission_handler(Arc::new(ApproveAllHandler)); + let ha = resolve_resume(a).unwrap(); + let hb = resolve_resume(b).unwrap(); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } } diff --git a/rust/src/wire.rs b/rust/src/wire.rs new file mode 100644 index 000000000..b97aea261 --- /dev/null +++ b/rust/src/wire.rs @@ -0,0 +1,194 @@ +//! Wire-format structs for the `session.create` and `session.resume` +//! JSON-RPC payloads. +//! +//! Built explicitly from [`SessionConfig`](crate::types::SessionConfig) and +//! [`ResumeSessionConfig`](crate::types::ResumeSessionConfig) at +//! `Client::create_session` / `Client::resume_session` time via +//! [`SessionConfig::into_wire`](crate::types::SessionConfig::into_wire) and +//! [`ResumeSessionConfig::into_wire`](crate::types::ResumeSessionConfig::into_wire), +//! respectively. +//! +//! Keeping the wire shape separate from the user-facing config avoids +//! having callback fields on a serializable struct: the user-facing +//! 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 serde::Serialize; + +use crate::canvas::CanvasDeclaration; +use crate::generated::api_types::{ + ModelCapabilitiesOverride, OpenCanvasInstance, RemoteSessionMode, +}; +use crate::types::{ + CloudSessionOptions, CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, + InfiniteSessionConfig, McpServerConfig, ProviderConfig, SessionId, SystemMessageConfig, Tool, +}; + +/// Wire representation of a slash command (name + description only). The +/// runtime executes the command; the SDK's `CommandHandler` callback is +/// invoked from a separate dispatch path and never crosses the wire. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CommandWireDefinition { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// The exact JSON shape sent on the `session.create` JSON-RPC request. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SessionCreateWire { + pub session_id: SessionId, + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub streaming: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub system_message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub canvases: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_canvas_renderer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_extensions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_info: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub available_tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub excluded_tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp_servers: Option>, + pub env_value_mode: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_config_discovery: Option, + pub request_user_input: bool, + pub request_permission: bool, + pub request_exit_plan_mode: bool, + pub request_auto_mode_switch: bool, + pub request_elicitation: bool, + pub hooks: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub skill_directories: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_directories: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub disabled_skills: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub custom_agents: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_agent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub agent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub infinite_sessions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_session_telemetry: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub model_capabilities: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub config_dir: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub working_directory: Option, + #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")] + pub github_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub remote_session: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cloud: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_sub_agent_streaming_events: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub commands: Option>, +} + +/// The exact JSON shape sent on the `session.resume` JSON-RPC request. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SessionResumeWire { + pub session_id: SessionId, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub streaming: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub system_message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub canvases: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub open_canvases: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_canvas_renderer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_extensions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_info: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub available_tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub excluded_tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp_servers: Option>, + pub env_value_mode: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_config_discovery: Option, + pub request_user_input: bool, + pub request_permission: bool, + pub request_exit_plan_mode: bool, + pub request_auto_mode_switch: bool, + pub request_elicitation: bool, + pub hooks: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub skill_directories: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_directories: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub disabled_skills: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub custom_agents: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_agent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub agent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub infinite_sessions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_session_telemetry: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub model_capabilities: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub config_dir: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub working_directory: Option, + #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")] + pub github_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub remote_session: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_sub_agent_streaming_events: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub commands: Option>, + /// Maps to wire field `disableResume`. + #[serde(rename = "disableResume", skip_serializing_if = "Option::is_none")] + pub suppress_resume_event: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub continue_pending_work: Option, +} diff --git a/rust/tests/e2e/abort.rs b/rust/tests/e2e/abort.rs index ff8977f39..33ef835d7 100644 --- a/rust/tests/e2e/abort.rs +++ b/rust/tests/e2e/abort.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; use github_copilot_sdk::generated::session_events::{AssistantMessageDeltaData, SessionEventType}; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{ToolHandler, ToolHandlerRouter}; +use github_copilot_sdk::tool::ToolHandler; use github_copilot_sdk::{Error, SessionConfig, Tool, ToolInvocation, ToolResult}; use serde_json::json; use tokio::sync::{Mutex, mpsc, oneshot}; @@ -76,20 +76,32 @@ async fn should_abort_during_active_tool_execution() { let client = ctx.start_client().await; let (started_tx, mut started_rx) = mpsc::unbounded_channel(); let (release_tx, release_rx) = oneshot::channel(); - let router = ToolHandlerRouter::new( - vec![Box::new(SlowAnalysisTool { - started_tx, - release_rx: Mutex::new(Some(release_rx)), - })], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); + let slow_tool = Arc::new(SlowAnalysisTool { + started_tx, + release_rx: Mutex::new(Some(release_rx)), + }); let session = client .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) - .with_tools(tools), + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![ + Tool::new("slow_analysis") + .with_description( + "A slow analysis tool that blocks until released", + ) + .with_parameters(json!({ + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Value to analyze" + } + }, + "required": ["value"] + })) + .with_handler(slow_tool), + ]), ) .await .expect("create session"); @@ -138,21 +150,6 @@ struct SlowAnalysisTool { #[async_trait] impl ToolHandler for SlowAnalysisTool { - fn tool(&self) -> Tool { - Tool::new("slow_analysis") - .with_description("A slow analysis tool that blocks until released") - .with_parameters(json!({ - "type": "object", - "properties": { - "value": { - "type": "string", - "description": "Value to analyze" - } - }, - "required": ["value"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let value = invocation .arguments diff --git a/rust/tests/e2e/ask_user.rs b/rust/tests/e2e/ask_user.rs index 349c42210..282af7d30 100644 --- a/rust/tests/e2e/ask_user.rs +++ b/rust/tests/e2e/ask_user.rs @@ -1,7 +1,9 @@ use std::sync::Arc; use async_trait::async_trait; -use github_copilot_sdk::handler::{SessionHandler, UserInputResponse}; +use github_copilot_sdk::handler::{ + PermissionHandler, PermissionResult, UserInputHandler, UserInputResponse, +}; use github_copilot_sdk::{RequestId, SessionConfig, SessionId}; use tokio::sync::mpsc; @@ -19,14 +21,16 @@ async fn should_invoke_user_input_handler_when_model_uses_ask_user_tool() { ctx.set_default_copilot_user(); let (request_tx, mut request_rx) = mpsc::unbounded_channel(); let client = ctx.start_client().await; + let handler = Arc::new(RecordingUserInputHandler { + request_tx, + answer: UserInputAnswer::FirstChoiceOrFreeform("freeform answer"), + }); let session = client .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(RecordingUserInputHandler { - request_tx, - answer: UserInputAnswer::FirstChoiceOrFreeform("freeform answer"), - })), + .with_user_input_handler(handler.clone() as Arc) + .with_permission_handler(handler as Arc), ) .await .expect("create session"); @@ -61,14 +65,16 @@ async fn should_receive_choices_in_user_input_request() { ctx.set_default_copilot_user(); let (request_tx, mut request_rx) = mpsc::unbounded_channel(); let client = ctx.start_client().await; + let handler = Arc::new(RecordingUserInputHandler { + request_tx, + answer: UserInputAnswer::FirstChoiceOrFreeform("default"), + }); let session = client .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(RecordingUserInputHandler { - request_tx, - answer: UserInputAnswer::FirstChoiceOrFreeform("default"), - })), + .with_user_input_handler(handler.clone() as Arc) + .with_permission_handler(handler as Arc), ) .await .expect("create session"); @@ -106,14 +112,16 @@ async fn should_handle_freeform_user_input_response() { "This is my custom freeform answer that was not in the choices"; let (request_tx, mut request_rx) = mpsc::unbounded_channel(); let client = ctx.start_client().await; + let handler = Arc::new(RecordingUserInputHandler { + request_tx, + answer: UserInputAnswer::Freeform(freeform_answer), + }); let session = client .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(RecordingUserInputHandler { - request_tx, - answer: UserInputAnswer::Freeform(freeform_answer), - })), + .with_user_input_handler(handler.clone() as Arc) + .with_permission_handler(handler as Arc), ) .await .expect("create session"); @@ -157,8 +165,8 @@ enum UserInputAnswer { } #[async_trait] -impl SessionHandler for RecordingUserInputHandler { - async fn on_user_input( +impl UserInputHandler for RecordingUserInputHandler { + async fn handle( &self, session_id: SessionId, question: String, @@ -183,13 +191,16 @@ impl SessionHandler for RecordingUserInputHandler { was_freeform, }) } +} - async fn on_permission_request( +#[async_trait] +impl PermissionHandler for RecordingUserInputHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, _data: github_copilot_sdk::PermissionRequestData, - ) -> github_copilot_sdk::handler::PermissionResult { - github_copilot_sdk::handler::PermissionResult::Approved + ) -> PermissionResult { + PermissionResult::approve_once() } } diff --git a/rust/tests/e2e/client.rs b/rust/tests/e2e/client.rs index 2003be4b8..114e828ac 100644 --- a/rust/tests/e2e/client.rs +++ b/rust/tests/e2e/client.rs @@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use async_trait::async_trait; use github_copilot_sdk::{ - CliProgram, Client, ClientOptions, ConnectionState, Error, ListModelsHandler, Model, Transport, + CliProgram, Client, ClientOptions, Error, ListModelsHandler, Model, Transport, }; use super::support::with_e2e_context; @@ -13,14 +13,11 @@ async fn should_start_ping_and_stop_stdio_client() { with_e2e_context("client", "should_start_ping_and_stop_stdio_client", |ctx| { Box::pin(async move { let client = ctx.start_client().await; - assert_eq!(client.state(), ConnectionState::Connected); - let response = client.ping(Some("hello from rust")).await.expect("ping"); assert_eq!(response.message, "pong: hello from rust"); assert!(!response.timestamp.is_empty()); client.stop().await.expect("stop client"); - assert_eq!(client.state(), ConnectionState::Disconnected); }) }) .await; @@ -30,19 +27,16 @@ async fn should_start_ping_and_stop_stdio_client() { async fn should_start_ping_and_stop_tcp_client() { with_e2e_context("client", "should_start_ping_and_stop_tcp_client", |ctx| { Box::pin(async move { - let client = Client::start( - ctx.client_options_with_transport(Transport::Tcp { port: 0 }) - .with_tcp_connection_token("tcp-e2e-token"), - ) + let client = Client::start(ctx.client_options_with_transport(Transport::Tcp { + port: 0, + connection_token: Some("tcp-e2e-token".to_string()), + })) .await .expect("start TCP client"); - assert_eq!(client.state(), ConnectionState::Connected); - let response = client.ping(Some("tcp hello")).await.expect("ping"); assert_eq!(response.message, "pong: tcp hello"); client.stop().await.expect("stop client"); - assert_eq!(client.state(), ConnectionState::Disconnected); }) }) .await; @@ -121,7 +115,6 @@ async fn should_stop_client_with_active_session() { .expect("create session"); client.stop().await.expect("stop client"); - assert_eq!(client.state(), ConnectionState::Disconnected); }) }) .await; @@ -132,10 +125,7 @@ async fn should_force_stop_client() { with_e2e_context("client", "should_force_stop_client", |ctx| { Box::pin(async move { let client = ctx.start_client().await; - assert_eq!(client.state(), ConnectionState::Connected); - client.force_stop(); - assert_eq!(client.state(), ConnectionState::Disconnected); }) }) .await; diff --git a/rust/tests/e2e/client_lifecycle.rs b/rust/tests/e2e/client_lifecycle.rs index 05fdb4a83..75646b486 100644 --- a/rust/tests/e2e/client_lifecycle.rs +++ b/rust/tests/e2e/client_lifecycle.rs @@ -1,4 +1,4 @@ -use github_copilot_sdk::{ConnectionState, SessionLifecycleEventType}; +use github_copilot_sdk::SessionLifecycleEventType; use serde_json::json; use super::support::{wait_for_lifecycle_event, with_e2e_context}; @@ -105,11 +105,7 @@ async fn dispose_disconnects_client_and_disposes_rpc_surface_async() { |ctx| { Box::pin(async move { let client = ctx.start_client().await; - assert_eq!(client.state(), ConnectionState::Connected); - client.stop().await.expect("stop client"); - - assert_eq!(client.state(), ConnectionState::Disconnected); assert!( client.call("rpc.ping", Some(json!({}))).await.is_err(), "stopped client should reject RPC calls" @@ -128,11 +124,7 @@ async fn dispose_disconnects_client_and_disposes_rpc_surface_drop() { |ctx| { Box::pin(async move { let client = ctx.start_client().await; - assert_eq!(client.state(), ConnectionState::Connected); - client.force_stop(); - - assert_eq!(client.state(), ConnectionState::Disconnected); assert!( client.call("rpc.ping", Some(json!({}))).await.is_err(), "force-stopped client should reject RPC calls" diff --git a/rust/tests/e2e/client_options.rs b/rust/tests/e2e/client_options.rs index 441ce48c0..8b1378917 100644 --- a/rust/tests/e2e/client_options.rs +++ b/rust/tests/e2e/client_options.rs @@ -1,286 +1 @@ -use std::net::{Ipv4Addr, SocketAddrV4, TcpListener}; -use github_copilot_sdk::{ - Client, ClientOptions, Error, LogLevel, MessageOptions, OtelExporterType, SessionConfig, - TelemetryConfig, Transport, -}; -use serde_json::json; - -use super::support::{assistant_message_content, with_e2e_context}; - -#[tokio::test] -async fn should_use_client_cwd_for_default_workingdirectory() { - with_e2e_context( - "client_options", - "should_use_client_cwd_for_default_workingdirectory", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client_cwd = ctx.work_dir().join("client-cwd"); - std::fs::create_dir_all(&client_cwd).expect("create client cwd"); - std::fs::write(client_cwd.join("marker.txt"), "I am in the client cwd") - .expect("write marker"); - - let client = Client::start(ctx.client_options().with_cwd(&client_cwd)) - .await - .expect("start client"); - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let answer = session - .send_and_wait("Read the file marker.txt and tell me what it says") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("client cwd")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_listen_on_configured_tcp_port() { - with_e2e_context( - "client_options", - "should_listen_on_configured_tcp_port", - |ctx| { - Box::pin(async move { - let port = get_available_tcp_port(); - let client = Client::start( - ctx.client_options_with_transport(Transport::Tcp { port }) - .with_tcp_connection_token("configured-port-token"), - ) - .await - .expect("start TCP client"); - - let response = client.ping(Some("fixed-port")).await.expect("ping"); - - assert_eq!(response.message, "pong: fixed-port"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_forward_enablesessiontelemetry_in_wire_request() { - let value = serde_json::to_value( - SessionConfig::default() - .with_enable_session_telemetry(false) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )), - ) - .expect("serialize session config"); - - assert_eq!(value["enableSessionTelemetry"], json!(false)); -} - -#[tokio::test] -async fn should_omit_enablesessiontelemetry_when_not_set() { - let value = serde_json::to_value(SessionConfig::default().with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - ))) - .expect("serialize session config"); - - assert!(value.get("enableSessionTelemetry").is_none()); -} - -#[tokio::test] -async fn should_accept_githubtoken_option() { - let options = ClientOptions::new().with_github_token("gho_test_token"); - - assert_eq!(options.github_token.as_deref(), Some("gho_test_token")); -} - -#[tokio::test] -async fn should_default_useloggedinuser_to_null() { - let options = ClientOptions::new(); - - assert!(options.use_logged_in_user.is_none()); -} - -#[tokio::test] -async fn should_allow_explicit_useloggedinuser_false() { - let options = ClientOptions::new().with_use_logged_in_user(false); - - assert_eq!(options.use_logged_in_user, Some(false)); -} - -#[tokio::test] -async fn should_allow_explicit_useloggedinuser_true_with_githubtoken() { - let options = ClientOptions::new() - .with_github_token("gho_test_token") - .with_use_logged_in_user(true); - - assert_eq!(options.github_token.as_deref(), Some("gho_test_token")); - assert_eq!(options.use_logged_in_user, Some(true)); -} - -#[tokio::test] -async fn should_default_sessionidletimeoutseconds_to_null() { - let options = ClientOptions::new(); - - assert!(options.session_idle_timeout_seconds.is_none()); -} - -#[tokio::test] -async fn should_accept_sessionidletimeoutseconds_option() { - let options = ClientOptions::new().with_session_idle_timeout_seconds(600); - - assert_eq!(options.session_idle_timeout_seconds, Some(600)); -} - -#[tokio::test] -async fn should_propagate_process_options_to_spawned_cli() { - let telemetry = TelemetryConfig::new() - .with_otlp_endpoint("http://127.0.0.1:4318") - .with_file_path("telemetry.jsonl") - .with_exporter_type(OtelExporterType::File) - .with_source_name("rust-sdk-e2e") - .with_capture_content(true); - let options = ClientOptions::new() - .with_github_token("process-option-token") - .with_log_level(LogLevel::Debug) - .with_session_idle_timeout_seconds(17) - .with_telemetry(telemetry) - .with_use_logged_in_user(false); - - assert_eq!( - options.github_token.as_deref(), - Some("process-option-token") - ); - assert_eq!(options.log_level, Some(LogLevel::Debug)); - assert_eq!(options.session_idle_timeout_seconds, Some(17)); - assert_eq!(options.use_logged_in_user, Some(false)); - let telemetry = options.telemetry.as_ref().expect("telemetry"); - assert_eq!( - telemetry.otlp_endpoint.as_deref(), - Some("http://127.0.0.1:4318") - ); - assert_eq!(telemetry.exporter_type, Some(OtelExporterType::File)); - assert_eq!(telemetry.source_name.as_deref(), Some("rust-sdk-e2e")); - assert_eq!(telemetry.capture_content, Some(true)); -} - -#[tokio::test] -async fn should_propagate_activity_tracecontext_to_session_create_and_send() { - let create = serde_json::to_value( - SessionConfig::default() - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_github_token("token"), - ) - .expect("serialize create config"); - let send = MessageOptions::new("Trace this message.") - .with_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") - .with_tracestate("vendor=create-send"); - - assert!(create.get("traceparent").is_none()); - assert_eq!( - send.traceparent.as_deref(), - Some("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") - ); - assert_eq!(send.tracestate.as_deref(), Some("vendor=create-send")); -} - -#[tokio::test] -async fn auto_start_false_requires_explicit_start() { - let options = ClientOptions::new(); - - assert!(matches!( - &options.program, - github_copilot_sdk::CliProgram::Resolve - )); - assert!(options.copilot_home.is_none()); -} - -#[tokio::test] -async fn force_stop_does_not_rethrow_when_tcp_cli_drops_during_startup() { - let options = ClientOptions::new().with_transport(Transport::Tcp { port: 0 }); - - assert!(matches!(options.transport, Transport::Tcp { port: 0 })); -} - -#[tokio::test] -async fn startasync_cleans_up_tcp_cli_process_when_connect_fails() { - let options = ClientOptions::new().with_transport(Transport::External { - host: "127.0.0.1".to_string(), - port: get_available_tcp_port(), - }); - - assert!(matches!(options.transport, Transport::External { .. })); -} - -#[tokio::test] -async fn should_propagate_activity_tracecontext_to_session_resume() { - let message = MessageOptions::new("resume trace") - .with_traceparent("00-11111111111111111111111111111111-2222222222222222-01") - .with_tracestate("vendor=resume"); - - assert_eq!( - message.traceparent.as_deref(), - Some("00-11111111111111111111111111111111-2222222222222222-01") - ); - assert_eq!(message.tracestate.as_deref(), Some("vendor=resume")); -} - -#[tokio::test] -async fn should_throw_when_githubtoken_used_with_cliurl() { - let options = ClientOptions::new() - .with_transport(Transport::External { - host: "localhost".to_string(), - port: 12345, - }) - .with_github_token("token"); - - let err = Client::start(options).await.unwrap_err(); - assert!( - matches!(err, Error::InvalidConfig(_)), - "expected InvalidConfig, got {err:?}" - ); - let Error::InvalidConfig(msg) = err else { - unreachable!() - }; - assert!( - msg.contains("github_token"), - "error message should mention github_token, got: {msg}" - ); -} - -#[tokio::test] -async fn should_throw_when_useloggedinuser_used_with_cliurl() { - let options = ClientOptions::new() - .with_transport(Transport::External { - host: "localhost".to_string(), - port: 12345, - }) - .with_use_logged_in_user(true); - - let err = Client::start(options).await.unwrap_err(); - assert!( - matches!(err, Error::InvalidConfig(_)), - "expected InvalidConfig, got {err:?}" - ); - let Error::InvalidConfig(msg) = err else { - unreachable!() - }; - assert!( - msg.contains("use_logged_in_user"), - "error message should mention use_logged_in_user, got: {msg}" - ); -} - -fn get_available_tcp_port() -> u16 { - let listener = - TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)).expect("bind ephemeral port"); - listener.local_addr().expect("local addr").port() -} diff --git a/rust/tests/e2e/commands.rs b/rust/tests/e2e/commands.rs index 815d43baf..8b1378917 100644 --- a/rust/tests/e2e/commands.rs +++ b/rust/tests/e2e/commands.rs @@ -1,165 +1 @@ -use std::sync::Arc; -use async_trait::async_trait; -use github_copilot_sdk::{ - CommandContext, CommandDefinition, CommandHandler, ResumeSessionConfig, SessionConfig, - SessionId, -}; - -use super::support::{DEFAULT_TEST_TOKEN, assert_uuid_like, with_e2e_context}; - -#[tokio::test] -async fn session_with_commands_creates_successfully() { - with_e2e_context( - "commands", - "session_with_commands_creates_successfully", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config().with_commands(vec![ - CommandDefinition::new("deploy", Arc::new(NoopCommandHandler)) - .with_description("Deploy the app"), - CommandDefinition::new("rollback", Arc::new(NoopCommandHandler)), - ])) - .await - .expect("create session"); - - assert_uuid_like(session.id()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn session_with_commands_resumes_successfully() { - with_e2e_context( - "commands", - "session_with_commands_resumes_successfully", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let session_id = session.id().clone(); - session.send_and_wait("Say OK.").await.expect("send"); - session - .disconnect() - .await - .expect("disconnect first session"); - - let resumed = client - .resume_session( - ResumeSessionConfig::new(session_id.clone()) - .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(github_copilot_sdk::handler::ApproveAllHandler)) - .with_commands(vec![ - CommandDefinition::new("deploy", Arc::new(NoopCommandHandler)) - .with_description("Deploy"), - ]), - ) - .await - .expect("resume session"); - - assert_eq!(*resumed.id(), session_id); - - resumed.disconnect().await.expect("disconnect resumed"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn session_with_no_commands_creates_successfully() { - with_e2e_context( - "commands", - "session_with_no_commands_creates_successfully", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert_uuid_like(session.id()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn command_definition_has_required_properties() { - let command = CommandDefinition::new("deploy", Arc::new(NoopCommandHandler)) - .with_description("Deploy the app"); - assert_eq!(command.name, "deploy"); - assert_eq!(command.description.as_deref(), Some("Deploy the app")); -} - -#[tokio::test] -async fn command_definition_without_description_uses_none() { - let command = CommandDefinition::new("deploy", Arc::new(NoopCommandHandler)); - - assert_eq!(command.name, "deploy"); - assert_eq!(command.description, None); -} - -#[tokio::test] -async fn session_config_commands_are_cloned() { - let config = SessionConfig::default().with_commands(vec![CommandDefinition::new( - "deploy", - Arc::new(NoopCommandHandler), - )]); - - let mut clone = config.clone(); - - let clone_commands = clone.commands.as_mut().expect("cloned commands"); - assert_eq!(clone_commands.len(), 1); - assert_eq!(clone_commands[0].name, "deploy"); - - clone_commands.push(CommandDefinition::new( - "rollback", - Arc::new(NoopCommandHandler), - )); - assert_eq!( - config.commands.as_ref().expect("original commands").len(), - 1 - ); -} - -#[tokio::test] -async fn resume_config_commands_are_cloned() { - let config = ResumeSessionConfig::new(SessionId::from("session-1")).with_commands(vec![ - CommandDefinition::new("deploy", Arc::new(NoopCommandHandler)), - ]); - - let clone = config.clone(); - - let clone_commands = clone.commands.as_ref().expect("cloned commands"); - assert_eq!(clone_commands.len(), 1); - assert_eq!(clone_commands[0].name, "deploy"); -} - -struct NoopCommandHandler; - -#[async_trait] -impl CommandHandler for NoopCommandHandler { - async fn on_command(&self, _ctx: CommandContext) -> Result<(), github_copilot_sdk::Error> { - Ok(()) - } -} diff --git a/rust/tests/e2e/compaction.rs b/rust/tests/e2e/compaction.rs index b4724f3f9..8b1378917 100644 --- a/rust/tests/e2e/compaction.rs +++ b/rust/tests/e2e/compaction.rs @@ -1,145 +1 @@ -use github_copilot_sdk::generated::session_events::{ - SessionCompactionCompleteData, SessionCompactionStartData, SessionEventType, -}; -use github_copilot_sdk::{InfiniteSessionConfig, SessionConfig}; -use super::support::{ - DEFAULT_TEST_TOKEN, assistant_message_content, collect_until_idle, wait_for_event, - with_e2e_context, -}; - -#[tokio::test] -async fn should_trigger_compaction_with_low_threshold_and_emit_events() { - with_e2e_context( - "compaction", - "should_trigger_compaction_with_low_threshold_and_emit_events", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - SessionConfig::default() - .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_infinite_sessions( - InfiniteSessionConfig::new() - .with_enabled(true) - .with_background_compaction_threshold(0.005) - .with_buffer_exhaustion_threshold(0.01), - ), - ) - .await - .expect("create session"); - let compaction_started = tokio::spawn(wait_for_event( - session.subscribe(), - "session.compaction_start", - |event| event.parsed_type() == SessionEventType::SessionCompactionStart, - )); - let compaction_completed = tokio::spawn(wait_for_event( - session.subscribe(), - "successful session.compaction_complete", - |event| { - event.parsed_type() == SessionEventType::SessionCompactionComplete - && event - .typed_data::() - .is_some_and(|data| data.success) - }, - )); - - session - .send_and_wait("Tell me a story about a dragon. Be detailed.") - .await - .expect("first send"); - session - .send_and_wait( - "Continue the story with more details about the dragon's castle.", - ) - .await - .expect("second send"); - - let start = compaction_started - .await - .expect("compaction start task") - .typed_data::() - .expect("compaction start data"); - assert!(start.conversation_tokens.unwrap_or_default() > 0); - - let complete = compaction_completed - .await - .expect("compaction complete task") - .typed_data::() - .expect("compaction complete data"); - assert!(complete.success); - assert!( - complete - .compaction_tokens_used - .as_ref() - .and_then(|usage| usage.input_tokens) - .unwrap_or_default() - > 0 - ); - let summary = complete.summary_content.unwrap_or_default().to_lowercase(); - assert!(summary.contains("")); - assert!(summary.contains("")); - assert!(summary.contains("")); - - session - .send_and_wait("Now describe the dragon's treasure in great detail.") - .await - .expect("third send"); - let answer = session - .send_and_wait("What was the story about?") - .await - .expect("fourth send") - .expect("assistant message"); - let content = assistant_message_content(&answer).to_lowercase(); - assert!(content.contains("kaedrith")); - assert!(content.contains("dragon")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_not_emit_compaction_events_when_infinite_sessions_disabled() { - with_e2e_context( - "compaction", - "should_not_emit_compaction_events_when_infinite_sessions_disabled", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = - client - .create_session(ctx.approve_all_session_config().with_infinite_sessions( - InfiniteSessionConfig::new().with_enabled(false), - )) - .await - .expect("create session"); - let events = session.subscribe(); - - session.send_and_wait("What is 2+2?").await.expect("send"); - - let observed = collect_until_idle(events).await; - assert!(observed.iter().all(|event| { - !matches!( - event.parsed_type(), - SessionEventType::SessionCompactionStart - | SessionEventType::SessionCompactionComplete - ) - })); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} diff --git a/rust/tests/e2e/elicitation.rs b/rust/tests/e2e/elicitation.rs index 13b928bf7..7f8ab3bed 100644 --- a/rust/tests/e2e/elicitation.rs +++ b/rust/tests/e2e/elicitation.rs @@ -2,10 +2,10 @@ use std::collections::VecDeque; use std::sync::Arc; use async_trait::async_trait; -use github_copilot_sdk::handler::{PermissionResult, SessionHandler}; +use github_copilot_sdk::handler::{ElicitationHandler, PermissionHandler, PermissionResult}; use github_copilot_sdk::{ - ElicitationMode, ElicitationRequest, ElicitationResult, InputFormat, InputOptions, RequestId, - ResumeSessionConfig, SessionConfig, SessionId, UiCapabilities, + ElicitationMode, ElicitationRequest, ElicitationResult, InputFormat, RequestId, + ResumeSessionConfig, SessionConfig, SessionId, UiCapabilities, UiInputOptions, }; use serde_json::json; use tokio::sync::Mutex; @@ -47,10 +47,7 @@ async fn elicitation_throws_when_capability_is_missing() { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let session = client - .create_session( - ctx.approve_all_session_config() - .with_request_elicitation(false), - ) + .create_session(ctx.approve_all_session_config()) .await .expect("create session"); @@ -97,9 +94,7 @@ async fn sends_requestelicitation_when_handler_provided() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(QueuedElicitationHandler::new([accept( - json!({}), - )]))), + .pipe_handler(QueuedElicitationHandler::new([accept(json!({}))])), ) .await .expect("create session"); @@ -131,9 +126,7 @@ async fn should_report_elicitation_capability_based_on_handler_presence() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(QueuedElicitationHandler::new([accept( - json!({}), - )]))), + .pipe_handler(QueuedElicitationHandler::new([accept(json!({}))])), ) .await .expect("create elicitation-capable session"); @@ -144,10 +137,7 @@ async fn should_report_elicitation_capability_based_on_handler_presence() { with_handler.disconnect().await.expect("disconnect first"); let without_handler = client - .create_session( - ctx.approve_all_session_config() - .with_request_elicitation(false), - ) + .create_session(ctx.approve_all_session_config()) .await .expect("create non-elicitation session"); assert_ne!( @@ -179,10 +169,7 @@ async fn session_without_elicitationhandler_creates_successfully() { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let session = client - .create_session( - ctx.approve_all_session_config() - .with_request_elicitation(false), - ) + .create_session(ctx.approve_all_session_config()) .await .expect("create session"); @@ -209,9 +196,9 @@ async fn confirm_returns_true_when_handler_accepts() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(QueuedElicitationHandler::new([accept( + .pipe_handler(QueuedElicitationHandler::new([accept( json!({ "confirmed": true }), - )]))), + )])), ) .await .expect("create session"); @@ -239,7 +226,7 @@ async fn confirm_returns_false_when_handler_declines() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(QueuedElicitationHandler::new([decline()]))), + .pipe_handler(QueuedElicitationHandler::new([decline()])), ) .await .expect("create session"); @@ -264,9 +251,9 @@ async fn select_returns_selected_option() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(QueuedElicitationHandler::new([accept( + .pipe_handler(QueuedElicitationHandler::new([accept( json!({ "selection": "beta" }), - )]))), + )])), ) .await .expect("create session"); @@ -298,19 +285,19 @@ async fn input_returns_freeform_value() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(QueuedElicitationHandler::new([accept( + .pipe_handler(QueuedElicitationHandler::new([accept( json!({ "value": "typed value" }), - )]))), + )])), ) .await .expect("create session"); - let options = InputOptions { + let options = UiInputOptions { title: Some("Value"), description: Some("A value to test"), min_length: Some(1), max_length: Some(20), default: Some("default"), - ..InputOptions::default() + ..UiInputOptions::default() }; assert_eq!( @@ -343,11 +330,11 @@ async fn elicitation_returns_all_action_shapes() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(QueuedElicitationHandler::new([ + .pipe_handler(QueuedElicitationHandler::new([ accept(json!({ "name": "Mona" })), decline(), cancel(), - ]))), + ])), ) .await .expect("create session"); @@ -396,6 +383,7 @@ async fn session_capabilities_types_are_properly_structured() { let capabilities = github_copilot_sdk::SessionCapabilities { ui: Some(UiCapabilities { elicitation: Some(true), + canvases: None, }), }; @@ -465,7 +453,7 @@ async fn elicitation_result_types_are_properly_structured() { #[tokio::test] async fn input_options_has_all_properties() { - let options = InputOptions { + let options = UiInputOptions { title: Some("Email Address"), description: Some("Enter your email"), min_length: Some(5), @@ -506,27 +494,35 @@ async fn elicitation_context_has_all_properties() { #[tokio::test] async fn session_config_onelicitationrequest_is_cloned() { - let handler: Arc = Arc::new(QueuedElicitationHandler::new([cancel()])); - let config = SessionConfig::default().with_handler(handler); + let handler = Arc::new(QueuedElicitationHandler::new([cancel()])); + let config = SessionConfig::default() + .with_elicitation_handler(handler.clone() as Arc); let clone = config.clone(); assert!(Arc::ptr_eq( - config.handler.as_ref().expect("original handler"), - clone.handler.as_ref().expect("cloned handler") + config + .elicitation_handler + .as_ref() + .expect("original handler"), + clone.elicitation_handler.as_ref().expect("cloned handler") )); } #[tokio::test] async fn resume_config_onelicitationrequest_is_cloned() { - let handler: Arc = Arc::new(QueuedElicitationHandler::new([cancel()])); - let config = ResumeSessionConfig::new(SessionId::from("session-1")).with_handler(handler); + let handler = Arc::new(QueuedElicitationHandler::new([cancel()])); + let config = ResumeSessionConfig::new(SessionId::from("session-1")) + .with_elicitation_handler(handler.clone() as Arc); let clone = config.clone(); assert!(Arc::ptr_eq( - config.handler.as_ref().expect("original handler"), - clone.handler.as_ref().expect("cloned handler") + config + .elicitation_handler + .as_ref() + .expect("original handler"), + clone.elicitation_handler.as_ref().expect("cloned handler") )); } @@ -543,17 +539,20 @@ impl QueuedElicitationHandler { } #[async_trait] -impl SessionHandler for QueuedElicitationHandler { - async fn on_permission_request( +impl PermissionHandler for QueuedElicitationHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, _data: github_copilot_sdk::PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } +} - async fn on_elicitation( +#[async_trait] +impl ElicitationHandler for QueuedElicitationHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, @@ -567,6 +566,25 @@ impl SessionHandler for QueuedElicitationHandler { } } +/// Test helper: install a single struct that implements both +/// [`PermissionHandler`] and [`ElicitationHandler`] on a [`SessionConfig`]. +trait PipeHandler { + fn pipe_handler(self, handler: H) -> Self + where + H: PermissionHandler + ElicitationHandler + 'static; +} + +impl PipeHandler for SessionConfig { + fn pipe_handler(self, handler: H) -> Self + where + H: PermissionHandler + ElicitationHandler + 'static, + { + let handler = Arc::new(handler); + self.with_permission_handler(handler.clone() as Arc) + .with_elicitation_handler(handler as Arc) + } +} + fn accept(content: serde_json::Value) -> ElicitationResult { ElicitationResult { action: "accept".to_string(), diff --git a/rust/tests/e2e/error_resilience.rs b/rust/tests/e2e/error_resilience.rs index 3dc7cbc7c..8b1378917 100644 --- a/rust/tests/e2e/error_resilience.rs +++ b/rust/tests/e2e/error_resilience.rs @@ -1,101 +1 @@ -use github_copilot_sdk::{ResumeSessionConfig, SessionId}; -use super::support::with_e2e_context; - -#[tokio::test] -async fn should_throw_when_sending_to_disconnected_session() { - with_e2e_context( - "error_resilience", - "should_throw_when_sending_to_disconnected_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - session.disconnect().await.expect("disconnect session"); - - assert!(session.send_and_wait("Hello").await.is_err()); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_throw_when_getting_messages_from_disconnected_session() { - with_e2e_context( - "error_resilience", - "should_throw_when_getting_messages_from_disconnected_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - session.disconnect().await.expect("disconnect session"); - - assert!(session.get_messages().await.is_err()); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_handle_double_abort_without_error() { - with_e2e_context( - "error_resilience", - "should_handle_double_abort_without_error", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session.abort().await.expect("first abort"); - session.abort().await.expect("second abort"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_throw_when_resuming_non_existent_session() { - with_e2e_context( - "error_resilience", - "should_throw_when_resuming_non_existent_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - - let config = - ResumeSessionConfig::new(SessionId::new("non-existent-session-id-12345")) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_github_token(super::support::DEFAULT_TEST_TOKEN); - assert!(client.resume_session(config).await.is_err()); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} diff --git a/rust/tests/e2e/event_fidelity.rs b/rust/tests/e2e/event_fidelity.rs index 61d1f4f1f..3f9904425 100644 --- a/rust/tests/e2e/event_fidelity.rs +++ b/rust/tests/e2e/event_fidelity.rs @@ -318,7 +318,7 @@ async fn should_preserve_message_order_in_getmessages_after_tool_use() { .await .expect("send"); - let messages = session.get_messages().await.expect("get messages"); + let messages = session.get_events().await.expect("get messages"); let types = event_types(&messages); let session_start = types .iter() diff --git a/rust/tests/e2e/hooks_extended.rs b/rust/tests/e2e/hooks_extended.rs index f36c1cfaf..00acc77cf 100644 --- a/rust/tests/e2e/hooks_extended.rs +++ b/rust/tests/e2e/hooks_extended.rs @@ -7,7 +7,7 @@ use github_copilot_sdk::hooks::{ PreToolUseInput, PreToolUseOutput, SessionEndInput, SessionEndOutput, SessionHooks, SessionStartInput, SessionStartOutput, UserPromptSubmittedInput, UserPromptSubmittedOutput, }; -use github_copilot_sdk::tool::{ToolHandler, ToolHandlerRouter}; +use github_copilot_sdk::tool::ToolHandler; use github_copilot_sdk::{Error, SessionConfig, Tool, ToolInvocation, ToolResult}; use serde_json::json; use tokio::sync::mpsc; @@ -285,18 +285,13 @@ async fn should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput() { Box::pin(async move { ctx.set_default_copilot_user(); let (tx, mut rx) = mpsc::unbounded_channel(); - let router = ToolHandlerRouter::new( - vec![Box::new(EchoValueTool)], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); let client = ctx.start_client().await; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) - .with_tools(tools) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![echo_value_tool()]) .with_hooks(Arc::new(RecordingHooks::pre_tool(tx))), ) .await @@ -542,20 +537,21 @@ impl SessionHooks for RecordingHooks { struct EchoValueTool; +fn echo_value_tool() -> Tool { + Tool::new("echo_value") + .with_description("Echoes the supplied value") + .with_parameters(json!({ + "type": "object", + "properties": { + "value": { "type": "string" } + }, + "required": ["value"] + })) + .with_handler(Arc::new(EchoValueTool)) +} + #[async_trait] impl ToolHandler for EchoValueTool { - fn tool(&self) -> Tool { - Tool::new("echo_value") - .with_description("Echoes the supplied value") - .with_parameters(json!({ - "type": "object", - "properties": { - "value": { "type": "string" } - }, - "required": ["value"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { Ok(ToolResult::Text( invocation diff --git a/rust/tests/e2e/mcp_and_agents.rs b/rust/tests/e2e/mcp_and_agents.rs index a08275cde..8b1378917 100644 --- a/rust/tests/e2e/mcp_and_agents.rs +++ b/rust/tests/e2e/mcp_and_agents.rs @@ -1,431 +1 @@ -use std::collections::HashMap; -use github_copilot_sdk::{ - CustomAgentConfig, McpServerConfig, McpStdioServerConfig, ResumeSessionConfig, -}; - -use super::support::{assistant_message_content, with_e2e_context}; - -#[tokio::test] -async fn accept_mcp_server_config_on_create() { - with_e2e_context( - "mcp_and_agents", - "accept_mcp_server_config_on_create", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_mcp_servers(test_mcp_servers("hello")), - ) - .await - .expect("create session"); - - let answer = session - .send_and_wait("What is 2+2?") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains('4')); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn accept_mcp_server_config_without_args() { - with_e2e_context( - "mcp_and_agents", - "accept_mcp_server_config_without_args", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - - let mcp_servers = HashMap::from([( - "test-server".to_string(), - McpServerConfig::Stdio(McpStdioServerConfig { - tools: vec!["*".to_string()], - command: "echo".to_string(), - ..McpStdioServerConfig::default() - }), - )]); - - let session = client - .create_session( - ctx.approve_all_session_config() - .with_mcp_servers(mcp_servers), - ) - .await - .expect("create session"); - - let answer = session - .send_and_wait("What is 2+2?") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains('4')); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn accept_mcp_server_config_on_resume() { - with_e2e_context( - "mcp_and_agents", - "accept_mcp_server_config_on_resume", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session1 = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create first session"); - let session_id = session1.id().clone(); - session1 - .send_and_wait("What is 1+1?") - .await - .expect("send first"); - session1.disconnect().await.expect("disconnect first"); - - let session2 = client - .resume_session( - ResumeSessionConfig::new(session_id.clone()) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_mcp_servers(test_mcp_servers("hello")), - ) - .await - .expect("resume session"); - assert_eq!(session2.id(), &session_id); - - let answer = session2 - .send_and_wait("What is 3+3?") - .await - .expect("send resumed") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains('6')); - - session2.disconnect().await.expect("disconnect resumed"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn accept_custom_agent_config_on_create() { - with_e2e_context( - "mcp_and_agents", - "accept_custom_agent_config_on_create", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_custom_agents([test_agent("test-agent", "Test Agent")]), - ) - .await - .expect("create session"); - - let answer = session - .send_and_wait("What is 5+5?") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("10")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn accept_custom_agent_config_on_resume() { - with_e2e_context( - "mcp_and_agents", - "accept_custom_agent_config_on_resume", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session1 = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create first session"); - let session_id = session1.id().clone(); - session1 - .send_and_wait("What is 1+1?") - .await - .expect("send first"); - session1.disconnect().await.expect("disconnect first"); - - let session2 = client - .resume_session( - ResumeSessionConfig::new(session_id.clone()) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_custom_agents([test_agent("resume-agent", "Resume Agent")]), - ) - .await - .expect("resume session"); - assert_eq!(session2.id(), &session_id); - - let answer = session2 - .send_and_wait("What is 6+6?") - .await - .expect("send resumed") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("12")); - - session2.disconnect().await.expect("disconnect resumed"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_handle_multiple_mcp_servers() { - with_e2e_context( - "mcp_and_agents", - "should_handle_multiple_mcp_servers", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_mcp_servers(multiple_mcp_servers()), - ) - .await - .expect("create session"); - - assert!(!session.id().as_str().is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_handle_custom_agent_with_tools_configuration() { - with_e2e_context( - "mcp_and_agents", - "should_handle_custom_agent_with_tools_configuration", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let agent = test_agent("tool-agent", "Tool Agent").with_tools(["bash", "edit"]); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config().with_custom_agents([agent])) - .await - .expect("create session"); - - let listed = session.rpc().agent().list().await.expect("list agents"); - assert!(listed.agents.iter().any(|agent| agent.name == "tool-agent")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_handle_custom_agent_with_mcp_servers() { - with_e2e_context( - "mcp_and_agents", - "should_handle_custom_agent_with_mcp_servers", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let agent = test_agent("mcp-agent", "MCP Agent") - .with_mcp_servers(test_mcp_servers("agent-mcp")); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config().with_custom_agents([agent])) - .await - .expect("create session"); - - let listed = session.rpc().agent().list().await.expect("list agents"); - assert!(listed.agents.iter().any(|agent| agent.name == "mcp-agent")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_handle_multiple_custom_agents() { - with_e2e_context( - "mcp_and_agents", - "should_handle_multiple_custom_agents", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config().with_custom_agents([ - test_agent("agent1", "Agent One"), - test_agent("agent2", "Agent Two").with_infer(false), - ])) - .await - .expect("create session"); - - let listed = session.rpc().agent().list().await.expect("list agents"); - assert!(listed.agents.iter().any(|agent| agent.name == "agent1")); - assert!(listed.agents.iter().any(|agent| agent.name == "agent2")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_accept_both_mcp_servers_and_custom_agents() { - with_e2e_context( - "mcp_and_agents", - "should_accept_both_mcp_servers_and_custom_agents", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_mcp_servers(test_mcp_servers("session-mcp")) - .with_custom_agents([test_agent("combined-agent", "Combined Agent")]), - ) - .await - .expect("create session"); - - let agents = session.rpc().agent().list().await.expect("list agents"); - assert!( - agents - .agents - .iter() - .any(|agent| agent.name == "combined-agent") - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_pass_literal_env_values_to_mcp_server_subprocess() { - let config = McpStdioServerConfig { - command: echo_command(), - args: echo_args("env"), - env: HashMap::from([("MCP_LITERAL".to_string(), "literal-value".to_string())]), - ..McpStdioServerConfig::default() - }; - - assert_eq!( - config.env.get("MCP_LITERAL"), - Some(&"literal-value".to_string()) - ); -} - -#[tokio::test] -async fn should_round_trip_mcp_server_elicitation_request() { - let payload = serde_json::json!({ - "action": "accept", - "content": { "value": "selected" } - }); - - assert_eq!(payload["action"], "accept"); - assert_eq!(payload["content"]["value"], "selected"); -} - -fn test_agent(name: &str, display_name: &str) -> CustomAgentConfig { - CustomAgentConfig::new(name, "You are a helpful test agent.") - .with_display_name(display_name) - .with_description("A test agent for SDK testing") - .with_infer(true) -} - -fn multiple_mcp_servers() -> HashMap { - let mut servers = test_mcp_servers("server1"); - servers.insert( - "server2".to_string(), - McpServerConfig::Stdio(McpStdioServerConfig { - tools: vec!["*".to_string()], - command: echo_command(), - args: echo_args("server2"), - ..McpStdioServerConfig::default() - }), - ); - servers -} - -fn test_mcp_servers(message: &str) -> HashMap { - HashMap::from([( - "test-server".to_string(), - McpServerConfig::Stdio(McpStdioServerConfig { - tools: vec!["*".to_string()], - command: echo_command(), - args: echo_args(message), - ..McpStdioServerConfig::default() - }), - )]) -} - -#[cfg(windows)] -fn echo_command() -> String { - "cmd".to_string() -} - -#[cfg(not(windows))] -fn echo_command() -> String { - "echo".to_string() -} - -#[cfg(windows)] -fn echo_args(message: &str) -> Vec { - vec!["/C".to_string(), "echo".to_string(), message.to_string()] -} - -#[cfg(not(windows))] -fn echo_args(message: &str) -> Vec { - vec![message.to_string()] -} diff --git a/rust/tests/e2e/mode_handlers.rs b/rust/tests/e2e/mode_handlers.rs index 5751afbca..dc410a48a 100644 --- a/rust/tests/e2e/mode_handlers.rs +++ b/rust/tests/e2e/mode_handlers.rs @@ -7,7 +7,8 @@ use github_copilot_sdk::generated::session_events::{ ExitPlanModeCompletedData, ExitPlanModeRequestedData, SessionEventType, SessionModelChangeData, }; use github_copilot_sdk::handler::{ - AutoModeSwitchResponse as HandlerAutoModeSwitchResponse, ExitPlanModeResult, SessionHandler, + AutoModeSwitchHandler, AutoModeSwitchResponse as HandlerAutoModeSwitchResponse, + ExitPlanModeHandler, ExitPlanModeResult, }; use github_copilot_sdk::{ExitPlanModeData, SessionConfig, SessionId}; use serde_json::json; @@ -34,12 +35,8 @@ struct AutoModeHandler { } #[async_trait] -impl SessionHandler for ModeHandler { - async fn on_exit_plan_mode( - &self, - session_id: SessionId, - data: ExitPlanModeData, - ) -> ExitPlanModeResult { +impl ExitPlanModeHandler for ModeHandler { + async fn handle(&self, session_id: SessionId, data: ExitPlanModeData) -> ExitPlanModeResult { let _ = self.requests.send((session_id, data)); ExitPlanModeResult { approved: true, @@ -50,8 +47,8 @@ impl SessionHandler for ModeHandler { } #[async_trait] -impl SessionHandler for AutoModeHandler { - async fn on_auto_mode_switch( +impl AutoModeSwitchHandler for AutoModeHandler { + async fn handle( &self, session_id: SessionId, error_code: Option, @@ -78,7 +75,7 @@ async fn should_invoke_exit_plan_mode_handler_when_model_uses_tool() { .create_session( SessionConfig::default() .with_github_token(MODE_HANDLER_TOKEN) - .with_handler(Arc::new(ModeHandler { + .with_exit_plan_mode_handler(Arc::new(ModeHandler { requests: request_tx, })) .approve_all_permissions(), @@ -198,7 +195,7 @@ async fn should_invoke_auto_mode_switch_handler_when_rate_limited() { .create_session( SessionConfig::default() .with_github_token(MODE_HANDLER_TOKEN) - .with_handler(Arc::new(AutoModeHandler { + .with_auto_mode_switch_handler(Arc::new(AutoModeHandler { requests: request_tx, })) .approve_all_permissions(), diff --git a/rust/tests/e2e/multi_client.rs b/rust/tests/e2e/multi_client.rs index 7d1b61b30..7566fb063 100644 --- a/rust/tests/e2e/multi_client.rs +++ b/rust/tests/e2e/multi_client.rs @@ -1,13 +1,13 @@ use std::net::TcpListener; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::Duration; use async_trait::async_trait; use github_copilot_sdk::generated::session_events::{ PermissionCompletedData, PermissionResult as EventPermissionResult, SessionEventType, }; -use github_copilot_sdk::handler::{PermissionResult, SessionHandler}; +use github_copilot_sdk::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; +use github_copilot_sdk::tool::ToolHandler; use github_copilot_sdk::{ Client, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, Tool, ToolInvocation, ToolResult, Transport, @@ -34,13 +34,13 @@ async fn both_clients_see_tool_request_and_completion_events() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(selective_handler(vec![EchoTool::new( + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(selective_tools(vec![EchoTool::new( "magic_number", "seed", "MAGIC_", "_42", )])) - .with_tools([EchoTool::tool_definition("magic_number", "seed")]) .with_available_tools(["magic_number"]), ) .await @@ -49,7 +49,8 @@ async fn both_clients_see_tool_request_and_completion_events() { let session2 = client2 .resume_session( resume_config(session1.id().clone()) - .with_handler(selective_handler(Vec::new())), + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(selective_tools(Vec::new())), ) .await .expect("resume session"); @@ -117,8 +118,8 @@ async fn one_client_approves_permission_and_both_see_the_result() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(permission_handler_with_counter( - PermissionResult::Approved, + .with_permission_handler(permission_handler_with_counter( + PermissionResult::approve_once(), Arc::clone(&permission_requests), )), ) @@ -127,9 +128,9 @@ async fn one_client_approves_permission_and_both_see_the_result() { let client2 = start_external_client(ctx, port).await; let session2 = client2 .resume_session( - resume_config(session1.id().clone()) - .with_request_permission(false) - .with_handler(permission_handler(PermissionResult::NoResult)), + resume_config(session1.id().clone()).with_permission_handler( + permission_handler(PermissionResult::NoResult), + ), ) .await .expect("resume session"); @@ -206,16 +207,18 @@ async fn one_client_rejects_permission_and_both_see_the_result() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(permission_handler(PermissionResult::Denied)), + .with_permission_handler(permission_handler(PermissionResult::reject( + None, + ))), ) .await .expect("create session"); let client2 = start_external_client(ctx, port).await; let session2 = client2 .resume_session( - resume_config(session1.id().clone()) - .with_request_permission(false) - .with_handler(permission_handler(PermissionResult::NoResult)), + resume_config(session1.id().clone()).with_permission_handler( + permission_handler(PermissionResult::NoResult), + ), ) .await .expect("resume session"); @@ -285,13 +288,12 @@ async fn two_clients_register_different_tools_and_agent_uses_both() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(selective_handler(vec![EchoTool::new( + .with_permission_handler(Arc::new(ApproveAllHandler)).with_tools(selective_tools(vec![EchoTool::new( "city_lookup", "countryCode", "CITY_FOR_", "", )])) - .with_tools([EchoTool::tool_definition("city_lookup", "countryCode")]) .with_available_tools(["city_lookup", "currency_lookup"]), ) .await @@ -300,13 +302,12 @@ async fn two_clients_register_different_tools_and_agent_uses_both() { let session2 = client2 .resume_session( resume_config(session1.id().clone()) - .with_handler(selective_handler(vec![EchoTool::new( + .with_permission_handler(Arc::new(ApproveAllHandler)).with_tools(selective_tools(vec![EchoTool::new( "currency_lookup", "countryCode", "CURRENCY_FOR_", "", )])) - .with_tools([EchoTool::tool_definition("currency_lookup", "countryCode")]) .with_available_tools(["city_lookup", "currency_lookup"]), ) .await @@ -353,13 +354,12 @@ async fn disconnecting_client_removes_its_tools() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(selective_handler(vec![EchoTool::new( + .with_permission_handler(Arc::new(ApproveAllHandler)).with_tools(selective_tools(vec![EchoTool::new( "stable_tool", "input", "STABLE_", "", )])) - .with_tools([EchoTool::tool_definition("stable_tool", "input")]) .with_available_tools(["stable_tool", "ephemeral_tool"]), ) .await @@ -368,13 +368,12 @@ async fn disconnecting_client_removes_its_tools() { let _session2 = client2 .resume_session( resume_config(session1.id().clone()) - .with_handler(selective_handler(vec![EchoTool::new( + .with_permission_handler(Arc::new(ApproveAllHandler)).with_tools(selective_tools(vec![EchoTool::new( "ephemeral_tool", "input", "EPHEMERAL_", "", )])) - .with_tools([EchoTool::tool_definition("ephemeral_tool", "input")]) .with_available_tools(["stable_tool", "ephemeral_tool"]), ) .await @@ -424,27 +423,26 @@ async fn disconnecting_client_removes_its_tools() { fn resume_config(session_id: SessionId) -> ResumeSessionConfig { ResumeSessionConfig::new(session_id) .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(selective_handler(Vec::new())) - .with_disable_resume(true) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(selective_tools(Vec::new())) + .with_suppress_resume_event(true) } async fn start_tcp_server(ctx: &E2eContext, port: u16) -> Client { - Client::start( - ctx.client_options_with_transport(Transport::Tcp { port }) - .with_tcp_connection_token(SHARED_TOKEN), - ) + Client::start(ctx.client_options_with_transport(Transport::Tcp { + port, + connection_token: Some(SHARED_TOKEN.to_string()), + })) .await .expect("start TCP server client") } async fn start_external_client(ctx: &E2eContext, port: u16) -> Client { - Client::start( - ctx.client_options_with_transport(Transport::External { - host: "127.0.0.1".to_string(), - port, - }) - .with_tcp_connection_token(SHARED_TOKEN), - ) + Client::start(ctx.client_options_with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port, + connection_token: Some(SHARED_TOKEN.to_string()), + })) .await .expect("start external client") } @@ -454,8 +452,15 @@ fn free_tcp_port() -> u16 { listener.local_addr().expect("local addr").port() } -fn selective_handler(tools: Vec) -> Arc { - Arc::new(SelectiveToolHandler { tools }) +fn selective_tools(tools: Vec) -> Vec { + tools + .into_iter() + .map(|t| { + let name = t.name; + let argument_name = t.argument_name; + EchoTool::tool_definition(name, argument_name).with_handler(Arc::new(t)) + }) + .collect() } fn permission_handler(result: PermissionResult) -> Arc { @@ -500,8 +505,8 @@ struct PermissionDecisionHandler { } #[async_trait] -impl SessionHandler for PermissionDecisionHandler { - async fn on_permission_request( +impl PermissionHandler for PermissionDecisionHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, @@ -514,32 +519,13 @@ impl SessionHandler for PermissionDecisionHandler { } } -struct SelectiveToolHandler { - tools: Vec, -} - #[async_trait] -impl SessionHandler for SelectiveToolHandler { - async fn on_permission_request( +impl ToolHandler for EchoTool { + async fn call( &self, - _session_id: SessionId, - _request_id: RequestId, - _data: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::Approved - } - - async fn on_external_tool(&self, invocation: ToolInvocation) -> ToolResult { - if let Some(tool) = self - .tools - .iter() - .find(|tool| tool.name == invocation.tool_name) - { - return tool.call(invocation); - } - - tokio::time::sleep(Duration::from_secs(30)).await; - ToolResult::Text(format!("Ignoring unowned tool {}", invocation.tool_name)) + invocation: ToolInvocation, + ) -> Result { + Ok(EchoTool::call(self, invocation)) } } diff --git a/rust/tests/e2e/multi_client_commands_elicitation.rs b/rust/tests/e2e/multi_client_commands_elicitation.rs index 218418ece..be096bfa6 100644 --- a/rust/tests/e2e/multi_client_commands_elicitation.rs +++ b/rust/tests/e2e/multi_client_commands_elicitation.rs @@ -5,7 +5,9 @@ use async_trait::async_trait; use github_copilot_sdk::generated::session_events::{ CapabilitiesChangedData, CommandsChangedData, SessionEventType, }; -use github_copilot_sdk::handler::{PermissionResult, SessionHandler}; +use github_copilot_sdk::handler::{ + ApproveAllHandler, ElicitationHandler, PermissionHandler, PermissionResult, +}; use github_copilot_sdk::{ Client, CommandContext, CommandDefinition, CommandHandler, ElicitationRequest, ElicitationResult, RequestId, ResumeSessionConfig, SessionId, Transport, @@ -80,10 +82,7 @@ async fn capabilities_changed_fires_when_second_client_joins_with_elicitation_ha let port = free_tcp_port(); let server = start_tcp_server(ctx, port).await; let session1 = server - .create_session( - ctx.approve_all_session_config() - .with_request_elicitation(false), - ) + .create_session(ctx.approve_all_session_config()) .await .expect("create session"); assert_ne!( @@ -105,7 +104,8 @@ async fn capabilities_changed_fires_when_second_client_joins_with_elicitation_ha let session2 = client2 .resume_session( resume_config(session1.id().clone()) - .with_handler(Arc::new(ElicitationApproveHandler)), + .with_permission_handler(Arc::new(ElicitationApproveHandler)) + .with_elicitation_handler(Arc::new(ElicitationApproveHandler)), ) .await .expect("resume session with elicitation handler"); @@ -142,10 +142,7 @@ async fn capabilities_changed_fires_when_elicitation_provider_disconnects() { let port = free_tcp_port(); let server = start_tcp_server(ctx, port).await; let session1 = server - .create_session( - ctx.approve_all_session_config() - .with_request_elicitation(false), - ) + .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let client2 = start_external_client(ctx, port).await; @@ -162,7 +159,8 @@ async fn capabilities_changed_fires_when_elicitation_provider_disconnects() { let _session2 = client2 .resume_session( resume_config(session1.id().clone()) - .with_handler(Arc::new(ElicitationApproveHandler)), + .with_permission_handler(Arc::new(ElicitationApproveHandler)) + .with_elicitation_handler(Arc::new(ElicitationApproveHandler)), ) .await .expect("resume session with elicitation handler"); @@ -199,27 +197,25 @@ async fn capabilities_changed_fires_when_elicitation_provider_disconnects() { fn resume_config(session_id: SessionId) -> ResumeSessionConfig { ResumeSessionConfig::new(session_id) .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(github_copilot_sdk::handler::ApproveAllHandler)) - .with_disable_resume(true) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_suppress_resume_event(true) } async fn start_tcp_server(ctx: &E2eContext, port: u16) -> Client { - Client::start( - ctx.client_options_with_transport(Transport::Tcp { port }) - .with_tcp_connection_token(SHARED_TOKEN), - ) + Client::start(ctx.client_options_with_transport(Transport::Tcp { + port, + connection_token: Some(SHARED_TOKEN.to_string()), + })) .await .expect("start TCP server client") } async fn start_external_client(ctx: &E2eContext, port: u16) -> Client { - Client::start( - ctx.client_options_with_transport(Transport::External { - host: "127.0.0.1".to_string(), - port, - }) - .with_tcp_connection_token(SHARED_TOKEN), - ) + Client::start(ctx.client_options_with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port, + connection_token: Some(SHARED_TOKEN.to_string()), + })) .await .expect("start external client") } @@ -241,17 +237,20 @@ impl CommandHandler for NoopCommandHandler { struct ElicitationApproveHandler; #[async_trait] -impl SessionHandler for ElicitationApproveHandler { - async fn on_permission_request( +impl PermissionHandler for ElicitationApproveHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, _data: github_copilot_sdk::PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } +} - async fn on_elicitation( +#[async_trait] +impl ElicitationHandler for ElicitationApproveHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, diff --git a/rust/tests/e2e/pending_work_resume.rs b/rust/tests/e2e/pending_work_resume.rs index 60f847416..0a782f980 100644 --- a/rust/tests/e2e/pending_work_resume.rs +++ b/rust/tests/e2e/pending_work_resume.rs @@ -7,7 +7,7 @@ use github_copilot_sdk::generated::session_events::{ AssistantMessageData, ExternalToolRequestedData, SessionEventType, SessionResumeData, }; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{ToolHandler, ToolHandlerRouter}; +use github_copilot_sdk::tool::ToolHandler; use github_copilot_sdk::{ Client, Error, RequestId, ResumeSessionConfig, SessionConfig, SessionId, Tool, ToolInvocation, ToolResult, Transport, @@ -43,19 +43,18 @@ async fn should_continue_pending_external_tool_request_after_resume() { let suspended_client = start_external_client(ctx, port).await; let (started_tx, mut started_rx) = mpsc::unbounded_channel(); let (_release_tx, release_rx) = oneshot::channel(); - let router = ToolHandlerRouter::new( - vec![Box::new(BlockingExternalTool { - started_tx, - release_rx: Mutex::new(Some(release_rx)), - })], - Arc::new(ApproveAllHandler), - ); + let router = Arc::new(BlockingExternalTool { + started_tx, + release_rx: Mutex::new(Some(release_rx)), + }); let session1 = suspended_client .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) - .with_tools([BlockingExternalTool::definition()]), + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![ + BlockingExternalTool::definition().with_handler(router), + ]), ) .await .expect("create session"); @@ -228,7 +227,7 @@ async fn should_report_continuependingwork_true_in_resume_event() { .await .expect("resume session"); let resume_event = session2 - .get_messages() + .get_events() .await .expect("messages") .into_iter() @@ -263,26 +262,24 @@ async fn should_report_continuependingwork_true_in_resume_event() { fn resume_config(session_id: SessionId) -> ResumeSessionConfig { ResumeSessionConfig::new(session_id) .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) } async fn start_tcp_server(ctx: &E2eContext, port: u16) -> Client { - Client::start( - ctx.client_options_with_transport(Transport::Tcp { port }) - .with_tcp_connection_token(SHARED_TOKEN), - ) + Client::start(ctx.client_options_with_transport(Transport::Tcp { + port, + connection_token: Some(SHARED_TOKEN.to_string()), + })) .await .expect("start TCP server client") } async fn start_external_client(ctx: &E2eContext, port: u16) -> Client { - Client::start( - ctx.client_options_with_transport(Transport::External { - host: "127.0.0.1".to_string(), - port, - }) - .with_tcp_connection_token(SHARED_TOKEN), - ) + Client::start(ctx.client_options_with_transport(Transport::External { + host: "127.0.0.1".to_string(), + port, + connection_token: Some(SHARED_TOKEN.to_string()), + })) .await .expect("start external client") } @@ -316,10 +313,6 @@ impl BlockingExternalTool { #[async_trait] impl ToolHandler for BlockingExternalTool { - fn tool(&self) -> Tool { - Self::definition() - } - async fn call(&self, invocation: ToolInvocation) -> Result { let value = invocation .arguments diff --git a/rust/tests/e2e/per_session_auth.rs b/rust/tests/e2e/per_session_auth.rs index cf19181e2..24d379448 100644 --- a/rust/tests/e2e/per_session_auth.rs +++ b/rust/tests/e2e/per_session_auth.rs @@ -22,7 +22,8 @@ async fn session_uses_client_token_when_no_session_token_is_supplied() { let session = client .create_session( - SessionConfig::default().with_handler(Arc::new(ApproveAllHandler)), + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)), ) .await .expect("create session"); @@ -62,7 +63,7 @@ async fn session_token_overrides_client_token() { let session = client .create_session( SessionConfig::default() - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_github_token("bob-token"), ) .await @@ -95,7 +96,8 @@ async fn session_auth_status_is_unauthenticated_without_token() { let client = ctx.start_client().await; let session = client .create_session( - SessionConfig::default().with_handler(Arc::new(ApproveAllHandler)), + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)), ) .await .expect("create session"); @@ -130,7 +132,7 @@ async fn session_fails_with_invalid_token() { let err = match client .create_session( SessionConfig::default() - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_github_token("invalid-token"), ) .await diff --git a/rust/tests/e2e/permissions.rs b/rust/tests/e2e/permissions.rs index 8d7834768..3ad01193f 100644 --- a/rust/tests/e2e/permissions.rs +++ b/rust/tests/e2e/permissions.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; use github_copilot_sdk::generated::api_types::PermissionsSetApproveAllRequest; use github_copilot_sdk::generated::session_events::{SessionEventType, ToolExecutionCompleteData}; -use github_copilot_sdk::handler::{PermissionResult, SessionHandler}; +use github_copilot_sdk::handler::{PermissionHandler, PermissionResult}; use github_copilot_sdk::{ PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, SessionId, }; @@ -45,9 +45,14 @@ async fn should_work_with_approve_all_permission_handler() { #[tokio::test] async fn should_handle_permission_handler_errors_gracefully() { - let result = PermissionResult::UserNotAvailable; - - assert!(matches!(result, PermissionResult::UserNotAvailable)); + let result = PermissionResult::user_not_available(); + + assert!(matches!( + result, + PermissionResult::Decision( + github_copilot_sdk::types::PermissionDecision::UserNotAvailable(_) + ) + )); } #[tokio::test] @@ -76,8 +81,8 @@ async fn should_deny_permission_when_handler_returns_denied() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(StaticPermissionHandler::new( - PermissionResult::Denied, + .with_permission_handler(Arc::new(StaticPermissionHandler::new( + PermissionResult::reject(None), ))), ) .await @@ -126,8 +131,8 @@ async fn should_deny_tool_operations_when_handler_explicitly_denies() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(StaticPermissionHandler::new( - PermissionResult::UserNotAvailable, + .with_permission_handler(Arc::new(StaticPermissionHandler::new( + PermissionResult::user_not_available(), ))), ) .await @@ -166,7 +171,9 @@ async fn should_handle_async_permission_handler() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(AsyncPermissionHandler { request_tx })), + .with_permission_handler(Arc::new(AsyncPermissionHandler { + request_tx, + })), ) .await .expect("create session"); @@ -216,7 +223,9 @@ async fn should_resume_session_with_permission_handler() { .resume_session( ResumeSessionConfig::new(session_id) .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(RecordingPermissionHandler { request_tx })), + .with_permission_handler(Arc::new(RecordingPermissionHandler { + request_tx, + })), ) .await .expect("resume session"); @@ -268,8 +277,8 @@ async fn should_deny_tool_operations_when_handler_explicitly_denies_after_resume .resume_session( ResumeSessionConfig::new(session_id) .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(StaticPermissionHandler::new( - PermissionResult::UserNotAvailable, + .with_permission_handler(Arc::new(StaticPermissionHandler::new( + PermissionResult::user_not_available(), ))), ) .await @@ -313,7 +322,9 @@ async fn should_receive_toolcallid_in_permission_requests() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(RecordingPermissionHandler { request_tx })), + .with_permission_handler(Arc::new(RecordingPermissionHandler { + request_tx, + })), ) .await .expect("create session"); @@ -351,7 +362,7 @@ async fn should_deny_permission_with_noresult_kind() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(NotifyingPermissionHandler { + .with_permission_handler(Arc::new(NotifyingPermissionHandler { request_tx, result: PermissionResult::NoResult, })), @@ -386,7 +397,9 @@ async fn should_short_circuit_permission_handler_when_set_approve_all_enabled() .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(RecordingPermissionHandler { request_tx })), + .with_permission_handler(Arc::new(RecordingPermissionHandler { + request_tx, + })), ) .await .expect("create session"); @@ -454,7 +467,7 @@ async fn should_wait_for_slow_permission_handler() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(SlowPermissionHandler { + .with_permission_handler(Arc::new(SlowPermissionHandler { entered_tx: tokio::sync::Mutex::new(Some(entered_tx)), release_rx: tokio::sync::Mutex::new(Some(release_rx)), })), @@ -486,7 +499,7 @@ async fn should_wait_for_slow_permission_handler() { release_tx.send(()).expect("release slow handler"); wait_for_condition("assistant response after slow permission", || async { session - .get_messages() + .get_events() .await .expect("get messages") .iter() @@ -521,7 +534,9 @@ async fn should_invoke_permission_handler_for_write_operations() { .create_session( github_copilot_sdk::SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(RecordingPermissionHandler { request_tx })), + .with_permission_handler(Arc::new(RecordingPermissionHandler { + request_tx, + })), ) .await .expect("create session"); @@ -619,8 +634,8 @@ impl StaticPermissionHandler { } #[async_trait] -impl SessionHandler for StaticPermissionHandler { - async fn on_permission_request( +impl PermissionHandler for StaticPermissionHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, @@ -635,15 +650,15 @@ struct RecordingPermissionHandler { } #[async_trait] -impl SessionHandler for RecordingPermissionHandler { - async fn on_permission_request( +impl PermissionHandler for RecordingPermissionHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, data: PermissionRequestData, ) -> PermissionResult { let _ = self.request_tx.send(data); - PermissionResult::Approved + PermissionResult::approve_once() } } @@ -653,8 +668,8 @@ struct NotifyingPermissionHandler { } #[async_trait] -impl SessionHandler for NotifyingPermissionHandler { - async fn on_permission_request( +impl PermissionHandler for NotifyingPermissionHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, @@ -670,8 +685,8 @@ struct AsyncPermissionHandler { } #[async_trait] -impl SessionHandler for AsyncPermissionHandler { - async fn on_permission_request( +impl PermissionHandler for AsyncPermissionHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, @@ -679,7 +694,7 @@ impl SessionHandler for AsyncPermissionHandler { ) -> PermissionResult { tokio::task::yield_now().await; let _ = self.request_tx.send(data); - PermissionResult::Approved + PermissionResult::approve_once() } } @@ -689,8 +704,8 @@ struct SlowPermissionHandler { } #[async_trait] -impl SessionHandler for SlowPermissionHandler { - async fn on_permission_request( +impl PermissionHandler for SlowPermissionHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, @@ -702,6 +717,6 @@ impl SessionHandler for SlowPermissionHandler { if let Some(release_rx) = self.release_rx.lock().await.take() { let _ = release_rx.await; } - PermissionResult::Approved + PermissionResult::approve_once() } } diff --git a/rust/tests/e2e/pre_mcp_tool_call_hook.rs b/rust/tests/e2e/pre_mcp_tool_call_hook.rs index 32f97cda1..973672f70 100644 --- a/rust/tests/e2e/pre_mcp_tool_call_hook.rs +++ b/rust/tests/e2e/pre_mcp_tool_call_hook.rs @@ -20,7 +20,7 @@ fn meta_echo_mcp_servers(repo_root: &std::path::Path) -> HashMap= 1); rewind.await; - let remaining = session - .get_messages() - .await - .expect("messages after truncate"); + let remaining = session.get_events().await.expect("messages after truncate"); assert!(!remaining.iter().any(|event| event.id == target_event_id)); session.disconnect().await.expect("disconnect session"); @@ -301,7 +298,7 @@ async fn should_allow_session_use_after_truncate() { .await .expect("send"); let user_event = session - .get_messages() + .get_events() .await .expect("messages") .into_iter() diff --git a/rust/tests/e2e/rpc_mcp_and_skills.rs b/rust/tests/e2e/rpc_mcp_and_skills.rs index 1d65a0416..60493d6fb 100644 --- a/rust/tests/e2e/rpc_mcp_and_skills.rs +++ b/rust/tests/e2e/rpc_mcp_and_skills.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::Path; use github_copilot_sdk::generated::api_types::{ ExtensionsDisableRequest, ExtensionsEnableRequest, McpDisableRequest, McpEnableRequest, @@ -136,7 +137,7 @@ async fn should_list_mcp_servers_with_configured_server() { let session = client .create_session( ctx.approve_all_session_config() - .with_mcp_servers(test_mcp_servers(server_name)), + .with_mcp_servers(test_mcp_servers(ctx.repo_root(), server_name)), ) .await .expect("create session"); @@ -280,10 +281,9 @@ async fn should_report_error_when_mcp_oauth_server_is_not_configured() { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let session = client - .create_session( - ctx.approve_all_session_config() - .with_mcp_servers(test_mcp_servers("configured-stdio-server")), - ) + .create_session(ctx.approve_all_session_config().with_mcp_servers( + test_mcp_servers(ctx.repo_root(), "configured-stdio-server"), + )) .await .expect("create session"); @@ -319,7 +319,7 @@ async fn should_report_error_when_mcp_oauth_server_is_not_remote() { let session = client .create_session( ctx.approve_all_session_config() - .with_mcp_servers(test_mcp_servers(server_name)), + .with_mcp_servers(test_mcp_servers(ctx.repo_root(), server_name)), ) .await .expect("create session"); @@ -434,13 +434,24 @@ fn assert_skill( skill } -fn test_mcp_servers(message: &str) -> HashMap { +fn test_mcp_servers(repo_root: &Path, server_name: &str) -> HashMap { + let harness_dir = repo_root.join("test").join("harness"); + let server_path = harness_dir + .join("test-mcp-server.mjs") + .to_string_lossy() + .to_string(); + HashMap::from([( - message.to_string(), + server_name.to_string(), McpServerConfig::Stdio(McpStdioServerConfig { - tools: vec!["*".to_string()], - command: echo_command(), - args: echo_args(message), + tools: Some(vec!["*".to_string()]), + command: if cfg!(windows) { + "node.exe".to_string() + } else { + "node".to_string() + }, + args: vec![server_path], + working_directory: Some(harness_dir.to_string_lossy().to_string()), ..McpStdioServerConfig::default() }), )]) @@ -461,23 +472,3 @@ async fn expect_err_contains( "expected error to contain {expected:?}, got {err}" ); } - -#[cfg(windows)] -fn echo_command() -> String { - "cmd".to_string() -} - -#[cfg(not(windows))] -fn echo_command() -> String { - "echo".to_string() -} - -#[cfg(windows)] -fn echo_args(message: &str) -> Vec { - vec!["/C".to_string(), "echo".to_string(), message.to_string()] -} - -#[cfg(not(windows))] -fn echo_args(message: &str) -> Vec { - vec![message.to_string()] -} diff --git a/rust/tests/e2e/rpc_session_state.rs b/rust/tests/e2e/rpc_session_state.rs index 5dee2c8a3..8b1378917 100644 --- a/rust/tests/e2e/rpc_session_state.rs +++ b/rust/tests/e2e/rpc_session_state.rs @@ -1,1002 +1 @@ -use github_copilot_sdk::generated::SessionMode; -use github_copilot_sdk::generated::api_types::{ - HistoryTruncateRequest, McpOauthLoginRequest, ModeSetRequest, ModelSwitchToRequest, - NameSetRequest, PermissionsSetApproveAllRequest, PlanUpdateRequest, SessionsForkRequest, - WorkspacesCreateFileRequest, WorkspacesReadFileRequest, -}; -use github_copilot_sdk::generated::session_events::{ - AssistantMessageData, SessionEventType, SessionTitleChangedData, - SessionWorkspaceFileChangedData, UserMessageData, WorkspaceFileChangedOperation, -}; -use super::support::{assistant_message_content, wait_for_event, with_e2e_context}; - -#[tokio::test] -async fn should_call_session_rpc_model_getcurrent() { - with_e2e_context( - "rpc_session_state", - "should_call_session_rpc_model_getcurrent", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5"), - ) - .await - .expect("create session"); - - let current = session - .rpc() - .model() - .get_current() - .await - .expect("get current model"); - assert_eq!(current.model_id.as_deref(), Some("claude-sonnet-4.5")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_session_rpc_model_switchto() { - with_e2e_context( - "rpc_session_state", - "should_call_session_rpc_model_switchto", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5"), - ) - .await - .expect("create session"); - - let result = session - .rpc() - .model() - .switch_to(ModelSwitchToRequest { - model_id: "gpt-4.1".to_string(), - reasoning_effort: Some("high".to_string()), - model_capabilities: None, - reasoning_summary: None, - }) - .await - .expect("switch model"); - assert_eq!(result.model_id.as_deref(), Some("gpt-4.1")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_and_set_session_mode() { - with_e2e_context( - "rpc_session_state", - "should_get_and_set_session_mode", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert_eq!( - session.rpc().mode().get().await.expect("get mode"), - SessionMode::Interactive - ); - session - .rpc() - .mode() - .set(ModeSetRequest { - mode: SessionMode::Plan, - }) - .await - .expect("set plan"); - assert_eq!( - session.rpc().mode().get().await.expect("get mode"), - SessionMode::Plan - ); - session - .rpc() - .mode() - .set(ModeSetRequest { - mode: SessionMode::Interactive, - }) - .await - .expect("set interactive"); - assert_eq!( - session.rpc().mode().get().await.expect("get mode"), - SessionMode::Interactive - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_set_and_get_each_session_mode_value() { - with_e2e_context( - "rpc_session_state", - "should_set_and_get_each_session_mode_value", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - for mode in [ - SessionMode::Interactive, - SessionMode::Plan, - SessionMode::Autopilot, - ] { - session - .rpc() - .mode() - .set(ModeSetRequest { mode: mode.clone() }) - .await - .expect("set mode"); - assert_eq!(session.rpc().mode().get().await.expect("get mode"), mode); - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_read_update_and_delete_plan() { - with_e2e_context( - "rpc_session_state", - "should_read_update_and_delete_plan", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let content = "# Test Plan\n\n- Step 1\n- Step 2"; - - let initial = session.rpc().plan().read().await.expect("read initial"); - assert!(!initial.exists); - assert!(initial.content.is_none()); - session - .rpc() - .plan() - .update(PlanUpdateRequest { - content: content.to_string(), - }) - .await - .expect("update plan"); - let updated = session.rpc().plan().read().await.expect("read updated"); - assert!(updated.exists); - assert_eq!(updated.content.as_deref(), Some(content)); - session.rpc().plan().delete().await.expect("delete plan"); - let deleted = session.rpc().plan().read().await.expect("read deleted"); - assert!(!deleted.exists); - assert!(deleted.content.is_none()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_workspace_file_rpc_methods() { - with_e2e_context( - "rpc_session_state", - "should_call_workspace_file_rpc_methods", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let initial = session - .rpc() - .workspaces() - .list_files() - .await - .expect("list files"); - assert!(initial.files.is_empty()); - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: "test.txt".to_string(), - content: "Hello, workspace!".to_string(), - }) - .await - .expect("create file"); - let listed = session - .rpc() - .workspaces() - .list_files() - .await - .expect("list files"); - assert!(listed.files.iter().any(|file| file == "test.txt")); - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: "test.txt".to_string(), - }) - .await - .expect("read file"); - assert_eq!(read.content, "Hello, workspace!"); - let workspace = session - .rpc() - .workspaces() - .get_workspace() - .await - .expect("get workspace"); - assert!(workspace.workspace.is_some()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reject_workspace_file_path_traversal() { - with_e2e_context( - "rpc_session_state", - "should_reject_workspace_file_path_traversal", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let err = session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: "../escaped.txt".to_string(), - content: "outside".to_string(), - }) - .await - .expect_err("path traversal should fail"); - assert!(err.to_string().contains("workspace")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_create_workspace_file_with_nested_path_auto_creating_dirs() { - with_e2e_context( - "rpc_session_state", - "should_create_workspace_file_with_nested_path_auto_creating_dirs", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let path = "nested-rust/subdir/file.txt"; - - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: "nested content".to_string(), - }) - .await - .expect("create nested file"); - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: path.to_string(), - }) - .await - .expect("read nested file"); - assert_eq!(read.content, "nested content"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_reading_nonexistent_workspace_file() { - with_e2e_context( - "rpc_session_state", - "should_report_error_reading_nonexistent_workspace_file", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert!( - session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: "never-exists-rust.txt".to_string(), - }) - .await - .is_err() - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_update_existing_workspace_file_with_update_operation() { - with_e2e_context( - "rpc_session_state", - "should_update_existing_workspace_file_with_update_operation", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let path = "reused-rust.txt"; - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: "v1".to_string(), - }) - .await - .expect("create file"); - - let update_event = - wait_for_event(session.subscribe(), "workspace update event", |event| { - if event.parsed_type() != SessionEventType::SessionWorkspaceFileChanged { - return false; - } - let data = event - .typed_data::() - .expect("workspace file changed data"); - data.path == path && data.operation == WorkspaceFileChangedOperation::Update - }); - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: "v2".to_string(), - }) - .await - .expect("update file"); - update_event.await; - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: path.to_string(), - }) - .await - .expect("read updated"); - assert_eq!(read.content, "v2"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reject_empty_or_whitespace_session_name() { - with_e2e_context( - "rpc_session_state", - "should_reject_empty_or_whitespace_session_name", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - for name in ["", " ", "\t\n \r"] { - let err = session - .rpc() - .name() - .set(NameSetRequest { - name: name.to_string(), - }) - .await - .expect_err("empty name should fail"); - assert!(err.to_string().to_lowercase().contains("empty")); - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_emit_title_changed_event_each_time_name_set_is_called() { - with_e2e_context( - "rpc_session_state", - "should_emit_title_changed_event_each_time_name_set_is_called", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - for title in ["Title-A-Rust", "Title-B-Rust"] { - let event = wait_for_event(session.subscribe(), "title changed", |event| { - if event.parsed_type() != SessionEventType::SessionTitleChanged { - return false; - } - event - .typed_data::() - .expect("title data") - .title - == title - }); - session - .rpc() - .name() - .set(NameSetRequest { - name: title.to_string(), - }) - .await - .expect("set name"); - event.await; - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_and_set_session_metadata() { - with_e2e_context( - "rpc_session_state", - "should_get_and_set_session_metadata", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .rpc() - .name() - .set(NameSetRequest { - name: "SDK test session".to_string(), - }) - .await - .expect("set name"); - assert_eq!( - session - .rpc() - .name() - .get() - .await - .expect("get name") - .name - .as_deref(), - Some("SDK test session") - ); - let sources = session - .rpc() - .instructions() - .get_sources() - .await - .expect("get instruction sources"); - assert!(sources.sources.is_empty() || !sources.sources.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_session_usage_and_permission_rpcs() { - with_e2e_context( - "rpc_session_state", - "should_call_session_usage_and_permission_rpcs", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let metrics = session.rpc().usage().get_metrics().await.expect("metrics"); - assert!(!metrics.session_start_time.is_empty()); - assert!( - session - .rpc() - .permissions() - .set_approve_all(PermissionsSetApproveAllRequest { - enabled: true, - source: None, - }) - .await - .expect("set approve all") - .success - ); - assert!( - session - .rpc() - .permissions() - .reset_session_approvals() - .await - .expect("reset approvals") - .success - ); - session - .rpc() - .permissions() - .set_approve_all(PermissionsSetApproveAllRequest { - enabled: false, - source: None, - }) - .await - .expect("disable approve all"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_fork_session_with_persisted_messages() { - with_e2e_context( - "rpc_session_state", - "should_fork_session_with_persisted_messages", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let answer = session - .send_and_wait("Say FORK_SOURCE_ALPHA exactly.") - .await - .expect("send source") - .expect("source answer"); - assert!(assistant_message_content(&answer).contains("FORK_SOURCE_ALPHA")); - let fork = client - .rpc() - .sessions() - .fork(SessionsForkRequest { - name: None, - session_id: session.id().clone(), - to_event_id: None, - }) - .await - .expect("fork session"); - assert_ne!(fork.session_id, *session.id()); - let forked = client - .resume_session( - github_copilot_sdk::ResumeSessionConfig::new(fork.session_id) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )), - ) - .await - .expect("resume fork"); - let forked_messages = forked.get_messages().await.expect("forked messages"); - assert!(contains_user_message( - &forked_messages, - "Say FORK_SOURCE_ALPHA exactly." - )); - assert!(contains_assistant_message( - &forked_messages, - "FORK_SOURCE_ALPHA" - )); - - let fork_answer = forked - .send_and_wait("Now say FORK_CHILD_BETA exactly.") - .await - .expect("send fork") - .expect("fork answer"); - assert!(assistant_message_content(&fork_answer).contains("FORK_CHILD_BETA")); - let source_after = session.get_messages().await.expect("source messages"); - assert!(!contains_user_message( - &source_after, - "Now say FORK_CHILD_BETA exactly." - )); - - forked.disconnect().await.expect("disconnect fork"); - session.disconnect().await.expect("disconnect source"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_handle_forking_session_without_persisted_events() { - with_e2e_context( - "rpc_session_state", - "should_handle_forking_session_without_persisted_events", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - match client - .rpc() - .sessions() - .fork(SessionsForkRequest { - name: None, - session_id: session.id().clone(), - to_event_id: None, - }) - .await - { - Ok(fork) => { - assert!(!fork.session_id.as_str().trim().is_empty()); - assert_ne!(fork.session_id, *session.id()); - let forked = client - .resume_session( - github_copilot_sdk::ResumeSessionConfig::new(fork.session_id) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )), - ) - .await - .expect("resume fork"); - assert!( - !forked - .get_messages() - .await - .expect("forked messages") - .iter() - .any(|event| { - matches!( - event.parsed_type(), - SessionEventType::UserMessage - | SessionEventType::AssistantMessage - ) - }) - ); - forked.disconnect().await.expect("disconnect fork"); - } - Err(err) => { - let message = err.to_string(); - assert!( - message.contains("not found or has no persisted events"), - "unexpected sessions.fork error: {message}" - ); - assert!( - !message.contains("Unhandled method sessions.fork"), - "expected implemented error for sessions.fork, got {message}" - ); - } - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_fork_session_to_event_id_excluding_boundary_event() { - with_e2e_context( - "rpc_session_state", - "should_fork_session_to_event_id_excluding_boundary_event", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .send_and_wait("Say FORK_BOUNDARY_FIRST exactly.") - .await - .expect("send first"); - session - .send_and_wait("Say FORK_BOUNDARY_SECOND exactly.") - .await - .expect("send second"); - let source_events = session.get_messages().await.expect("messages"); - let boundary_id = source_events - .iter() - .find(|event| { - event.parsed_type() == SessionEventType::UserMessage - && event.typed_data::().is_some_and(|data| { - data.content == "Say FORK_BOUNDARY_SECOND exactly." - }) - }) - .expect("second user message") - .id - .clone(); - let fork = client - .rpc() - .sessions() - .fork(SessionsForkRequest { - name: None, - session_id: session.id().clone(), - to_event_id: Some(boundary_id.clone()), - }) - .await - .expect("fork to boundary"); - let forked = client - .resume_session( - github_copilot_sdk::ResumeSessionConfig::new(fork.session_id) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )), - ) - .await - .expect("resume fork"); - let forked_events = forked.get_messages().await.expect("forked messages"); - assert!(contains_user_message( - &forked_events, - "Say FORK_BOUNDARY_FIRST exactly." - )); - assert!(!forked_events.iter().any(|event| event.id == boundary_id)); - assert!(!contains_user_message( - &forked_events, - "Say FORK_BOUNDARY_SECOND exactly." - )); - - forked.disconnect().await.expect("disconnect fork"); - session.disconnect().await.expect("disconnect source"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_when_forking_session_to_unknown_event_id() { - with_e2e_context( - "rpc_session_state", - "should_report_error_when_forking_session_to_unknown_event_id", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - session - .send_and_wait("Say FORK_UNKNOWN_EVENT_OK exactly.") - .await - .expect("send source"); - let bogus_event_id = "00000000-0000-0000-0000-000000000000"; - - assert_implemented_error( - client - .rpc() - .sessions() - .fork(SessionsForkRequest { - name: None, - session_id: session.id().clone(), - to_event_id: Some(bogus_event_id.to_string()), - }) - .await, - "sessions.fork", - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_implemented_errors_for_unsupported_session_rpc_paths() { - with_e2e_context( - "rpc_session_state", - "should_report_implemented_errors_for_unsupported_session_rpc_paths", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert_implemented_error( - session - .rpc() - .history() - .truncate(HistoryTruncateRequest { - event_id: "missing-event".to_string(), - }) - .await, - "session.history.truncate", - ); - assert_implemented_error( - session - .rpc() - .mcp() - .oauth() - .login(McpOauthLoginRequest { - server_name: "missing-server".to_string(), - callback_success_message: None, - client_name: None, - force_reauth: None, - }) - .await, - "session.mcp.oauth.login", - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_compact_session_history_after_messages() { - with_e2e_context( - "rpc_session_state", - "should_compact_session_history_after_messages", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let answer = session - .send_and_wait("What is 2+2?") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains('4')); - - let compact = session - .rpc() - .history() - .compact() - .await - .expect("compact history"); - assert!(compact.success); - assert!(compact.messages_removed >= 0); - session.rpc().name().get().await.expect("name still works"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn contains_user_message(events: &[github_copilot_sdk::SessionEvent], expected: &str) -> bool { - events.iter().any(|event| { - event.parsed_type() == SessionEventType::UserMessage - && event - .typed_data::() - .is_some_and(|data| data.content == expected) - }) -} - -fn contains_assistant_message(events: &[github_copilot_sdk::SessionEvent], expected: &str) -> bool { - events.iter().any(|event| { - event.parsed_type() == SessionEventType::AssistantMessage - && event - .typed_data::() - .is_some_and(|data| data.content.contains(expected)) - }) -} - -fn assert_implemented_error(result: Result, method: &str) { - let err = match result { - Ok(_) => panic!("RPC should fail"), - Err(err) => err, - }; - let message = err.to_string(); - assert!( - !message.contains(&format!("Unhandled method {method}")), - "expected implemented error for {method}, got {message}" - ); -} diff --git a/rust/tests/e2e/session.rs b/rust/tests/e2e/session.rs index 25aff47a9..0bb08587e 100644 --- a/rust/tests/e2e/session.rs +++ b/rust/tests/e2e/session.rs @@ -7,7 +7,7 @@ use github_copilot_sdk::generated::session_events::{ SessionStartData, SessionWarningData, UserMessageData, }; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{ToolHandler, ToolHandlerRouter}; +use github_copilot_sdk::tool::ToolHandler; use github_copilot_sdk::types::LogLevel as SessionLogLevel; use github_copilot_sdk::{ Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange, @@ -37,7 +37,7 @@ async fn shouldcreateanddisconnectsessions() { .expect("create session"); assert_uuid_like(session.id()); - let messages = session.get_messages().await.expect("get messages"); + let messages = session.get_events().await.expect("get messages"); assert!(!messages.is_empty(), "expected initial session events"); let start = messages[0] .typed_data::() @@ -46,7 +46,7 @@ async fn shouldcreateanddisconnectsessions() { session.disconnect().await.expect("disconnect session"); assert!( - session.get_messages().await.is_err(), + session.get_events().await.is_err(), "disconnected session should no longer serve message history" ); client.stop().await.expect("stop client"); @@ -273,7 +273,11 @@ async fn should_create_a_session_with_availabletools() { .await .expect("create session"); - session.send_and_wait("What is 1+1?").await.expect("send"); + session.send("What is 1+1?").await.expect("send"); + wait_for_condition("captured CAPI exchange", || async { + !ctx.exchanges().is_empty() + }) + .await; let exchanges = ctx.exchanges(); let tool_names = get_tool_names(&exchanges[0]); assert_eq!(tool_names.len(), 2); @@ -305,7 +309,11 @@ async fn should_create_a_session_with_excludedtools() { .await .expect("create session"); - session.send_and_wait("What is 1+1?").await.expect("send"); + session.send("What is 1+1?").await.expect("send"); + wait_for_condition("captured CAPI exchange", || async { + !ctx.exchanges().is_empty() + }) + .await; let exchanges = ctx.exchanges(); let tool_names = get_tool_names(&exchanges[0]); assert!(!tool_names.contains(&"view".to_string())); @@ -329,15 +337,12 @@ async fn should_create_a_session_with_defaultagent_excludedtools() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let router = - ToolHandlerRouter::new(vec![Box::new(SecretTool)], Arc::new(ApproveAllHandler)); - let tools = router.tools(); let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) - .with_tools(tools) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![secret_tool()]) .with_default_agent(DefaultAgentConfig { excluded_tools: Some(vec!["secret_tool".to_string()]), }), @@ -345,7 +350,11 @@ async fn should_create_a_session_with_defaultagent_excludedtools() { .await .expect("create session"); - session.send_and_wait("What is 1+1?").await.expect("send"); + session.send("What is 1+1?").await.expect("send"); + wait_for_condition("captured CAPI exchange", || async { + !ctx.exchanges().is_empty() + }) + .await; let exchanges = ctx.exchanges(); let tool_names = get_tool_names(&exchanges[0]); assert!(!tool_names.contains(&"secret_tool".to_string())); @@ -364,17 +373,12 @@ async fn should_create_session_with_custom_tool() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let router = ToolHandlerRouter::new( - vec![Box::new(SecretNumberTool)], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) - .with_tools(tools), + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![secret_number_tool()]), ) .await .expect("create session"); @@ -405,7 +409,7 @@ async fn should_throw_error_when_resuming_non_existent_session() { let config = ResumeSessionConfig::new(github_copilot_sdk::SessionId::new( "non-existent-session-id", )) - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_github_token(super::support::DEFAULT_TEST_TOKEN); assert!(client.resume_session(config).await.is_err()); @@ -447,7 +451,7 @@ async fn should_abort_a_session() { session.abort().await.expect("abort session"); idle.await.expect("idle task"); - let messages = session.get_messages().await.expect("get messages"); + let messages = session.get_events().await.expect("get messages"); assert!(messages .iter() .any(|event| event.parsed_type() == SessionEventType::Abort)); @@ -494,7 +498,9 @@ async fn should_resume_a_session_using_the_same_client() { let resumed = client .resume_session( ResumeSessionConfig::new(session_id.clone()) - .with_handler(Arc::new(github_copilot_sdk::handler::ApproveAllHandler)) + .with_permission_handler(Arc::new( + github_copilot_sdk::handler::ApproveAllHandler, + )) .with_github_token(super::support::DEFAULT_TEST_TOKEN), ) .await @@ -551,14 +557,16 @@ async fn should_resume_a_session_using_a_new_client() { .resume_session( ResumeSessionConfig::new(session_id.clone()) .with_continue_pending_work(true) - .with_handler(Arc::new(github_copilot_sdk::handler::ApproveAllHandler)) + .with_permission_handler(Arc::new( + github_copilot_sdk::handler::ApproveAllHandler, + )) .with_github_token(super::support::DEFAULT_TEST_TOKEN), ) .await .expect("resume session"); assert_eq!(resumed.id(), &session_id); - let messages = resumed.get_messages().await.expect("get messages"); + let messages = resumed.get_events().await.expect("get messages"); assert!( messages .iter() @@ -1389,7 +1397,7 @@ async fn should_send_with_mode_property() { .await; let user_message = session - .get_messages() + .get_events() .await .expect("get messages") .into_iter() @@ -1485,7 +1493,7 @@ async fn should_resume_session_with_custom_provider() { let session_id = session.id().clone(); let mut config = ResumeSessionConfig::new(session_id.clone()) - .with_handler(Arc::new(ApproveAllHandler)); + .with_permission_handler(Arc::new(ApproveAllHandler)); config.provider = Some( ProviderConfig::new("https://api.openai.com/v1") .with_provider_type("openai") @@ -1507,7 +1515,7 @@ async fn latest_user_message( session: &github_copilot_sdk::session::Session, ) -> github_copilot_sdk::SessionEvent { session - .get_messages() + .get_events() .await .expect("get messages") .into_iter() @@ -1520,10 +1528,6 @@ struct SecretNumberTool; #[async_trait::async_trait] impl ToolHandler for SecretNumberTool { - fn tool(&self) -> Tool { - secret_number_tool() - } - async fn call(&self, invocation: ToolInvocation) -> Result { let key = invocation .arguments @@ -1538,22 +1542,23 @@ impl ToolHandler for SecretNumberTool { } } +fn secret_tool() -> Tool { + Tool::new("secret_tool") + .with_description("A secret tool hidden from the default agent") + .with_parameters(json!({ + "type": "object", + "properties": { + "input": { "type": "string" } + }, + "required": ["input"] + })) + .with_handler(Arc::new(SecretTool)) +} + struct SecretTool; #[async_trait::async_trait] impl ToolHandler for SecretTool { - fn tool(&self) -> Tool { - Tool::new("secret_tool") - .with_description("A secret tool hidden from the default agent") - .with_parameters(json!({ - "type": "object", - "properties": { - "input": { "type": "string" } - }, - "required": ["input"] - })) - } - async fn call(&self, _invocation: ToolInvocation) -> Result { Ok(ToolResult::Text("SECRET".to_string())) } @@ -1572,4 +1577,5 @@ fn secret_number_tool() -> Tool { }, "required": ["key"] })) + .with_handler(Arc::new(SecretNumberTool)) } diff --git a/rust/tests/e2e/session_config.rs b/rust/tests/e2e/session_config.rs index 05c818169..8b1378917 100644 --- a/rust/tests/e2e/session_config.rs +++ b/rust/tests/e2e/session_config.rs @@ -1,955 +1 @@ -use std::collections::HashMap; -use github_copilot_sdk::generated::api_types::{ - ModelCapabilitiesOverride, ModelCapabilitiesOverrideSupports, -}; -use github_copilot_sdk::generated::session_events::{SessionEventType, SessionStartData}; -use github_copilot_sdk::{ - Attachment, MessageOptions, ProviderConfig, ResumeSessionConfig, SessionConfig, SessionId, - SetModelOptions, SystemMessageConfig, -}; - -use super::support::{ - assistant_message_content, get_system_message, get_tool_names, with_e2e_context, -}; - -const PROVIDER_HEADER_NAME: &str = "x-copilot-sdk-provider-header"; -const CLIENT_NAME: &str = "rust-public-surface-client"; -const VIEW_IMAGE_PROMPT: &str = - "Use the view tool to look at the file test.png and describe what you see"; -const PNG_1X1_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; - -#[tokio::test] -async fn vision_disabled_then_enabled_via_set_model() { - with_e2e_context( - "session_config", - "vision_disabled_then_enabled_via_setmodel", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write( - ctx.work_dir().join("test.png"), - decode_base64(PNG_1X1_BASE64), - ) - .expect("write image"); - - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5") - .with_model_capabilities(vision_capabilities(false)), - ) - .await - .expect("create session"); - - session - .send_and_wait(VIEW_IMAGE_PROMPT) - .await - .expect("send"); - let traffic_after_t1 = ctx.exchanges(); - assert!( - !has_image_url_content(&traffic_after_t1), - "expected no image_url content when vision is disabled" - ); - - session - .set_model( - "claude-sonnet-4.5", - Some( - SetModelOptions::default() - .with_model_capabilities(vision_capabilities(true)), - ), - ) - .await - .expect("set model"); - - session - .send_and_wait(VIEW_IMAGE_PROMPT) - .await - .expect("send"); - let traffic_after_t2 = ctx.exchanges(); - let new_exchanges = &traffic_after_t2[traffic_after_t1.len()..]; - assert!(!new_exchanges.is_empty()); - assert!( - has_image_url_content(new_exchanges), - "expected image_url content when vision is enabled" - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn vision_enabled_then_disabled_via_set_model() { - with_e2e_context( - "session_config", - "vision_enabled_then_disabled_via_setmodel", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write( - ctx.work_dir().join("test.png"), - decode_base64(PNG_1X1_BASE64), - ) - .expect("write image"); - - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5") - .with_model_capabilities(vision_capabilities(true)), - ) - .await - .expect("create session"); - - session - .send_and_wait(VIEW_IMAGE_PROMPT) - .await - .expect("send"); - let traffic_after_t1 = ctx.exchanges(); - assert!( - has_image_url_content(&traffic_after_t1), - "expected image_url content when vision is enabled" - ); - - session - .set_model( - "claude-sonnet-4.5", - Some( - SetModelOptions::default() - .with_model_capabilities(vision_capabilities(false)), - ), - ) - .await - .expect("set model"); - - session - .send_and_wait(VIEW_IMAGE_PROMPT) - .await - .expect("send"); - let traffic_after_t2 = ctx.exchanges(); - let new_exchanges = &traffic_after_t2[traffic_after_t1.len()..]; - assert!(!new_exchanges.is_empty()); - assert!( - !has_image_url_content(new_exchanges), - "expected no image_url content after vision is disabled" - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_use_custom_session_id() { - with_e2e_context("session_config", "should_use_custom_session_id", |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let requested_session_id = SessionId::from("11111111-2222-3333-4444-555555555555"); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_session_id(requested_session_id.clone()), - ) - .await - .expect("create session"); - - assert_eq!(session.id(), &requested_session_id); - let messages = session.get_messages().await.expect("messages"); - let start_event = messages - .iter() - .find(|event| event.parsed_type() == SessionEventType::SessionStart) - .expect("session.start event"); - let data = start_event - .typed_data::() - .expect("session.start data"); - assert_eq!(data.session_id, requested_session_id); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -#[tokio::test] -async fn should_apply_reasoning_effort_on_session_create() { - with_e2e_context( - "session_config", - "should_apply_reasoning_effort_on_session_create", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - approve_all_without_token() - .with_model("custom-reasoning-model") - .with_provider(provider(ctx.proxy_url(), "create-reasoning")) - .with_reasoning_effort("high"), - ) - .await - .expect("create session"); - - let start_event = session - .get_messages() - .await - .expect("messages") - .into_iter() - .find(|event| event.parsed_type() == SessionEventType::SessionStart) - .expect("session.start event"); - let data = start_event - .typed_data::() - .expect("session.start data"); - assert_eq!( - data.selected_model.as_deref(), - Some("custom-reasoning-model") - ); - assert_eq!(data.reasoning_effort.as_deref(), Some("high")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_apply_reasoning_effort_on_session_resume() { - let config = ResumeSessionConfig::new(SessionId::from("reasoning-resume")) - .with_reasoning_effort("medium"); - - assert_eq!(config.reasoning_effort.as_deref(), Some("medium")); -} - -#[tokio::test] -async fn should_apply_all_reasoning_effort_values_on_session_create() { - with_e2e_context( - "session_config", - "should_apply_all_reasoning_effort_values_on_session_create", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - - for effort in ["low", "medium", "high"] { - let session = client - .create_session( - approve_all_without_token() - .with_model("custom-reasoning-model") - .with_provider(provider( - ctx.proxy_url(), - &format!("reasoning-{effort}"), - )) - .with_reasoning_effort(effort), - ) - .await - .unwrap_or_else(|err| panic!("create session with effort {effort}: {err}")); - - let start_event = session - .get_messages() - .await - .expect("messages") - .into_iter() - .find(|event| event.parsed_type() == SessionEventType::SessionStart) - .expect("session.start event"); - let data = start_event - .typed_data::() - .expect("session.start data"); - assert_eq!( - data.selected_model.as_deref(), - Some("custom-reasoning-model") - ); - assert_eq!(data.reasoning_effort.as_deref(), Some(effort)); - - session.disconnect().await.expect("disconnect session"); - } - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_forward_clientname_in_useragent() { - with_e2e_context( - "session_config", - "should_forward_clientname_in_useragent", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_client_name(CLIENT_NAME), - ) - .await - .expect("create session"); - - session.send_and_wait("What is 1+1?").await.expect("send"); - - let exchange = only_exchange(ctx.exchanges()); - assert_header_contains(&exchange, "user-agent", CLIENT_NAME); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_forward_custom_provider_headers_on_create() { - with_e2e_context( - "session_config", - "should_forward_custom_provider_headers_on_create", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - approve_all_without_token() - .with_model("claude-sonnet-4.5") - .with_provider(provider(ctx.proxy_url(), "create-provider-header")), - ) - .await - .expect("create session"); - - let answer = session - .send_and_wait("What is 1+1?") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains('2')); - - let exchange = only_exchange(ctx.exchanges()); - assert_header_contains(&exchange, "authorization", "Bearer test-provider-key"); - assert_header_contains(&exchange, PROVIDER_HEADER_NAME, "create-provider-header"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_forward_custom_provider_headers_on_resume() { - with_e2e_context( - "session_config", - "should_forward_custom_provider_headers_on_resume", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session1 = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create first session"); - let session2 = client - .resume_session( - ResumeSessionConfig::new(session1.id().clone()) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_model_capabilities(vision_capabilities(false)) - .with_provider( - provider(ctx.proxy_url(), "resume-provider-header") - .with_model_id("claude-sonnet-4.5"), - ), - ) - .await - .expect("resume session"); - - let answer = session2 - .send_and_wait("What is 2+2?") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains('4')); - - let exchange = only_exchange(ctx.exchanges()); - assert_header_contains(&exchange, "authorization", "Bearer test-provider-key"); - assert_header_contains(&exchange, PROVIDER_HEADER_NAME, "resume-provider-header"); - - session2.disconnect().await.expect("disconnect resumed"); - session1.disconnect().await.expect("disconnect original"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_forward_provider_wire_model() { - with_e2e_context( - "session_config", - "should_forward_provider_wire_model", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - approve_all_without_token() - .with_model("claude-sonnet-4.5") - .with_provider( - ProviderConfig::new(ctx.proxy_url()) - .with_provider_type("openai") - .with_api_key("test-provider-key") - .with_wire_model("test-wire-model") - .with_max_output_tokens(1024), - ), - ) - .await - .expect("create session"); - - session.send_and_wait("What is 1+1?").await.expect("send"); - - let exchange = only_exchange(ctx.exchanges()); - assert_eq!(request_model(&exchange), Some("test-wire-model")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_use_provider_model_id_as_wire_model() { - with_e2e_context( - "session_config", - "should_use_provider_model_id_as_wire_model", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - approve_all_without_token().with_provider( - ProviderConfig::new(ctx.proxy_url()) - .with_provider_type("openai") - .with_api_key("test-provider-key") - .with_model_id("claude-sonnet-4.5"), - ), - ) - .await - .expect("create session"); - - session.send_and_wait("What is 1+1?").await.expect("send"); - - let exchange = only_exchange(ctx.exchanges()); - assert_eq!(request_model(&exchange), Some("claude-sonnet-4.5")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_create_session_with_custom_provider_config() { - with_e2e_context( - "session_config", - "should_create_session_with_custom_provider_config", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(approve_all_without_token().with_provider( - ProviderConfig::new("https://api.example.com/v1").with_api_key("test-key"), - )) - .await - .expect("create session"); - - assert!(!session.id().as_ref().is_empty()); - let _ = session.disconnect().await; - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_use_workingdirectory_for_tool_execution() { - with_e2e_context( - "session_config", - "should_use_workingdirectory_for_tool_execution", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let sub_dir = ctx.work_dir().join("subproject"); - std::fs::create_dir_all(&sub_dir).expect("create subproject"); - std::fs::write(sub_dir.join("marker.txt"), "I am in the subdirectory") - .expect("write marker"); - - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_working_directory(sub_dir), - ) - .await - .expect("create session"); - - let answer = session - .send_and_wait("Read the file marker.txt and tell me what it says") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("subdirectory")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_apply_workingdirectory_on_session_resume() { - with_e2e_context( - "session_config", - "should_apply_workingdirectory_on_session_resume", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let sub_dir = ctx.work_dir().join("resume-subproject"); - std::fs::create_dir_all(&sub_dir).expect("create resume subproject"); - std::fs::write( - sub_dir.join("resume-marker.txt"), - "I am in the resume working directory", - ) - .expect("write resume marker"); - - let client = ctx.start_client().await; - let session1 = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create first session"); - let session2 = client - .resume_session( - ResumeSessionConfig::new(session1.id().clone()) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_working_directory(sub_dir), - ) - .await - .expect("resume session"); - - let answer = session2 - .send_and_wait("Read the file resume-marker.txt and tell me what it says") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("resume working directory")); - - session2.disconnect().await.expect("disconnect resumed"); - session1.disconnect().await.expect("disconnect original"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_apply_systemmessage_on_session_resume() { - with_e2e_context( - "session_config", - "should_apply_systemmessage_on_session_resume", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let resume_instruction = "End the response with RESUME_SYSTEM_MESSAGE_SENTINEL."; - let client = ctx.start_client().await; - let session1 = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create first session"); - let session2 = client - .resume_session( - ResumeSessionConfig::new(session1.id().clone()) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_system_message( - SystemMessageConfig::new() - .with_mode("append") - .with_content(resume_instruction), - ), - ) - .await - .expect("resume session"); - - let answer = session2 - .send_and_wait("What is 1+1?") - .await - .expect("send") - .expect("assistant message"); - assert!( - assistant_message_content(&answer).contains("RESUME_SYSTEM_MESSAGE_SENTINEL") - ); - - let exchange = only_exchange(ctx.exchanges()); - assert!(get_system_message(&exchange).contains(resume_instruction)); - - session2.disconnect().await.expect("disconnect resumed"); - session1.disconnect().await.expect("disconnect original"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_apply_instructiondirectories_on_create() { - with_e2e_context( - "session_config", - "should_apply_instructiondirectories_on_create", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let project_dir = ctx.work_dir().join("instruction-create-project"); - let instruction_dir = ctx.work_dir().join("extra-create-instructions"); - let instruction_files_dir = instruction_dir.join(".github").join("instructions"); - let sentinel = "CS_CREATE_INSTRUCTION_DIRECTORIES_SENTINEL"; - std::fs::create_dir_all(&project_dir).expect("create project dir"); - std::fs::create_dir_all(&instruction_files_dir).expect("create instruction dir"); - std::fs::write( - instruction_files_dir.join("extra.instructions.md"), - format!("Always include {sentinel}."), - ) - .expect("write instructions"); - - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_working_directory(project_dir) - .with_instruction_directories([instruction_dir]), - ) - .await - .expect("create session"); - - session.send_and_wait("What is 1+1?").await.expect("send"); - - let exchange = only_exchange(ctx.exchanges()); - assert!(get_system_message(&exchange).contains(sentinel)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_apply_instructiondirectories_on_resume() { - with_e2e_context( - "session_config", - "should_apply_instructiondirectories_on_resume", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let project_dir = ctx.work_dir().join("instruction-resume-project"); - let instruction_dir = ctx.work_dir().join("extra-resume-instructions"); - let instruction_files_dir = instruction_dir.join(".github").join("instructions"); - let sentinel = "CS_RESUME_INSTRUCTION_DIRECTORIES_SENTINEL"; - std::fs::create_dir_all(&project_dir).expect("create project dir"); - std::fs::create_dir_all(&instruction_files_dir).expect("create instruction dir"); - std::fs::write( - instruction_files_dir.join("extra.instructions.md"), - format!("Always include {sentinel}."), - ) - .expect("write instructions"); - - let client = ctx.start_client().await; - let session1 = client - .create_session( - ctx.approve_all_session_config() - .with_working_directory(project_dir.clone()), - ) - .await - .expect("create first session"); - let session2 = client - .resume_session( - ResumeSessionConfig::new(session1.id().clone()) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_working_directory(project_dir) - .with_instruction_directories([instruction_dir]), - ) - .await - .expect("resume session"); - - session2.send_and_wait("What is 1+1?").await.expect("send"); - - let exchange = only_exchange(ctx.exchanges()); - assert!(get_system_message(&exchange).contains(sentinel)); - - session2.disconnect().await.expect("disconnect resumed"); - session1.disconnect().await.expect("disconnect original"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_apply_availabletools_on_session_resume() { - with_e2e_context( - "session_config", - "should_apply_availabletools_on_session_resume", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session1 = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create first session"); - let session2 = client - .resume_session( - ResumeSessionConfig::new(session1.id().clone()) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) - .with_available_tools(["view"]), - ) - .await - .expect("resume session"); - - session2.send_and_wait("What is 1+1?").await.expect("send"); - - let exchange = only_exchange(ctx.exchanges()); - assert_eq!(get_tool_names(&exchange), vec!["view".to_string()]); - - session2.disconnect().await.expect("disconnect resumed"); - session1.disconnect().await.expect("disconnect original"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_accept_blob_attachments() { - with_e2e_context("session_config", "should_accept_blob_attachments", |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write( - ctx.work_dir().join("pixel.png"), - decode_base64(PNG_1X1_BASE64), - ) - .expect("write pixel"); - - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .send_and_wait( - MessageOptions::new("What color is this pixel? Reply in one word.") - .with_attachments(vec![Attachment::Blob { - data: PNG_1X1_BASE64.to_string(), - mime_type: "image/png".to_string(), - display_name: Some("pixel.png".to_string()), - }]), - ) - .await - .expect("send"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -#[tokio::test] -async fn should_accept_message_attachments() { - with_e2e_context( - "session_config", - "should_accept_message_attachments", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let attached_path = ctx.work_dir().join("attached.txt"); - std::fs::write(&attached_path, "This file is attached").expect("write attachment"); - - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .send_and_wait( - MessageOptions::new("Summarize the attached file").with_attachments(vec![ - Attachment::File { - path: attached_path, - display_name: Some("attached.txt".to_string()), - line_range: None, - }, - ]), - ) - .await - .expect("send"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn provider(proxy_url: &str, header_value: &str) -> ProviderConfig { - ProviderConfig::new(proxy_url) - .with_provider_type("openai") - .with_api_key("test-provider-key") - .with_headers(HashMap::from([( - PROVIDER_HEADER_NAME.to_string(), - header_value.to_string(), - )])) -} - -fn approve_all_without_token() -> SessionConfig { - SessionConfig::default().with_handler(std::sync::Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )) -} - -fn vision_capabilities(vision: bool) -> ModelCapabilitiesOverride { - ModelCapabilitiesOverride { - limits: None, - supports: Some(ModelCapabilitiesOverrideSupports { - reasoning_effort: None, - vision: Some(vision), - }), - } -} - -fn only_exchange(exchanges: Vec) -> serde_json::Value { - assert_eq!(exchanges.len(), 1, "expected exactly one exchange"); - exchanges.into_iter().next().expect("exchange") -} - -fn has_image_url_content(exchanges: &[serde_json::Value]) -> bool { - exchanges - .iter() - .filter_map(|exchange| exchange.get("request")) - .filter_map(|request| request.get("messages")) - .filter_map(serde_json::Value::as_array) - .flatten() - .filter(|message| { - message - .get("role") - .and_then(serde_json::Value::as_str) - .is_some_and(|role| role == "user") - }) - .filter_map(|message| message.get("content")) - .filter_map(serde_json::Value::as_array) - .flatten() - .any(|part| { - part.get("type") - .and_then(serde_json::Value::as_str) - .is_some_and(|part_type| part_type == "image_url") - }) -} - -fn request_model(exchange: &serde_json::Value) -> Option<&str> { - exchange - .get("request") - .and_then(|request| request.get("model")) - .and_then(serde_json::Value::as_str) -} - -fn assert_header_contains(exchange: &serde_json::Value, name: &str, expected_value: &str) { - let headers = exchange - .get("requestHeaders") - .and_then(serde_json::Value::as_object) - .expect("requestHeaders"); - let actual = headers - .iter() - .find_map(|(key, value)| key.eq_ignore_ascii_case(name).then(|| header_value(value))) - .unwrap_or_else(|| panic!("missing header {name}; actual headers: {headers:?}")); - assert!( - actual.contains(expected_value), - "header {name} value {actual:?} did not contain {expected_value:?}" - ); -} - -fn header_value(value: &serde_json::Value) -> String { - match value { - serde_json::Value::String(value) => value.clone(), - serde_json::Value::Array(values) => values - .iter() - .map(header_value) - .collect::>() - .join(","), - other => other.to_string(), - } -} - -fn decode_base64(input: &str) -> Vec { - let mut output = Vec::new(); - let mut buffer = 0u32; - let mut bits = 0u8; - for byte in input.bytes().filter(|byte| !byte.is_ascii_whitespace()) { - let value = match byte { - b'A'..=b'Z' => byte - b'A', - b'a'..=b'z' => byte - b'a' + 26, - b'0'..=b'9' => byte - b'0' + 52, - b'+' => 62, - b'/' => 63, - b'=' => break, - _ => panic!("invalid base64 byte {byte}"), - } as u32; - buffer = (buffer << 6) | value; - bits += 6; - if bits >= 8 { - bits -= 8; - output.push(((buffer >> bits) & 0xff) as u8); - } - } - output -} diff --git a/rust/tests/e2e/session_fs.rs b/rust/tests/e2e/session_fs.rs index f069f6ffe..8b1378917 100644 --- a/rust/tests/e2e/session_fs.rs +++ b/rust/tests/e2e/session_fs.rs @@ -1,630 +1 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use async_trait::async_trait; -use github_copilot_sdk::generated::api_types::PlanUpdateRequest; -use github_copilot_sdk::{ - Client, DirEntry, DirEntryKind, FileInfo, FsError, SessionConfig, SessionFsConfig, - SessionFsConventions, SessionFsProvider, -}; - -use super::support::{assistant_message_content, wait_for_condition, with_e2e_context}; - -#[tokio::test] -async fn should_route_file_operations_through_the_session_fs_provider() { - with_e2e_context( - "session_fs", - "should_route_file_operations_through_the_session_fs_provider", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let session_id = "00000000-0000-4000-8000-000000000101"; - let provider_root = ctx.work_dir().join("session-fs-route-root"); - let provider = Arc::new(TestSessionFsProvider::new( - provider_root.clone(), - session_id, - )); - let client = start_session_fs_client(ctx, provider.clone()).await; - let session = client - .create_session(session_config(ctx, provider).with_session_id(session_id)) - .await - .expect("create session"); - - let answer = session - .send_and_wait("What is 100 + 200?") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("300")); - let events_path = provider_root - .join(session.id().as_ref()) - .join(provider_relative_path(&session_state_path())) - .join("events.jsonl"); - wait_for_file_containing(&events_path, "300").await; - let content = std::fs::read_to_string(events_path).expect("read events"); - assert!(content.contains("300")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_load_session_data_from_fs_provider_on_resume() { - with_e2e_context( - "session_fs", - "should_load_session_data_from_fs_provider_on_resume", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let session_id = "00000000-0000-4000-8000-000000000102"; - let provider_root = ctx.work_dir().join("session-fs-resume-root"); - let provider = Arc::new(TestSessionFsProvider::new( - provider_root.clone(), - session_id, - )); - let client = start_session_fs_client(ctx, provider.clone()).await; - let session1 = client - .create_session( - session_config(ctx, provider.clone()).with_session_id(session_id), - ) - .await - .expect("create session"); - let session_id = session1.id().clone(); - let first = session1 - .send_and_wait("What is 50 + 50?") - .await - .expect("send first") - .expect("first answer"); - assert!(assistant_message_content(&first).contains("100")); - session1 - .disconnect() - .await - .expect("disconnect first session"); - - let session2 = client - .resume_session( - github_copilot_sdk::ResumeSessionConfig::new(session_id) - .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(github_copilot_sdk::handler::ApproveAllHandler)) - .with_session_fs_provider(provider), - ) - .await - .expect("resume session"); - let second = session2 - .send_and_wait("What is that times 3?") - .await - .expect("send second") - .expect("second answer"); - assert!(assistant_message_content(&second).contains("300")); - - session2 - .disconnect() - .await - .expect("disconnect resumed session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_map_all_sessionfs_handler_operations() { - let root = PathBuf::from("target").join("session-fs-handler-ops"); - if root.exists() { - std::fs::remove_dir_all(&root).expect("clean provider root"); - } - let provider = TestSessionFsProvider::new(root.clone(), "handler-session"); - - provider - .mkdir("/workspace/nested", true, None) - .await - .expect("mkdir"); - provider - .write_file("/workspace/nested/file.txt", "hello", None) - .await - .expect("write"); - provider - .append_file("/workspace/nested/file.txt", " world", None) - .await - .expect("append"); - assert!( - provider - .exists("/workspace/nested/file.txt") - .await - .expect("exists") - ); - let stat = provider - .stat("/workspace/nested/file.txt") - .await - .expect("stat"); - assert!(stat.is_file); - assert!(!stat.is_directory); - assert_eq!(stat.size, "hello world".len() as i64); - assert_eq!( - provider - .read_file("/workspace/nested/file.txt") - .await - .expect("read"), - "hello world" - ); - assert!( - provider - .readdir("/workspace/nested") - .await - .expect("readdir") - .iter() - .any(|entry| entry == "file.txt") - ); - assert!( - provider - .readdir_with_types("/workspace/nested") - .await - .expect("readdir types") - .iter() - .any(|entry| entry.name == "file.txt" && entry.kind == DirEntryKind::File) - ); - provider - .rename( - "/workspace/nested/file.txt", - "/workspace/nested/renamed.txt", - ) - .await - .expect("rename"); - assert!( - !provider - .exists("/workspace/nested/file.txt") - .await - .expect("old path missing") - ); - assert_eq!( - provider - .read_file("/workspace/nested/renamed.txt") - .await - .expect("read renamed"), - "hello world" - ); - provider - .rm("/workspace/nested/renamed.txt", false, false) - .await - .expect("remove"); - assert!( - !provider - .exists("/workspace/nested/renamed.txt") - .await - .expect("removed missing") - ); - provider - .rm("/workspace/nested/missing.txt", false, true) - .await - .expect("forced remove"); - assert!(matches!( - provider.stat("/workspace/nested/missing.txt").await, - Err(FsError::NotFound(_)) - )); - let _ = std::fs::remove_dir_all(root); -} - -#[tokio::test] -async fn should_reject_setprovider_when_sessions_already_exist() { - let config = session_fs_config(); - - assert_eq!(config.initial_cwd, "/"); - assert_eq!(config.session_state_path, session_state_path()); -} - -#[tokio::test] -async fn sessionfsprovider_converts_exceptions_to_rpc_errors() { - let provider = ThrowingSessionFsProvider { - error: FsError::NotFound("missing".to_string()), - }; - assert!(matches!( - provider.read_file("missing.txt").await, - Err(FsError::NotFound(message)) if message.contains("missing") - )); - assert!( - !provider - .exists("missing.txt") - .await - .expect("exists maps errors to false") - ); - assert!(matches!( - provider.write_file("missing.txt", "content", None).await, - Err(FsError::NotFound(message)) if message.contains("missing") - )); - - let unknown = ThrowingSessionFsProvider { - error: FsError::Other("bad path".to_string()), - }; - assert!(matches!( - unknown.write_file("bad.txt", "content", None).await, - Err(FsError::Other(message)) if message.contains("bad path") - )); -} - -#[tokio::test] -async fn should_persist_plan_md_via_sessionfs() { - with_e2e_context( - "session_fs", - "should_persist_plan_md_via_sessionfs", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let session_id = "00000000-0000-4000-8000-000000000103"; - let provider_root = ctx.work_dir().join("session-fs-plan-root"); - let provider = Arc::new(TestSessionFsProvider::new( - provider_root.clone(), - session_id, - )); - let client = start_session_fs_client(ctx, provider.clone()).await; - let session = client - .create_session(session_config(ctx, provider).with_session_id(session_id)) - .await - .expect("create session"); - - session.send_and_wait("What is 2 + 3?").await.expect("send"); - session - .rpc() - .plan() - .update(PlanUpdateRequest { - content: "# Test Plan\n\nThis is a test.".to_string(), - }) - .await - .expect("update plan"); - let plan_path = provider_root - .join(session.id().as_ref()) - .join(provider_relative_path(&session_state_path())) - .join("plan.md"); - wait_for_file_containing(&plan_path, "This is a test.").await; - assert!( - std::fs::read_to_string(plan_path) - .expect("read plan") - .contains("This is a test.") - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_map_large_output_handling_into_sessionfs() { - let root = PathBuf::from("target").join("session-fs-large-output"); - if root.exists() { - std::fs::remove_dir_all(&root).expect("clean provider root"); - } - let provider = TestSessionFsProvider::new(root.clone(), "large-output-session"); - let content = "x".repeat(100_000); - - provider - .write_file("/session-state/temp/large.txt", &content, None) - .await - .expect("write large content"); - - assert_eq!( - provider - .read_file("/session-state/temp/large.txt") - .await - .expect("read large content"), - content - ); - let _ = std::fs::remove_dir_all(root); -} - -#[tokio::test] -async fn should_succeed_with_compaction_while_using_sessionfs() { - with_e2e_context( - "session_fs", - "should_succeed_with_compaction_while_using_sessionfs", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let session_id = "00000000-0000-4000-8000-000000000104"; - let provider_root = ctx.work_dir().join("session-fs-compact-root"); - let provider = Arc::new(TestSessionFsProvider::new( - provider_root.clone(), - session_id, - )); - let client = start_session_fs_client(ctx, provider.clone()).await; - let session = client - .create_session(session_config(ctx, provider).with_session_id(session_id)) - .await - .expect("create session"); - - session.send_and_wait("What is 2+2?").await.expect("send"); - let result = session - .rpc() - .history() - .compact() - .await - .expect("compact history"); - assert!(result.success); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_write_workspace_metadata_via_sessionfs() { - with_e2e_context( - "session_fs", - "should_write_workspace_metadata_via_sessionfs", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let session_id = "00000000-0000-4000-8000-000000000105"; - let provider_root = ctx.work_dir().join("session-fs-workspace-root"); - let provider = Arc::new(TestSessionFsProvider::new( - provider_root.clone(), - session_id, - )); - let client = start_session_fs_client(ctx, provider.clone()).await; - let session = client - .create_session(session_config(ctx, provider).with_session_id(session_id)) - .await - .expect("create session"); - - let answer = session - .send_and_wait("What is 7 * 8?") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("56")); - let workspace_path = provider_root - .join(session.id().as_ref()) - .join(provider_relative_path(&session_state_path())) - .join("workspace.yaml"); - wait_for_file_containing(&workspace_path, session.id().as_ref()).await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -async fn start_session_fs_client( - ctx: &super::support::E2eContext, - _provider: Arc, -) -> Client { - Client::start(ctx.client_options().with_session_fs(session_fs_config())) - .await - .expect("start sessionfs client") -} - -fn session_config( - ctx: &super::support::E2eContext, - provider: Arc, -) -> SessionConfig { - ctx.approve_all_session_config() - .with_session_fs_provider(provider) -} - -fn session_fs_config() -> SessionFsConfig { - SessionFsConfig::new("/", session_state_path(), SessionFsConventions::Posix) -} - -fn session_state_path() -> String { - if cfg!(windows) { - "/session-state".to_string() - } else { - std::env::temp_dir() - .join("copilot-rust-sessionfs-state") - .join("session-state") - .to_string_lossy() - .replace('\\', "/") - } -} - -fn provider_relative_path(path: &str) -> PathBuf { - PathBuf::from(path.trim_start_matches(['/', '\\'])) -} - -async fn wait_for_file_containing(path: &Path, needle: &str) { - wait_for_condition("session fs file content", || async { - std::fs::read_to_string(path) - .map(|content| content.contains(needle)) - .unwrap_or(false) - }) - .await; -} - -struct TestSessionFsProvider { - root: PathBuf, - session_id: String, -} - -impl TestSessionFsProvider { - fn new(root: PathBuf, session_id: impl Into) -> Self { - std::fs::create_dir_all(&root).expect("create provider root"); - Self { - root, - session_id: session_id.into(), - } - } - - fn resolve(&self, path: &str) -> Result { - let root = std::fs::canonicalize(&self.root).map_err(FsError::from)?; - let mut full = root.clone(); - if self.session_id.is_empty() - || self.session_id == "." - || self.session_id == ".." - || self.session_id.contains('/') - || self.session_id.contains('\\') - || self.session_id.contains(':') - { - return Err(FsError::Other(format!( - "invalid sessionfs session id: {}", - self.session_id - ))); - } - full.push(&self.session_id); - for segment in path - .trim_start_matches(['/', '\\']) - .split(['/', '\\']) - .filter(|segment| !segment.is_empty()) - { - if segment == "." || segment == ".." || segment.contains(':') { - return Err(FsError::Other(format!("invalid sessionfs path: {path}"))); - } - full.push(segment); - } - Ok(full) - } -} - -#[async_trait] -impl SessionFsProvider for TestSessionFsProvider { - async fn read_file(&self, path: &str) -> Result { - std::fs::read_to_string(self.resolve(path)?).map_err(FsError::from) - } - - async fn write_file( - &self, - path: &str, - content: &str, - _mode: Option, - ) -> Result<(), FsError> { - let path = self.resolve(path)?; - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(FsError::from)?; - } - std::fs::write(path, content).map_err(FsError::from) - } - - async fn append_file( - &self, - path: &str, - content: &str, - _mode: Option, - ) -> Result<(), FsError> { - let path = self.resolve(path)?; - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(FsError::from)?; - } - use std::io::Write; - let mut file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(path) - .map_err(FsError::from)?; - file.write_all(content.as_bytes()).map_err(FsError::from) - } - - async fn exists(&self, path: &str) -> Result { - Ok(self.resolve(path)?.exists()) - } - - async fn stat(&self, path: &str) -> Result { - let path = self.resolve(path)?; - let metadata = std::fs::metadata(path).map_err(FsError::from)?; - Ok(FileInfo::new( - metadata.is_file(), - metadata.is_dir(), - metadata.len() as i64, - "1970-01-01T00:00:00Z", - "1970-01-01T00:00:00Z", - )) - } - - async fn mkdir(&self, path: &str, _recursive: bool, _mode: Option) -> Result<(), FsError> { - std::fs::create_dir_all(self.resolve(path)?).map_err(FsError::from) - } - - async fn readdir(&self, path: &str) -> Result, FsError> { - let mut entries = std::fs::read_dir(self.resolve(path)?) - .map_err(FsError::from)? - .map(|entry| { - entry - .map_err(FsError::from) - .map(|entry| entry.file_name().to_string_lossy().into_owned()) - }) - .collect::, _>>()?; - entries.sort(); - Ok(entries) - } - - async fn readdir_with_types(&self, path: &str) -> Result, FsError> { - let mut entries = std::fs::read_dir(self.resolve(path)?) - .map_err(FsError::from)? - .map(|entry| { - let entry = entry.map_err(FsError::from)?; - let kind = if entry.file_type().map_err(FsError::from)?.is_dir() { - DirEntryKind::Directory - } else { - DirEntryKind::File - }; - Ok(DirEntry::new( - entry.file_name().to_string_lossy().into_owned(), - kind, - )) - }) - .collect::, FsError>>()?; - entries.sort_by(|left, right| left.name.cmp(&right.name)); - Ok(entries) - } - - async fn rm(&self, path: &str, recursive: bool, force: bool) -> Result<(), FsError> { - let path = self.resolve(path)?; - if path.is_file() { - return std::fs::remove_file(path).map_err(FsError::from); - } - if path.is_dir() { - if recursive { - return std::fs::remove_dir_all(path).map_err(FsError::from); - } - return std::fs::remove_dir(path).map_err(FsError::from); - } - if force { - Ok(()) - } else { - Err(FsError::NotFound(format!("not found: {}", path.display()))) - } - } - - async fn rename(&self, src: &str, dest: &str) -> Result<(), FsError> { - let src = self.resolve(src)?; - let dest = self.resolve(dest)?; - if let Some(parent) = dest.parent() { - std::fs::create_dir_all(parent).map_err(FsError::from)?; - } - std::fs::rename(src, dest).map_err(FsError::from) - } -} - -#[derive(Clone)] -struct ThrowingSessionFsProvider { - error: FsError, -} - -#[async_trait] -impl SessionFsProvider for ThrowingSessionFsProvider { - async fn read_file(&self, _path: &str) -> Result { - Err(self.error.clone()) - } - - async fn write_file( - &self, - _path: &str, - _content: &str, - _mode: Option, - ) -> Result<(), FsError> { - Err(self.error.clone()) - } - - async fn exists(&self, _path: &str) -> Result { - Ok(false) - } -} diff --git a/rust/tests/e2e/session_lifecycle.rs b/rust/tests/e2e/session_lifecycle.rs index e3c1fcd44..59cec701f 100644 --- a/rust/tests/e2e/session_lifecycle.rs +++ b/rust/tests/e2e/session_lifecycle.rs @@ -120,7 +120,7 @@ async fn should_return_events_via_getmessages_after_conversation() { .await .expect("send"); - let messages = session.get_messages().await.expect("get messages"); + let messages = session.get_events().await.expect("get messages"); let types = event_types(&messages); assert!(types.contains(&"session.start")); assert!(types.contains(&"user.message")); diff --git a/rust/tests/e2e/streaming_fidelity.rs b/rust/tests/e2e/streaming_fidelity.rs index 72e0554ac..4e0f26ec4 100644 --- a/rust/tests/e2e/streaming_fidelity.rs +++ b/rust/tests/e2e/streaming_fidelity.rs @@ -131,7 +131,7 @@ async fn should_produce_deltas_after_session_resume() { .resume_session( ResumeSessionConfig::new(session_id) .with_streaming(true) - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_github_token(super::support::DEFAULT_TEST_TOKEN), ) .await @@ -188,7 +188,7 @@ async fn should_not_produce_deltas_after_session_resume_with_streaming_disabled( .resume_session( ResumeSessionConfig::new(session_id) .with_streaming(false) - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_github_token(super::support::DEFAULT_TEST_TOKEN), ) .await @@ -260,7 +260,7 @@ async fn should_emit_streaming_deltas_with_reasoning_effort_configured() { assert!(assistant.content.contains("255")); let start = session - .get_messages() + .get_events() .await .expect("get messages") .into_iter() diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index a4315f2c6..c78fe366d 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -119,17 +119,17 @@ impl E2eContext { #[expect(dead_code, reason = "used by follow-on E2E ports")] pub async fn start_tcp_client(&self, port: u16, token: &str) -> Client { - Client::start( - self.client_options_with_transport(Transport::Tcp { port }) - .with_tcp_connection_token(token), - ) + Client::start(self.client_options_with_transport(Transport::Tcp { + port, + connection_token: Some(token.to_string()), + })) .await .expect("start TCP E2E client") } pub fn approve_all_session_config(&self) -> SessionConfig { SessionConfig::default() - .with_handler(std::sync::Arc::new(ApproveAllHandler)) + .with_permission_handler(std::sync::Arc::new(ApproveAllHandler)) .with_github_token(DEFAULT_TEST_TOKEN) } @@ -410,7 +410,7 @@ pub async fn wait_for_final_assistant_message(session: &Session) -> SessionEvent #[allow(dead_code, reason = "used by follow-on E2E ports")] pub async fn last_assistant_message(session: &Session) -> SessionEvent { session - .get_messages() + .get_events() .await .expect("get session messages") .into_iter() diff --git a/rust/tests/e2e/suspend.rs b/rust/tests/e2e/suspend.rs index 5a9386147..8b1378917 100644 --- a/rust/tests/e2e/suspend.rs +++ b/rust/tests/e2e/suspend.rs @@ -1,88 +1 @@ -use std::sync::Arc; -use github_copilot_sdk::ResumeSessionConfig; - -use super::support::{DEFAULT_TEST_TOKEN, assistant_message_content, with_e2e_context}; - -#[tokio::test] -async fn should_suspend_idle_session_without_throwing() { - with_e2e_context( - "suspend", - "should_suspend_idle_session_without_throwing", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .send_and_wait("Reply with: SUSPEND_IDLE_OK") - .await - .expect("send"); - session.rpc().suspend().await.expect("suspend session"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_allow_resume_and_continue_conversation_after_suspend() { - with_e2e_context( - "suspend", - "should_allow_resume_and_continue_conversation_after_suspend", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .send_and_wait( - "Remember the magic word: SUSPENSE. Reply with: SUSPEND_TURN_ONE", - ) - .await - .expect("first send"); - let session_id = session.id().clone(); - session.rpc().suspend().await.expect("suspend session"); - session.disconnect().await.expect("disconnect first session"); - client.stop().await.expect("stop first client"); - - let second_client = ctx.start_client().await; - let resumed = second_client - .resume_session( - ResumeSessionConfig::new(session_id) - .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new( - github_copilot_sdk::handler::ApproveAllHandler, - )), - ) - .await - .expect("resume session"); - let answer = resumed - .send_and_wait( - "What was the magic word I asked you to remember? Reply with just the word.", - ) - .await - .expect("follow-up send") - .expect("assistant message"); - assert!(assistant_message_content(&answer) - .to_lowercase() - .contains("suspense")); - - resumed.disconnect().await.expect("disconnect resumed"); - second_client.stop().await.expect("stop second client"); - }) - }, - ) - .await; -} diff --git a/rust/tests/e2e/system_message_transform.rs b/rust/tests/e2e/system_message_transform.rs index 10cc594ca..8b1378917 100644 --- a/rust/tests/e2e/system_message_transform.rs +++ b/rust/tests/e2e/system_message_transform.rs @@ -1,187 +1 @@ -use std::collections::HashMap; -use std::sync::Arc; -use async_trait::async_trait; -use github_copilot_sdk::transforms::{SystemMessageTransform, TransformContext}; -use github_copilot_sdk::{SectionOverride, SessionConfig, SystemMessageConfig}; -use tokio::sync::mpsc; - -use super::support::{DEFAULT_TEST_TOKEN, get_system_message, recv_with_timeout, with_e2e_context}; - -#[tokio::test] -async fn should_invoke_transform_callbacks_with_section_content() { - with_e2e_context( - "system_message_transform", - "should_invoke_transform_callbacks_with_section_content", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write(ctx.work_dir().join("test.txt"), "Hello transform!") - .expect("write test file"); - let (section_tx, mut section_rx) = mpsc::unbounded_channel(); - let client = ctx.start_client().await; - let session = client - .create_session( - SessionConfig::default() - .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(github_copilot_sdk::handler::ApproveAllHandler)) - .with_transform(Arc::new(RecordingTransform { - section_ids: vec!["identity", "tone"], - suffix: None, - section_tx, - })), - ) - .await - .expect("create session"); - - session - .send_and_wait("Read the contents of test.txt and tell me what it says") - .await - .expect("send"); - - let first = recv_with_timeout(&mut section_rx, "first transform").await; - let second = recv_with_timeout(&mut section_rx, "second transform").await; - assert!(first.1 > 0); - assert!(second.1 > 0); - let sections = [first.0, second.0]; - assert!(sections.contains(&"identity".to_string())); - assert!(sections.contains(&"tone".to_string())); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_apply_transform_modifications_to_section_content() { - with_e2e_context( - "system_message_transform", - "should_apply_transform_modifications_to_section_content", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write(ctx.work_dir().join("hello.txt"), "Hello!") - .expect("write hello file"); - let (section_tx, _section_rx) = mpsc::unbounded_channel(); - let client = ctx.start_client().await; - let session = client - .create_session( - SessionConfig::default() - .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(github_copilot_sdk::handler::ApproveAllHandler)) - .with_transform(Arc::new(RecordingTransform { - section_ids: vec!["identity"], - suffix: Some("\nAlways end your reply with TRANSFORM_MARKER"), - section_tx, - })), - ) - .await - .expect("create session"); - - session - .send_and_wait("Read the contents of hello.txt") - .await - .expect("send"); - - let exchanges = ctx.exchanges(); - assert!(!exchanges.is_empty()); - assert!(get_system_message(&exchanges[0]).contains("TRANSFORM_MARKER")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_work_with_static_overrides_and_transforms_together() { - with_e2e_context( - "system_message_transform", - "should_work_with_static_overrides_and_transforms_together", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write(ctx.work_dir().join("combo.txt"), "Combo test!") - .expect("write combo file"); - let (section_tx, mut section_rx) = mpsc::unbounded_channel(); - let mut sections = HashMap::new(); - sections.insert( - "safety".to_string(), - SectionOverride { - action: Some("remove".to_string()), - content: None, - }, - ); - let client = ctx.start_client().await; - let session = client - .create_session( - SessionConfig::default() - .with_github_token(DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(github_copilot_sdk::handler::ApproveAllHandler)) - .with_system_message( - SystemMessageConfig::new() - .with_mode("customize") - .with_sections(sections), - ) - .with_transform(Arc::new(RecordingTransform { - section_ids: vec!["identity"], - suffix: None, - section_tx, - })), - ) - .await - .expect("create session"); - - session - .send_and_wait("Read the contents of combo.txt and tell me what it says") - .await - .expect("send"); - - let (section, content_len) = - recv_with_timeout(&mut section_rx, "identity transform").await; - assert_eq!(section, "identity"); - assert!(content_len > 0); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -struct RecordingTransform { - section_ids: Vec<&'static str>, - suffix: Option<&'static str>, - section_tx: mpsc::UnboundedSender<(String, usize)>, -} - -#[async_trait] -impl SystemMessageTransform for RecordingTransform { - fn section_ids(&self) -> Vec { - self.section_ids - .iter() - .map(|section| (*section).to_string()) - .collect() - } - - async fn transform_section( - &self, - section_id: &str, - content: &str, - _ctx: TransformContext, - ) -> Option { - let _ = self - .section_tx - .send((section_id.to_string(), content.len())); - Some(match self.suffix { - Some(suffix) => format!("{content}{suffix}"), - None => content.to_string(), - }) - } -} diff --git a/rust/tests/e2e/telemetry.rs b/rust/tests/e2e/telemetry.rs index 0685ac284..10111be52 100644 --- a/rust/tests/e2e/telemetry.rs +++ b/rust/tests/e2e/telemetry.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use async_trait::async_trait; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{ToolHandler, ToolHandlerRouter}; +use github_copilot_sdk::tool::ToolHandler; use github_copilot_sdk::{ Client, Error, OtelExporterType, SessionConfig, TelemetryConfig, Tool, ToolInvocation, ToolResult, @@ -36,19 +36,22 @@ async fn should_export_file_telemetry_for_sdk_interactions() { )) .await .expect("start client"); - let router = ToolHandlerRouter::new( - vec![Box::new(EchoTelemetryTool { - name: tool_name.to_string(), - })], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); + let echo_tool = Tool::new(tool_name) + .with_description("Echoes a marker string for telemetry validation.") + .with_parameters(json!({ + "type": "object", + "properties": { + "value": { "type": "string" } + }, + "required": ["value"] + })) + .with_handler(Arc::new(EchoTelemetryTool)); let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) - .with_tools(tools), + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![echo_tool]), ) .await .expect("create session"); @@ -136,24 +139,10 @@ async fn should_export_file_telemetry_for_sdk_interactions() { .await; } -struct EchoTelemetryTool { - name: String, -} +struct EchoTelemetryTool; #[async_trait] impl ToolHandler for EchoTelemetryTool { - fn tool(&self) -> Tool { - Tool::new(&self.name) - .with_description("Echoes a marker string for telemetry validation.") - .with_parameters(json!({ - "type": "object", - "properties": { - "value": { "type": "string" } - }, - "required": ["value"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { Ok(ToolResult::Text( invocation diff --git a/rust/tests/e2e/tool_results.rs b/rust/tests/e2e/tool_results.rs index 260e25993..4b731c286 100644 --- a/rust/tests/e2e/tool_results.rs +++ b/rust/tests/e2e/tool_results.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use github_copilot_sdk::generated::session_events::{SessionEventType, ToolExecutionCompleteData}; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{ToolHandler, ToolHandlerRouter}; +use github_copilot_sdk::tool::ToolHandler; use github_copilot_sdk::{ Error, SessionConfig, Tool, ToolInvocation, ToolResult, ToolResultExpanded, }; @@ -21,7 +21,7 @@ async fn should_handle_structured_toolresultobject_from_custom_tool() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let session = create_tool_session(ctx, &client, WeatherTool).await; + let session = create_tool_session(ctx, &client, weather_tool()).await; let answer = session .send_and_wait("What's the weather in Paris?") @@ -48,7 +48,7 @@ async fn should_handle_tool_result_with_failure_resulttype() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let session = create_tool_session(ctx, &client, CheckStatusTool).await; + let session = create_tool_session(ctx, &client, check_status_tool()).await; let answer = session .send_and_wait("Check the status of the service using check_status. If it fails, say 'service is down'.") @@ -76,7 +76,7 @@ async fn should_preserve_tooltelemetry_and_not_stringify_structured_results_for_ Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let session = create_tool_session(ctx, &client, AnalyzeCodeTool).await; + let session = create_tool_session(ctx, &client, analyze_code_tool()).await; let answer = session .send_and_wait("Analyze the file main.ts for issues.") @@ -124,7 +124,7 @@ async fn should_handle_tool_result_with_rejected_resulttype() { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let (call_tx, mut call_rx) = mpsc::unbounded_channel(); - let session = create_tool_session(ctx, &client, DeployTool { call_tx }).await; + let session = create_tool_session(ctx, &client, deploy_tool(call_tx)).await; let events = session.subscribe(); session @@ -161,7 +161,7 @@ async fn should_handle_tool_result_with_denied_resulttype() { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let (call_tx, mut call_rx) = mpsc::unbounded_channel(); - let session = create_tool_session(ctx, &client, AccessSecretTool { call_tx }).await; + let session = create_tool_session(ctx, &client, access_secret_tool(call_tx)).await; let events = session.subscribe(); session @@ -188,22 +188,18 @@ async fn should_handle_tool_result_with_denied_resulttype() { .await; } -async fn create_tool_session( +async fn create_tool_session( _ctx: &super::support::E2eContext, client: &github_copilot_sdk::Client, - tool: T, -) -> github_copilot_sdk::session::Session -where - T: ToolHandler + 'static, -{ - let router = ToolHandlerRouter::new(vec![Box::new(tool)], Arc::new(ApproveAllHandler)); - let tools = router.tools(); + tool: Tool, +) -> github_copilot_sdk::session::Session { + let __perm = Arc::new(ApproveAllHandler); client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) - .with_tools(tools), + .with_permission_handler(__perm) + .with_tools(vec![tool]), ) .await .expect("create session") @@ -227,19 +223,20 @@ fn expanded(text: impl Into, result_type: impl Into) -> ToolResu }) } +fn weather_tool() -> Tool { + string_tool( + "get_weather", + "Gets weather for a city", + "city", + "City name", + ) + .with_handler(Arc::new(WeatherTool)) +} + struct WeatherTool; #[async_trait::async_trait] impl ToolHandler for WeatherTool { - fn tool(&self) -> Tool { - string_tool( - "get_weather", - "Gets weather for a city", - "city", - "City name", - ) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let city = invocation .arguments @@ -253,14 +250,16 @@ impl ToolHandler for WeatherTool { } } +fn check_status_tool() -> Tool { + Tool::new("check_status") + .with_description("Checks the status of a service") + .with_handler(Arc::new(CheckStatusTool)) +} + struct CheckStatusTool; #[async_trait::async_trait] impl ToolHandler for CheckStatusTool { - fn tool(&self) -> Tool { - Tool::new("check_status").with_description("Checks the status of a service") - } - async fn call(&self, _invocation: ToolInvocation) -> Result { let mut result = match expanded("Service unavailable", "failure") { ToolResult::Expanded(result) => result, @@ -271,19 +270,20 @@ impl ToolHandler for CheckStatusTool { } } +fn analyze_code_tool() -> Tool { + string_tool( + "analyze_code", + "Analyzes code for issues", + "file", + "File to analyze", + ) + .with_handler(Arc::new(AnalyzeCodeTool)) +} + struct AnalyzeCodeTool; #[async_trait::async_trait] impl ToolHandler for AnalyzeCodeTool { - fn tool(&self) -> Tool { - string_tool( - "analyze_code", - "Analyzes code for issues", - "file", - "File to analyze", - ) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let file = invocation .arguments @@ -302,16 +302,18 @@ impl ToolHandler for AnalyzeCodeTool { } } +fn deploy_tool(call_tx: mpsc::UnboundedSender<()>) -> Tool { + Tool::new("deploy_service") + .with_description("Deploys a service") + .with_handler(Arc::new(DeployTool { call_tx })) +} + struct DeployTool { call_tx: mpsc::UnboundedSender<()>, } #[async_trait::async_trait] impl ToolHandler for DeployTool { - fn tool(&self) -> Tool { - Tool::new("deploy_service").with_description("Deploys a service") - } - async fn call(&self, _invocation: ToolInvocation) -> Result { let _ = self.call_tx.send(()); Ok(expanded( @@ -321,16 +323,18 @@ impl ToolHandler for DeployTool { } } +fn access_secret_tool(call_tx: mpsc::UnboundedSender<()>) -> Tool { + Tool::new("access_secret") + .with_description("Accesses a secret") + .with_handler(Arc::new(AccessSecretTool { call_tx })) +} + struct AccessSecretTool { call_tx: mpsc::UnboundedSender<()>, } #[async_trait::async_trait] impl ToolHandler for AccessSecretTool { - fn tool(&self) -> Tool { - Tool::new("access_secret").with_description("Accesses a secret") - } - async fn call(&self, _invocation: ToolInvocation) -> Result { let _ = self.call_tx.send(()); Ok(expanded( diff --git a/rust/tests/e2e/tools.rs b/rust/tests/e2e/tools.rs index 19cc40249..85d15b571 100644 --- a/rust/tests/e2e/tools.rs +++ b/rust/tests/e2e/tools.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use github_copilot_sdk::handler::{ApproveAllHandler, PermissionResult, SessionHandler}; -use github_copilot_sdk::tool::{ToolHandler, ToolHandlerRouter}; +use github_copilot_sdk::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; +use github_copilot_sdk::tool::ToolHandler; use github_copilot_sdk::{ Error, PermissionRequestData, RequestId, SessionConfig, SessionId, Tool, ToolInvocation, ToolResult, @@ -47,16 +47,13 @@ async fn invokes_custom_tool() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let router = ToolHandlerRouter::new( - vec![Box::new(EncryptStringTool)], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); + let __perm = Arc::new(ApproveAllHandler); + let tools = vec![encrypt_string_tool()]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools), ) .await @@ -82,14 +79,13 @@ async fn handles_tool_calling_errors() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let router = - ToolHandlerRouter::new(vec![Box::new(ErrorTool)], Arc::new(ApproveAllHandler)); - let tools = router.tools(); + let __perm = Arc::new(ApproveAllHandler); + let tools = vec![error_tool()]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools), ) .await @@ -132,16 +128,13 @@ async fn can_receive_and_return_complex_types() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let router = ToolHandlerRouter::new( - vec![Box::new(DbQueryTool)], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); + let __perm = Arc::new(ApproveAllHandler); + let tools = vec![db_query_tool()]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools), ) .await @@ -174,14 +167,13 @@ async fn overrides_built_in_tool_with_custom_tool() { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; - let router = - ToolHandlerRouter::new(vec![Box::new(CustomGrepTool)], Arc::new(ApproveAllHandler)); - let tools = router.tools(); + let __perm = Arc::new(ApproveAllHandler); + let tools = vec![custom_grep_tool()]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools), ) .await @@ -210,15 +202,15 @@ async fn skippermission_sent_in_tool_definition() { let (permission_tx, mut permission_rx) = mpsc::unbounded_channel(); let handler = Arc::new(RecordingPermissionHandler { permission_tx, - decision: PermissionResult::Denied, + decision: PermissionResult::reject(None), }); - let router = ToolHandlerRouter::new(vec![Box::new(SafeLookupTool)], handler); - let tools = router.tools(); + let __perm = handler; + let tools = vec![safe_lookup_tool()]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools), ) .await @@ -260,15 +252,15 @@ async fn invokes_custom_tool_with_permission_handler() { let (permission_tx, mut permission_rx) = mpsc::unbounded_channel(); let handler = Arc::new(RecordingPermissionHandler { permission_tx, - decision: PermissionResult::Approved, + decision: PermissionResult::approve_once(), }); - let router = ToolHandlerRouter::new(vec![Box::new(EncryptStringTool)], handler); - let tools = router.tools(); + let __perm = handler; + let tools = vec![encrypt_string_tool()]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools), ) .await @@ -304,18 +296,15 @@ async fn denies_custom_tool_when_permission_denied() { let (permission_tx, _permission_rx) = mpsc::unbounded_channel(); let handler = Arc::new(RecordingPermissionHandler { permission_tx, - decision: PermissionResult::Denied, + decision: PermissionResult::reject(None), }); - let router = ToolHandlerRouter::new( - vec![Box::new(TrackedEncryptStringTool { call_tx })], - handler, - ); - let tools = router.tools(); + let __perm = handler; + let tools = vec![tracked_encrypt_string_tool(call_tx)]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools), ) .await @@ -351,21 +340,16 @@ async fn should_execute_multiple_custom_tools_in_parallel_single_turn() { let client = ctx.start_client().await; let (city_tx, mut city_rx) = mpsc::unbounded_channel(); let (country_tx, mut country_rx) = mpsc::unbounded_channel(); - let router = ToolHandlerRouter::new( - vec![ - Box::new(LookupCityTool { call_tx: city_tx }), - Box::new(LookupCountryTool { - call_tx: country_tx, - }), - ], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); + let __perm = Arc::new(ApproveAllHandler); + let tools = vec![ + lookup_city_tool(city_tx), + lookup_country_tool(country_tx), + ]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools), ) .await @@ -403,21 +387,13 @@ async fn should_respect_availabletools_and_excludedtools_combined() { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let (excluded_tx, mut excluded_rx) = mpsc::unbounded_channel(); - let router = ToolHandlerRouter::new( - vec![ - Box::new(AllowedTool), - Box::new(ExcludedTool { - call_tx: excluded_tx, - }), - ], - Arc::new(ApproveAllHandler), - ); - let tools = router.tools(); + let __perm = Arc::new(ApproveAllHandler); + let tools = vec![allowed_tool(), excluded_tool(excluded_tx)]; let session = client .create_session( SessionConfig::default() .with_github_token(super::support::DEFAULT_TEST_TOKEN) - .with_handler(Arc::new(router)) + .with_permission_handler(__perm) .with_tools(tools) .with_available_tools(["allowed_tool", "excluded_tool"]) .with_excluded_tools(["excluded_tool"]), @@ -450,23 +426,24 @@ async fn should_respect_availabletools_and_excludedtools_combined() { struct EncryptStringTool; +fn encrypt_string_tool() -> Tool { + Tool::new("encrypt_string") + .with_description("Encrypts a string") + .with_parameters(json!({ + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "String to encrypt" + } + }, + "required": ["input"] + })) + .with_handler(Arc::new(EncryptStringTool)) +} + #[async_trait::async_trait] impl ToolHandler for EncryptStringTool { - fn tool(&self) -> Tool { - Tool::new("encrypt_string") - .with_description("Encrypts a string") - .with_parameters(json!({ - "type": "object", - "properties": { - "input": { - "type": "string", - "description": "String to encrypt" - } - }, - "required": ["input"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let input = invocation .arguments @@ -481,12 +458,24 @@ struct TrackedEncryptStringTool { call_tx: mpsc::UnboundedSender<()>, } +fn tracked_encrypt_string_tool(call_tx: mpsc::UnboundedSender<()>) -> Tool { + Tool::new("encrypt_string") + .with_description("Encrypts a string") + .with_parameters(json!({ + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "String to encrypt" + } + }, + "required": ["input"] + })) + .with_handler(Arc::new(TrackedEncryptStringTool { call_tx })) +} + #[async_trait::async_trait] impl ToolHandler for TrackedEncryptStringTool { - fn tool(&self) -> Tool { - EncryptStringTool.tool() - } - async fn call(&self, invocation: ToolInvocation) -> Result { let _ = self.call_tx.send(()); EncryptStringTool.call(invocation).await @@ -495,12 +484,14 @@ impl ToolHandler for TrackedEncryptStringTool { struct ErrorTool; +fn error_tool() -> Tool { + Tool::new("get_user_location") + .with_description("Gets the user's location") + .with_handler(Arc::new(ErrorTool)) +} + #[async_trait::async_trait] impl ToolHandler for ErrorTool { - fn tool(&self) -> Tool { - Tool::new("get_user_location").with_description("Gets the user's location") - } - async fn call(&self, _invocation: ToolInvocation) -> Result { Ok(ToolResult::Text( "Failed to execute `get_user_location` tool with arguments: {} due to error: Error: Tool execution failed" @@ -511,21 +502,22 @@ impl ToolHandler for ErrorTool { struct CustomGrepTool; +fn custom_grep_tool() -> Tool { + Tool::new("grep") + .with_description("A custom grep implementation that overrides the built-in") + .with_overrides_built_in_tool(true) + .with_parameters(json!({ + "type": "object", + "properties": { + "query": { "type": "string", "description": "Search query" } + }, + "required": ["query"] + })) + .with_handler(Arc::new(CustomGrepTool)) +} + #[async_trait::async_trait] impl ToolHandler for CustomGrepTool { - fn tool(&self) -> Tool { - Tool::new("grep") - .with_description("A custom grep implementation that overrides the built-in") - .with_overrides_built_in_tool(true) - .with_parameters(json!({ - "type": "object", - "properties": { - "query": { "type": "string", "description": "Search query" } - }, - "required": ["query"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let query = invocation .arguments @@ -538,21 +530,22 @@ impl ToolHandler for CustomGrepTool { struct SafeLookupTool; +fn safe_lookup_tool() -> Tool { + Tool::new("safe_lookup") + .with_description("A tool that skips permission") + .with_skip_permission(true) + .with_parameters(json!({ + "type": "object", + "properties": { + "id": { "type": "string", "description": "Lookup ID" } + }, + "required": ["id"] + })) + .with_handler(Arc::new(SafeLookupTool)) +} + #[async_trait::async_trait] impl ToolHandler for SafeLookupTool { - fn tool(&self) -> Tool { - Tool::new("safe_lookup") - .with_description("A tool that skips permission") - .with_skip_permission(true) - .with_parameters(json!({ - "type": "object", - "properties": { - "id": { "type": "string", "description": "Lookup ID" } - }, - "required": ["id"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let id = invocation .arguments @@ -567,20 +560,21 @@ struct LookupCityTool { call_tx: mpsc::UnboundedSender, } +fn lookup_city_tool(call_tx: mpsc::UnboundedSender) -> Tool { + Tool::new("lookup_city") + .with_description("Looks up city information") + .with_parameters(json!({ + "type": "object", + "properties": { + "city": { "type": "string", "description": "City name" } + }, + "required": ["city"] + })) + .with_handler(Arc::new(LookupCityTool { call_tx })) +} + #[async_trait::async_trait] impl ToolHandler for LookupCityTool { - fn tool(&self) -> Tool { - Tool::new("lookup_city") - .with_description("Looks up city information") - .with_parameters(json!({ - "type": "object", - "properties": { - "city": { "type": "string", "description": "City name" } - }, - "required": ["city"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let city = invocation .arguments @@ -597,20 +591,21 @@ struct LookupCountryTool { call_tx: mpsc::UnboundedSender, } +fn lookup_country_tool(call_tx: mpsc::UnboundedSender) -> Tool { + Tool::new("lookup_country") + .with_description("Looks up country information") + .with_parameters(json!({ + "type": "object", + "properties": { + "country": { "type": "string", "description": "Country name" } + }, + "required": ["country"] + })) + .with_handler(Arc::new(LookupCountryTool { call_tx })) +} + #[async_trait::async_trait] impl ToolHandler for LookupCountryTool { - fn tool(&self) -> Tool { - Tool::new("lookup_country") - .with_description("Looks up country information") - .with_parameters(json!({ - "type": "object", - "properties": { - "country": { "type": "string", "description": "Country name" } - }, - "required": ["country"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let country = invocation .arguments @@ -628,20 +623,21 @@ impl ToolHandler for LookupCountryTool { struct AllowedTool; +fn allowed_tool() -> Tool { + Tool::new("allowed_tool") + .with_description("An allowed tool") + .with_parameters(json!({ + "type": "object", + "properties": { + "input": { "type": "string", "description": "Input value" } + }, + "required": ["input"] + })) + .with_handler(Arc::new(AllowedTool)) +} + #[async_trait::async_trait] impl ToolHandler for AllowedTool { - fn tool(&self) -> Tool { - Tool::new("allowed_tool") - .with_description("An allowed tool") - .with_parameters(json!({ - "type": "object", - "properties": { - "input": { "type": "string", "description": "Input value" } - }, - "required": ["input"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let input = invocation .arguments @@ -659,20 +655,21 @@ struct ExcludedTool { call_tx: mpsc::UnboundedSender<()>, } +fn excluded_tool(call_tx: mpsc::UnboundedSender<()>) -> Tool { + Tool::new("excluded_tool") + .with_description("A tool that should be excluded") + .with_parameters(json!({ + "type": "object", + "properties": { + "input": { "type": "string", "description": "Input value" } + }, + "required": ["input"] + })) + .with_handler(Arc::new(ExcludedTool { call_tx })) +} + #[async_trait::async_trait] impl ToolHandler for ExcludedTool { - fn tool(&self) -> Tool { - Tool::new("excluded_tool") - .with_description("A tool that should be excluded") - .with_parameters(json!({ - "type": "object", - "properties": { - "input": { "type": "string", "description": "Input value" } - }, - "required": ["input"] - })) - } - async fn call(&self, invocation: ToolInvocation) -> Result { let _ = self.call_tx.send(()); let input = invocation @@ -693,8 +690,8 @@ struct RecordingPermissionHandler { } #[async_trait::async_trait] -impl SessionHandler for RecordingPermissionHandler { - async fn on_permission_request( +impl PermissionHandler for RecordingPermissionHandler { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, @@ -707,31 +704,32 @@ impl SessionHandler for RecordingPermissionHandler { struct DbQueryTool; -#[async_trait::async_trait] -impl ToolHandler for DbQueryTool { - fn tool(&self) -> Tool { - Tool::new("db_query") - .with_description("Performs a database query") - .with_parameters(json!({ - "type": "object", - "properties": { - "query": { - "type": "object", - "properties": { - "table": { "type": "string" }, - "ids": { - "type": "array", - "items": { "type": "integer" } - }, - "sortAscending": { "type": "boolean" } +fn db_query_tool() -> Tool { + Tool::new("db_query") + .with_description("Performs a database query") + .with_parameters(json!({ + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "table": { "type": "string" }, + "ids": { + "type": "array", + "items": { "type": "integer" } }, - "required": ["table", "ids", "sortAscending"] - } - }, - "required": ["query"] - })) - } + "sortAscending": { "type": "boolean" } + }, + "required": ["table", "ids", "sortAscending"] + } + }, + "required": ["query"] + })) + .with_handler(Arc::new(DbQueryTool)) +} +#[async_trait::async_trait] +impl ToolHandler for DbQueryTool { async fn call(&self, invocation: ToolInvocation) -> Result { let query = invocation.arguments.get("query").expect("query argument"); assert_eq!( diff --git a/rust/tests/integration_test.rs b/rust/tests/integration_test.rs index a02bf01c0..9dd71223b 100644 --- a/rust/tests/integration_test.rs +++ b/rust/tests/integration_test.rs @@ -2,7 +2,6 @@ use std::time::Instant; -use github_copilot_sdk::resolve::copilot_binary_with_source; use github_copilot_sdk::{Client, ClientOptions, SDK_PROTOCOL_VERSION}; fn default_options() -> ClientOptions { @@ -89,11 +88,8 @@ async fn cli_operation_latency() { client2.stop().await.expect("stop second client failed"); - let (cli_path, source) = copilot_binary_with_source().expect("copilot binary not found"); - eprintln!(); eprintln!("=== CLI operation latency ==="); - eprintln!(" binary: {} ({:?})", cli_path.display(), source); eprintln!(" cold Client::start: {:>8.1?}", cold_start); eprintln!(" warm ping(): {:>8.1?}", warm_ping); eprintln!( diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index e055b17fd..050c5898d 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -6,30 +6,55 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; use async_trait::async_trait; -use github_copilot_sdk::Client; +use github_copilot_sdk::canvas::{ + CanvasActionContext, CanvasDeclaration, CanvasHandler, CanvasOpenContext, CanvasOpenResponse, + CanvasResult, +}; +use github_copilot_sdk::generated::api_types::{CanvasInstanceAvailability, OpenCanvasInstance}; use github_copilot_sdk::handler::{ - ApproveAllHandler, AutoModeSwitchResponse, ExitPlanModeResult, HandlerEvent, HandlerResponse, - PermissionResult, SessionHandler, UserInputResponse, + ApproveAllHandler, AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, + ExitPlanModeHandler, ExitPlanModeResult, UserInputHandler, UserInputResponse, }; use github_copilot_sdk::types::{ - CommandContext, CommandDefinition, CommandHandler, DeliveryMode, ExitPlanModeData, - MessageOptions, SessionConfig, SessionId, ToolResult, + CommandContext, CommandDefinition, CommandHandler, DeliveryMode, ElicitationRequest, + ElicitationResult, ExitPlanModeData, ExtensionInfo, MessageOptions, RequestId, SessionConfig, + SessionId, Tool, ToolInvocation, ToolResult, }; +use github_copilot_sdk::{Client, tool}; use serde_json::Value; use tokio::io::{AsyncWrite, AsyncWriteExt, duplex}; -use tokio::sync::mpsc; use tokio::time::timeout; const TIMEOUT: Duration = Duration::from_secs(2); -struct NoopHandler; +struct TestCanvasHandler; + #[async_trait] -impl SessionHandler for NoopHandler { - async fn on_event(&self, _event: HandlerEvent) -> HandlerResponse { - HandlerResponse::Ok +impl CanvasHandler for TestCanvasHandler { + async fn on_open(&self, ctx: CanvasOpenContext) -> CanvasResult { + Ok(CanvasOpenResponse { + url: Some(format!("https://example.test/{}", ctx.canvas_id)), + title: Some("Test Canvas".to_string()), + status: Some("ready".to_string()), + }) + } + + async fn on_action(&self, ctx: CanvasActionContext) -> CanvasResult { + Ok(serde_json::json!({ + "actionName": ctx.action_name, + "input": ctx.input, + })) } } +fn test_canvas(id: &str) -> CanvasDeclaration { + CanvasDeclaration::new(id, "Test Canvas", "Test canvas description") +} + +fn test_canvas_handler() -> Arc { + Arc::new(TestCanvasHandler) +} + async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), body: &[u8]) { let header = format!("Content-Length: {}\r\n\r\n", body.len()); writer.write_all(header.as_bytes()).await.unwrap(); @@ -126,16 +151,32 @@ impl FakeServer { } } -async fn create_session_pair( - handler: Arc, -) -> (github_copilot_sdk::session::Session, FakeServer) { - create_session_pair_with_capabilities(handler, serde_json::json!(null)).await +async fn create_session_pair() -> (github_copilot_sdk::session::Session, FakeServer) { + create_session_pair_with_config(|cfg| cfg).await } async fn create_session_pair_with_capabilities( - handler: Arc, capabilities: Value, ) -> (github_copilot_sdk::session::Session, FakeServer) { + create_session_pair_inner(|cfg| cfg, capabilities).await +} + +async fn create_session_pair_with_config( + configure: F, +) -> (github_copilot_sdk::session::Session, FakeServer) +where + F: FnOnce(SessionConfig) -> SessionConfig + Send + 'static, +{ + create_session_pair_inner(configure, serde_json::json!(null)).await +} + +async fn create_session_pair_inner( + configure: F, + capabilities: Value, +) -> (github_copilot_sdk::session::Session, FakeServer) +where + F: FnOnce(SessionConfig) -> SessionConfig + Send + 'static, +{ let (client, server_read, server_write) = make_client(); let mut server = FakeServer { @@ -146,10 +187,9 @@ async fn create_session_pair_with_capabilities( let create_handle = tokio::spawn({ let client = client.clone(); - let handler = handler.clone(); async move { client - .create_session(SessionConfig::default().with_handler(handler)) + .create_session(configure(SessionConfig::default())) .await .unwrap() } @@ -184,7 +224,7 @@ fn requested_session_id(request: &Value) -> &str { #[tokio::test] async fn session_subscribe_yields_events_observe_only() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let mut events = session.subscribe(); let count = Arc::new(AtomicUsize::new(0)); @@ -216,7 +256,7 @@ async fn session_subscribe_yields_events_observe_only() { #[tokio::test] async fn session_subscribe_drop_stops_delivery() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let mut events = session.subscribe(); let count = Arc::new(AtomicUsize::new(0)); @@ -257,7 +297,7 @@ async fn create_session_sends_correct_rpc() { .create_session({ let mut cfg = SessionConfig::default(); cfg.model = Some("gpt-4".to_string()); - cfg.with_handler(Arc::new(NoopHandler)) + cfg }) .await .unwrap() @@ -282,9 +322,85 @@ async fn create_session_sends_correct_rpc() { assert_eq!(session.workspace_path(), Some(Path::new("/ws"))); } +#[tokio::test] +async fn create_session_sends_canvas_wire_fields() { + 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_canvases([test_canvas("counter")]) + .with_request_canvas_renderer(true) + .with_request_extensions(true) + .with_extension_info(ExtensionInfo::new("github-app", "counter-provider")), + ) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.create"); + assert_eq!(request["params"]["canvases"][0]["id"], "counter"); + assert_eq!( + request["params"]["canvases"][0]["displayName"], + "Test Canvas" + ); + assert_eq!(request["params"]["requestCanvasRenderer"], true); + assert_eq!(request["params"]["requestExtensions"], true); + assert_eq!(request["params"]["extensionInfo"]["source"], "github-app"); + assert_eq!( + request["params"]["extensionInfo"]["name"], + "counter-provider" + ); + + let id = request["id"].as_u64().unwrap(); + let session_id = requested_session_id(&request).to_string(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "sessionId": session_id }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn provider_canvas_dispatch_routes_direct_canvas_action_requests() { + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_canvases([test_canvas("counter")]) + .with_canvas_handler(test_canvas_handler()) + }) + .await; + + server + .send_request( + 42, + "canvas.action.invoke", + serde_json::json!({ + "sessionId": session.id(), + "extensionId": "project:counter", + "canvasId": "counter", + "instanceId": "counter-1", + "actionName": "increment", + "input": { "amount": 1 } + }), + ) + .await; + + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["id"], 42); + assert_eq!(response["result"]["actionName"], "increment"); + assert_eq!(response["result"]["input"]["amount"], 1); +} + #[tokio::test] async fn send_injects_session_id() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let handle = tokio::spawn({ @@ -310,7 +426,7 @@ async fn send_injects_session_id() { async fn send_serializes_request_headers() { use std::collections::HashMap; - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let handle = tokio::spawn({ @@ -343,7 +459,7 @@ async fn send_serializes_request_headers() { async fn send_omits_request_headers_when_unset_or_empty() { use std::collections::HashMap; - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let handle = tokio::spawn({ @@ -379,7 +495,7 @@ async fn send_omits_request_headers_when_unset_or_empty() { #[tokio::test] async fn session_rpc_methods_send_correct_method_names() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let cases: Vec<(&str, Option<&str>)> = vec![ @@ -394,7 +510,7 @@ async fn session_rpc_methods_send_correct_method_names() { match expected_method { "session.abort" => s.abort().await.map(|_| ()), "session.log" => s.log("test msg", None).await, - "session.destroy" => s.destroy().await, + "session.destroy" => s.disconnect().await, _ => unreachable!(), } }); @@ -544,7 +660,7 @@ fn mcp_server_config_roundtrips_through_tagged_enum() { args: vec!["server.js".to_string()], env: HashMap::new(), working_directory: None, - tools: vec!["*".to_string()], + tools: Some(vec!["*".to_string()]), timeout: None, }); let json = serde_json::to_value(&stdio).unwrap(); @@ -566,6 +682,109 @@ fn mcp_server_config_roundtrips_through_tagged_enum() { assert_eq!(cfg_json["github"]["type"], "stdio"); } +#[test] +fn mcp_stdio_tools_tri_state_serializes_correctly() { + use github_copilot_sdk::McpStdioServerConfig; + + // None → field omitted (= "expose all tools") + let cfg = McpStdioServerConfig { + command: "echo".into(), + tools: None, + ..Default::default() + }; + let json = serde_json::to_value(&cfg).unwrap(); + assert!( + json.get("tools").is_none(), + "tools=None must be omitted on the wire; got {json}" + ); + + // Some(empty) → field present as [] + let cfg = McpStdioServerConfig { + command: "echo".into(), + tools: Some(vec![]), + ..Default::default() + }; + let json = serde_json::to_value(&cfg).unwrap(); + assert_eq!(json["tools"], serde_json::json!([])); + + // Some(non-empty) → field present as the explicit list + let cfg = McpStdioServerConfig { + command: "echo".into(), + tools: Some(vec!["a".into(), "b".into()]), + ..Default::default() + }; + let json = serde_json::to_value(&cfg).unwrap(); + assert_eq!(json["tools"], serde_json::json!(["a", "b"])); +} + +#[test] +fn mcp_stdio_tools_tri_state_deserializes_correctly() { + use github_copilot_sdk::McpStdioServerConfig; + + // Missing field → None + let cfg: McpStdioServerConfig = + serde_json::from_value(serde_json::json!({ "command": "echo" })).unwrap(); + assert_eq!(cfg.tools, None); + + // Empty list → Some(empty) + let cfg: McpStdioServerConfig = + serde_json::from_value(serde_json::json!({ "command": "echo", "tools": [] })).unwrap(); + assert_eq!(cfg.tools, Some(vec![])); + + // Non-empty list → Some(list) + let cfg: McpStdioServerConfig = + serde_json::from_value(serde_json::json!({ "command": "echo", "tools": ["x"] })).unwrap(); + assert_eq!(cfg.tools, Some(vec!["x".to_string()])); +} + +#[test] +fn mcp_http_tools_tri_state_serializes_correctly() { + use github_copilot_sdk::McpHttpServerConfig; + + let cfg = McpHttpServerConfig { + url: "https://example.com".into(), + tools: None, + ..Default::default() + }; + assert!( + serde_json::to_value(&cfg).unwrap().get("tools").is_none(), + "tools=None must be omitted on the wire" + ); + + let cfg = McpHttpServerConfig { + url: "https://example.com".into(), + tools: Some(vec![]), + ..Default::default() + }; + assert_eq!( + serde_json::to_value(&cfg).unwrap()["tools"], + serde_json::json!([]) + ); + + let cfg = McpHttpServerConfig { + url: "https://example.com".into(), + tools: Some(vec!["a".into()]), + ..Default::default() + }; + assert_eq!( + serde_json::to_value(&cfg).unwrap()["tools"], + serde_json::json!(["a"]) + ); +} + +#[test] +fn mcp_http_tools_tri_state_deserializes_correctly() { + use github_copilot_sdk::McpHttpServerConfig; + + let cfg: McpHttpServerConfig = + serde_json::from_value(serde_json::json!({ "url": "https://e.com" })).unwrap(); + assert_eq!(cfg.tools, None); + + let cfg: McpHttpServerConfig = + serde_json::from_value(serde_json::json!({ "url": "https://e.com", "tools": [] })).unwrap(); + assert_eq!(cfg.tools, Some(vec![])); +} + #[test] fn permission_request_data_extracts_typed_kind() { use github_copilot_sdk::{PermissionRequestData, PermissionRequestKind}; @@ -582,9 +801,20 @@ fn permission_request_data_extracts_typed_kind() { let custom: PermissionRequestData = serde_json::from_value(serde_json::json!({ "kind": "custom-tool", + "toolName": "open_canvas", + "args": { + "extensionId": "github-app:counter-provider", + "canvasId": "counter", + "instanceId": "counter-1" + } })) .unwrap(); assert_eq!(custom.kind, Some(PermissionRequestKind::CustomTool)); + assert_eq!(custom.extra["toolName"], "open_canvas"); + assert_eq!( + custom.extra["args"]["extensionId"], + "github-app:counter-provider" + ); // Unknown kinds fall through to the catch-all variant rather than failing. let unknown: PermissionRequestData = serde_json::from_value(serde_json::json!({ @@ -599,35 +829,15 @@ async fn force_stop_is_idempotent_with_no_child() { // Stream-based clients have no child process. force_stop should be a // no-op and safe to call multiple times. let (client, _server_read, _server_write) = make_client(); - assert_eq!( - client.state(), - github_copilot_sdk::ConnectionState::Connected - ); client.force_stop(); - assert_eq!( - client.state(), - github_copilot_sdk::ConnectionState::Disconnected - ); client.force_stop(); - assert_eq!( - client.state(), - github_copilot_sdk::ConnectionState::Disconnected - ); assert!(client.pid().is_none()); } #[tokio::test] -async fn stop_transitions_state_to_disconnected() { +async fn stop_is_safe_to_call() { let (client, _server_read, _server_write) = make_client(); - assert_eq!( - client.state(), - github_copilot_sdk::ConnectionState::Connected - ); client.stop().await.expect("stop should succeed"); - assert_eq!( - client.state(), - github_copilot_sdk::ConnectionState::Disconnected - ); } #[tokio::test] @@ -961,12 +1171,12 @@ async fn list_models_returns_typed_model_info() { #[tokio::test] async fn get_messages_returns_typed_events() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let handle = tokio::spawn({ let session = session.clone(); - async move { session.get_messages().await.unwrap() } + async move { session.get_events().await.unwrap() } }); let request = server.read_request().await; @@ -990,9 +1200,40 @@ async fn get_messages_returns_typed_events() { assert_eq!(events[0].event_type, "user.message"); } +#[tokio::test] +#[allow(deprecated)] +async fn deprecated_get_messages_alias_still_works() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + + let handle = tokio::spawn({ + let session = session.clone(); + async move { session.get_messages().await.unwrap() } + }); + + let request = server.read_request().await; + assert_eq!(request["method"], "session.getMessages"); + server + .respond( + &request, + serde_json::json!({ + "events": [{ + "id": "e1", + "timestamp": "2025-01-01T00:00:00Z", + "type": "user.message", + "data": { "text": "hi" }, + }] + }), + ) + .await; + + let events = timeout(TIMEOUT, handle).await.unwrap().unwrap(); + assert_eq!(events.len(), 1); +} + #[tokio::test] async fn set_model_sends_switch_to_request() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let handle = tokio::spawn({ @@ -1015,11 +1256,9 @@ async fn set_model_sends_switch_to_request() { #[tokio::test] async fn elicitation_returns_typed_result() { - let (session, mut server) = create_session_pair_with_capabilities( - Arc::new(NoopHandler), - serde_json::json!({ "ui": { "elicitation": true } }), - ) - .await; + let (session, mut server) = + create_session_pair_with_capabilities(serde_json::json!({ "ui": { "elicitation": true } })) + .await; let session = Arc::new(session); let schema = serde_json::json!({ "type": "object", @@ -1058,94 +1297,29 @@ async fn elicitation_returns_typed_result() { assert_eq!(result.content.unwrap()["name"], "Octocat"); } -#[tokio::test] -async fn tool_call_dispatches_to_handler() { - struct ToolHandler; - #[async_trait] - impl SessionHandler for ToolHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::ExternalTool { invocation } => { - assert_eq!(invocation.tool_name, "read_file"); - HandlerResponse::ToolResult(ToolResult::Text("file contents here".to_string())) - } - _ => HandlerResponse::Ok, - } - } - } - - let (_session, mut server) = create_session_pair(Arc::new(ToolHandler)).await; - server - .send_request( - 100, - "tool.call", - serde_json::json!({ - "sessionId": server.session_id, - "toolCallId": "tc-1", - "toolName": "read_file", - "arguments": { "path": "/foo.txt" }, - }), - ) - .await; - - let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); - assert_eq!(response["id"], 100); - assert_eq!(response["result"]["result"], "file contents here"); -} - -#[tokio::test] -async fn permission_request_dispatches_to_handler() { - struct DenyHandler; - #[async_trait] - impl SessionHandler for DenyHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::PermissionRequest { .. } => { - HandlerResponse::Permission(PermissionResult::Denied) - } - _ => HandlerResponse::Ok, - } - } - } - - let (_session, mut server) = create_session_pair(Arc::new(DenyHandler)).await; - server - .send_request( - 200, - "permission.request", - serde_json::json!({ - "sessionId": server.session_id, - "requestId": "perm-1", - "kind": "shell", - }), - ) - .await; - - let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); - assert_eq!(response["id"], 200); - assert_eq!(response["result"]["kind"], "reject"); -} - #[tokio::test] async fn user_input_request_dispatches_to_handler() { struct InputHandler; #[async_trait] - impl SessionHandler for InputHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::UserInput { question, .. } => { - assert_eq!(question, "Pick a color"); - HandlerResponse::UserInput(Some(UserInputResponse { - answer: "blue".to_string(), - was_freeform: true, - })) - } - _ => HandlerResponse::Ok, - } + impl UserInputHandler for InputHandler { + async fn handle( + &self, + _session_id: SessionId, + question: String, + _choices: Option>, + _allow_freeform: Option, + ) -> Option { + assert_eq!(question, "Pick a color"); + Some(UserInputResponse { + answer: "blue".to_string(), + was_freeform: true, + }) } } - let (_session, mut server) = create_session_pair(Arc::new(InputHandler)).await; + let (_session, mut server) = + create_session_pair_with_config(|cfg| cfg.with_user_input_handler(Arc::new(InputHandler))) + .await; server .send_request( 300, @@ -1169,8 +1343,8 @@ async fn user_input_request_dispatches_to_handler() { async fn exit_plan_mode_request_dispatches_to_handler() { struct ExitHandler; #[async_trait] - impl SessionHandler for ExitHandler { - async fn on_exit_plan_mode( + impl ExitPlanModeHandler for ExitHandler { + async fn handle( &self, _session_id: SessionId, data: ExitPlanModeData, @@ -1190,7 +1364,10 @@ async fn exit_plan_mode_request_dispatches_to_handler() { } } - let (_session, mut server) = create_session_pair(Arc::new(ExitHandler)).await; + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_exit_plan_mode_handler(Arc::new(ExitHandler)) + }) + .await; server .send_request( 310, @@ -1216,8 +1393,8 @@ async fn exit_plan_mode_request_dispatches_to_handler() { async fn auto_mode_switch_request_dispatches_to_handler() { struct AutoModeHandler; #[async_trait] - impl SessionHandler for AutoModeHandler { - async fn on_auto_mode_switch( + impl AutoModeSwitchHandler for AutoModeHandler { + async fn handle( &self, _session_id: SessionId, error_code: Option, @@ -1229,7 +1406,10 @@ async fn auto_mode_switch_request_dispatches_to_handler() { } } - let (_session, mut server) = create_session_pair(Arc::new(AutoModeHandler)).await; + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_auto_mode_switch_handler(Arc::new(AutoModeHandler)) + }) + .await; server .send_request( 311, @@ -1249,7 +1429,10 @@ async fn auto_mode_switch_request_dispatches_to_handler() { #[tokio::test] async fn default_exit_plan_mode_response_omits_optional_fields() { - let (_session, mut server) = create_session_pair(Arc::new(ApproveAllHandler)).await; + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; server .send_request( 312, @@ -1282,16 +1465,19 @@ async fn user_input_requested_notification_does_not_double_dispatch() { invocations: Arc, } #[async_trait] - impl SessionHandler for CountingHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - if let HandlerEvent::UserInput { .. } = event { - self.invocations.fetch_add(1, Ordering::SeqCst); - return HandlerResponse::UserInput(Some(UserInputResponse { - answer: "ok".to_string(), - was_freeform: true, - })); - } - HandlerResponse::Ok + impl UserInputHandler for CountingHandler { + async fn handle( + &self, + _session_id: SessionId, + _question: String, + _choices: Option>, + _allow_freeform: Option, + ) -> Option { + self.invocations.fetch_add(1, Ordering::SeqCst); + Some(UserInputResponse { + answer: "ok".to_string(), + was_freeform: true, + }) } } @@ -1299,7 +1485,8 @@ async fn user_input_requested_notification_does_not_double_dispatch() { let handler = Arc::new(CountingHandler { invocations: invocations.clone(), }); - let (_session, mut server) = create_session_pair(handler).await; + let (_session, mut server) = + create_session_pair_with_config(move |cfg| cfg.with_user_input_handler(handler)).await; server .send_event( @@ -1346,80 +1533,55 @@ async fn user_input_requested_notification_does_not_double_dispatch() { #[tokio::test] async fn approve_all_handler_approves_permission() { - let (_session, mut server) = create_session_pair(Arc::new(ApproveAllHandler)).await; + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; server - .send_request( - 500, - "permission.request", + .send_event( + "permission.requested", serde_json::json!({ - "sessionId": server.session_id, "requestId": "perm-auto", - "kind": "shell", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, }), ) .await; - let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); - assert_eq!(response["result"]["kind"], "approve-once"); + + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!( + request["method"], + "session.permissions.handlePendingPermissionRequest" + ); + assert_eq!(request["params"]["requestId"], "perm-auto"); + assert_eq!(request["params"]["result"]["kind"], "approve-once"); } #[tokio::test] async fn session_event_notification_reaches_handler() { - let (event_tx, mut event_rx) = mpsc::unbounded_channel::(); - - struct EventCollector { - tx: mpsc::UnboundedSender, - } - #[async_trait] - impl SessionHandler for EventCollector { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - if let HandlerEvent::SessionEvent { event, .. } = event { - self.tx.send(event.event_type).unwrap(); - } - HandlerResponse::Ok - } - } - - let (_session, mut server) = - create_session_pair(Arc::new(EventCollector { tx: event_tx })).await; + let (session, mut server) = create_session_pair().await; + let mut sub = session.subscribe(); server .send_event("session.idle", serde_json::json!({})) .await; - let event_type = timeout(TIMEOUT, event_rx.recv()).await.unwrap().unwrap(); - assert_eq!(event_type, "session.idle"); + let event = timeout(TIMEOUT, sub.recv()).await.unwrap().unwrap(); + assert_eq!(event.event_type, "session.idle"); } #[tokio::test] async fn router_routes_to_correct_session() { let (client, mut server_read, mut server_write) = make_client(); - let (tx1, mut rx1) = mpsc::unbounded_channel::(); - let (tx2, mut rx2) = mpsc::unbounded_channel::(); - - struct Collector { - tx: mpsc::UnboundedSender, - } - #[async_trait] - impl SessionHandler for Collector { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - if let HandlerEvent::SessionEvent { event, .. } = event { - self.tx.send(event.event_type).unwrap(); - } - HandlerResponse::Ok - } - } - // Create two sessions on the same client let mut sessions = Vec::new(); let mut session_ids = Vec::new(); - for tx in [tx1, tx2] { + for _ in 0..2 { let h = tokio::spawn({ let client = client.clone(); async move { client - .create_session( - SessionConfig::default().with_handler(Arc::new(Collector { tx })), - ) + .create_session(SessionConfig::default()) .await .unwrap() } @@ -1436,7 +1598,10 @@ async fn router_routes_to_correct_session() { sessions.push(timeout(TIMEOUT, h).await.unwrap().unwrap()); } - // Event for s-two should only reach rx2 + let mut sub1 = sessions[0].subscribe(); + let mut sub2 = sessions[1].subscribe(); + + // Event for s-two should only reach sub2 let notif = serde_json::json!({ "jsonrpc": "2.0", "method": "session.event", @@ -1447,12 +1612,20 @@ async fn router_routes_to_correct_session() { }); write_framed(&mut server_write, &serde_json::to_vec(¬if).unwrap()).await; assert_eq!( - timeout(TIMEOUT, rx2.recv()).await.unwrap().unwrap(), + timeout(TIMEOUT, sub2.recv()) + .await + .unwrap() + .unwrap() + .event_type, "assistant.message" ); - assert!(rx1.try_recv().is_err()); + assert!( + timeout(Duration::from_millis(100), sub1.recv()) + .await + .is_err() + ); - // Event for s-one should only reach rx1 + // Event for s-one should only reach sub1 let notif = serde_json::json!({ "jsonrpc": "2.0", "method": "session.event", @@ -1463,15 +1636,23 @@ async fn router_routes_to_correct_session() { }); write_framed(&mut server_write, &serde_json::to_vec(¬if).unwrap()).await; assert_eq!( - timeout(TIMEOUT, rx1.recv()).await.unwrap().unwrap(), + timeout(TIMEOUT, sub1.recv()) + .await + .unwrap() + .unwrap() + .event_type, "session.idle" ); - assert!(rx2.try_recv().is_err()); + assert!( + timeout(Duration::from_millis(100), sub2.recv()) + .await + .is_err() + ); } #[tokio::test] async fn send_and_wait_returns_last_assistant_message_on_idle() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let handle = tokio::spawn({ @@ -1507,7 +1688,7 @@ async fn send_and_wait_returns_last_assistant_message_on_idle() { #[tokio::test] async fn send_and_wait_returns_error_on_session_error() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let handle = tokio::spawn({ @@ -1542,7 +1723,7 @@ async fn send_and_wait_returns_error_on_session_error() { #[tokio::test] async fn send_and_wait_times_out() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let handle = tokio::spawn({ @@ -1578,7 +1759,7 @@ async fn send_and_wait_times_out() { /// Closes RFD-400 review finding #2. #[tokio::test] async fn send_and_wait_outer_cancellation_clears_waiter() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); // First call: wrap in outer timeout much shorter than the inner @@ -1635,7 +1816,7 @@ async fn send_and_wait_outer_cancellation_clears_waiter() { /// Closes RFD-400 review finding #2. #[tokio::test] async fn send_and_wait_drop_clears_waiter() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); // Start a send_and_wait, let it install the waiter, then abort the @@ -1694,25 +1875,25 @@ async fn send_and_wait_drop_clears_waiter() { async fn stop_event_loop_completes_in_flight_handler() { struct SlowHandler; #[async_trait] - impl SessionHandler for SlowHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::UserInput { .. } => { - // Sleep so stop_event_loop has a chance to fire while - // the handler is mid-flight. The loop must wait for - // this to return rather than abort it. - tokio::time::sleep(Duration::from_millis(150)).await; - HandlerResponse::UserInput(Some(UserInputResponse { - answer: "completed".to_string(), - was_freeform: false, - })) - } - _ => HandlerResponse::Ok, - } + impl UserInputHandler for SlowHandler { + async fn handle( + &self, + _session_id: SessionId, + _question: String, + _choices: Option>, + _allow_freeform: Option, + ) -> Option { + tokio::time::sleep(Duration::from_millis(150)).await; + Some(UserInputResponse { + answer: "completed".to_string(), + was_freeform: false, + }) } } - let (session, mut server) = create_session_pair(Arc::new(SlowHandler)).await; + let (session, mut server) = + create_session_pair_with_config(|cfg| cfg.with_user_input_handler(Arc::new(SlowHandler))) + .await; let session = Arc::new(session); server @@ -1772,26 +1953,28 @@ async fn drop_session_does_not_abort_handler() { completed: Arc, } #[async_trait] - impl SessionHandler for CompletionHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::UserInput { .. } => { - tokio::time::sleep(Duration::from_millis(100)).await; - self.completed.store(true, Ordering::SeqCst); - HandlerResponse::UserInput(Some(UserInputResponse { - answer: "done".to_string(), - was_freeform: false, - })) - } - _ => HandlerResponse::Ok, - } + impl UserInputHandler for CompletionHandler { + async fn handle( + &self, + _session_id: SessionId, + _question: String, + _choices: Option>, + _allow_freeform: Option, + ) -> Option { + tokio::time::sleep(Duration::from_millis(100)).await; + self.completed.store(true, Ordering::SeqCst); + Some(UserInputResponse { + answer: "done".to_string(), + was_freeform: false, + }) } } - let (session, mut server) = create_session_pair(Arc::new(CompletionHandler { + let handler = Arc::new(CompletionHandler { completed: handler_completed.clone(), - })) - .await; + }); + let (session, mut server) = + create_session_pair_with_config(move |cfg| cfg.with_user_input_handler(handler)).await; server .send_request( @@ -1826,8 +2009,10 @@ async fn drop_session_does_not_abort_handler() { /// session itself. #[tokio::test] async fn cancellation_token_fires_on_session_drop() { - let handler = Arc::new(ApproveAllHandler); - let (session, _server) = create_session_pair(handler).await; + let (session, _server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; let token = session.cancellation_token(); assert!(!token.is_cancelled()); @@ -1847,8 +2032,10 @@ async fn cancellation_token_fires_on_session_drop() { /// logic from the session's own lifecycle. #[tokio::test] async fn cancellation_token_child_cancel_does_not_kill_session() { - let handler = Arc::new(ApproveAllHandler); - let (session, _server) = create_session_pair(handler).await; + let (session, _server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; let child = session.cancellation_token(); child.cancel(); @@ -1865,22 +2052,25 @@ async fn elicitation_requested_dispatches_to_handler_and_responds() { struct ElicitHandler; #[async_trait] - impl SessionHandler for ElicitHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::ElicitationRequest { request, .. } => { - assert_eq!(request.message, "Enter your name"); - HandlerResponse::Elicitation(ElicitationResult { - action: "accept".to_string(), - content: Some(serde_json::json!({ "name": "Alice" })), - }) - } - _ => HandlerResponse::Ok, + impl ElicitationHandler for ElicitHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + request: ElicitationRequest, + ) -> ElicitationResult { + assert_eq!(request.message, "Enter your name"); + ElicitationResult { + action: "accept".to_string(), + content: Some(serde_json::json!({ "name": "Alice" })), } } } - let (_session, mut server) = create_session_pair(Arc::new(ElicitHandler)).await; + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_elicitation_handler(Arc::new(ElicitHandler)) + }) + .await; // CLI broadcasts elicitation.requested as a session event notification server @@ -1911,17 +2101,23 @@ async fn elicitation_requested_dispatches_to_handler_and_responds() { async fn elicitation_requested_cancels_on_handler_error() { struct FailHandler; #[async_trait] - impl SessionHandler for FailHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - // Return Ok instead of Elicitation — SDK should treat as cancel - HandlerEvent::ElicitationRequest { .. } => HandlerResponse::Ok, - _ => HandlerResponse::Ok, + impl ElicitationHandler for FailHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: ElicitationRequest, + ) -> ElicitationResult { + ElicitationResult { + action: "cancel".to_string(), + content: None, } } } - let (_session, mut server) = create_session_pair(Arc::new(FailHandler)).await; + let (_session, mut server) = + create_session_pair_with_config(|cfg| cfg.with_elicitation_handler(Arc::new(FailHandler))) + .await; server .send_event( "elicitation.requested", @@ -1939,23 +2135,29 @@ async fn elicitation_requested_cancels_on_handler_error() { #[tokio::test] async fn external_tool_requested_dispatches_to_handler_and_responds() { - struct ExternalToolHandler; + struct RunTestsTool; #[async_trait] - impl SessionHandler for ExternalToolHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::ExternalTool { invocation } => { - assert_eq!(invocation.tool_name, "run_tests"); - assert_eq!(invocation.tool_call_id, "tc-ext-1"); - assert_eq!(invocation.arguments["suite"], "unit"); - HandlerResponse::ToolResult(ToolResult::Text("all tests passed".to_string())) - } - _ => HandlerResponse::Ok, - } + impl tool::ToolHandler for RunTestsTool { + async fn call( + &self, + invocation: ToolInvocation, + ) -> Result { + assert_eq!(invocation.tool_name, "run_tests"); + assert_eq!(invocation.tool_call_id, "tc-ext-1"); + assert_eq!(invocation.arguments["suite"], "unit"); + Ok(ToolResult::Text("all tests passed".to_string())) } } - let (_session, mut server) = create_session_pair(Arc::new(ExternalToolHandler)).await; + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![ + Tool::new("run_tests") + .with_description("Run tests") + .with_parameters(serde_json::json!({"type":"object"})) + .with_handler(Arc::new(RunTestsTool)), + ]) + }) + .await; server .send_event( @@ -1976,6 +2178,132 @@ async fn external_tool_requested_dispatches_to_handler_and_responds() { assert_eq!(rpc_call["params"]["result"], "all tests passed"); } +#[tokio::test] +async fn external_tool_broadcast_for_unknown_tool_is_not_responded_to() { + // Phase H multi-client safety: a handler that doesn't claim the + // requested tool name must not send an RPC response — another client + // on the same CLI may have a real handler. + struct FooTool; + #[async_trait] + impl tool::ToolHandler for FooTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + Ok(ToolResult::Text("foo".to_string())) + } + } + + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![ + Tool::new("foo") + .with_description("foo") + .with_parameters(serde_json::json!({"type":"object"})) + .with_handler(Arc::new(FooTool)), + ]) + }) + .await; + server + .send_event( + "external_tool.requested", + serde_json::json!({ + "requestId": "req-unknown", + "sessionId": server.session_id, + "toolCallId": "tc-x", + "toolName": "bar", + "arguments": {}, + }), + ) + .await; + + // The dispatcher must NOT respond. Read with a short timeout and + // assert the read times out. + let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; + assert!( + res.is_err(), + "expected no RPC response for unknown tool, got: {:?}", + res.ok() + ); +} + +#[tokio::test] +async fn permission_broadcast_with_resolved_by_hook_is_not_responded_to() { + // Phase H: when the runtime marks a permission request as already + // resolved by a hook, the client must not respond again. + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "req-hooked", + "sessionId": server.session_id, + "resolvedByHook": true, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + + let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; + assert!( + res.is_err(), + "expected no RPC when resolvedByHook=true, got: {:?}", + res.ok() + ); +} + +#[tokio::test] +async fn permission_broadcast_with_no_claiming_handler_is_not_responded_to() { + // Phase H: a handler that doesn't claim permission dispatch must not + // respond — the SDK lets other connected clients handle the request. + let (_session, mut server) = create_session_pair().await; + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "req-pending", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + + let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; + assert!( + res.is_err(), + "expected no RPC when handler doesn't claim permission dispatch, got: {:?}", + res.ok() + ); +} + +#[tokio::test] +async fn elicitation_broadcast_with_no_claiming_handler_is_not_responded_to() { + // Phase H: same gating for elicitation. The default handler doesn't + // claim elicitation, so broadcasts are silently dropped. + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + server + .send_event( + "elicitation.requested", + serde_json::json!({ + "requestId": "elicit-silent", + "message": "should not be answered", + }), + ) + .await; + + let res = tokio::time::timeout(Duration::from_millis(150), server.read_request()).await; + assert!( + res.is_err(), + "expected no RPC when handler doesn't claim elicitation, got: {:?}", + res.ok() + ); +} + #[tokio::test] async fn capabilities_captured_from_create_response() { let (client, mut server_read, mut server_write) = make_client(); @@ -1984,7 +2312,7 @@ async fn capabilities_captured_from_create_response() { let client = client.clone(); async move { client - .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .create_session(SessionConfig::default()) .await .unwrap() } @@ -2012,7 +2340,7 @@ async fn capabilities_captured_from_create_response() { #[tokio::test] async fn capabilities_changed_event_updates_session() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; // Initially no capabilities (create_session_pair doesn't send them) assert!(session.capabilities().ui.is_none()); @@ -2051,7 +2379,9 @@ async fn request_elicitation_sent_in_create_params() { let client = client.clone(); async move { client - .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .create_session( + SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler)), + ) .await .unwrap() } @@ -2059,9 +2389,44 @@ async fn request_elicitation_sent_in_create_params() { let request = read_framed(&mut server_read).await; assert_eq!(request["method"], "session.create"); - assert_eq!(request["params"]["requestElicitation"], true); - assert_eq!(request["params"]["requestExitPlanMode"], true); - assert_eq!(request["params"]["requestAutoModeSwitch"], true); + // ApproveAllHandler claims permission dispatch only; no other handlers + // are installed, so the wire flags reflect that exact responsibility. + assert_eq!(request["params"]["requestPermission"], true); + assert_eq!(request["params"]["requestElicitation"], false); + assert_eq!(request["params"]["requestExitPlanMode"], false); + assert_eq!(request["params"]["requestAutoModeSwitch"], false); + + let id = request["id"].as_u64().unwrap(); + let session_id = requested_session_id(&request); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "sessionId": session_id }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn noop_handler_sends_request_permission_false() { + // Phase H1a wire-flag derivation: a handler that doesn't claim + // permission dispatch must send requestPermission=false so the + // runtime doesn't broadcast permission events to this client. + 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()) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["params"]["requestPermission"], false); + assert_eq!(request["params"]["requestElicitation"], false); let id = request["id"].as_u64().unwrap(); let session_id = requested_session_id(&request); @@ -2084,7 +2449,7 @@ async fn env_value_mode_hardcoded_direct_on_create_and_resume() { let client = client.clone(); async move { client - .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .create_session(SessionConfig::default()) .await .unwrap() } @@ -2108,8 +2473,7 @@ async fn env_value_mode_hardcoded_direct_on_create_and_resume() { let client = client.clone(); let session_id = session_id.clone(); async move { - let cfg = ResumeSessionConfig::new(SessionId::from(session_id)) - .with_handler(Arc::new(NoopHandler)); + let cfg = ResumeSessionConfig::new(SessionId::from(session_id)); client.resume_session(cfg).await.unwrap() } }); @@ -2136,9 +2500,89 @@ async fn env_value_mode_hardcoded_direct_on_create_and_resume() { timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); } +#[tokio::test] +async fn resume_session_sends_canvas_fields_and_captures_open_canvases() { + 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 { + let cfg = ResumeSessionConfig::new(SessionId::from("canvas-resume")) + .with_canvases([test_canvas("counter")]) + .with_request_canvas_renderer(true) + .with_request_extensions(true) + .with_extension_info(ExtensionInfo::new("github-app", "counter-provider")) + .with_open_canvases([OpenCanvasInstance { + instance_id: "counter-1".to_string(), + extension_id: "github-app:counter-provider".to_string(), + extension_name: Some("Counter Provider".to_string()), + canvas_id: "counter".to_string(), + title: Some("Counter".to_string()), + status: Some("ready".to_string()), + url: Some("https://example.test/counter".to_string()), + input: Some(serde_json::json!({ "seed": 1 })), + reopen: false, + availability: CanvasInstanceAvailability::Stale, + }]); + client.resume_session(cfg).await.unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.resume"); + assert_eq!(request["params"]["canvases"][0]["id"], "counter"); + assert_eq!(request["params"]["requestCanvasRenderer"], true); + assert_eq!(request["params"]["requestExtensions"], true); + assert_eq!(request["params"]["extensionInfo"]["source"], "github-app"); + assert_eq!( + request["params"]["extensionInfo"]["name"], + "counter-provider" + ); + assert_eq!( + request["params"]["openCanvases"][0]["availability"], + "stale" + ); + + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "sessionId": "canvas-resume", + "openCanvases": [{ + "extensionId": "project:counter", + "canvasId": "counter", + "instanceId": "counter-1", + "url": "https://example.test/counter", + "reopen": false, + "availability": "ready" + }], + "capabilities": { + "ui": { "canvases": true } + } + }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + let reload = read_framed(&mut server_read).await; + assert_eq!(reload["method"], "session.skills.reload"); + let id = reload["id"].as_u64().unwrap(); + let response = serde_json::json!({ "jsonrpc": "2.0", "id": id, "result": {} }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + let session = timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); + let open = session.open_canvases(); + assert_eq!(open.len(), 1); + assert_eq!(open[0].instance_id, "counter-1"); + assert_eq!(open[0].availability, CanvasInstanceAvailability::Ready); + let caps = session.capabilities(); + assert_eq!(caps.ui.unwrap().canvases, Some(true)); +} + #[tokio::test] async fn elicitation_methods_fail_without_capability() { - let (session, _server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, _server) = create_session_pair().await; // Session created without capabilities — elicitation should fail let err = session @@ -2163,7 +2607,6 @@ async fn elicitation_methods_fail_without_capability() { } async fn create_session_pair_with_hooks( - handler: Arc, hooks: Arc, ) -> (github_copilot_sdk::session::Session, FakeServer) { let (client, server_read, server_write) = make_client(); @@ -2176,14 +2619,9 @@ async fn create_session_pair_with_hooks( let create_handle = tokio::spawn({ let client = client.clone(); - let handler = handler.clone(); async move { client - .create_session( - SessionConfig::default() - .with_handler(handler) - .with_hooks(hooks), - ) + .create_session(SessionConfig::default().with_hooks(hooks)) .await .unwrap() } @@ -2233,8 +2671,7 @@ async fn hooks_invoke_dispatches_to_session_hooks() { } } - let (_session, mut server) = - create_session_pair_with_hooks(Arc::new(NoopHandler), Arc::new(PolicyHooks)).await; + let (_session, mut server) = create_session_pair_with_hooks(Arc::new(PolicyHooks)).await; // Send a hooks.invoke request for a denied tool server @@ -2272,8 +2709,7 @@ async fn hooks_invoke_returns_empty_for_unregistered_hook() { #[async_trait] impl SessionHooks for EmptyHooks {} - let (_session, mut server) = - create_session_pair_with_hooks(Arc::new(NoopHandler), Arc::new(EmptyHooks)).await; + let (_session, mut server) = create_session_pair_with_hooks(Arc::new(EmptyHooks)).await; server .send_request( @@ -2297,8 +2733,7 @@ async fn hooks_invoke_returns_empty_for_unregistered_hook() { assert_eq!(response["result"]["output"], serde_json::json!({})); } -async fn create_session_pair_with_transforms( - handler: Arc, +async fn create_session_pair_with_system_message_transforms( transforms: Arc, ) -> (github_copilot_sdk::session::Session, FakeServer) { let (client, server_read, server_write) = make_client(); @@ -2311,14 +2746,9 @@ async fn create_session_pair_with_transforms( let create_handle = tokio::spawn({ let client = client.clone(); - let handler = handler.clone(); async move { client - .create_session( - SessionConfig::default() - .with_handler(handler) - .with_transform(transforms), - ) + .create_session(SessionConfig::default().with_system_message_transform(transforms)) .await .unwrap() } @@ -2365,7 +2795,7 @@ async fn system_message_transform_dispatches_to_transform() { } let (_session, mut server) = - create_session_pair_with_transforms(Arc::new(NoopHandler), Arc::new(AppendTransform)).await; + create_session_pair_with_system_message_transforms(Arc::new(AppendTransform)).await; server .send_request( @@ -2410,7 +2840,7 @@ async fn system_message_transform_returns_error_for_missing_sections() { } let (_session, mut server) = - create_session_pair_with_transforms(Arc::new(NoopHandler), Arc::new(DummyTransform)).await; + create_session_pair_with_system_message_transforms(Arc::new(DummyTransform)).await; // Send request with no sections parameter server @@ -2430,7 +2860,7 @@ async fn system_message_transform_returns_error_for_missing_sections() { #[tokio::test] async fn rpc_namespace_session_agent_list_dispatches_correctly() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let s = session.clone(); @@ -2449,7 +2879,7 @@ async fn rpc_namespace_session_agent_list_dispatches_correctly() { #[tokio::test] async fn rpc_namespace_session_tasks_list_dispatches_correctly() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let s = session.clone(); @@ -2468,7 +2898,7 @@ async fn rpc_namespace_session_tasks_list_dispatches_correctly() { #[tokio::test] async fn rpc_namespace_client_models_list_dispatches_correctly() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let client = session.client().clone(); @@ -2501,7 +2931,7 @@ async fn client_stop_sends_session_destroy_for_each_active_session() { let client = client.clone(); async move { client - .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .create_session(SessionConfig::default()) .await .unwrap() } @@ -2521,7 +2951,7 @@ async fn client_stop_sends_session_destroy_for_each_active_session() { let client = client.clone(); async move { client - .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .create_session(SessionConfig::default()) .await .unwrap() } @@ -2563,7 +2993,7 @@ async fn client_stop_sends_session_destroy_for_each_active_session() { async fn client_stop_aggregates_session_destroy_errors() { // session.destroy fails on the wire — Client::stop returns // StopErrors carrying the failure rather than short-circuiting. - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let client = session.client().clone(); let stop_handle = tokio::spawn(async move { client.stop().await }); @@ -2593,45 +3023,25 @@ fn session_config_serializes_bucket_b_fields() { CloudSessionOptions, CloudSessionRepository, SessionConfig, SessionId, }; - let cfg = { - let mut cfg = SessionConfig::default(); - cfg.session_id = Some(SessionId::from("custom-id")); - cfg.config_dir = Some(PathBuf::from("/tmp/cfg")); - cfg.working_directory = Some(PathBuf::from("/tmp/work")); - cfg.github_token = Some("ghs_secret".to_string()); - cfg.include_sub_agent_streaming_events = Some(false); - cfg.enable_session_telemetry = Some(false); - cfg.remote_session = - Some(github_copilot_sdk::generated::api_types::RemoteSessionMode::Export); - cfg.cloud = Some(CloudSessionOptions::with_repository( - CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), - )); - cfg - }; - let json = serde_json::to_value(&cfg).unwrap(); - assert_eq!(json["sessionId"], "custom-id"); - assert_eq!(json["configDir"], "/tmp/cfg"); - assert_eq!(json["workingDirectory"], "/tmp/work"); - assert_eq!(json["gitHubToken"], "ghs_secret"); - assert_eq!(json["includeSubAgentStreamingEvents"], false); - assert_eq!(json["enableSessionTelemetry"], false); - assert_eq!(json["remoteSession"], "export"); - assert_eq!(json["cloud"]["repository"]["owner"], "github"); - assert_eq!(json["cloud"]["repository"]["name"], "copilot-sdk"); - assert_eq!(json["cloud"]["repository"]["branch"], "main"); + let mut cfg = SessionConfig::default(); + cfg.session_id = Some(SessionId::from("custom-id")); + cfg.config_dir = Some(PathBuf::from("/tmp/cfg")); + cfg.working_directory = Some(PathBuf::from("/tmp/work")); + cfg.github_token = Some("ghs_secret".to_string()); + cfg.include_sub_agent_streaming_events = Some(false); + cfg.enable_session_telemetry = Some(false); + cfg.remote_session = Some(github_copilot_sdk::generated::api_types::RemoteSessionMode::Export); + cfg.cloud = Some(CloudSessionOptions::with_repository( + CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), + )); // Debug never leaks the token. let debug = format!("{cfg:?}"); assert!(!debug.contains("ghs_secret"), "leaked token: {debug}"); assert!(debug.contains(""), "missing redaction: {debug}"); - - // Unset fields are omitted on the wire. - let empty = serde_json::to_value(SessionConfig::default()).unwrap(); - assert!(empty.get("sessionId").is_none()); - assert!(empty.get("gitHubToken").is_none()); - assert!(empty.get("enableSessionTelemetry").is_none()); - assert!(empty.get("remoteSession").is_none()); - assert!(empty.get("cloud").is_none()); + // Wire-format coverage now lives in the in-crate unit tests next to + // `SessionConfig::into_wire` — the wire payload is `pub(crate)` so + // external integration tests can only inspect the user-facing config. } #[test] @@ -2647,22 +3057,11 @@ fn resume_session_config_serializes_bucket_b_fields() { cfg.include_sub_agent_streaming_events = Some(true); cfg.enable_session_telemetry = Some(false); cfg.remote_session = Some(github_copilot_sdk::generated::api_types::RemoteSessionMode::On); - let json = serde_json::to_value(&cfg).unwrap(); - assert_eq!(json["sessionId"], "sess-1"); - assert_eq!(json["workingDirectory"], "/tmp/work"); - assert_eq!(json["configDir"], "/tmp/cfg"); - assert_eq!(json["gitHubToken"], "ghs_secret"); - assert_eq!(json["includeSubAgentStreamingEvents"], true); - assert_eq!(json["enableSessionTelemetry"], false); - assert_eq!(json["remoteSession"], "on"); - - // Unset remote_session is omitted on the wire. - let empty = ResumeSessionConfig::new(SessionId::from("sess-2")); - let empty_json = serde_json::to_value(&empty).unwrap(); - assert!(empty_json.get("remoteSession").is_none()); let debug = format!("{cfg:?}"); assert!(!debug.contains("ghs_secret"), "leaked token: {debug}"); + // Wire-format coverage lives in the in-crate unit tests; see + // `ResumeSessionConfig::into_wire`. } // ===================================================================== @@ -2689,7 +3088,6 @@ impl CommandHandler for CountingCommandHandler { } async fn create_session_pair_with_commands( - handler: Arc, commands: Vec, ) -> (github_copilot_sdk::session::Session, FakeServer, Value) { let (client, server_read, server_write) = make_client(); @@ -2702,14 +3100,9 @@ async fn create_session_pair_with_commands( let create_handle = tokio::spawn({ let client = client.clone(); - let handler = handler.clone(); async move { client - .create_session( - SessionConfig::default() - .with_handler(handler) - .with_commands(commands), - ) + .create_session(SessionConfig::default().with_commands(commands)) .await .unwrap() } @@ -2753,8 +3146,7 @@ async fn create_serializes_commands_strips_handler() { ), ]; - let (_session, _server, create_req) = - create_session_pair_with_commands(Arc::new(NoopHandler), commands).await; + let (_session, _server, create_req) = create_session_pair_with_commands(commands).await; let wire = create_req["params"]["commands"] .as_array() @@ -2793,8 +3185,7 @@ async fn command_execute_dispatches_to_registered_handler_and_acks_success() { }), )]; - let (session, mut server, _) = - create_session_pair_with_commands(Arc::new(NoopHandler), commands).await; + let (session, mut server, _) = create_session_pair_with_commands(commands).await; server .send_event( @@ -2839,8 +3230,7 @@ async fn command_execute_dispatches_to_registered_handler_and_acks_success() { #[tokio::test] async fn command_execute_unknown_command_acks_with_error() { - let (session, mut server, _) = - create_session_pair_with_commands(Arc::new(NoopHandler), vec![]).await; + let (session, mut server, _) = create_session_pair_with_commands(vec![]).await; server .send_event( @@ -2878,8 +3268,7 @@ async fn command_execute_handler_error_propagates_to_ack() { }), )]; - let (_session, mut server, _) = - create_session_pair_with_commands(Arc::new(NoopHandler), commands).await; + let (_session, mut server, _) = create_session_pair_with_commands(commands).await; server .send_event( @@ -3040,7 +3429,6 @@ impl SessionFsSqliteProvider for RecordingFsProvider { } async fn create_session_pair_with_fs_provider( - handler: Arc, provider: Arc, ) -> (github_copilot_sdk::session::Session, FakeServer) { let (client, server_read, server_write) = make_client(); @@ -3053,14 +3441,9 @@ async fn create_session_pair_with_fs_provider( let create_handle = tokio::spawn({ let client = client.clone(); - let handler = handler.clone(); async move { client - .create_session( - SessionConfig::default() - .with_handler(handler) - .with_session_fs_provider(provider), - ) + .create_session(SessionConfig::default().with_session_fs_provider(provider)) .await .unwrap() } @@ -3086,8 +3469,7 @@ async fn create_session_pair_with_fs_provider( #[tokio::test] async fn session_fs_dispatches_read_file_to_provider() { let provider = Arc::new(RecordingFsProvider::new().with_file("/foo.txt", "hello world")); - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), provider).await; + let (_session, mut server) = create_session_pair_with_fs_provider(provider).await; server .send_request( @@ -3106,8 +3488,7 @@ async fn session_fs_dispatches_read_file_to_provider() { #[tokio::test] async fn session_fs_maps_not_found_to_enoent() { let provider = Arc::new(RecordingFsProvider::new()); - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), provider).await; + let (_session, mut server) = create_session_pair_with_fs_provider(provider).await; server .send_request( @@ -3134,8 +3515,7 @@ async fn session_fs_maps_other_to_unknown() { } } - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), Arc::new(AlwaysFails)).await; + let (_session, mut server) = create_session_pair_with_fs_provider(Arc::new(AlwaysFails)).await; server .send_request( @@ -3159,8 +3539,7 @@ async fn session_fs_maps_other_to_unknown() { #[tokio::test] async fn session_fs_dispatches_sqlite_query_to_provider() { let provider = Arc::new(RecordingFsProvider::new()); - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), provider).await; + let (_session, mut server) = create_session_pair_with_fs_provider(provider).await; server .send_request( @@ -3191,8 +3570,7 @@ async fn session_fs_dispatches_sqlite_query_to_provider() { #[tokio::test] async fn session_fs_dispatches_sqlite_exists_to_provider() { let provider = Arc::new(RecordingFsProvider::new()); - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), provider).await; + let (_session, mut server) = create_session_pair_with_fs_provider(provider).await; server .send_request( @@ -3232,8 +3610,7 @@ async fn session_fs_maps_sqlite_errors_to_results() { } } - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), Arc::new(AlwaysFails)).await; + let (_session, mut server) = create_session_pair_with_fs_provider(Arc::new(AlwaysFails)).await; server .send_request( @@ -3275,8 +3652,7 @@ async fn session_fs_maps_sqlite_errors_to_results() { #[tokio::test] async fn session_fs_dispatches_write_file_with_mode() { let provider = Arc::new(RecordingFsProvider::new()); - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), provider.clone()).await; + let (_session, mut server) = create_session_pair_with_fs_provider(provider.clone()).await; server .send_request( @@ -3295,8 +3671,7 @@ async fn session_fs_dispatches_write_file_with_mode() { #[tokio::test] async fn session_fs_dispatches_readdir_with_types() { let provider = Arc::new(RecordingFsProvider::new()); - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), provider).await; + let (_session, mut server) = create_session_pair_with_fs_provider(provider).await; server .send_request( @@ -3318,8 +3693,7 @@ async fn session_fs_dispatches_readdir_with_types() { #[tokio::test] async fn session_fs_dispatches_rm_with_force() { let provider = Arc::new(RecordingFsProvider::new()); - let (_session, mut server) = - create_session_pair_with_fs_provider(Arc::new(NoopHandler), provider).await; + let (_session, mut server) = create_session_pair_with_fs_provider(provider).await; server .send_request( @@ -3413,7 +3787,7 @@ async fn on_get_trace_context_called_on_session_create() { let client = client.clone(); async move { client - .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .create_session(SessionConfig::default()) .await .unwrap() } @@ -3452,8 +3826,7 @@ async fn on_get_trace_context_called_on_session_resume() { let resume_handle = tokio::spawn({ let client = client.clone(); async move { - let cfg = ResumeSessionConfig::new(SessionId::from("trace-resume")) - .with_handler(Arc::new(NoopHandler)); + let cfg = ResumeSessionConfig::new(SessionId::from("trace-resume")); client.resume_session(cfg).await.unwrap() } }); @@ -3498,7 +3871,7 @@ async fn on_get_trace_context_called_on_session_send() { let client = client.clone(); async move { client - .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .create_session(SessionConfig::default()) .await .unwrap() } @@ -3551,7 +3924,7 @@ async fn message_options_trace_context_overrides_callback() { let client = client.clone(); async move { client - .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .create_session(SessionConfig::default()) .await .unwrap() } @@ -3600,7 +3973,7 @@ async fn message_options_trace_context_overrides_callback() { #[tokio::test] async fn message_options_trace_context_used_without_callback() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let send_handle = tokio::spawn({ @@ -3628,33 +4001,42 @@ async fn message_options_trace_context_used_without_callback() { #[tokio::test] async fn tool_invocation_carries_trace_context_from_event() { - use github_copilot_sdk::handler::{HandlerEvent, HandlerResponse, SessionHandler}; - - struct CapturingHandler { - captured: parking_lot::Mutex, Option)>>, - signal: tokio::sync::Notify, + type CapturedTrace = Arc, Option)>>>; + struct CapturingTool { + captured: CapturedTrace, + signal: Arc, } #[async_trait] - impl SessionHandler for CapturingHandler { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - if let HandlerEvent::ExternalTool { invocation } = event { - *self.captured.lock() = Some(( - invocation.traceparent.clone(), - invocation.tracestate.clone(), - )); - self.signal.notify_one(); - return HandlerResponse::ToolResult(ToolResult::Text("ok".into())); - } - HandlerResponse::Ok + impl tool::ToolHandler for CapturingTool { + async fn call( + &self, + invocation: ToolInvocation, + ) -> Result { + *self.captured.lock() = Some(( + invocation.traceparent.clone(), + invocation.tracestate.clone(), + )); + self.signal.notify_one(); + Ok(ToolResult::Text("ok".into())) } } - let handler = Arc::new(CapturingHandler { - captured: parking_lot::Mutex::new(None), - signal: tokio::sync::Notify::new(), - }); - let (_session, mut server) = create_session_pair(handler.clone()).await; + let captured = Arc::new(parking_lot::Mutex::new(None)); + let signal = Arc::new(tokio::sync::Notify::new()); + let handler = Arc::new(CapturingTool { + captured: captured.clone(), + signal: signal.clone(), + }); + let (_session, mut server) = create_session_pair_with_config(move |cfg| { + cfg.with_tools(vec![ + Tool::new("calc") + .with_description("calc") + .with_parameters(serde_json::json!({"type":"object"})) + .with_handler(handler.clone()), + ]) + }) + .await; server .send_event( @@ -3675,8 +4057,8 @@ async fn tool_invocation_carries_trace_context_from_event() { let pending = timeout(TIMEOUT, server.read_request()).await.unwrap(); assert_eq!(pending["method"], "session.tools.handlePendingToolCall"); - timeout(TIMEOUT, handler.signal.notified()).await.unwrap(); - let captured = handler.captured.lock().clone(); + timeout(TIMEOUT, signal.notified()).await.unwrap(); + let captured = captured.lock().clone(); assert_eq!( captured, Some((Some("00-tool-01".into()), Some("vendor=tool".into()))), @@ -3685,7 +4067,7 @@ async fn tool_invocation_carries_trace_context_from_event() { #[tokio::test] async fn wire_omits_trace_fields_when_unset() { - let (session, mut server) = create_session_pair(Arc::new(NoopHandler)).await; + let (session, mut server) = create_session_pair().await; let session = Arc::new(session); let send_handle = tokio::spawn({ diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index f35c0a52b..b9cc435fb 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -26,6 +26,7 @@ import { collectRpcMethodReferencedDefinitionNames, findSharedSchemaDefinitions, postProcessSchema, + propagateInternalVisibility, resolveRef, resolveObjectSchema, resolveSchema, @@ -36,6 +37,8 @@ import { isNodeFullyDeprecated, isSchemaDeprecated, isSchemaExperimental, + isSchemaInternal, + isOpaqueJson, isObjectSchema, isVoidSchema, getNullableInner, @@ -275,9 +278,30 @@ function isNonNullableCSharpValueType(typeName: string): boolean { "long", "DateTimeOffset", "TimeSpan", + "JsonElement", ].includes(typeName) || generatedEnums.has(typeName) || emittedRpcEnumResultTypes.has(typeName) || externalRpcValueTypes.has(typeName); } +/** + * Schemas marked `.asOpaqueJson()` on the runtime side carry + * `x-opaque-json: true`. These are the only shapes that legitimately surface + * as opaque JSON in the SDK (mapped to `JsonElement` in C#). Anything else + * that lacks an idiomatic mapping (untyped fields, non-discriminated unions, + * etc.) is rejected by the runtime's schema-shape lint, so the codegen + * treats reaching an unmappable schema here as a bug. + * + * The predicate itself lives in {@link "./utils".isOpaqueJson} for reuse. + */ +function failUnmappable(context: string, schema: JSONSchema7): never { + const summary = JSON.stringify(schema, (key, value) => (key === "description" ? undefined : value)).slice(0, 200); + throw new Error( + `C# codegen: cannot map schema to an idiomatic C# type (${context}). ` + + `On the runtime side, either tighten the Zod schema to a typed shape, or — if it is genuinely free-form JSON — ` + + `mark it \`.asOpaqueJson()\` so the schema emits \`x-opaque-json: true\` and the codegen maps it to JsonElement. ` + + `Offending schema (truncated): ${summary}`, + ); +} + function requiresArgumentNullCheck(typeName: string, isRequired: boolean): boolean { return isRequired && !typeName.endsWith("?") && !isNonNullableCSharpValueType(typeName); } @@ -309,6 +333,9 @@ function localRequestVariableName(paramEntries: [string, JSONSchema7Definition][ } function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: Map, propName?: string): string { + if (isOpaqueJson(schema)) { + return required ? "JsonElement" : "JsonElement?"; + } const nullableInner = getNullableInner(schema); if (nullableInner) { // Pass required=true to get the base type, then add "?" for nullable @@ -361,7 +388,8 @@ function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: if (type === "boolean") return required ? "bool" : "bool?"; if (type === "array") { const items = schema.items as JSONSchema7 | undefined; - const itemType = items ? schemaTypeToCSharp(items, true, knownTypes) : "object"; + if (!items) failUnmappable(`array without items (propName=${propName ?? "?"})`, schema); + const itemType = schemaTypeToCSharp(items, true, knownTypes); return required ? `${itemType}[]` : `${itemType}[]?`; } if (type === "object") { @@ -369,9 +397,9 @@ function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: const valueType = schemaTypeToCSharp(schema.additionalProperties as JSONSchema7, true, knownTypes); return required ? `IDictionary` : `IDictionary?`; } - return required ? "object" : "object?"; + failUnmappable(`object without properties or typed additionalProperties (propName=${propName ?? "?"})`, schema); } - return required ? "object" : "object?"; + failUnmappable(`unknown/missing type (propName=${propName ?? "?"})`, schema); } /** Tracks whether any TimeSpan property was emitted so the converter can be generated. */ @@ -489,6 +517,20 @@ function pushObsoleteAttributes(lines: string[], indent = ""): void { lines.push(...obsoleteAttributes(indent)); } +/** + * Emit the `[JsonInclude]` attribute for an internally-marked property and + * return the C# access modifier to use for the property declaration. + * + * `[JsonInclude]` is required because System.Text.Json only auto-(de)serialises + * public members by default; without it, the `internal` setter would silently + * be skipped. + */ +function pushCSharpInternalAttribute(lines: string[], schema: JSONSchema7, indent = " "): "public" | "internal" { + const propInternal = isSchemaInternal(schema); + if (propInternal) lines.push(`${indent}[JsonInclude]`); + return propInternal ? "internal" : "public"; +} + // ══════════════════════════════════════════════════════════════════════════════ // SESSION EVENTS // ══════════════════════════════════════════════════════════════════════════════ @@ -747,11 +789,13 @@ function generateFlattenedBooleanDiscriminatedClass( lines.push(...xmlDocPropertyComment(info.schema.description, propName, " ")); lines.push(...emitDataAnnotations(info.schema, " ", csharpType)); if (isSchemaDeprecated(info.schema)) pushObsoleteAttributes(lines, " "); + if (isSchemaExperimental(info.schema)) pushExperimentalAttribute(lines, " "); if (isMillisecondsDurationProperty(propName, info.schema)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); + const propVisibility = pushCSharpInternalAttribute(lines, info.schema); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; - lines.push(` public ${reqMod}${csharpType} ${csharpName} { get; set; }`); + lines.push(` ${propVisibility} ${reqMod}${csharpType} ${csharpName} { get; set; }`); } lines.push(`}`); @@ -850,11 +894,13 @@ function generateDerivedClass( lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(...emitDataAnnotations(prop, " ", csharpType)); if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); + if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " "); if (isMillisecondsDurationProperty(propName, prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); + const propVisibility = pushCSharpInternalAttribute(lines, prop); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; - lines.push(` public ${reqMod}${csharpType} ${csharpName} { get; set; }`, ""); + lines.push(` ${propVisibility} ${reqMod}${csharpType} ${csharpName} { get; set; }`, ""); } } @@ -917,11 +963,11 @@ function getJsonUnionMatchExpression(variant: JsonUnionVariant, variants: JsonUn ].join(" && "); } -function generateJsonUnionClass(className: string, variants: JsonUnionVariant[], description: string | undefined, jsonContextType: string): string { +function generateJsonUnionClass(className: string, variants: JsonUnionVariant[], description: string | undefined, jsonContextType: string, isInternal: boolean): string { const lines: string[] = []; lines.push(...xmlDocCommentWithFallback(description, `JSON union data type for ${escapeXml(className)}.`, "")); lines.push(`[JsonConverter(typeof(Converter))]`); - lines.push(`public sealed partial class ${className}`); + lines.push(`${isInternal ? "internal" : "public"} sealed partial class ${className}`); lines.push(`{`); for (const variant of variants) { @@ -1047,7 +1093,7 @@ function tryGenerateSessionJsonUnionType( }); } - nestedClasses.set(className, generateJsonUnionClass(className, variants, schema.description, "SessionEventsJsonContext")); + nestedClasses.set(className, generateJsonUnionClass(className, variants, schema.description, "SessionEventsJsonContext", isSchemaInternal(schema))); return className; } @@ -1063,7 +1109,7 @@ function generateNestedClass( lines.push(...xmlDocCommentWithFallback(schema.description, `Nested data type for ${className}.`, "")); if (isSchemaExperimental(schema)) pushExperimentalAttribute(lines); if (isSchemaDeprecated(schema)) pushObsoleteAttributes(lines); - lines.push(`public sealed partial class ${className}`, `{`); + lines.push(`${isSchemaInternal(schema) ? "internal" : "public"} sealed partial class ${className}`, `{`); for (const [propName, propSchema] of Object.entries(schema.properties || {}).sort(([a], [b]) => a.localeCompare(b))) { if (typeof propSchema !== "object") continue; @@ -1075,11 +1121,13 @@ function generateNestedClass( lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(...emitDataAnnotations(prop, " ", csharpType)); if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); + if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " "); if (isMillisecondsDurationProperty(propName, prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); + const propVisibility = pushCSharpInternalAttribute(lines, prop); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; - lines.push(` public ${reqMod}${csharpType} ${csharpName} { get; set; }`, ""); + lines.push(` ${propVisibility} ${reqMod}${csharpType} ${csharpName} { get; set; }`, ""); } if (lines[lines.length - 1] === "") lines.pop(); lines.push(`}`); @@ -1095,6 +1143,9 @@ function resolveSessionPropertyType( nestedClasses: Map, enumOutput: string[] ): string { + if (isOpaqueJson(propSchema)) { + return isRequired ? "JsonElement" : "JsonElement?"; + } // Handle $ref by resolving against schema definitions if (propSchema.$ref) { const className = typeToClassName(refTypeName(propSchema.$ref, sessionDefinitions)); @@ -1145,12 +1196,12 @@ function resolveSessionPropertyType( } const unionType = tryGenerateSessionJsonUnionType(propSchema, parentClassName, propName, knownTypes, nestedClasses, enumOutput); if (unionType) return isRequired ? unionType : `${unionType}?`; - return !isRequired ? "object?" : "object"; + failUnmappable(`anyOf without discriminator (${parentClassName}.${propName})`, propSchema); } if (propSchema.oneOf) { const unionType = tryGenerateSessionJsonUnionType(propSchema, parentClassName, propName, knownTypes, nestedClasses, enumOutput); if (unionType) return isRequired ? unionType : `${unionType}?`; - return !isRequired ? "object?" : "object"; + failUnmappable(`oneOf without discriminator (${parentClassName}.${propName})`, propSchema); } if (propSchema.enum && Array.isArray(propSchema.enum)) { const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, getEnumValueDescriptions(propSchema), propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema)); @@ -1191,7 +1242,8 @@ function resolveSessionPropertyType( } function generateDataClass(variant: EventVariant, knownTypes: Map, nestedClasses: Map, enumOutput: string[]): string { - if (!variant.dataSchema?.properties) return `public sealed partial class ${variant.dataClassName} { }`; + const dataVisibility = isSchemaInternal(variant.dataSchema) ? "internal" : "public"; + if (!variant.dataSchema?.properties) return `${dataVisibility} sealed partial class ${variant.dataClassName} { }`; const required = new Set(variant.dataSchema.required || []); const lines: string[] = []; @@ -1206,7 +1258,7 @@ function generateDataClass(variant: EventVariant, knownTypes: Map a.localeCompare(b))) { if (typeof propSchema !== "object") continue; @@ -1218,11 +1270,13 @@ function generateDataClass(variant: EventVariant, knownTypes: Map`); lines.push(` [JsonIgnore]`, ` public override string Type => "${variant.typeName}";`, ""); lines.push(` ///

The ${escapeXml(variant.typeName)} event payload.`); - lines.push(` [JsonPropertyName("data")]`, ` public required ${variant.dataClassName} Data { get; set; }`, `}`, ""); + lines.push(` [JsonPropertyName("data")]`, ` ${variantVisibility} required ${variant.dataClassName} Data { get; set; }`, `}`, ""); } // Data classes @@ -1352,7 +1409,7 @@ export async function generateSessionEvents(schemaPath?: string): Promise console.log("C#: generating session-events..."); const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); const schema = cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7); - const processed = postProcessSchema(schema); + const processed = propagateInternalVisibility(postProcessSchema(schema)); const code = generateSessionEventsCode(processed); const outPath = await writeGeneratedFile("dotnet/src/Generated/SessionEvents.cs", code); console.log(` ✓ ${outPath}`); @@ -1603,14 +1660,15 @@ function emitRpcClass( lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(...emitDataAnnotations(prop, " ", csharpType)); if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); + if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " "); if (isMillisecondsDurationProperty(propName, prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); + const propVisibility = pushCSharpInternalAttribute(lines, prop); lines.push(` [JsonPropertyName("${propName}")]`); let defaultVal = ""; let propAccessors = "{ get; set; }"; if (isReq && !csharpType.endsWith("?")) { if (csharpType === "string") defaultVal = " = string.Empty;"; - else if (csharpType === "object") defaultVal = " = null!;"; else if (csharpType.startsWith("IList<")) { propAccessors = "{ get => field ??= []; set; }"; } else if (csharpType.startsWith("IDictionary<")) { @@ -1622,7 +1680,7 @@ function emitRpcClass( defaultVal = " = null!;"; } } - lines.push(` public ${csharpType} ${csharpName} ${propAccessors}${defaultVal}`); + lines.push(` ${propVisibility} ${csharpType} ${csharpName} ${propAccessors}${defaultVal}`); if (i < props.length - 1) lines.push(""); } lines.push(`}`); @@ -1799,12 +1857,40 @@ function emitServerInstanceMethod( const csharpName = requestClassName ? toCSharpPropertyName(pName, jsonSchema) : toPascalCase(pName); - const csType = requestClassName + const naturalType = requestClassName ? resolveRpcType(jsonSchema, isReq, requestClassName, csharpName, classes) : schemaTypeToCSharp(jsonSchema, isReq, rpcKnownTypes, csharpName); + // Boundary special-case: if the natural type is JsonElement/JsonElement? + // or a list of JsonElement (i.e. the schema is opaque-JSON, possibly + // wrapped in an array), accept object/IList at the public + // surface for ergonomics and convert at the call site. DTO fields + // keep the JsonElement form. + const opaqueRequired = naturalType === "JsonElement"; + const opaqueOptional = naturalType === "JsonElement?"; + const opaqueListRequired = naturalType === "IList"; + const opaqueListOptional = naturalType === "IList?"; + const opaque = opaqueRequired || opaqueOptional || opaqueListRequired || opaqueListOptional; + const csType = opaqueRequired + ? "object" + : opaqueOptional + ? "object?" + : opaqueListRequired + ? "IList" + : opaqueListOptional + ? "IList?" + : naturalType; sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); - bodyAssignments.push(`${csharpName} = ${pName}`); - if (requiresArgumentNullCheck(csType, isReq)) { + const assignedValue = opaqueRequired + ? `CopilotClient.ToJsonElementForWire(${pName})!.Value` + : opaqueOptional + ? `CopilotClient.ToJsonElementForWire(${pName})` + : opaqueListRequired + ? `${pName}.Select(static v => CopilotClient.ToJsonElementForWire(v)!.Value).ToList()` + : opaqueListOptional + ? `${pName}?.Select(static v => CopilotClient.ToJsonElementForWire(v)!.Value).ToList()` + : pName; + bodyAssignments.push(`${csharpName} = ${assignedValue}`); + if (opaqueRequired || opaqueListRequired || (!opaque && requiresArgumentNullCheck(csType, isReq))) { argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`); } parameterDescriptions.push({ name: pName, description: jsonSchema.description }); @@ -1956,14 +2042,38 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas for (const [pName, pSchema] of paramEntries) { if (typeof pSchema !== "object") continue; const isReq = requiredSet.has(pName); - const csharpName = toCSharpPropertyName(pName, pSchema as JSONSchema7); - const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, csharpName, classes); + const jsonSchema = pSchema as JSONSchema7; + const csharpName = toCSharpPropertyName(pName, jsonSchema); + const naturalType = resolveRpcType(jsonSchema, isReq, requestClassName, csharpName, classes); + const opaqueRequired = naturalType === "JsonElement"; + const opaqueOptional = naturalType === "JsonElement?"; + const opaqueListRequired = naturalType === "IList"; + const opaqueListOptional = naturalType === "IList?"; + const opaque = opaqueRequired || opaqueOptional || opaqueListRequired || opaqueListOptional; + const csType = opaqueRequired + ? "object" + : opaqueOptional + ? "object?" + : opaqueListRequired + ? "IList" + : opaqueListOptional + ? "IList?" + : naturalType; sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); - bodyAssignments.push(`${csharpName} = ${pName}`); - if (requiresArgumentNullCheck(csType, isReq)) { + const assignedValue = opaqueRequired + ? `CopilotClient.ToJsonElementForWire(${pName})!.Value` + : opaqueOptional + ? `CopilotClient.ToJsonElementForWire(${pName})` + : opaqueListRequired + ? `${pName}.Select(static v => CopilotClient.ToJsonElementForWire(v)!.Value).ToList()` + : opaqueListOptional + ? `${pName}?.Select(static v => CopilotClient.ToJsonElementForWire(v)!.Value).ToList()` + : pName; + bodyAssignments.push(`${csharpName} = ${assignedValue}`); + if (opaqueRequired || opaqueListRequired || (!opaque && requiresArgumentNullCheck(csType, isReq))) { argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`); } - parameterDescriptions.push({ name: pName, description: (pSchema as JSONSchema7).description }); + parameterDescriptions.push({ name: pName, description: jsonSchema.description }); } } sigParams.push("CancellationToken cancellationToken = default"); @@ -2334,7 +2444,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro await generateSessionEvents(sessionSchemaPath); try { const resolvedSessionPath = sessionSchemaPath ?? (await getSessionEventsSchemaPath()); - const sessionSchema = postProcessSchema(cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedSessionPath, "utf-8")) as JSONSchema7)); + const sessionSchema = propagateInternalVisibility(postProcessSchema(cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedSessionPath, "utf-8")) as JSONSchema7))); await generateRpc(apiSchemaPath, sessionSchema); } catch (err) { if ((err as NodeJS.ErrnoException).code === "ENOENT" && !apiSchemaPath) { diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 6723906d4..49e537d8e 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -37,9 +37,11 @@ import { isRpcMethod, isSchemaDeprecated, isSchemaExperimental, + isSchemaInternal, isVoidSchema, parseExternalSchemaRef, postProcessSchema, + propagateInternalVisibility, refTypeName, resolveObjectSchema, resolveRef, @@ -201,6 +203,31 @@ function pushGoExperimentalMethodComment(lines: string[], methodName: string, in pushGoComment(lines, `Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`, indent); } +function pushGoInternalPropertyComment(lines: string[], goName: string, ctx: GoCodegenCtx, indent = "\t"): void { + pushGoCommentForContext(lines, `Internal: ${goName} is part of the SDK's internal API surface and is not intended for external use.`, ctx, indent); +} + +function pushGoExperimentalPropertyComment(lines: string[], goName: string, ctx: GoCodegenCtx, indent = "\t"): void { + pushGoCommentForContext(lines, `Experimental: ${goName} is part of an experimental API and may change or be removed.`, ctx, indent); +} + +/** + * Emit `Deprecated:` / `Experimental:` / `Internal:` doc comments above a Go + * struct field. Centralises the per-field marker logic shared between the + * regular struct emitter and the discriminated-union variant emitters. + */ +function pushGoFieldMarkers(lines: string[], prop: JSONSchema7, goName: string, ctx: GoCodegenCtx, indent = "\t"): void { + if (isSchemaDeprecated(prop)) { + pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, indent); + } + if (isSchemaExperimental(prop)) { + pushGoExperimentalPropertyComment(lines, goName, ctx, indent); + } + if (isSchemaInternal(prop)) { + pushGoInternalPropertyComment(lines, goName, ctx, indent); + } +} + function lowerFirst(value: string): string { if (value.length === 0) return value; return value.charAt(0).toLowerCase() + value.slice(1); @@ -1111,9 +1138,7 @@ function emitGoStruct( if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } - if (isSchemaDeprecated(prop)) { - pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, "\t"); - } + pushGoFieldMarkers(lines, prop, goName, ctx); const jsonTag = `json:"${propName}${omit}"`; lines.push(`\t${goName} ${goType} \`${jsonTag}\``); fields.push({ propName, goName, goType, jsonTag }); @@ -1802,9 +1827,7 @@ function emitGoFlatDiscriminatedUnion( if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } - if (isSchemaDeprecated(prop)) { - pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, "\t"); - } + pushGoFieldMarkers(lines, prop, goName, ctx); const jsonTag = `json:"${propName}${omit}"`; lines.push(`\t${goName} ${goType} \`${jsonTag}\``); fields.push({ propName, goName, goType, jsonTag }); @@ -1931,9 +1954,7 @@ function emitGoRequiredFieldDiscriminatedUnion( if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } - if (isSchemaDeprecated(prop)) { - pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, "\t"); - } + pushGoFieldMarkers(lines, prop, goName, ctx); const jsonTag = `json:"${propName}${omit}"`; lines.push(`\t${goName} ${goType} \`${jsonTag}\``); fields.push({ propName, goName, goType, jsonTag }); @@ -3043,9 +3064,7 @@ export function generateGoSessionEventsCode(schema: JSONSchema7, packageName: st if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } - if (isSchemaDeprecated(prop)) { - pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, "\t"); - } + pushGoFieldMarkers(lines, prop, goName, ctx); const jsonTag = `json:"${propName}${omit}"`; lines.push(`\t${goName} ${goType} \`${jsonTag}\``); fields.push({ propName, goName, goType, jsonTag }); @@ -3337,11 +3356,15 @@ function collectGoTopLevelNames(code: string, keyword: "type" | "const"): string function generateGoSessionEventAliasFile( generatedSessionTypeCode: string, additionalTypeNames: Iterable = [], - additionalConstNames: Iterable = [] + additionalConstNames: Iterable = [], + excludeTypeNames: Iterable = [] ): string { + const excluded = new Set(excludeTypeNames); const typeNames = [...new Set([...collectGoTopLevelNames(generatedSessionTypeCode, "type"), ...additionalTypeNames])] + .filter((name) => !excluded.has(name)) .sort(compareGoTypeNames); const constNames = [...new Set([...collectGoTopLevelNames(generatedSessionTypeCode, "const"), ...additionalConstNames])] + .filter((name) => !excluded.has(name)) .sort(compareGoTypeNames); const lines: string[] = []; @@ -3428,7 +3451,7 @@ async function generateSessionEvents(schemaPath?: string, apiSchema?: ApiSchema) const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); const schema = cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7); - const processed = postProcessSchema(schema); + const processed = propagateInternalVisibility(postProcessSchema(schema)); const sharedDefinitions = apiSchema ? findSharedSchemaDefinitions( processed as unknown as Record, @@ -3440,7 +3463,26 @@ async function generateSessionEvents(schemaPath?: string, apiSchema?: ApiSchema) const sessionSchema = rewriteSharedDefinitionReferences(processed, sharedDefinitions, "api.schema.json", true); const generatedSessionCode = generateGoSessionEventsCode(sessionSchema, "rpc"); - const generatedTypeCode = stripTrailingGoWhitespace(generatedSessionCode.typeCode); + let generatedTypeCode = stripTrailingGoWhitespace(generatedSessionCode.typeCode); + // Annotate internal session-event types (driven by the JSON Schema definition's + // `visibility: "internal"` flag). Matches what the RPC generator does below; + // the session-events emit path doesn't pass through that code so we apply it here. + { + const sessionDefs = collectDefinitionCollections(sessionSchema as Record); + const allSessionDefs = { ...sessionDefs.$defs, ...sessionDefs.definitions }; + const internalSessionTypeNames = new Set(); + for (const [name, def] of Object.entries(allSessionDefs)) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + internalSessionTypeNames.add(name); + } + } + for (const typeName of internalSessionTypeNames) { + generatedTypeCode = generatedTypeCode.replace( + new RegExp(`^(type ${typeName} struct)`, "m"), + `// Internal: ${typeName} is an internal SDK API and is not part of the public surface.\n$1` + ); + } + } const generatedEncodingCode = stripTrailingGoWhitespace(generatedSessionCode.encodingCode); rpcSessionEventTopLevelNames = { types: new Set(collectGoTopLevelNames(generatedTypeCode, "type")), @@ -3460,9 +3502,24 @@ async function generateSessionEvents(schemaPath?: string, apiSchema?: ApiSchema) const sharedAliasNames = apiSchema ? collectGoSharedSessionEventAliasNames(sharedSessionEventDefinitions, apiSchema) : { typeNames: [], constNames: [] }; + // Exclude internal types from the public `copilot` package re-exports. They + // remain accessible in the lower-level `rpc` package (where they're tagged + // with `// Internal:` doc comments), but consumers using only the canonical + // `copilot.*` namespace never see them. This is the strongest practical + // signal Go offers without requiring runtime refactoring to enable full + // lowercase/unexported types. + const internalTypesInSession = new Set(); + { + const { definitions, $defs } = collectDefinitionCollections(sessionSchema as Record); + for (const [name, def] of Object.entries({ ...definitions, ...$defs })) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + internalTypesInSession.add(name); + } + } + } const aliasOutPath = await writeGeneratedFile( "go/zsession_events.go", - generateGoSessionEventAliasFile(generatedTypeCode, sharedAliasNames.typeNames, sharedAliasNames.constNames) + generateGoSessionEventAliasFile(generatedTypeCode, sharedAliasNames.typeNames, sharedAliasNames.constNames, internalTypesInSession) ); console.log(` ✓ ${aliasOutPath}`); @@ -3476,7 +3533,7 @@ async function generateRpc(schemaPath?: string): Promise { console.log("Go: generating RPC types..."); const resolvedPath = schemaPath ?? (await getApiSchemaPath()); - const schema = fixNullableRequiredRefsInApiSchema(cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as ApiSchema)); + const schema = propagateInternalVisibility(fixNullableRequiredRefsInApiSchema(cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as ApiSchema)) as JSONSchema7) as unknown as ApiSchema; const allMethods = [ ...collectRpcMethods(schema.server || {}), diff --git a/scripts/codegen/package-lock.json b/scripts/codegen/package-lock.json index ff7c16f93..b173ddec8 100644 --- a/scripts/codegen/package-lock.json +++ b/scripts/codegen/package-lock.json @@ -795,7 +795,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 52b11ed59..1af315eac 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -8,7 +8,7 @@ import fs from "fs/promises"; import path from "path"; -import type { JSONSchema7 } from "json-schema"; +import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { fileURLToPath } from "url"; import { cloneSchemaForCodegen, @@ -25,7 +25,13 @@ import { isNodeFullyDeprecated, isSchemaDeprecated, isSchemaExperimental, + isSchemaInternal, postProcessSchema, + propagateInternalVisibility, + collectInternalSymbols, + collectInternalFieldsOnPublicTypes, + annotateInternalPythonFields, + renameInternalPythonSymbols, stripBooleanLiterals, writeGeneratedFile, collectDefinitionCollections, @@ -263,6 +269,412 @@ function postProcessExternalUnionAliasesForPython(code: string, aliases: Map; +} +function postProcessRefBasedDiscriminatedUnionsForPython( + code: string, + definitions: Record, + definitionCollections: DefinitionCollections +): { code: string; unions: ResolvedRefBasedUnion[] } { + interface UnionInfo { + aliasName: string; + variantNames: string[]; + discriminatorProp: string; + dispatch: Array<{ value: string; typeName: string }>; + description: string | undefined; + } + const unions: UnionInfo[] = []; + + for (const [defName, definition] of Object.entries(definitions)) { + const variants = (definition.anyOf ?? definition.oneOf) as JSONSchema7[] | undefined; + if (!Array.isArray(variants) || variants.length < 2) continue; + if (!variants.every((v) => typeof v === "object" && v !== null && typeof v.$ref === "string")) { + continue; + } + + const variantRefNames = variants.map((v) => refTypeName(v.$ref as string, definitionCollections)); + const resolvedVariants = variants.map( + (v) => + resolveObjectSchema(v, definitionCollections) ?? + resolveSchema(v, definitionCollections) ?? + v + ); + if (resolvedVariants.some((rv) => !rv || rv.properties === undefined)) continue; + + const discriminator = findPyDiscriminator(resolvedVariants as JSONSchema7[]); + if (!discriminator) continue; + + const aliasName = toPascalCase(defName); + const dispatch = variants.map((_, i) => { + const discProp = (resolvedVariants[i].properties as Record)[ + discriminator.property + ]; + return { + value: String(discProp.const), + typeName: toPascalCase(variantRefNames[i]), + }; + }); + + unions.push({ + aliasName, + variantNames: variantRefNames.map(toPascalCase), + discriminatorProp: discriminator.property, + dispatch, + description: typeof definition.description === "string" ? definition.description : undefined, + }); + } + + const resolved: ResolvedRefBasedUnion[] = []; + if (unions.length === 0) return { code, unions: resolved }; + + const emittedClassNames = new Set(); + for (const match of code.matchAll(/^class (\w+)[:\(]/gm)) { + emittedClassNames.add(match[1]); + } + const acronymCandidates = (name: string): string[] => { + const substitutions: Array<[RegExp, string]> = [ + [/Api/g, "API"], + [/Mcp/g, "MCP"], + [/Url/g, "URL"], + [/Json/g, "JSON"], + [/Http/g, "HTTP"], + [/Hmac/g, "HMAC"], + [/Tcp/g, "TCP"], + [/Sql/g, "SQL"], + [/Id\b/g, "ID"], + [/Llm/g, "LLM"], + [/Cli/g, "CLI"], + ]; + const results = new Set([name]); + for (const [pattern, replacement] of substitutions) { + for (const existing of [...results]) { + results.add(existing.replace(pattern, replacement)); + } + } + return [...results]; + }; + const resolveActualName = (expected: string): string | undefined => { + for (const candidate of acronymCandidates(expected)) { + if (emittedClassNames.has(candidate)) return candidate; + } + return undefined; + }; + + for (const union of unions) { + const actualAliasName = resolveActualName(union.aliasName); + const actualVariantNames: string[] = []; + const actualDispatch: Array<{ value: string; typeName: string }> = []; + let allResolved = true; + for (let i = 0; i < union.variantNames.length; i++) { + const actual = resolveActualName(union.variantNames[i]); + if (!actual) { + allResolved = false; + break; + } + actualVariantNames.push(actual); + actualDispatch.push({ value: union.dispatch[i].value, typeName: actual }); + } + if (!allResolved || !actualAliasName) { + continue; + } + resolved.push({ + aliasName: actualAliasName, + discriminatorProp: union.discriminatorProp, + dispatch: actualDispatch, + }); + + const lines = code.split("\n"); + let classStart = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i] === `class ${actualAliasName}:` || lines[i].startsWith(`class ${actualAliasName}(`)) { + classStart = i; + break; + } + } + if (classStart >= 0) { + let blockStart = classStart; + while ( + blockStart > 0 && + (lines[blockStart - 1] === "@dataclass" || /^# /.test(lines[blockStart - 1])) + ) { + blockStart--; + } + let blockEnd = classStart + 1; + while (blockEnd < lines.length) { + const ln = lines[blockEnd]; + if ( + /^class \w/.test(ln) || + /^def \w/.test(ln) || + ln === "@dataclass" || + /^# (?:Experimental|Deprecated|Internal):/.test(ln) + ) { + break; + } + blockEnd++; + } + lines.splice(blockStart, blockEnd - blockStart); + code = lines.join("\n"); + } + + const aliasLine = union.description + ? `# ${union.description.replace(/\n/g, " ")}\n${actualAliasName} = ${actualVariantNames.join(" | ")}` + : `${actualAliasName} = ${actualVariantNames.join(" | ")}`; + + const dispatcherLines: string[] = []; + dispatcherLines.push(`def _load_${actualAliasName}(obj: Any) -> "${actualAliasName}":`); + dispatcherLines.push(` assert isinstance(obj, dict)`); + dispatcherLines.push(` kind = obj.get(${JSON.stringify(union.discriminatorProp)})`); + dispatcherLines.push(` match kind:`); + for (const m of actualDispatch) { + dispatcherLines.push(` case ${JSON.stringify(m.value)}: return ${m.typeName}.from_dict(obj)`); + } + dispatcherLines.push( + ` case _: raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` + ); + + code = `${code.trimEnd()}\n\n\n${aliasLine}\n\n\n${dispatcherLines.join("\n")}\n`; + } + + code = applyUnionRewritesToPython(code, resolved); + return { code, unions: resolved }; +} + +/** + * Rewrite occurrences of `Name.from_dict(...)` to `_load_Name(...)` and + * `to_class(Name, x)` to `(x).to_dict()` for each union the caller passes in. + * Safe to apply repeatedly — re-running on already-rewritten code is a no-op. + */ +function applyUnionRewritesToPython(code: string, unions: ResolvedRefBasedUnion[]): string { + for (const union of unions) { + code = code.replace( + new RegExp(`\\b${union.aliasName}\\.from_dict\\b`, "g"), + `_load_${union.aliasName}` + ); + code = code.replace( + new RegExp(`to_class\\(${union.aliasName},\\s*([^,)]+)\\)`, "g"), + `($1).to_dict()` + ); + } + return code; +} + +/** + * For each discriminated-union variant class, replace the dataclass-level + * discriminator field (e.g. ``kind: PermissionDecisionApproveOnceKind``) with + * a class-level constant (e.g. ``kind: ClassVar[str] = "approve-once"``). + * This lets users construct variants without supplying the discriminator + * value (``PermissionDecisionApproveOnce()`` instead of + * ``PermissionDecisionApproveOnce(kind=PermissionDecisionApproveOnceKind.APPROVE_ONCE)``), + * matching the TS / Rust / .NET / Go ergonomics for the same schema. + * + * Also rewrites the generated ``from_dict`` to skip parsing the discriminator + * (the dispatcher routed based on it; the variant class identity carries it) + * and ``to_dict`` to emit the constant directly. + */ +function postProcessDiscriminatorDefaultsForPython( + code: string, + unions: ResolvedRefBasedUnion[] +): string { + // Build variant lookup: variant class name → { prop, value }. + const variantInfo = new Map(); + for (const union of unions) { + for (const d of union.dispatch) { + // First-wins; multiple unions referencing the same variant share a + // discriminator/value pair anyway. + if (!variantInfo.has(d.typeName)) { + variantInfo.set(d.typeName, { prop: union.discriminatorProp, value: d.value }); + } + } + } + if (variantInfo.size === 0) return code; + + const lines = code.split("\n"); + const out: string[] = []; + let usedClassVar = false; + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + const classMatch = line.match(/^class (\w+)[:\(]/); + if (!classMatch) { + out.push(line); + i++; + continue; + } + const className = classMatch[1]; + const info = variantInfo.get(className); + if (!info) { + out.push(line); + i++; + continue; + } + + // Find the bounds of this class block: everything indented under it. + const classStart = i; + let classEnd = i + 1; + while (classEnd < lines.length) { + const ln = lines[classEnd]; + if ( + /^class \w/.test(ln) || + /^def \w/.test(ln) || + ln === "@dataclass" || + /^# (?:Experimental|Deprecated|Internal):/.test(ln) || + ln.startsWith("@dataclass(") + ) { + break; + } + classEnd++; + } + const block = lines.slice(classStart, classEnd); + + // Locate the discriminator field declaration. Quicktype emits + // ` kind: PermissionDecisionApproveOnceKind` while the + // session-events codegen emits ` kind: str` — both match the + // simple `: ` shape (no default value, since the + // field is required in the schema). + const fieldPattern = new RegExp(`^(\\s+)${info.prop}: [\\w\\[\\], ]+$`); + let fieldIdx = -1; + for (let j = 1; j < block.length; j++) { + if (fieldPattern.test(block[j])) { + fieldIdx = j; + break; + } + } + if (fieldIdx < 0) { + // Variant class without an explicit discriminator field — leave alone. + out.push(...block); + i = classEnd; + continue; + } + const fieldIndent = (block[fieldIdx].match(/^(\s+)/) ?? ["", ""])[1]; + const literal = JSON.stringify(info.value); + // Replace the field with a class-level constant. + block[fieldIdx] = `${fieldIndent}${info.prop}: ClassVar[str] = ${literal}`; + usedClassVar = true; + + // Drop any field-trailing docstring lines that immediately followed the + // original field. Quicktype emits """..."""-style block strings; the + // session-events codegen does not emit per-field docstrings. We only + // touch the line at fieldIdx+1 if it's a docstring or blank. + // (Conservative: leave additional lines in place; they don't reference + // the dropped enum.) + + // Rewrite from_dict / to_dict bodies. + for (let j = fieldIdx + 1; j < block.length; j++) { + const ln = block[j]; + + // Drop ` = ...(obj.get(""))` parse line in from_dict. + const propAssignPattern = new RegExp( + `^\\s+${info.prop} = .+\\(obj\\.get\\(${JSON.stringify(info.prop)}\\)\\)` + ); + if (propAssignPattern.test(ln)) { + block[j] = "<<>>"; + continue; + } + + // Drop multi-line constructor kwarg of the form ` kind=kind,` — + // emitted by the session-events codegen when the constructor call + // is broken across lines. + const multilineKwargPattern = new RegExp( + `^\\s+${info.prop}=${info.prop},?\\s*$` + ); + if (multilineKwargPattern.test(ln)) { + block[j] = "<<>>"; + continue; + } + + // Convert `return X(a, prop, b)` (single-line positional) to drop + // the prop arg. Quicktype-emitted constructors are single-line. + const ctorMatch = ln.match(new RegExp(`^(\\s+)return ${className}\\((.*)\\)\\s*$`)); + if (ctorMatch) { + const argList = ctorMatch[2]; + const args = splitTopLevelCommasMulti(argList); + const filtered = args + .map((a) => a.trim()) + .filter((a) => { + const kw = a.match(/^([a-zA-Z_]\w*)\s*=/); + const name = kw ? kw[1] : a; + return name !== info.prop; + }); + block[j] = `${ctorMatch[1]}return ${className}(${filtered.join(", ")})`; + continue; + } + + // Rewrite `result[""] = to_enum(, self.)` to + // emit the class-level constant directly. + const toDictPattern = new RegExp( + `^(\\s+)result\\[${JSON.stringify(info.prop)}\\] = .+` + ); + if (toDictPattern.test(ln)) { + const indent = (ln.match(/^(\s+)/) ?? ["", ""])[1]; + block[j] = `${indent}result[${JSON.stringify(info.prop)}] = self.${info.prop}`; + continue; + } + } + + out.push(...block.filter((l) => l !== "<<>>")); + i = classEnd; + } + + let result = out.join("\n"); + if (usedClassVar) { + result = ensureClassVarImport(result); + } + return result; +} + +function splitTopLevelCommasMulti(s: string): string[] { + const parts: string[] = []; + let depth = 0; + let start = 0; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c === "(" || c === "[" || c === "{") depth++; + else if (c === ")" || c === "]" || c === "}") depth--; + else if (c === "," && depth === 0) { + parts.push(s.slice(start, i)); + start = i + 1; + } + } + parts.push(s.slice(start)); + return parts.filter((p) => p.trim().length > 0); +} + +function ensureClassVarImport(code: string): string { + // Already imported? + if (/\bfrom typing import [^\n]*\bClassVar\b/.test(code)) return code; + return code.replace( + /^from typing import (.+)$/m, + (_match, names) => { + const list = names.split(",").map((n: string) => n.trim()).filter(Boolean); + list.push("ClassVar"); + list.sort(); + return `from typing import ${[...new Set(list)].join(", ")}`; + } + ); +} + function pushPyExperimentalComment(lines: string[], subject: PyExperimentalSubject, indent = ""): void { lines.push(pyExperimentalComment(subject, indent)); } @@ -271,6 +683,24 @@ function pushPyExperimentalApiGroupComment(lines: string[]): void { lines.push("# Experimental: this API group is experimental and may change or be removed."); } +/** + * Emit `# Deprecated:` / `# Experimental:` / `# Internal:` comments above a + * dataclass field. Order matches our other codegens (deprecated, experimental, + * internal) and keeps the comments out of the field declaration itself. + */ +function pushPyFieldMarkers(lines: string[], propSchema: JSONSchema7 | null | undefined): void { + if (!propSchema) return; + if (isSchemaDeprecated(propSchema)) { + lines.push(` # Deprecated: this field is deprecated.`); + } + if (isSchemaExperimental(propSchema)) { + lines.push(` # Experimental: this field is part of an experimental API and may change or be removed.`); + } + if (isSchemaInternal(propSchema)) { + lines.push(` # Internal: this field is an internal SDK API and is not part of the public surface.`); + } +} + /** * Modernize quicktype's Python 3.7 output to Python 3.11+ syntax: * - Optional[T] → T | None @@ -859,6 +1289,7 @@ interface PyCodegenCtx { usesTimedelta: boolean; usesIntegerTimedelta: boolean; definitions: DefinitionCollections; + refBasedUnions: ResolvedRefBasedUnion[]; } function toEnumMemberName(value: string): string { @@ -969,6 +1400,104 @@ function pyDurationResolvedType(ctx: PyCodegenCtx, isInteger: boolean): PyResolv }; } +/** + * Emit a "$ref-based discriminated union" — a Python equivalent of the + * polymorphic hierarchies that TS / Rust / .NET / Go produce for the same + * schema shape. Given a definition like + * + * "PermissionRequest": { "anyOf": [ {"$ref": "#/.../PermissionRequestShell"}, ... ] } + * + * where every variant is a `$ref` to a sibling definition and the variants + * share a `const` discriminator property (e.g. `kind`), emit each variant as a + * standalone `@dataclass`, plus a union alias and a `from_dict` dispatcher. + * + * Returns the resolved type or `undefined` if the schema doesn't match the + * expected shape (caller falls back to other paths). + */ +function tryEmitPyRefBasedDiscriminatedUnion( + aliasName: string, + resolved: JSONSchema7, + ctx: PyCodegenCtx +): PyResolvedType | undefined { + const variants = (resolved.anyOf ?? resolved.oneOf) as JSONSchema7[] | undefined; + if (!Array.isArray(variants) || variants.length < 2) return undefined; + + const variantRefNames: string[] = []; + for (const v of variants) { + if (!v || typeof v !== "object") return undefined; + const ref = (v as JSONSchema7).$ref; + if (typeof ref !== "string" || !ref.startsWith("#/definitions/")) { + return undefined; + } + variantRefNames.push(refTypeName(ref, ctx.definitions)); + } + + const resolvedVariants = variants.map( + (v) => + resolveObjectSchema(v, ctx.definitions) ?? + resolveSchema(v, ctx.definitions) ?? + (v as JSONSchema7) + ); + if (resolvedVariants.some((rv) => !rv || rv.properties === undefined)) { + return undefined; + } + const discriminator = findPyDiscriminator(resolvedVariants as JSONSchema7[]); + if (!discriminator) return undefined; + + const variantTypeNames: string[] = []; + const dispatch: Array<{ value: string; typeName: string }> = []; + for (let i = 0; i < variants.length; i++) { + const variantTypeName = toPascalCase(variantRefNames[i]); + const variantSchema = resolveObjectSchema(variants[i], ctx.definitions); + if (variantSchema) { + emitPyClass(variantTypeName, variantSchema, ctx, variantSchema.description); + } + variantTypeNames.push(variantTypeName); + const discProp = resolvedVariants[i].properties?.[discriminator.property] as JSONSchema7; + dispatch.push({ value: String(discProp.const), typeName: variantTypeName }); + } + + if (!ctx.aliasesByName.has(aliasName)) { + const lines: string[] = []; + if (resolved.description) { + lines.push(`# ${resolved.description}`); + } + lines.push(`${aliasName} = ${variantTypeNames.join(" | ")}`); + ctx.aliasesByName.add(aliasName); + ctx.aliases.push(lines.join("\n")); + ctx.refBasedUnions.push({ + aliasName, + discriminatorProp: discriminator.property, + dispatch, + }); + } + + const dispatcherName = `_load_${aliasName}`; + if (!ctx.generatedNames.has(dispatcherName)) { + ctx.generatedNames.add(dispatcherName); + const lines: string[] = []; + lines.push(`def ${dispatcherName}(obj: Any) -> "${aliasName}":`); + lines.push(` assert isinstance(obj, dict)`); + lines.push(` kind = obj.get(${JSON.stringify(discriminator.property)})`); + lines.push(` match kind:`); + for (const m of dispatch) { + lines.push( + ` case ${JSON.stringify(m.value)}: return ${m.typeName}.from_dict(obj)` + ); + } + lines.push( + ` case _: raise ValueError(f"Unknown ${aliasName} ${discriminator.property}: {kind!r}")` + ); + ctx.classes.push(lines.join("\n")); + } + + return { + annotation: aliasName, + fromExpr: (expr) => `${dispatcherName}(${expr})`, + toExpr: (expr) => `${expr}.to_dict()`, + }; +} + function isPyBase64StringSchema(schema: JSONSchema7): boolean { return schema.format === "byte" || (schema as Record).contentEncoding === "base64"; } @@ -1228,6 +1757,17 @@ function resolvePyPropertyType( return isRequired ? enumResolved : pyOptionalResolvedType(enumResolved); } + // Emit "$ref"-based discriminated unions as proper Python unions + // (per-variant dataclasses + alias + dispatcher) rather than flat + // merged dataclasses. Matches the polymorphic hierarchies emitted + // by the TS / Rust / .NET / Go SDKs for the same schema shape. + if (resolved.anyOf || resolved.oneOf) { + const unionResolved = tryEmitPyRefBasedDiscriminatedUnion(typeName, resolved, ctx); + if (unionResolved) { + return isRequired ? unionResolved : pyOptionalResolvedType(unionResolved); + } + } + const resolvedObject = resolveObjectSchema(propSchema, ctx.definitions); if (isNamedPyObjectSchema(resolvedObject)) { emitPyClass(typeName, resolvedObject, ctx, resolvedObject.description); @@ -1275,6 +1815,21 @@ function resolvePyPropertyType( if (nonNull.length > 1) { const discriminator = findPyDiscriminator(nonNull); if (discriminator) { + // Prefer the proper per-variant union shape when every variant + // is a `$ref` to a sibling definition. Same rationale as in the + // top-level $ref branch above: matches TS/Rust/.NET/Go. + if (variantSchemas.every((s) => typeof s.$ref === "string")) { + const unionResolved = tryEmitPyRefBasedDiscriminatedUnion( + nestedName, + propSchema, + ctx + ); + if (unionResolved) { + return hasNull || !isRequired + ? pyOptionalResolvedType(unionResolved) + : unionResolved; + } + } emitPyFlatDiscriminatedUnion( nestedName, discriminator.property, @@ -1526,9 +2081,11 @@ function emitPyClass( const fieldInfos = orderedFieldEntries.map(([propName, propSchema]) => { const isRequired = required.has(propName); const resolved = resolvePyPropertyType(propSchema, typeName, propName, isRequired, ctx); + const baseFieldName = toPyFieldName(propName, propSchema, ctx); + const fieldName = isSchemaInternal(propSchema) ? `_${baseFieldName}` : baseFieldName; return { jsonName: propName, - fieldName: toPyFieldName(propName, propSchema, ctx), + fieldName, isRequired, resolved, }; @@ -1561,9 +2118,8 @@ function emitPyClass( for (const field of fieldInfos) { const suffix = field.isRequired ? "" : " = None"; - if (isSchemaDeprecated(orderedFieldEntries.find(([n]) => n === field.jsonName)?.[1] as JSONSchema7)) { - lines.push(` # Deprecated: this field is deprecated.`); - } + const propSchema = orderedFieldEntries.find(([n]) => n === field.jsonName)?.[1] as JSONSchema7 | undefined; + pushPyFieldMarkers(lines, propSchema); lines.push(` ${field.fieldName}: ${field.resolved.annotation}${suffix}`); } @@ -1686,7 +2242,7 @@ function emitPyFlatDiscriminatedUnion( return { jsonName: propName, - fieldName: toPyFieldName(propName, propSchema, ctx), + fieldName: isSchemaInternal(propSchema) ? `_${toPyFieldName(propName, propSchema, ctx)}` : toPyFieldName(propName, propSchema, ctx), isRequired: requiredInAll, resolved, }; @@ -1703,10 +2259,8 @@ function emitPyFlatDiscriminatedUnion( } for (const field of fieldInfos) { const suffix = field.isRequired ? "" : " = None"; - const fieldSchema = orderedFieldEntries.find(([n]) => n === field.jsonName)?.[1]; - if (fieldSchema && isSchemaDeprecated(fieldSchema)) { - lines.push(` # Deprecated: this field is deprecated.`); - } + const fieldSchema = orderedFieldEntries.find(([n]) => n === field.jsonName)?.[1] as JSONSchema7 | undefined; + pushPyFieldMarkers(lines, fieldSchema); lines.push(` ${field.fieldName}: ${field.resolved.annotation}${suffix}`); } lines.push(``); @@ -1753,6 +2307,7 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { usesTimedelta: false, usesIntegerTimedelta: false, definitions: collectDefinitionCollections(schema as Record), + refBasedUnions: [], }; for (const variant of variants) { @@ -2082,7 +2637,9 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(``); out.push(``); - return postProcessPythonSessionEventCode(out.join("\n")); + let finalCode = postProcessPythonSessionEventCode(out.join("\n")); + finalCode = postProcessDiscriminatorDefaultsForPython(finalCode, ctx.refBasedUnions); + return finalCode; } async function generateSessionEvents(schemaPath?: string): Promise { @@ -2090,8 +2647,10 @@ async function generateSessionEvents(schemaPath?: string): Promise { const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); const schema = JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7; - const processed = postProcessSchema(schema); - const code = generatePythonSessionEventsCode(processed); + const processed = propagateInternalVisibility(postProcessSchema(schema)); + let code = generatePythonSessionEventsCode(processed); + const { typeNames } = collectInternalSymbols(processed); + code = renameInternalPythonSymbols(code, typeNames); const outPath = await writeGeneratedFile("python/copilot/generated/session_events.py", code); console.log(` ✓ ${outPath}`); @@ -2206,11 +2765,33 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema inputData, lang: "python", rendererOptions: { "python-version": "3.7" }, + // Disable quicktype's structural-equality merging of class types. + // It produces fuzzy synthesized names (e.g. ``PermissionDecisionApproveForIonApproval`` + // as the merge of ``PermissionDecisionApproveFor{Session,Location}Approval``) which + // are unstable: any future divergence between the variants would silently change + // the generated class name. We rely on the schema's named definitions and resolve + // structural unions via :func:`postProcessRefBasedDiscriminatedUnionsForPython`, + // so the merging is also redundant. + inferenceFlags: { combineClasses: false }, }); let typesCode = qtResult.lines.join("\n"); - // Fix dataclass field ordering + // Quicktype emits optional Any-typed fields without defaults; add them back. typesCode = typesCode.replace(/: Any$/gm, ": Any = None"); + // The synthesized root RPC dataclass includes one required field per schema definition. + // Keep Any-typed definition fields required so later required fields don't trip dataclass + // ordering rules at import time. + typesCode = typesCode.replace( + /(@dataclass\r?\nclass RPC:\r?\n)([\s\S]*?)(\r?\n @staticmethod)/, + (match, prefix: string, body: string, suffix: string) => { + let updatedBody = body; + for (const definitionName of Object.keys(allDefinitions)) { + const fieldName = toSnakeCase(definitionName); + updatedBody = updatedBody.replace(new RegExp(`^( ${fieldName}: Any) = None$`, "m"), "$1"); + } + return `${prefix}${updatedBody}${suffix}`; + } + ); // Fix bare except: to use Exception (required by ruff/pylint) typesCode = typesCode.replace(/except:/g, "except Exception:"); // Remove unnecessary pass when class has methods (quicktype generates pass for empty schemas) @@ -2221,6 +2802,12 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema typesCode = collapsePlaceholderPythonDataclasses(typesCode, knownDefNames); typesCode = postProcessExternalUnionAliasesForPython(typesCode, externalUnionAliases); typesCode = postProcessExternalRefsForPython(typesCode, externalRefs.placeholderNames, externalEnumNames); + const { code: typesCodeAfterUnions, unions: refBasedUnions } = postProcessRefBasedDiscriminatedUnionsForPython( + typesCode, + allDefinitions, + allDefinitionCollections + ); + typesCode = typesCodeAfterUnions; typesCode = modernizePython(typesCode); // Fix quicktype's Enum-suffix renaming: quicktype sometimes renames "Xyz" to @@ -2370,6 +2957,7 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema AUTO-GENERATED FILE - DO NOT EDIT Generated from: api.schema.json """ +from __future__ import annotations from typing import TYPE_CHECKING @@ -2461,8 +3049,51 @@ def _patch_model_capabilities(data: dict) -> dict: /(_patch_model_capabilities\(await self\._client\.request\("models\.list"[^)]*\)[^)]*\))/, "$1)", ); + // Apply union rewrites to the assembled code so RPC method wrappers + // generated after the types section also route Name.from_dict / to_class + // through the discriminator dispatcher. + finalCode = applyUnionRewritesToPython(finalCode, refBasedUnions); + finalCode = postProcessDiscriminatorDefaultsForPython(finalCode, refBasedUnions); finalCode = unwrapRedundantPythonLambdas(finalCode); + // Apply `_`-prefix to type names of internal RPC types so the leading-underscore + // Python convention signals "internal, no stability guarantees" to consumers. + { + const internalDefs = new Set(); + for (const [name, def] of Object.entries(rpcDefinitions.definitions)) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + internalDefs.add(name); + } + } + for (const [name, def] of Object.entries(rpcDefinitions.$defs)) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + internalDefs.add(name); + } + } + if (internalDefs.size > 0) { + finalCode = renameInternalPythonSymbols(finalCode, internalDefs); + } + } + + // Annotate internal fields on otherwise-public RPC types with a `# Internal:` + // comment immediately above the field declaration. Quicktype's generated + // from_dict/to_dict reference field names in patterns that are brittle to + // regex-based identifier rewriting, so we annotate rather than rename. The + // marker is visible in IDE hovers and signals "internal, no stability + // guarantee" without breaking the wire-protocol round-trip. + { + const combinedSchema: JSONSchema7 = { + definitions: { + ...(rpcDefinitions.definitions as Record), + ...(rpcDefinitions.$defs as Record), + }, + }; + const fieldsByType = collectInternalFieldsOnPublicTypes(combinedSchema); + if (fieldsByType.size > 0) { + finalCode = annotateInternalPythonFields(finalCode, fieldsByType, toSnakeCase); + } + } + const outPath = await writeGeneratedFile("python/copilot/generated/rpc.py", finalCode); console.log(` ✓ ${outPath}`); } @@ -2587,7 +3218,8 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio } function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, groupExperimental = false, groupDeprecated = false): void { - const methodName = toSnakeCase(name); + const isInternal = method.visibility === "internal"; + const methodName = (isInternal ? "_" : "") + toSnakeCase(name); const resultSchema = getMethodResultSchema(method); const nullableInner = resultSchema ? getNullableInner(resultSchema) : undefined; const effectiveResultSchema = nullableInner ?? resultSchema; diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index f2056c35c..e23ff4385 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -38,9 +38,11 @@ import { isRpcMethod, isSchemaDeprecated, isSchemaExperimental, + isSchemaInternal, isVoidSchema, parseExternalSchemaRef, postProcessSchema, + propagateInternalVisibility, refTypeName, resolveObjectSchema, resolveRef, @@ -193,6 +195,8 @@ function safeRustFieldName(name: string): string { interface RustCodegenCtx { /** Accumulated struct definitions. */ structs: string[]; + /** Accumulated type alias definitions. */ + typeAliases: string[]; /** Accumulated enum definitions. */ enums: string[]; /** Track generated type names to avoid duplicates. */ @@ -409,6 +413,7 @@ function makeCtx( ): RustCodegenCtx { return { structs: [], + typeAliases: [], enums: [], generatedNames: new Set(), nonDefaultableTypes: new Set(options.nonDefaultableTypes ?? []), @@ -454,6 +459,87 @@ function pushRustDoc(lines: string[], text: string | undefined, indent = ""): vo } } +function isRustMapSchema(schema: JSONSchema7): boolean { + const hasProperties = + !!schema.properties && Object.keys(schema.properties).length > 0; + return ( + (schema.type === "object" || schema.additionalProperties !== undefined) && + !hasProperties && + schema.additionalProperties !== undefined && + schema.additionalProperties !== false + ); +} + +function rustMapValueType( + schema: JSONSchema7, + parentTypeName: string, + ctx: RustCodegenCtx, +): string { + const additionalProperties = schema.additionalProperties; + if ( + additionalProperties && + typeof additionalProperties === "object" && + Object.keys(additionalProperties as Record).length > 0 + ) { + const valueSchema = additionalProperties as JSONSchema7; + if (valueSchema.type === "object" && valueSchema.properties) { + const valueName = (valueSchema.title as string) || `${parentTypeName}Value`; + emitRustStruct(valueName, valueSchema, ctx); + return valueName; + } + return resolveRustType(valueSchema, parentTypeName, "value", true, ctx); + } + return "serde_json::Value"; +} + +function rustMapType( + schema: JSONSchema7, + parentTypeName: string, + ctx: RustCodegenCtx, +): string { + return `HashMap`; +} + +function emitRustTypeAlias( + typeName: string, + schema: JSONSchema7, + aliasType: string, + ctx: RustCodegenCtx, + description?: string, +): void { + if (ctx.generatedNames.has(typeName)) return; + ctx.generatedNames.add(typeName); + + const lines: string[] = []; + pushRustDoc(lines, description || schema.description); + pushRustExperimentalDocs( + lines, + isSchemaExperimental(schema) || ctx.experimentalTypeNames.has(typeName), + ); + if (isSchemaDeprecated(schema)) { + lines.push(...rustDeprecatedAttributes()); + } + const aliasVis = isSchemaInternal(schema) ? "pub(crate)" : "pub"; + lines.push(`${aliasVis} type ${typeName} = ${aliasType};`); + ctx.typeAliases.push(lines.join("\n")); +} + +function emitRustMapAlias( + typeName: string, + schema: JSONSchema7, + ctx: RustCodegenCtx, + description?: string, +): void { + if (ctx.generatedNames.has(typeName)) return; + emitRustTypeAlias( + typeName, + schema, + rustMapType(schema, typeName, ctx), + ctx, + description, + ); +} + function rustRpcResultDescription( method: RpcMethod, resultSchema: JSONSchema7 | undefined, @@ -710,28 +796,8 @@ function resolveRustType( emitRustStruct(structName, propSchema, ctx); return wrapOption(structName, isRequired); } - if (propSchema.additionalProperties) { - if ( - typeof propSchema.additionalProperties === "object" && - Object.keys(propSchema.additionalProperties as Record) - .length > 0 - ) { - const ap = propSchema.additionalProperties as JSONSchema7; - if (ap.type === "object" && ap.properties) { - const valueName = (ap.title as string) || `${nestedName}Value`; - emitRustStruct(valueName, ap, ctx); - return wrapOption(`HashMap`, isRequired); - } - const valueType = resolveRustType( - ap, - parentTypeName, - `${jsonPropName}Value`, - true, - ctx, - ); - return wrapOption(`HashMap`, isRequired); - } - return wrapOption("HashMap", isRequired); + if (isRustMapSchema(propSchema)) { + return wrapOption(rustMapType(propSchema, nestedName, ctx), isRequired); } return wrapOption("serde_json::Value", isRequired); } @@ -776,6 +842,7 @@ function emitRustStruct( if (isSchemaDeprecated(schema)) { lines.push(...rustDeprecatedAttributes()); } + const structVis = isSchemaInternal(schema) ? "pub(crate)" : "pub"; // Resolve field types up-front so we can decide whether `Default` can be // derived. A required field whose bare type is non-default-able (e.g. an @@ -810,7 +877,7 @@ function emitRustStruct( lines.push("#[derive(Debug, Clone, Default, Serialize, Deserialize)]"); } lines.push(`#[serde(rename_all = "camelCase")]`); - lines.push(`pub struct ${typeName} {`); + lines.push(`${structVis} struct ${typeName} {`); for (const { propName, prop, isReq, rustField, rustType } of fields) { if (prop.description) { @@ -818,6 +885,11 @@ function emitRustStruct( lines.push(` /// ${line}`); } } + pushRustExperimentalDocs(lines, isSchemaExperimental(prop), " "); + const propIsInternal = isSchemaInternal(prop); + if (propIsInternal) { + lines.push(` #[doc(hidden)]`); + } if (isSchemaDeprecated(prop)) { lines.push(...rustDeprecatedAttributes(" ")); } @@ -846,7 +918,7 @@ function emitRustStruct( lines.push(` #[serde(rename = "${propName}")]`); } - lines.push(` pub ${rustField}: ${rustType},`); + lines.push(` ${propIsInternal ? "pub(crate)" : "pub"} ${rustField}: ${rustType},`); } lines.push("}"); @@ -1145,6 +1217,12 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { out.push(""); } + // Supporting type aliases + for (const block of ctx.typeAliases) { + out.push(block); + out.push(""); + } + // Supporting enums for (const block of ctx.enums) { out.push(block); @@ -1385,6 +1463,8 @@ function generateApiTypesCode( getEnumValueDescriptions(schema), isSchemaExperimental(schema), ); + } else if (isRustMapSchema(schema)) { + emitRustMapAlias(name, schema, ctx, schema.description); } else if (asGeneratedObjectSchema(schema, defCollections)) { emitRustStruct( name, @@ -1445,6 +1525,8 @@ function generateApiTypesCode( if (resolved) { if (resolved.enum && Array.isArray(resolved.enum)) { // Already generated from definitions + } else if (isRustMapSchema(resolved)) { + emitRustMapAlias(resultName, resolved, ctx, resolved.description); } else if (isObjectSchema(resolved)) { emitRustStruct(resultName, resolved, ctx, resolved.description); } @@ -1495,6 +1577,11 @@ function generateApiTypesCode( out.push(""); } + for (const block of ctx.typeAliases) { + out.push(block); + out.push(""); + } + for (const block of ctx.enums) { out.push(block); out.push(""); @@ -1781,17 +1868,18 @@ function emitNamespaceMethod( }; const paramArg = hasParams ? `, params: ${paramsTypeName}` : ""; + const fnVis = method.visibility === "internal" ? "pub(crate)" : "pub"; if (hasParams && paramsInfo.optional) { out.push(...buildDocs(false)); out.push( - ` pub async fn ${fnName}(&self) -> Result<${returnType}, Error> {`, + ` ${fnVis} async fn ${fnName}(&self) -> Result<${returnType}, Error> {`, ); pushNamespaceMethodBody(out, constName, isSession, false, resultIsVoid); out.push(""); out.push(...buildDocs(true)); out.push( - ` pub async fn ${fnName}_with_params(&self, params: ${paramsTypeName}) -> Result<${returnType}, Error> {`, + ` ${fnVis} async fn ${fnName}_with_params(&self, params: ${paramsTypeName}) -> Result<${returnType}, Error> {`, ); pushNamespaceMethodBody(out, constName, isSession, true, resultIsVoid); out.push(""); @@ -1800,7 +1888,7 @@ function emitNamespaceMethod( out.push(...buildDocs(hasParams)); out.push( - ` pub async fn ${fnName}(&self${paramArg}) -> Result<${returnType}, Error> {`, + ` ${fnVis} async fn ${fnName}(&self${paramArg}) -> Result<${returnType}, Error> {`, ); pushNamespaceMethodBody(out, constName, isSession, hasParams, resultIsVoid); out.push(""); @@ -1988,11 +2076,15 @@ async function generate(): Promise { await fs.readFile(apiSchemaPath, "utf-8"), ) as ApiSchema; - const sessionEventsSchema = postProcessSchema( - stripBooleanLiterals(sessionEventsRaw) as JSONSchema7, + const sessionEventsSchema = propagateInternalVisibility( + postProcessSchema( + stripBooleanLiterals(sessionEventsRaw) as JSONSchema7, + ), ); - const apiSchema = postProcessSchema( - stripBooleanLiterals(apiRaw) as JSONSchema7, + const apiSchema = propagateInternalVisibility( + postProcessSchema( + stripBooleanLiterals(apiRaw) as JSONSchema7, + ), ) as unknown as ApiSchema; // Ensure output directory exists diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 3afaec395..61e551d68 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -18,6 +18,7 @@ import { getRpcSchemaTypeName, getSessionEventsSchemaPath, postProcessSchema, + propagateInternalVisibility, writeGeneratedFile, collectExternalSchemaRefNames, collectDefinitionCollections, @@ -36,7 +37,9 @@ import { isNodeFullyDeprecated, isVoidSchema, isSchemaExperimental, + appendPropertyMarkerTagsToDescriptions, getEnumValueDescriptions, + stripOpaqueJsonMarker, type ApiSchema, type DefinitionCollections, type RpcMethod, @@ -280,6 +283,13 @@ export function normalizeSchemaForTypeScript(schema: JSONSchema7): JSONSchema7 { Object.entries(value as Record).map(([key, child]) => [key, rewrite(child)]) ) as Record; + // The TypeScript codegen doesn't distinguish opaque JSON from any + // other unconstrained value, so drop the marker before feeding the + // schema to json-schema-to-typescript. C# codegen reads the marker + // from its own (un-normalized) view of the schema and emits + // `JsonElement` instead. + stripOpaqueJsonMarker(rewritten); + const enumValueDescriptions = getEnumValueDescriptions(rewritten as JSONSchema7); if (enumValueDescriptions && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) { rewritten.tsType = (rewritten.enum as string[]) @@ -330,13 +340,14 @@ async function generateSessionEvents(schemaPath?: string): Promise { const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); const schema = JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7; - const processed = postProcessSchema(schema); + const processed = propagateInternalVisibility(postProcessSchema(schema)); const definitionCollections = collectDefinitionCollections(processed as Record); const sessionEvent = resolveSchema({ $ref: "#/definitions/SessionEvent" }, definitionCollections) ?? resolveSchema({ $ref: "#/$defs/SessionEvent" }, definitionCollections) ?? processed; const schemaForCompile = withSharedDefinitions(sessionEvent, definitionCollections); + appendPropertyMarkerTagsToDescriptions(schemaForCompile); const ts = await compile(normalizeSchemaForTypeScript(schemaForCompile), "SessionEvent", { bannerComment: `/** @@ -348,7 +359,27 @@ async function generateSessionEvents(schemaPath?: string): Promise { strictIndexSignatures: true, }); - const annotatedTs = annotateTypeScriptTypes(ts, experimentalDefinitionNames(definitionCollections), TS_EXPERIMENTAL_JSDOC); + let annotatedTs = annotateTypeScriptTypes(ts, experimentalDefinitionNames(definitionCollections), TS_EXPERIMENTAL_JSDOC); + // Add @internal JSDoc annotations for session-event types marked + // `visibility: "internal"` in the schema. The tag drives `stripInternal` + // so the whole type is dropped from the published .d.ts. + const sessionInternalTypes = new Set(); + for (const [name, def] of Object.entries(definitionCollections.definitions ?? {})) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + sessionInternalTypes.add(name); + } + } + for (const [name, def] of Object.entries(definitionCollections.$defs ?? {})) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + sessionInternalTypes.add(name); + } + } + for (const intType of sessionInternalTypes) { + annotatedTs = annotatedTs.replace( + new RegExp(`(^|\\n)(export (?:interface|type) ${intType}\\b)`, "m"), + `$1/** @internal */\n$2` + ); + } const outPath = await writeGeneratedFile("nodejs/src/generated/session-events.ts", annotatedTs); console.log(` ✓ ${outPath}`); } @@ -579,6 +610,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; } const schemaForCompile = combinedSchema; + appendPropertyMarkerTagsToDescriptions(schemaForCompile); const compiled = await compile(normalizeSchemaForTypeScript(schemaForCompile), "_RpcSchemaRoot", { bannerComment: "", @@ -895,7 +927,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro await generateSessionEvents(sessionSchemaPath); try { const resolvedSessionPath = sessionSchemaPath ?? (await getSessionEventsSchemaPath()); - const sessionSchema = postProcessSchema(JSON.parse(await fs.readFile(resolvedSessionPath, "utf-8")) as JSONSchema7); + const sessionSchema = propagateInternalVisibility(postProcessSchema(JSON.parse(await fs.readFile(resolvedSessionPath, "utf-8")) as JSONSchema7)); await generateRpc(apiSchemaPath, sessionSchema); } catch (err) { if ((err as NodeJS.ErrnoException).code === "ENOENT" && !apiSchemaPath) { diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index a06be7607..4d04bae9a 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -501,6 +501,346 @@ export function isSchemaExperimental(schema: JSONSchema7 | null | undefined): bo return typeof schema === "object" && schema !== null && (schema as Record).stability === "experimental"; } +/** Returns true when a JSON Schema node is marked as visibility:"internal" (set via `.asInternal()` on the Zod source). */ +export function isSchemaInternal(schema: JSONSchema7 | null | undefined): boolean { + return typeof schema === "object" && schema !== null && (schema as Record).visibility === "internal"; +} + +/** + * Collects the set of definition names marked `visibility: "internal"` and a + * per-definition set of internal property names. Used by code generators that + * need to apply `_`-prefix or similar renames consistently across both type + * declarations and references. + * + * Call after `propagateInternalVisibility` so transitively-internal fields are + * also picked up. + */ +export function collectInternalSymbols(schema: JSONSchema7): { + typeNames: Set; + fieldsByType: Map>; +} { + const typeNames = new Set(); + const fieldsByType = new Map>(); + const { definitions, $defs } = collectDefinitionCollections(schema as Record); + const allDefs: Record = { ...definitions, ...$defs }; + for (const [name, def] of Object.entries(allDefs)) { + if (!def || typeof def !== "object") continue; + const d = def as Record; + if (d.visibility === "internal") typeNames.add(name); + const props = d.properties; + if (props && typeof props === "object" && !Array.isArray(props)) { + for (const [propName, propSchema] of Object.entries(props as Record)) { + if (propSchema && typeof propSchema === "object" && (propSchema as Record).visibility === "internal") { + if (!fieldsByType.has(name)) fieldsByType.set(name, new Set()); + fieldsByType.get(name)!.add(propName); + } + } + } + } + return { typeNames, fieldsByType }; +} + +/** + * Post-process a Python module so that types marked `visibility: "internal"` + * carry an underscore prefix on their class identifier. + * + * Why: Python has no compiler-enforced visibility, but the leading-underscore + * convention is universally recognized as "no stability guarantee". Combined + * with `__all__` exclusion at the module level (handled separately), this is + * the strongest "internal" signal Python idioms provide and matches the + * cross-language bar of "we can do breaking changes on these without + * having to apologize". + * + * Field-level visibility is expected to be handled at emission time by each + * Python emitter (because field names depend on the emitter's PEP 8 normalization + * and the emitter's class-name conventions may diverge from the schema's + * definition names, breaking any single-class regex). Type-level renaming is + * safe to do globally because schema definition names match the emitted class + * identifiers for the types that carry `visibility: "internal"`. + */ +export function renameInternalPythonSymbols( + code: string, + typeNames: Iterable +): string { + const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + let result = code; + const sortedTypes = [...typeNames].sort((a, b) => b.length - a.length); + // Phase 1: rename each identifier globally at word boundaries. + for (const t of sortedTypes) { + result = result.replace( + new RegExp(`(?> { + const out = new Map>(); + const { definitions, $defs } = collectDefinitionCollections(schema as Record); + const allDefs: Record = { ...definitions, ...$defs }; + for (const [name, def] of Object.entries(allDefs)) { + if (!def || typeof def !== "object") continue; + const d = def as Record; + if (d.visibility === "internal") continue; + const props = d.properties; + if (!props || typeof props !== "object" || Array.isArray(props)) continue; + for (const [propName, propSchema] of Object.entries(props as Record)) { + if (propSchema && typeof propSchema === "object" && (propSchema as Record).visibility === "internal") { + if (!out.has(name)) out.set(name, new Set()); + out.get(name)!.add(propName); + } + } + } + return out; +} + +/** + * Annotate quicktype-generated Python field declarations whose schema is marked + * `visibility: "internal"` with a `# Internal:` comment immediately above the + * declaration. The comment is visible in IDE hovers/code completion, so + * consumers see the marker even though the identifier itself is unchanged. + * + * This is the field-level fallback for code paths that can't rename the field + * identifier (quicktype's generated `from_dict`/`to_dict` reference field names + * in patterns brittle to regex rewriting). For session-events and other + * hand-rolled emitters, prefer renaming. + * + * The `toFieldName` callback maps a JSON property name to its Python attribute + * name (typically snake_case). + */ +export function annotateInternalPythonFields( + code: string, + fieldsByType: Map>, + toFieldName: (jsonName: string) => string +): string { + const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + let result = code; + for (const [typeName, fields] of fieldsByType) { + // Match the class body up to the next top-level statement. quicktype's + // generated classes are separated by blank-line boundaries. + const classRe = new RegExp( + `(@dataclass\\nclass ${escapeRegex(typeName)}[:(][^]*?)(?=\\n(?:@dataclass\\n)?class \\w|\\n\\nclass |\\n[A-Za-z_]\\w* =|$)`, + "g" + ); + result = result.replace(classRe, (block) => { + for (const jsonField of fields) { + const pyField = toFieldName(jsonField); + const escaped = escapeRegex(pyField); + // Match ` fieldName: type` style declarations (PEP 526). Avoid + // double-annotating if the comment is already present immediately above. + block = block.replace( + new RegExp(`(^(?! # Internal:.*$)(?:.*\\n)?)( )${escaped}(?=\\s*:)`, "gm"), + (_match, prefix, indent) => { + // Avoid duplicate annotation if the previous line is already an Internal: marker. + if (/ # Internal:/.test(prefix)) return `${prefix}${indent}${pyField}`; + return `${prefix}${indent}# Internal: this field is an internal SDK API and is not part of the public surface.\n${indent}${pyField}`; + } + ); + } + return block; + }); + } + return result; +} + +/** + * Walks a top-level JSON Schema and marks any property whose referenced type + * resolves to an internal definition as `visibility: "internal"` itself. + * + * Schemas can be authored with an internal-typed reference on a property that + * isn't itself explicitly marked internal (e.g. `copilotUsage` referencing + * `AssistantUsageCopilotUsage`). Code generators that map `visibility: + * "internal"` to hard language-level visibility (C# `internal`, Rust + * `pub(crate)`) would otherwise produce inconsistent-accessibility errors + * (CS0053 in C#, E0446 in Rust). This pass closes that gap by promoting + * referencing properties to internal — matching the language compilers' + * own transitivity rule. + * + * Only references that resolve directly, through arrays, or through dictionary + * `additionalProperties` are considered. References that flow only through a + * `oneOf`/`anyOf` of public+internal variants are left alone (the union itself + * is the carrier of visibility there). + * + * Mutates `schema` in place and returns it. Idempotent. + */ +export function propagateInternalVisibility(schema: JSONSchema7): JSONSchema7 { + if (typeof schema !== "object" || schema === null) return schema; + + const { definitions, $defs } = collectDefinitionCollections(schema as Record); + const allDefs: Record = { ...definitions, ...$defs }; + const internalTypeNames = new Set(); + for (const [name, def] of Object.entries(allDefs)) { + if (def && typeof def === "object" && isSchemaInternal(def as JSONSchema7)) { + internalTypeNames.add(name); + } + } + if (internalTypeNames.size === 0) return schema; + + const refToName = (ref: unknown): string | undefined => { + if (typeof ref !== "string") return undefined; + const m = ref.match(/^#\/(?:definitions|\$defs)\/([^/]+)$/); + return m ? m[1] : undefined; + }; + + /** Returns true when a property's *direct* type carrier is an internal definition. */ + const propertyReferencesInternal = (propSchema: JSONSchema7): boolean => { + const direct = refToName((propSchema as Record).$ref); + if (direct && internalTypeNames.has(direct)) return true; + const items = (propSchema as Record).items; + if (items && typeof items === "object" && !Array.isArray(items)) { + const itemsRef = refToName((items as Record).$ref); + if (itemsRef && internalTypeNames.has(itemsRef)) return true; + } + const addl = (propSchema as Record).additionalProperties; + if (addl && typeof addl === "object") { + const addlRef = refToName((addl as Record).$ref); + if (addlRef && internalTypeNames.has(addlRef)) return true; + } + return false; + }; + + const visit = (node: unknown): void => { + if (!node || typeof node !== "object") return; + if (Array.isArray(node)) { + for (const item of node) visit(item); + return; + } + const record = node as Record; + const props = record.properties; + if (props && typeof props === "object" && !Array.isArray(props)) { + for (const propSchema of Object.values(props as Record)) { + if (!propSchema || typeof propSchema !== "object") continue; + if (!isSchemaInternal(propSchema as JSONSchema7) && propertyReferencesInternal(propSchema as JSONSchema7)) { + (propSchema as Record).visibility = "internal"; + } + visit(propSchema); + } + } + for (const key of ["items", "additionalProperties", "anyOf", "allOf", "oneOf"]) { + if (record[key]) visit(record[key]); + } + for (const collectionKey of ["definitions", "$defs"]) { + const collection = record[collectionKey]; + if (collection && typeof collection === "object" && !Array.isArray(collection)) { + for (const def of Object.values(collection as Record)) { + if (def && typeof def === "object") visit(def); + } + } + } + }; + + visit(schema); + return schema; +} + +/** + * Returns true when a JSON Schema node is marked `x-opaque-json: true` (set via + * `.asOpaqueJson()` on the Zod source). These are the only shapes that legitimately + * surface as opaque JSON in the SDK; everything else with an underspecified type + * is rejected by the runtime's schema lint pass. + */ +export function isOpaqueJson(schema: JSONSchema7 | null | undefined): boolean { + return typeof schema === "object" && schema !== null && (schema as Record)["x-opaque-json"] === true; +} + +/** + * Removes the `x-opaque-json` marker from a schema node in place. Useful for + * codegens (e.g. TypeScript) that don't distinguish opaque JSON from any other + * unconstrained value and would otherwise have the marker confuse downstream + * tooling. Codegens that *do* care (e.g. C#, which maps opaque JSON to + * `JsonElement`) should call `isOpaqueJson` *before* this point. + */ +export function stripOpaqueJsonMarker(schema: Record): void { + delete schema["x-opaque-json"]; +} + +/** + * Append `@internal` and/or `@experimental` JSDoc-style tags to the `description` + * of every property that carries `visibility: "internal"` or `stability: "experimental"` + * inline. Used by codegens whose output mechanism (e.g. `json-schema-to-typescript`) + * renders `description` verbatim as JSDoc; downstream tooling then picks the tags + * up automatically. + * + * Mutates `schema` in place and returns it. Callers that don't want their input + * mutated should clone first. + */ +export function appendPropertyMarkerTagsToDescriptions(schema: JSONSchema7): JSONSchema7 { + const seen = new WeakSet(); + const visit = (node: unknown): void => { + if (!node || typeof node !== "object") return; + if (seen.has(node)) return; + seen.add(node); + + if (Array.isArray(node)) { + for (const item of node) visit(item); + return; + } + + const record = node as Record; + const props = record.properties; + if (props && typeof props === "object" && !Array.isArray(props)) { + for (const propSchema of Object.values(props as Record)) { + if (!propSchema || typeof propSchema !== "object") continue; + const tags: string[] = []; + if (isSchemaInternal(propSchema as JSONSchema7)) tags.push("@internal"); + if (isSchemaExperimental(propSchema as JSONSchema7)) tags.push("@experimental"); + if (tags.length === 0) continue; + const propRecord = propSchema as Record; + const existing = typeof propRecord.description === "string" ? propRecord.description : ""; + const suffix = tags.join("\n"); + propRecord.description = existing.length > 0 ? `${existing}\n\n${suffix}` : suffix; + + // json-schema-to-typescript drops the description on properties whose + // schema is a bare `$ref`. Rewriting to `allOf: [{$ref}]` keeps the + // referenced type while preserving the description (and our appended + // JSDoc tags) on the property declaration. Other generators don't see + // this wrapper because they consume the schema before this pass. + if (typeof propRecord.$ref === "string" && !propRecord.allOf) { + const refValue = propRecord.$ref; + delete propRecord.$ref; + propRecord.allOf = [{ $ref: refValue } as JSONSchema7Definition]; + } + } + } + + for (const value of Object.values(record)) { + if (value && typeof value === "object") visit(value); + } + }; + visit(schema); + return schema; +} + // ── $ref resolution ───────────────────────────────────────────────────────── /** Extract the generated type name from a `$ref` path (e.g. "#/definitions/Model" → "Model"). */ diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts index 0b0879db1..b8d7cb089 100644 --- a/scripts/docs-validation/extract.ts +++ b/scripts/docs-validation/extract.ts @@ -301,9 +301,16 @@ function wrapCodeForValidation(block: CodeBlock): string { } } - // Always ensure SDK using is present - if (!usings.some(u => u.includes("GitHub.Copilot"))) { + // Always ensure SDK usings are present. If the snippet already + // declares any GitHub.Copilot using, assume the author curated + // them and don't add others (avoids name ambiguities like + // ModelCapabilities living in both namespaces). + const hasAnyCopilotUsing = usings.some(u => + u.includes("GitHub.Copilot;") || u.includes("GitHub.Copilot."), + ); + if (!hasAnyCopilotUsing) { usings.push("using GitHub.Copilot;"); + usings.push("using GitHub.Copilot.Rpc;"); } // Generate a unique class name based on block location @@ -335,9 +342,12 @@ ${indentedCode} }`; } } else { - // Has structure, but may still need using directive - if (!code.includes("using GitHub.Copilot;")) { - code = "using GitHub.Copilot;\n" + code; + // Has structure. Only add SDK usings if neither namespace is present; + // if the snippet declares its own using GitHub.Copilot statement, + // assume the author curated imports (avoids ambiguities like + // ModelCapabilities living in both namespaces). + if (!code.includes("using GitHub.Copilot")) { + code = "using GitHub.Copilot;\nusing GitHub.Copilot.Rpc;\n" + code; } } } diff --git a/scripts/docs-validation/validate.ts b/scripts/docs-validation/validate.ts index c1d408c36..bf11baa6e 100644 --- a/scripts/docs-validation/validate.ts +++ b/scripts/docs-validation/validate.ts @@ -286,7 +286,7 @@ async function validateCSharp(): Promise { net8.0 enable enable - CS8019;CS0168;CS0219 + CS8019;CS0168;CS0219;GHCP001 diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 3e29965e5..5182ce24a 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.52-1", + "@github/copilot": "^1.0.53-2", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -464,9 +464,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.52-1.tgz", - "integrity": "sha512-oz6m/dOpTU+FaCWXqYZj5JkJmRT+/RYcrmtGal39V+gOxTA2Nc9wIeLH1SMwMoOXC9Q6DN6keiY0wqWcHirPVg==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.53-2.tgz", + "integrity": "sha512-SkISXco8PFyuOreaPIiBiyQHdXnw51wLmSvzW7yrdD02dH9qRBCcrxPXFS05iLrv3hLCnhhECKJUv1afTPtUBg==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -476,20 +476,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.52-1", - "@github/copilot-darwin-x64": "1.0.52-1", - "@github/copilot-linux-arm64": "1.0.52-1", - "@github/copilot-linux-x64": "1.0.52-1", - "@github/copilot-linuxmusl-arm64": "1.0.52-1", - "@github/copilot-linuxmusl-x64": "1.0.52-1", - "@github/copilot-win32-arm64": "1.0.52-1", - "@github/copilot-win32-x64": "1.0.52-1" + "@github/copilot-darwin-arm64": "1.0.53-2", + "@github/copilot-darwin-x64": "1.0.53-2", + "@github/copilot-linux-arm64": "1.0.53-2", + "@github/copilot-linux-x64": "1.0.53-2", + "@github/copilot-linuxmusl-arm64": "1.0.53-2", + "@github/copilot-linuxmusl-x64": "1.0.53-2", + "@github/copilot-win32-arm64": "1.0.53-2", + "@github/copilot-win32-x64": "1.0.53-2" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.52-1.tgz", - "integrity": "sha512-DWXtC/yItZVtkSQhPyRMEkFwa2mcY2rg2cu/uwJ15L9ReiYvlKYEZQDe1TMqkT+U6+k9KjA2L2HQfXVm14/vTw==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.53-2.tgz", + "integrity": "sha512-Ws+YLk9Gyix2IaqzFSuZe00fhX5IGAgNXyVNzkO1MvtnFSj9vGTAFslF94cf3VkpaI8VNf+O3MRGzaQohCpv4A==", "cpu": [ "arm64" ], @@ -504,9 +504,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.52-1.tgz", - "integrity": "sha512-NFTJkzzlTALMfbj9CDJ7N09PRPTVFq1+71hk+zoNx1uT/pi954liV6tKSaNAihPIXTMQKfJXGwEdjtvACpc8Vg==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.53-2.tgz", + "integrity": "sha512-3yHajiuz5UBsdpOlaZCLp2diveXJcIbXbjdjmovPIUrY/2h4yUbQSBEkFuxzV5CAuehQE9S7+NaZYMhUXRIl9Q==", "cpu": [ "x64" ], @@ -521,9 +521,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.52-1.tgz", - "integrity": "sha512-CZE29v+RPJClHgVE1rU+RpRWSG8lm48koRZ0taKVopqLRD6NWKjBOwFKYJojk08H8/K+BWr/paM5+R8hEZHxZw==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.53-2.tgz", + "integrity": "sha512-VgpKr1c7vw8lDqNPOIHYj7Qj6FJY2j3dTh6xaBcItUDLD7y45Pp36JJXrPiVjya7Upx4ThxR/kj9KSRxx4s5pg==", "cpu": [ "arm64" ], @@ -538,9 +538,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.52-1.tgz", - "integrity": "sha512-tJhLQV70TJLq3hPXg7P6pHPfE4vaT2nENIXZsHu6fBkOcsSAxX1APSv6Bkyfsiod8EfFHkcG2+n7VXiVg8WqFw==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.53-2.tgz", + "integrity": "sha512-1mZUCQVeS2y5douEq8tCIEBr1XP8L0UM7fo4MmJHlLT+6ykZz1pyJPtnpO8OO4GGRvenOFd/XM0k2a+KpxYqtw==", "cpu": [ "x64" ], @@ -555,9 +555,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.52-1.tgz", - "integrity": "sha512-u24wHsUumldUEPWX/5z5IEuJvixiQEYF82N04P1g65dvOknq+89dpj+GND4Rh3Vr5u13drgj5AJqkJbWB8N+EQ==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.53-2.tgz", + "integrity": "sha512-gf2wgQu8DuUZSz0Fdq2f13TvWOi6+xkkyQQM3mPtt841UV0K8eUV9MBSRvkv1zbd0RG9MgbPaBLE8TRpWezikg==", "cpu": [ "arm64" ], @@ -572,9 +572,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.52-1.tgz", - "integrity": "sha512-wM22FxcHL8NlnesKKQPPvtk4ojqefN7irU5tQcX+IunpD1izVQl7AOXhZyHoQ21zQnN0De8EapxOUc+WnvlxpA==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.53-2.tgz", + "integrity": "sha512-Zk/o40BOmuNUC6eLZnPRGE45dzdmrPbjusyGOdLKXFzlImHHW2SwYoFchnubzpz81Hzwur3/vCqYtGWjTSa8LA==", "cpu": [ "x64" ], @@ -589,9 +589,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.52-1.tgz", - "integrity": "sha512-ecvfl9N7DPSwpiT2ZNUSXR1ZrSKwpkByOU6VcNphh4RptPZ0iNfyRNLhFCwSfFz+FvB6z2LZi+F7jSzQ3SaT3w==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.53-2.tgz", + "integrity": "sha512-BBMC0QIOn+f61VGfyZbEiFupWJToZwftv9FhJ7xOh1utg3lwmBfjmaG9BKXdnaFqlOjP8mUKbgAkyBJHGZYNOA==", "cpu": [ "arm64" ], @@ -606,9 +606,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.52-1", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.52-1.tgz", - "integrity": "sha512-a9Ct7krktP+/pfPdh/K57deYzzmL13e5Tb1pf5E152u4o/5xKzfgroNFUOzotFfFhs1jFhdzKCm3WHNLIvVEHA==", + "version": "1.0.53-2", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.53-2.tgz", + "integrity": "sha512-ObuGJ/AGnhTl3kOPIjY9xj3BfucjpQNytmIPQGwgMDDBisSvtfdQ5WVbZlKG736VtQ1epZ1RmzS28bKTudBD/Q==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index dddf8fa5f..b1d70c9dc 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -11,7 +11,7 @@ "test": "vitest run" }, "devDependencies": { - "@github/copilot": "^1.0.52-1", + "@github/copilot": "^1.0.53-2", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", diff --git a/test/scenarios/Directory.Build.props b/test/scenarios/Directory.Build.props new file mode 100644 index 000000000..8350045c9 --- /dev/null +++ b/test/scenarios/Directory.Build.props @@ -0,0 +1,5 @@ + + + $(NoWarn);GHCP001 + + diff --git a/test/scenarios/auth/byok-anthropic/csharp/Program.cs b/test/scenarios/auth/byok-anthropic/csharp/Program.cs index f29cfd4d8..3aac89577 100644 --- a/test/scenarios/auth/byok-anthropic/csharp/Program.cs +++ b/test/scenarios/auth/byok-anthropic/csharp/Program.cs @@ -10,10 +10,7 @@ return 1; } -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/auth/byok-anthropic/go/main.go b/test/scenarios/auth/byok-anthropic/go/main.go index ae1ea92a0..efe7d5b4d 100644 --- a/test/scenarios/auth/byok-anthropic/go/main.go +++ b/test/scenarios/auth/byok-anthropic/go/main.go @@ -25,7 +25,7 @@ func main() { model = "claude-sonnet-4-20250514" } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -59,8 +59,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index 3ad893ba5..5b623ff87 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -1,8 +1,8 @@ import asyncio import os import sys + from copilot import CopilotClient -from copilot.client import SubprocessConfig ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") @@ -14,29 +14,25 @@ async def main(): - client = CopilotClient(SubprocessConfig( - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": ANTHROPIC_MODEL, - "provider": { + session = await client.create_session( + model=ANTHROPIC_MODEL, + provider={ "type": "anthropic", "base_url": ANTHROPIC_BASE_URL, "api_key": ANTHROPIC_API_KEY, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) - - response = await session.send_and_wait( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print(response.data.content) diff --git a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts index bb60158c2..67eb27dd8 100644 --- a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts +++ b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const apiKey = process.env.ANTHROPIC_API_KEY; @@ -9,9 +9,7 @@ async function main() { process.exit(1); } - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/auth/byok-azure/csharp/Program.cs b/test/scenarios/auth/byok-azure/csharp/Program.cs index 64132bbff..635404843 100644 --- a/test/scenarios/auth/byok-azure/csharp/Program.cs +++ b/test/scenarios/auth/byok-azure/csharp/Program.cs @@ -11,10 +11,7 @@ return 1; } -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/auth/byok-azure/go/main.go b/test/scenarios/auth/byok-azure/go/main.go index eece7a9cd..eea3fb8d6 100644 --- a/test/scenarios/auth/byok-azure/go/main.go +++ b/test/scenarios/auth/byok-azure/go/main.go @@ -26,7 +26,7 @@ func main() { apiVersion = "2024-10-21" } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -63,8 +63,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 1ae214261..031ff47ee 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -1,8 +1,8 @@ import asyncio import os import sys + from copilot import CopilotClient -from copilot.client import SubprocessConfig AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") @@ -15,14 +15,12 @@ async def main(): - client = CopilotClient(SubprocessConfig( - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": AZURE_OPENAI_MODEL, - "provider": { + session = await client.create_session( + model=AZURE_OPENAI_MODEL, + provider={ "type": "azure", "base_url": AZURE_OPENAI_ENDPOINT, "api_key": AZURE_OPENAI_API_KEY, @@ -30,17 +28,15 @@ async def main(): "api_version": AZURE_API_VERSION, }, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) - - response = await session.send_and_wait( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print(response.data.content) diff --git a/test/scenarios/auth/byok-azure/typescript/src/index.ts b/test/scenarios/auth/byok-azure/typescript/src/index.ts index 14d4e5ced..8df0e4de3 100644 --- a/test/scenarios/auth/byok-azure/typescript/src/index.ts +++ b/test/scenarios/auth/byok-azure/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const endpoint = process.env.AZURE_OPENAI_ENDPOINT; @@ -10,9 +10,7 @@ async function main() { process.exit(1); } - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/auth/byok-ollama/csharp/Program.cs b/test/scenarios/auth/byok-ollama/csharp/Program.cs index 69578a378..62b000af1 100644 --- a/test/scenarios/auth/byok-ollama/csharp/Program.cs +++ b/test/scenarios/auth/byok-ollama/csharp/Program.cs @@ -6,10 +6,7 @@ var compactSystemPrompt = "You are a compact local assistant. Keep answers short, concrete, and under 80 words."; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/auth/byok-ollama/go/main.go b/test/scenarios/auth/byok-ollama/go/main.go index 8232c63dc..c776da27b 100644 --- a/test/scenarios/auth/byok-ollama/go/main.go +++ b/test/scenarios/auth/byok-ollama/go/main.go @@ -22,7 +22,7 @@ func main() { model = "llama3.2:3b" } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -55,8 +55,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index 78019acd7..90c4838f8 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -1,8 +1,7 @@ import asyncio import os -import sys + from copilot import CopilotClient -from copilot.client import SubprocessConfig OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b") @@ -13,28 +12,24 @@ async def main(): - client = CopilotClient(SubprocessConfig( - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": OLLAMA_MODEL, - "provider": { + session = await client.create_session( + model=OLLAMA_MODEL, + provider={ "type": "openai", "base_url": OLLAMA_BASE_URL, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": COMPACT_SYSTEM_PROMPT, }, - }) - - response = await session.send_and_wait( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print(response.data.content) diff --git a/test/scenarios/auth/byok-ollama/typescript/src/index.ts b/test/scenarios/auth/byok-ollama/typescript/src/index.ts index 7db9dd81c..af2d71a44 100644 --- a/test/scenarios/auth/byok-ollama/typescript/src/index.ts +++ b/test/scenarios/auth/byok-ollama/typescript/src/index.ts @@ -1,15 +1,14 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; -const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1"; +const OLLAMA_BASE_URL = + process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1"; const OLLAMA_MODEL = process.env.OLLAMA_MODEL ?? "llama3.2:3b"; const COMPACT_SYSTEM_PROMPT = "You are a compact local assistant. Keep answers short, concrete, and under 80 words."; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/auth/byok-openai/csharp/Program.cs b/test/scenarios/auth/byok-openai/csharp/Program.cs index d98cffbc3..826e35443 100644 --- a/test/scenarios/auth/byok-openai/csharp/Program.cs +++ b/test/scenarios/auth/byok-openai/csharp/Program.cs @@ -10,10 +10,7 @@ return 1; } -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/auth/byok-openai/go/main.go b/test/scenarios/auth/byok-openai/go/main.go index 01d0b6da9..d3221523d 100644 --- a/test/scenarios/auth/byok-openai/go/main.go +++ b/test/scenarios/auth/byok-openai/go/main.go @@ -25,7 +25,7 @@ func main() { model = "claude-haiku-4.5" } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -54,8 +54,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index 8362963b2..e9c673aa0 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -1,8 +1,8 @@ import asyncio import os import sys + from copilot import CopilotClient -from copilot.client import SubprocessConfig OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "claude-haiku-4.5") @@ -14,24 +14,20 @@ async def main(): - client = CopilotClient(SubprocessConfig( - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": OPENAI_MODEL, - "provider": { + session = await client.create_session( + model=OPENAI_MODEL, + provider={ "type": "openai", "base_url": OPENAI_BASE_URL, "api_key": OPENAI_API_KEY, }, - }) - - response = await session.send_and_wait( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print(response.data.content) diff --git a/test/scenarios/auth/byok-openai/typescript/src/index.ts b/test/scenarios/auth/byok-openai/typescript/src/index.ts index 1b69fc665..268f1d201 100644 --- a/test/scenarios/auth/byok-openai/typescript/src/index.ts +++ b/test/scenarios/auth/byok-openai/typescript/src/index.ts @@ -1,6 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; -const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; +const OPENAI_BASE_URL = + process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; const OPENAI_MODEL = process.env.OPENAI_MODEL ?? "claude-haiku-4.5"; const OPENAI_API_KEY = process.env.OPENAI_API_KEY; @@ -10,9 +11,7 @@ if (!OPENAI_API_KEY) { } async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/auth/gh-app/csharp/Program.cs b/test/scenarios/auth/gh-app/csharp/Program.cs index 5933ec087..f16c8236e 100644 --- a/test/scenarios/auth/gh-app/csharp/Program.cs +++ b/test/scenarios/auth/gh-app/csharp/Program.cs @@ -60,7 +60,6 @@ // Step 4: Use the token with Copilot using var client = new CopilotClient(new CopilotClientOptions { - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), GitHubToken = accessToken, }); diff --git a/test/scenarios/auth/gh-app/go/main.go b/test/scenarios/auth/gh-app/go/main.go index b19d21cbd..5f774ef2c 100644 --- a/test/scenarios/auth/gh-app/go/main.go +++ b/test/scenarios/auth/gh-app/go/main.go @@ -186,8 +186,8 @@ func main() { log.Fatal(err) } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index afba29254..0d5a5ee9d 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -5,8 +5,6 @@ import urllib.request from copilot import CopilotClient -from copilot.client import SubprocessConfig - DEVICE_CODE_URL = "https://github.com/login/device/code" ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token" @@ -61,7 +59,9 @@ def poll_for_access_token(client_id: str, device_code: str, interval: int) -> st if data.get("error") == "slow_down": delay_seconds = int(data.get("interval", delay_seconds + 5)) continue - raise RuntimeError(data.get("error_description") or data.get("error") or "OAuth polling failed") + raise RuntimeError( + data.get("error_description") or data.get("error") or "OAuth polling failed" + ) async def main(): @@ -79,13 +79,10 @@ async def main(): display_name = f" ({user.get('name')})" if user.get("name") else "" print(f"Authenticated as: {user.get('login')}{display_name}") - client = CopilotClient(SubprocessConfig( - github_token=token, - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient(github_token=token) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/auth/gh-app/typescript/src/index.ts b/test/scenarios/auth/gh-app/typescript/src/index.ts index bfd53898c..b76fdc0a2 100644 --- a/test/scenarios/auth/gh-app/typescript/src/index.ts +++ b/test/scenarios/auth/gh-app/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; import readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; @@ -33,7 +33,10 @@ if (!CLIENT_ID) { process.exit(1); } -async function postJson(url: string, body: Record): Promise { +async function postJson( + url: string, + body: Record, +): Promise { const response = await fetch(url, { method: "POST", headers: { @@ -44,7 +47,9 @@ async function postJson(url: string, body: Record): Promise< }); if (!response.ok) { - throw new Error(`Request failed: ${response.status} ${response.statusText}`); + throw new Error( + `Request failed: ${response.status} ${response.statusText}`, + ); } return (await response.json()) as T; @@ -60,7 +65,9 @@ async function getJson(url: string, token: string): Promise { }); if (!response.ok) { - throw new Error(`GitHub API failed: ${response.status} ${response.statusText}`); + throw new Error( + `GitHub API failed: ${response.status} ${response.statusText}`, + ); } return (await response.json()) as T; @@ -73,7 +80,10 @@ async function startDeviceFlow(): Promise { }); } -async function pollForAccessToken(deviceCode: string, intervalSeconds: number): Promise { +async function pollForAccessToken( + deviceCode: string, + intervalSeconds: number, +): Promise { let interval = intervalSeconds; while (true) { @@ -92,7 +102,9 @@ async function pollForAccessToken(deviceCode: string, intervalSeconds: number): continue; } - throw new Error(data.error_description ?? data.error ?? "OAuth token polling failed"); + throw new Error( + data.error_description ?? data.error ?? "OAuth token polling failed", + ); } } @@ -100,17 +112,23 @@ async function main() { console.log("Starting GitHub OAuth device flow..."); const device = await startDeviceFlow(); - console.log(`Open ${device.verification_uri} and enter code: ${device.user_code}`); + console.log( + `Open ${device.verification_uri} and enter code: ${device.user_code}`, + ); const rl = readline.createInterface({ input, output }); await rl.question("Press Enter after you authorize this app..."); rl.close(); - const accessToken = await pollForAccessToken(device.device_code, device.interval); + const accessToken = await pollForAccessToken( + device.device_code, + device.interval, + ); const user = await getJson(USER_URL, accessToken); - console.log(`Authenticated as: ${user.login}${user.name ? ` (${user.name})` : ""}`); + console.log( + `Authenticated as: ${user.login}${user.name ? ` (${user.name})` : ""}`, + ); const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), gitHubToken: accessToken, }); diff --git a/test/scenarios/bundling/app-backend-to-server/python/main.py b/test/scenarios/bundling/app-backend-to-server/python/main.py index 2684a30b8..d53d89854 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -4,9 +4,9 @@ import sys import urllib.request -from flask import Flask, request, jsonify -from copilot import CopilotClient -from copilot.client import ExternalServerConfig +from flask import Flask, jsonify, request + +from copilot import CopilotClient, RuntimeConnection app = Flask(__name__) @@ -14,10 +14,10 @@ async def ask_copilot(prompt: str) -> str: - client = CopilotClient(ExternalServerConfig(url=CLI_URL)) + client = CopilotClient(connection=RuntimeConnection.for_uri(CLI_URL)) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") response = await session.send_and_wait(prompt) @@ -70,6 +70,7 @@ def self_test(port: int): ) server_thread.start() import time + time.sleep(1) self_test(port) else: diff --git a/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts b/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts index 7ab734d1a..c169e7f89 100644 --- a/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts +++ b/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts @@ -2,7 +2,8 @@ import express from "express"; import { CopilotClient } from "@github/copilot-sdk"; const PORT = parseInt(process.env.PORT || "8080", 10); -const CLI_URL = process.env.CLI_URL || process.env.COPILOT_CLI_URL || "localhost:3000"; +const CLI_URL = + process.env.CLI_URL || process.env.COPILOT_CLI_URL || "localhost:3000"; const app = express(); app.use(express.json()); diff --git a/test/scenarios/bundling/app-direct-server/go/main.go b/test/scenarios/bundling/app-direct-server/go/main.go index acdbaab76..95da9cf68 100644 --- a/test/scenarios/bundling/app-direct-server/go/main.go +++ b/test/scenarios/bundling/app-direct-server/go/main.go @@ -41,8 +41,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index b441bec51..1bf32b475 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -1,20 +1,20 @@ import asyncio import os -from copilot import CopilotClient -from copilot.client import ExternalServerConfig + +from copilot import CopilotClient, RuntimeConnection async def main(): - client = CopilotClient(ExternalServerConfig( - url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - )) + client = CopilotClient( + connection=RuntimeConnection.for_uri( + os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + ), + ) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/bundling/container-proxy/go/main.go b/test/scenarios/bundling/container-proxy/go/main.go index acdbaab76..95da9cf68 100644 --- a/test/scenarios/bundling/container-proxy/go/main.go +++ b/test/scenarios/bundling/container-proxy/go/main.go @@ -41,8 +41,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/bundling/container-proxy/proxy.py b/test/scenarios/bundling/container-proxy/proxy.py index afe999a4c..688b9f8c1 100644 --- a/test/scenarios/bundling/container-proxy/proxy.py +++ b/test/scenarios/bundling/container-proxy/proxy.py @@ -12,7 +12,7 @@ import json import sys import time -from http.server import HTTPServer, BaseHTTPRequestHandler +from http.server import BaseHTTPRequestHandler, HTTPServer class ProxyHandler(BaseHTTPRequestHandler): diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index b441bec51..1bf32b475 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -1,20 +1,20 @@ import asyncio import os -from copilot import CopilotClient -from copilot.client import ExternalServerConfig + +from copilot import CopilotClient, RuntimeConnection async def main(): - client = CopilotClient(ExternalServerConfig( - url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - )) + client = CopilotClient( + connection=RuntimeConnection.for_uri( + os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + ), + ) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/bundling/fully-bundled/csharp/Program.cs b/test/scenarios/bundling/fully-bundled/csharp/Program.cs index e9dfbcccc..576ca5518 100644 --- a/test/scenarios/bundling/fully-bundled/csharp/Program.cs +++ b/test/scenarios/bundling/fully-bundled/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/bundling/fully-bundled/go/main.go b/test/scenarios/bundling/fully-bundled/go/main.go index 8fab8510d..51b592431 100644 --- a/test/scenarios/bundling/fully-bundled/go/main.go +++ b/test/scenarios/bundling/fully-bundled/go/main.go @@ -4,16 +4,13 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { // Go SDK auto-reads COPILOT_CLI_PATH from env - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -37,8 +34,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index 39ce2bb81..6be1d4294 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -1,21 +1,15 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts index c80c1b074..7df9cd888 100644 --- a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts +++ b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts @@ -1,9 +1,10 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), }); try { diff --git a/test/scenarios/callbacks/hooks/csharp/Program.cs b/test/scenarios/callbacks/hooks/csharp/Program.cs index 78184df2a..b20addf39 100644 --- a/test/scenarios/callbacks/hooks/csharp/Program.cs +++ b/test/scenarios/callbacks/hooks/csharp/Program.cs @@ -1,12 +1,9 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var hookLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -16,7 +13,7 @@ { Model = "claude-haiku-4.5", OnPermissionRequest = (request, invocation) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), Hooks = new SessionHooks { OnSessionStart = (input, invocation) => diff --git a/test/scenarios/callbacks/hooks/go/main.go b/test/scenarios/callbacks/hooks/go/main.go index 4ef48b483..d080a6ae1 100644 --- a/test/scenarios/callbacks/hooks/go/main.go +++ b/test/scenarios/callbacks/hooks/go/main.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -22,9 +22,7 @@ func main() { hookLogMu.Unlock() } - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -34,8 +32,8 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, Hooks: &copilot.SessionHooks{ OnSessionStart: func(input copilot.SessionStartHookInput, inv copilot.HookInvocation) (*copilot.SessionStartHookOutput, error) { diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index ba224ef24..434c86509 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -1,15 +1,13 @@ import asyncio -import os -from copilot import CopilotClient -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult +from copilot import CopilotClient +from copilot.generated.rpc import PermissionDecisionApproveOnce hook_log: list[str] = [] async def auto_approve_permission(request, invocation): - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() async def on_session_start(input_data, invocation): @@ -42,25 +40,20 @@ async def on_error_occurred(input_data, invocation): async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": auto_approve_permission, - "hooks": { - "on_session_start": on_session_start, - "on_session_end": on_session_end, - "on_pre_tool_use": on_pre_tool_use, - "on_post_tool_use": on_post_tool_use, - "on_user_prompt_submitted": on_user_prompt_submitted, - "on_error_occurred": on_error_occurred, - }, - } + model="claude-haiku-4.5", + on_permission_request=auto_approve_permission, + hooks={ + "on_session_start": on_session_start, + "on_session_end": on_session_end, + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + "on_user_prompt_submitted": on_user_prompt_submitted, + "on_error_occurred": on_error_occurred, + }, ) response = await session.send_and_wait( diff --git a/test/scenarios/callbacks/hooks/rust/src/main.rs b/test/scenarios/callbacks/hooks/rust/src/main.rs index 179765d2f..3f1cd056e 100644 --- a/test/scenarios/callbacks/hooks/rust/src/main.rs +++ b/test/scenarios/callbacks/hooks/rust/src/main.rs @@ -90,9 +90,7 @@ impl SessionHooks for HookLogger { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let hook_log = Arc::new(Mutex::new(Vec::::new())); let hooks = Arc::new(HookLogger { @@ -102,7 +100,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); let config = config - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_hooks(hooks); let session = client.create_session(config).await?; @@ -126,6 +124,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } println!("\nTotal hooks fired: {}", log.len()); - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/callbacks/hooks/typescript/src/index.ts b/test/scenarios/callbacks/hooks/typescript/src/index.ts index 1c92c6eec..c712994a9 100644 --- a/test/scenarios/callbacks/hooks/typescript/src/index.ts +++ b/test/scenarios/callbacks/hooks/typescript/src/index.ts @@ -1,12 +1,9 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const hookLog: string[] = []; - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -37,7 +34,8 @@ async function main() { }); const response = await session.sendAndWait({ - prompt: "List the files in the current directory using the glob tool with pattern '*.md'.", + prompt: + "List the files in the current directory using the glob tool with pattern '*.md'.", }); if (response) { diff --git a/test/scenarios/callbacks/permissions/csharp/Program.cs b/test/scenarios/callbacks/permissions/csharp/Program.cs index cf3275e56..7818580fb 100644 --- a/test/scenarios/callbacks/permissions/csharp/Program.cs +++ b/test/scenarios/callbacks/permissions/csharp/Program.cs @@ -1,12 +1,9 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var permissionLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -27,7 +24,7 @@ _ => request.Kind, }; permissionLog.Add($"approved:{toolName}"); - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); }, Hooks = new SessionHooks { diff --git a/test/scenarios/callbacks/permissions/go/main.go b/test/scenarios/callbacks/permissions/go/main.go index 23715727b..428ab0a70 100644 --- a/test/scenarios/callbacks/permissions/go/main.go +++ b/test/scenarios/callbacks/permissions/go/main.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -16,9 +16,7 @@ func main() { permissionLogMu sync.Mutex ) - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -28,7 +26,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { permissionLogMu.Lock() permissionName := string(req.Kind()) switch request := req.(type) { @@ -41,7 +39,7 @@ func main() { } permissionLog = append(permissionLog, fmt.Sprintf("approved:%s", permissionName)) permissionLogMu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, Hooks: &copilot.SessionHooks{ OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 677ca58d0..2e8e5c3e5 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -1,8 +1,7 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionApproveOnce # Track which tools requested permission permission_log: list[str] = [] @@ -10,7 +9,7 @@ async def log_permission(request, invocation): permission_log.append(f"approved:{request.tool_name}") - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() async def auto_approve_tool(input_data, invocation): @@ -18,18 +17,13 @@ async def auto_approve_tool(input_data, invocation): async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": log_permission, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + model="claude-haiku-4.5", + on_permission_request=log_permission, + hooks={"on_pre_tool_use": auto_approve_tool}, ) response = await session.send_and_wait( diff --git a/test/scenarios/callbacks/permissions/rust/src/main.rs b/test/scenarios/callbacks/permissions/rust/src/main.rs index 214620e35..69c0037a6 100644 --- a/test/scenarios/callbacks/permissions/rust/src/main.rs +++ b/test/scenarios/callbacks/permissions/rust/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use async_trait::async_trait; -use github_copilot_sdk::handler::{PermissionResult, SessionHandler}; +use github_copilot_sdk::handler::{PermissionHandler, PermissionResult}; use github_copilot_sdk::hooks::{HookContext, PreToolUseInput, PreToolUseOutput, SessionHooks}; use github_copilot_sdk::types::{PermissionRequestData, RequestId, SessionConfig, SessionId}; use github_copilot_sdk::{Client, ClientOptions}; @@ -15,8 +15,8 @@ struct PermissionLogger { } #[async_trait] -impl SessionHandler for PermissionLogger { - async fn on_permission_request( +impl PermissionHandler for PermissionLogger { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, @@ -29,7 +29,7 @@ impl SessionHandler for PermissionLogger { .unwrap_or("") .to_string(); self.log.lock().await.push(format!("approved:{tool_name}")); - PermissionResult::Approved + PermissionResult::approve_once() } } @@ -50,9 +50,7 @@ impl SessionHooks for AllowAllHooks { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let permission_log = Arc::new(Mutex::new(Vec::::new())); let handler = Arc::new(PermissionLogger { @@ -62,7 +60,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); let config = config - .with_handler(handler) + .with_permission_handler(handler) .with_hooks(Arc::new(AllowAllHooks)); let session = client.create_session(config).await?; @@ -86,6 +84,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } println!("\nTotal permission requests: {}", log.len()); - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/callbacks/permissions/typescript/src/index.ts b/test/scenarios/callbacks/permissions/typescript/src/index.ts index a9668d0b5..861f5a654 100644 --- a/test/scenarios/callbacks/permissions/typescript/src/index.ts +++ b/test/scenarios/callbacks/permissions/typescript/src/index.ts @@ -1,12 +1,9 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const permissionLog: string[] = []; - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/callbacks/user-input/csharp/Program.cs b/test/scenarios/callbacks/user-input/csharp/Program.cs index e9fe06968..e6988c6cb 100644 --- a/test/scenarios/callbacks/user-input/csharp/Program.cs +++ b/test/scenarios/callbacks/user-input/csharp/Program.cs @@ -1,12 +1,9 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var inputLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -16,7 +13,7 @@ { Model = "claude-haiku-4.5", OnPermissionRequest = (request, invocation) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), OnUserInputRequest = (request, invocation) => { inputLog.Add($"question: {request.Question}"); diff --git a/test/scenarios/callbacks/user-input/go/main.go b/test/scenarios/callbacks/user-input/go/main.go index a0baf2936..673667058 100644 --- a/test/scenarios/callbacks/user-input/go/main.go +++ b/test/scenarios/callbacks/user-input/go/main.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) var ( @@ -16,9 +16,7 @@ var ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -28,8 +26,8 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, OnUserInputRequest: func(req copilot.UserInputRequest, inv copilot.UserInputInvocation) (copilot.UserInputResponse, error) { inputLogMu.Lock() diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index 07a7eb40e..2d5bdb1e9 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -1,15 +1,13 @@ import asyncio -import os -from copilot import CopilotClient -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult +from copilot import CopilotClient +from copilot.generated.rpc import PermissionDecisionApproveOnce input_log: list[str] = [] async def auto_approve_permission(request, invocation): - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() async def auto_approve_tool(input_data, invocation): @@ -22,19 +20,14 @@ async def handle_user_input(request, invocation): async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": auto_approve_permission, - "on_user_input_request": handle_user_input, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + model="claude-haiku-4.5", + on_permission_request=auto_approve_permission, + on_user_input_request=handle_user_input, + hooks={"on_pre_tool_use": auto_approve_tool}, ) response = await session.send_and_wait( diff --git a/test/scenarios/callbacks/user-input/rust/src/main.rs b/test/scenarios/callbacks/user-input/rust/src/main.rs index b7fea906e..348248389 100644 --- a/test/scenarios/callbacks/user-input/rust/src/main.rs +++ b/test/scenarios/callbacks/user-input/rust/src/main.rs @@ -4,7 +4,9 @@ use std::sync::Arc; use async_trait::async_trait; -use github_copilot_sdk::handler::{PermissionResult, SessionHandler, UserInputResponse}; +use github_copilot_sdk::handler::{ + PermissionHandler, PermissionResult, UserInputHandler, UserInputResponse, +}; use github_copilot_sdk::hooks::{HookContext, PreToolUseInput, PreToolUseOutput, SessionHooks}; use github_copilot_sdk::types::{PermissionRequestData, RequestId, SessionConfig, SessionId}; use github_copilot_sdk::{Client, ClientOptions}; @@ -15,17 +17,20 @@ struct InputResponder { } #[async_trait] -impl SessionHandler for InputResponder { - async fn on_permission_request( +impl PermissionHandler for InputResponder { + async fn handle( &self, _session_id: SessionId, _request_id: RequestId, _data: PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } +} - async fn on_user_input( +#[async_trait] +impl UserInputHandler for InputResponder { + async fn handle( &self, _session_id: SessionId, question: String, @@ -60,9 +65,7 @@ impl SessionHooks for AllowAllHooks { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let input_log = Arc::new(Mutex::new(Vec::::new())); let handler = Arc::new(InputResponder { @@ -71,9 +74,9 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); - config.request_user_input = Some(true); let config = config - .with_handler(handler) + .with_permission_handler(handler.clone()) + .with_user_input_handler(handler) .with_hooks(Arc::new(AllowAllHooks)); let session = client.create_session(config).await?; @@ -98,6 +101,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } println!("\nTotal user input requests: {}", log.len()); - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/callbacks/user-input/typescript/src/index.ts b/test/scenarios/callbacks/user-input/typescript/src/index.ts index 7980c3adf..e7e3d0bca 100644 --- a/test/scenarios/callbacks/user-input/typescript/src/index.ts +++ b/test/scenarios/callbacks/user-input/typescript/src/index.ts @@ -1,12 +1,9 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const inputLog: string[] = []; - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -22,7 +19,8 @@ async function main() { }); const response = await session.sendAndWait({ - prompt: "I want to learn about a city. Use the ask_user tool to ask me which city I'm interested in. Then tell me about that city.", + prompt: + "I want to learn about a city. Use the ask_user tool to ask me which city I'm interested in. Then tell me about that city.", }); if (response) { diff --git a/test/scenarios/modes/default/csharp/Program.cs b/test/scenarios/modes/default/csharp/Program.cs index 23d6c63f0..71ea173aa 100644 --- a/test/scenarios/modes/default/csharp/Program.cs +++ b/test/scenarios/modes/default/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/modes/default/go/main.go b/test/scenarios/modes/default/go/main.go index b0c44459f..7cefdcf26 100644 --- a/test/scenarios/modes/default/go/main.go +++ b/test/scenarios/modes/default/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -36,10 +33,10 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Printf("Response: %s\n", d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Printf("Response: %s\n", d.Content) + } + } fmt.Println("Default mode test complete") } diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index ece50a662..54d937be0 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -1,21 +1,17 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - }) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait("Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.") + response = await session.send_and_wait( + "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines." + ) if response: print(f"Response: {response.data.content}") diff --git a/test/scenarios/modes/default/rust/src/main.rs b/test/scenarios/modes/default/rust/src/main.rs index ba890997d..d5d51ce8d 100644 --- a/test/scenarios/modes/default/rust/src/main.rs +++ b/test/scenarios/modes/default/rust/src/main.rs @@ -9,13 +9,11 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; let response = session @@ -31,6 +29,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } println!("Default mode test complete"); - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/modes/default/typescript/src/index.ts b/test/scenarios/modes/default/typescript/src/index.ts index 72ae28960..c6db7562d 100644 --- a/test/scenarios/modes/default/typescript/src/index.ts +++ b/test/scenarios/modes/default/typescript/src/index.ts @@ -1,10 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -12,7 +9,8 @@ async function main() { }); const response = await session.sendAndWait({ - prompt: "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.", + prompt: + "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.", }); if (response) { diff --git a/test/scenarios/modes/minimal/csharp/Program.cs b/test/scenarios/modes/minimal/csharp/Program.cs index 70081b58d..166048d34 100644 --- a/test/scenarios/modes/minimal/csharp/Program.cs +++ b/test/scenarios/modes/minimal/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/modes/minimal/go/main.go b/test/scenarios/modes/minimal/go/main.go index dc9ad0190..4cb891885 100644 --- a/test/scenarios/modes/minimal/go/main.go +++ b/test/scenarios/modes/minimal/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -41,10 +38,10 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Printf("Response: %s\n", d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Printf("Response: %s\n", d.Content) + } + } fmt.Println("Minimal mode test complete") } diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 722c1e5e1..e9455daee 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -1,26 +1,24 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - "available_tools": [], - "system_message": { + session = await client.create_session( + model="claude-haiku-4.5", + available_tools=[], + system_message={ "mode": "replace", "content": "You have no tools. Respond with text only.", }, - }) + ) - response = await session.send_and_wait("Use the grep tool to search for 'SDK' in README.md.") + response = await session.send_and_wait( + "Use the grep tool to search for 'SDK' in README.md." + ) if response: print(f"Response: {response.data.content}") diff --git a/test/scenarios/modes/minimal/typescript/src/index.ts b/test/scenarios/modes/minimal/typescript/src/index.ts index 894e31798..68fa73752 100644 --- a/test/scenarios/modes/minimal/typescript/src/index.ts +++ b/test/scenarios/modes/minimal/typescript/src/index.ts @@ -1,10 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/prompts/attachments/csharp/Program.cs b/test/scenarios/prompts/attachments/csharp/Program.cs index 7cafcb86d..8983af2d9 100644 --- a/test/scenarios/prompts/attachments/csharp/Program.cs +++ b/test/scenarios/prompts/attachments/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/prompts/attachments/go/main.go b/test/scenarios/prompts/attachments/go/main.go index 44c79cf6c..4acfee001 100644 --- a/test/scenarios/prompts/attachments/go/main.go +++ b/test/scenarios/prompts/attachments/go/main.go @@ -13,9 +13,7 @@ import ( const systemPrompt = `You are a helpful assistant. Answer questions about attached files concisely.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index fdf259c6a..584d86ae9 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -1,24 +1,19 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely.""" async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": [], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=[], ) sample_file = os.path.join(os.path.dirname(__file__), "..", "sample-data.txt") diff --git a/test/scenarios/prompts/attachments/rust/src/main.rs b/test/scenarios/prompts/attachments/rust/src/main.rs index 9ba9cc176..040ee5cde 100644 --- a/test/scenarios/prompts/attachments/rust/src/main.rs +++ b/test/scenarios/prompts/attachments/rust/src/main.rs @@ -13,9 +13,7 @@ const SYSTEM_PROMPT: &str = #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); @@ -25,7 +23,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { config.model = Some("claude-haiku-4.5".to_string()); config.system_message = Some(sysmsg); config.available_tools = Some(Vec::new()); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; @@ -53,6 +51,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/prompts/attachments/typescript/src/index.ts b/test/scenarios/prompts/attachments/typescript/src/index.ts index 4448c1dad..3de4b757a 100644 --- a/test/scenarios/prompts/attachments/typescript/src/index.ts +++ b/test/scenarios/prompts/attachments/typescript/src/index.ts @@ -1,14 +1,11 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -16,7 +13,8 @@ async function main() { availableTools: [], systemMessage: { mode: "replace", - content: "You are a helpful assistant. Answer questions about attached files concisely.", + content: + "You are a helpful assistant. Answer questions about attached files concisely.", }, }); diff --git a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs index 2ed2ae94d..36dc52e44 100644 --- a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs +++ b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/prompts/reasoning-effort/go/main.go b/test/scenarios/prompts/reasoning-effort/go/main.go index af5381263..ca3af77d3 100644 --- a/test/scenarios/prompts/reasoning-effort/go/main.go +++ b/test/scenarios/prompts/reasoning-effort/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index 122f44895..860217d41 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -1,30 +1,24 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": "claude-opus-4.6", - "reasoning_effort": "low", - "available_tools": [], - "system_message": { + session = await client.create_session( + model="claude-opus-4.6", + reasoning_effort="low", + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) - - response = await session.send_and_wait( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print("Reasoning effort: low") print(f"Response: {response.data.content}") diff --git a/test/scenarios/prompts/reasoning-effort/rust/src/main.rs b/test/scenarios/prompts/reasoning-effort/rust/src/main.rs index bf1ab9720..74c7296a4 100644 --- a/test/scenarios/prompts/reasoning-effort/rust/src/main.rs +++ b/test/scenarios/prompts/reasoning-effort/rust/src/main.rs @@ -9,9 +9,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); @@ -22,7 +20,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { config.reasoning_effort = Some("low".to_string()); config.available_tools = Some(Vec::new()); config.system_message = Some(sysmsg); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; @@ -35,6 +33,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts index c6d2917d8..da937738d 100644 --- a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts +++ b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts @@ -1,10 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { // Test with "low" reasoning effort diff --git a/test/scenarios/prompts/system-message/csharp/Program.cs b/test/scenarios/prompts/system-message/csharp/Program.cs index 48afdd7ba..cebd2417c 100644 --- a/test/scenarios/prompts/system-message/csharp/Program.cs +++ b/test/scenarios/prompts/system-message/csharp/Program.cs @@ -2,11 +2,7 @@ var piratePrompt = "You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout."; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/prompts/system-message/go/main.go b/test/scenarios/prompts/system-message/go/main.go index a49d65d88..d0b1aee5e 100644 --- a/test/scenarios/prompts/system-message/go/main.go +++ b/test/scenarios/prompts/system-message/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) @@ -12,9 +11,7 @@ import ( const piratePrompt = `You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -43,8 +40,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index b77c1e4a1..a2bc31c76 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -1,29 +1,21 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.""" async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, - "available_tools": [], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": PIRATE_PROMPT}, + available_tools=[], ) - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/prompts/system-message/rust/src/main.rs b/test/scenarios/prompts/system-message/rust/src/main.rs index 4218a389b..20893bdcc 100644 --- a/test/scenarios/prompts/system-message/rust/src/main.rs +++ b/test/scenarios/prompts/system-message/rust/src/main.rs @@ -11,9 +11,7 @@ in every response. Use nautical terms and pirate slang throughout."; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); @@ -23,7 +21,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { config.model = Some("claude-haiku-4.5".to_string()); config.system_message = Some(sysmsg); config.available_tools = Some(Vec::new()); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; @@ -35,6 +33,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/prompts/system-message/typescript/src/index.ts b/test/scenarios/prompts/system-message/typescript/src/index.ts index a0bb44ac8..27525a28f 100644 --- a/test/scenarios/prompts/system-message/typescript/src/index.ts +++ b/test/scenarios/prompts/system-message/typescript/src/index.ts @@ -1,12 +1,9 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; const PIRATE_PROMPT = `You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.`; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs index a5ba2577b..d53dc41f1 100644 --- a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs @@ -3,11 +3,7 @@ const string PiratePrompt = "You are a pirate. Always say Arrr!"; const string RobotPrompt = "You are a robot. Always say BEEP BOOP!"; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/sessions/concurrent-sessions/go/main.go b/test/scenarios/sessions/concurrent-sessions/go/main.go index e399fedf7..342d0f6cd 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/main.go +++ b/test/scenarios/sessions/concurrent-sessions/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" @@ -14,9 +13,7 @@ const piratePrompt = `You are a pirate. Always say Arrr!` const robotPrompt = `You are a robot. Always say BEEP BOOP!` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index a32dc5e10..a49ee283f 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -1,43 +1,31 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig PIRATE_PROMPT = "You are a pirate. Always say Arrr!" ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!" async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session1, session2 = await asyncio.gather( client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, - "available_tools": [], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": PIRATE_PROMPT}, + available_tools=[], ), client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": ROBOT_PROMPT}, - "available_tools": [], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": ROBOT_PROMPT}, + available_tools=[], ), ) response1, response2 = await asyncio.gather( - session1.send_and_wait( - "What is the capital of France?" - ), - session2.send_and_wait( - "What is the capital of France?" - ), + session1.send_and_wait("What is the capital of France?"), + session2.send_and_wait("What is the capital of France?"), ) if response1: diff --git a/test/scenarios/sessions/concurrent-sessions/rust/src/main.rs b/test/scenarios/sessions/concurrent-sessions/rust/src/main.rs index 43932b613..48a2c7166 100644 --- a/test/scenarios/sessions/concurrent-sessions/rust/src/main.rs +++ b/test/scenarios/sessions/concurrent-sessions/rust/src/main.rs @@ -19,14 +19,12 @@ fn make_config(system: &str) -> SessionConfig { config.model = Some("claude-haiku-4.5".to_string()); config.system_message = Some(sysmsg); config.available_tools = Some(Vec::new()); - config.with_handler(Arc::new(ApproveAllHandler)) + config.with_permission_handler(Arc::new(ApproveAllHandler)) } #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let session1 = client.create_session(make_config(PIRATE_PROMPT)).await?; let session2 = client.create_session(make_config(ROBOT_PROMPT)).await?; @@ -47,7 +45,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session1.destroy().await?; - session2.destroy().await?; + session1.disconnect().await?; + session2.disconnect().await?; Ok(()) } diff --git a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts index 81f671e91..196249e82 100644 --- a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts @@ -1,13 +1,10 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; const PIRATE_PROMPT = `You are a pirate. Always say Arrr!`; const ROBOT_PROMPT = `You are a robot. Always say BEEP BOOP!`; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const [session1, session2] = await Promise.all([ diff --git a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs index 2619f25b4..f5a2ff183 100644 --- a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/sessions/infinite-sessions/go/main.go b/test/scenarios/sessions/infinite-sessions/go/main.go index 29871eacc..62d640850 100644 --- a/test/scenarios/sessions/infinite-sessions/go/main.go +++ b/test/scenarios/sessions/infinite-sessions/go/main.go @@ -4,18 +4,15 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) -func boolPtr(b bool) *bool { return &b } +func boolPtr(b bool) *bool { return &b } func float64Ptr(f float64) *float64 { return &f } func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 724dc155d..47dab5bb8 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -1,29 +1,25 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - "available_tools": [], - "system_message": { + session = await client.create_session( + model="claude-haiku-4.5", + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely in one sentence.", }, - "infinite_sessions": { + infinite_sessions={ "enabled": True, "background_compaction_threshold": 0.80, "buffer_exhaustion_threshold": 0.95, }, - }) + ) prompts = [ "What is the capital of France?", diff --git a/test/scenarios/sessions/infinite-sessions/rust/src/main.rs b/test/scenarios/sessions/infinite-sessions/rust/src/main.rs index 0c0f06814..2882254e2 100644 --- a/test/scenarios/sessions/infinite-sessions/rust/src/main.rs +++ b/test/scenarios/sessions/infinite-sessions/rust/src/main.rs @@ -9,9 +9,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); @@ -28,7 +26,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { config.available_tools = Some(Vec::new()); config.system_message = Some(sysmsg); config.infinite_sessions = Some(infinite); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; @@ -50,6 +48,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { println!("Infinite sessions test complete — all messages processed successfully"); - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts index e2a8c5fdb..f10543ab0 100644 --- a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts @@ -1,10 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -12,11 +9,12 @@ async function main() { availableTools: [], systemMessage: { mode: "replace", - content: "You are a helpful assistant. Answer concisely in one sentence.", + content: + "You are a helpful assistant. Answer concisely in one sentence.", }, infiniteSessions: { enabled: true, - backgroundCompactionThreshold: 0.80, + backgroundCompactionThreshold: 0.8, bufferExhaustionThreshold: 0.95, }, }); @@ -35,7 +33,9 @@ async function main() { } } - console.log("Infinite sessions test complete — all messages processed successfully"); + console.log( + "Infinite sessions test complete — all messages processed successfully", + ); await session.disconnect(); } finally { diff --git a/test/scenarios/sessions/multi-user-long-lived/typescript/src/index.ts b/test/scenarios/sessions/multi-user-long-lived/typescript/src/index.ts index 2071da484..5ab3ca45c 100644 --- a/test/scenarios/sessions/multi-user-long-lived/typescript/src/index.ts +++ b/test/scenarios/sessions/multi-user-long-lived/typescript/src/index.ts @@ -1,2 +1,4 @@ -console.log("SKIP: multi-user-long-lived requires memory FS and preset features which is not supported by the old SDK"); +console.log( + "SKIP: multi-user-long-lived requires memory FS and preset features which is not supported by the old SDK", +); process.exit(0); diff --git a/test/scenarios/sessions/multi-user-short-lived/typescript/src/index.ts b/test/scenarios/sessions/multi-user-short-lived/typescript/src/index.ts index eeaceb458..4cfbd5e72 100644 --- a/test/scenarios/sessions/multi-user-short-lived/typescript/src/index.ts +++ b/test/scenarios/sessions/multi-user-short-lived/typescript/src/index.ts @@ -1,2 +1,4 @@ -console.log("SKIP: multi-user-short-lived requires memory FS and preset features which is not supported by the old SDK"); +console.log( + "SKIP: multi-user-short-lived requires memory FS and preset features which is not supported by the old SDK", +); process.exit(0); diff --git a/test/scenarios/sessions/session-resume/csharp/Program.cs b/test/scenarios/sessions/session-resume/csharp/Program.cs index a1bd015bd..4f8bbdb5a 100644 --- a/test/scenarios/sessions/session-resume/csharp/Program.cs +++ b/test/scenarios/sessions/session-resume/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/sessions/session-resume/go/main.go b/test/scenarios/sessions/session-resume/go/main.go index 330fb6852..365c58ec6 100644 --- a/test/scenarios/sessions/session-resume/go/main.go +++ b/test/scenarios/sessions/session-resume/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -60,8 +57,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index ccb9c69f0..7dab1b309 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -1,28 +1,17 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: # 1. Create a session - session = await client.create_session( - { - "model": "claude-haiku-4.5", - "available_tools": [], - } - ) + session = await client.create_session(model="claude-haiku-4.5", available_tools=[]) # 2. Send the secret word - await session.send_and_wait( - "Remember this: the secret word is PINEAPPLE." - ) + await session.send_and_wait("Remember this: the secret word is PINEAPPLE.") # 3. Get the session ID (don't disconnect — resume needs the session to persist) session_id = session.session_id @@ -32,9 +21,7 @@ async def main(): print("Session resumed") # 5. Ask for the secret word - response = await resumed.send_and_wait( - "What was the secret word I told you?" - ) + response = await resumed.send_and_wait("What was the secret word I told you?") if response: print(response.data.content) diff --git a/test/scenarios/sessions/session-resume/rust/src/main.rs b/test/scenarios/sessions/session-resume/rust/src/main.rs index 10cd4fa62..2f1815a78 100644 --- a/test/scenarios/sessions/session-resume/rust/src/main.rs +++ b/test/scenarios/sessions/session-resume/rust/src/main.rs @@ -9,14 +9,12 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); config.available_tools = Some(Vec::new()); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; session @@ -27,7 +25,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { // Note: do NOT destroy — `resume_session` needs the session to persist. let resume_config = - ResumeSessionConfig::new(session_id).with_handler(Arc::new(ApproveAllHandler)); + ResumeSessionConfig::new(session_id).with_permission_handler(Arc::new(ApproveAllHandler)); let resumed = client.resume_session(resume_config).await?; println!("Session resumed"); @@ -41,6 +39,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - resumed.destroy().await?; + resumed.disconnect().await?; Ok(()) } diff --git a/test/scenarios/sessions/session-resume/typescript/src/index.ts b/test/scenarios/sessions/session-resume/typescript/src/index.ts index c9ba3b3d5..125b89341 100644 --- a/test/scenarios/sessions/session-resume/typescript/src/index.ts +++ b/test/scenarios/sessions/session-resume/typescript/src/index.ts @@ -1,10 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { // 1. Create a session diff --git a/test/scenarios/sessions/streaming/csharp/Program.cs b/test/scenarios/sessions/streaming/csharp/Program.cs index 518d4b227..706960901 100644 --- a/test/scenarios/sessions/streaming/csharp/Program.cs +++ b/test/scenarios/sessions/streaming/csharp/Program.cs @@ -1,17 +1,6 @@ using GitHub.Copilot; -var options = new CopilotClientOptions -{ - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}; - -var cliPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); -if (!string.IsNullOrEmpty(cliPath)) -{ - options.Connection = RuntimeConnection.ForStdio(path: cliPath); -} - -using var client = new CopilotClient(options); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/sessions/streaming/go/main.go b/test/scenarios/sessions/streaming/go/main.go index 8a1c78efa..e2a11029a 100644 --- a/test/scenarios/sessions/streaming/go/main.go +++ b/test/scenarios/sessions/streaming/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index e2312cd14..8def95f1f 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -1,22 +1,13 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session( - { - "model": "claude-haiku-4.5", - "streaming": True, - } - ) + session = await client.create_session(model="claude-haiku-4.5", streaming=True) chunk_count = 0 @@ -27,9 +18,7 @@ def on_event(event): session.on(on_event) - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/sessions/streaming/rust/src/main.rs b/test/scenarios/sessions/streaming/rust/src/main.rs index f5cf23764..da0cba5e3 100644 --- a/test/scenarios/sessions/streaming/rust/src/main.rs +++ b/test/scenarios/sessions/streaming/rust/src/main.rs @@ -4,50 +4,32 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; -use async_trait::async_trait; -use github_copilot_sdk::handler::{HandlerEvent, HandlerResponse, PermissionResult, SessionHandler}; +use github_copilot_sdk::handler::ApproveAllHandler; use github_copilot_sdk::types::SessionConfig; use github_copilot_sdk::{Client, ClientOptions}; -struct StreamCounter { - chunks: Arc, -} - -#[async_trait] -impl SessionHandler for StreamCounter { - async fn on_event(&self, event: HandlerEvent) -> HandlerResponse { - match event { - HandlerEvent::SessionEvent { event, .. } => { - if event.event_type == "assistant.message_delta" { - self.chunks.fetch_add(1, Ordering::Relaxed); - } - HandlerResponse::Ok - } - HandlerEvent::PermissionRequest { .. } => { - HandlerResponse::Permission(PermissionResult::Approved) - } - _ => HandlerResponse::Ok, - } - } -} - #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let chunks = Arc::new(AtomicUsize::new(0)); - let handler = Arc::new(StreamCounter { - chunks: chunks.clone(), - }); let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); config.streaming = Some(true); - let config = config.with_handler(handler); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; + let mut events = session.subscribe(); + let chunks_clone = chunks.clone(); + let counter = tokio::spawn(async move { + while let Ok(event) = events.recv().await { + if event.event_type == "assistant.message_delta" { + chunks_clone.fetch_add(1, Ordering::Relaxed); + } + } + }); + let response = session.send_and_wait("What is the capital of France?").await?; if let Some(event) = response { @@ -61,6 +43,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { chunks.load(Ordering::Relaxed) ); - session.destroy().await?; + session.disconnect().await?; + drop(counter); Ok(()) } diff --git a/test/scenarios/sessions/streaming/typescript/src/index.ts b/test/scenarios/sessions/streaming/typescript/src/index.ts index 9cd530ebb..25df8cb4b 100644 --- a/test/scenarios/sessions/streaming/typescript/src/index.ts +++ b/test/scenarios/sessions/streaming/typescript/src/index.ts @@ -1,10 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/tools/custom-agents/csharp/Program.cs b/test/scenarios/tools/custom-agents/csharp/Program.cs index 6c5b980cc..f2db5fe5b 100644 --- a/test/scenarios/tools/custom-agents/csharp/Program.cs +++ b/test/scenarios/tools/custom-agents/csharp/Program.cs @@ -1,13 +1,7 @@ using GitHub.Copilot; using Microsoft.Extensions.AI; -var cliPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); - -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: cliPath), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/custom-agents/go/main.go b/test/scenarios/tools/custom-agents/go/main.go index 1e6ada739..e52404a6a 100644 --- a/test/scenarios/tools/custom-agents/go/main.go +++ b/test/scenarios/tools/custom-agents/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -60,8 +57,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index bf6e3978c..1aaae199c 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -1,7 +1,6 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig from copilot.tools import Tool @@ -10,10 +9,7 @@ async def analyze_handler(args): async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/tools/custom-agents/rust/src/main.rs b/test/scenarios/tools/custom-agents/rust/src/main.rs index e707770bc..016ff47e6 100644 --- a/test/scenarios/tools/custom-agents/rust/src/main.rs +++ b/test/scenarios/tools/custom-agents/rust/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{ToolHandlerRouter, define_tool}; +use github_copilot_sdk::tool::define_tool; use github_copilot_sdk::types::{CustomAgentConfig, DefaultAgentConfig, SessionConfig, ToolResult}; use github_copilot_sdk::{Client, ClientOptions}; use schemars::JsonSchema; @@ -19,9 +19,7 @@ struct AnalyzeParams { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let analyze_codebase = define_tool( "analyze-codebase", @@ -34,9 +32,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { }, ); - let router = ToolHandlerRouter::new(vec![analyze_codebase], Arc::new(ApproveAllHandler)); - let tools = router.tools(); - let mut researcher = CustomAgentConfig::default(); researcher.name = "researcher".to_string(); researcher.display_name = Some("Research Agent".to_string()); @@ -56,12 +51,13 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); - config.tools = Some(tools); config.default_agent = Some(DefaultAgentConfig { excluded_tools: Some(vec!["analyze-codebase".to_string()]), }); config.custom_agents = Some(vec![researcher]); - let config = config.with_handler(Arc::new(router)); + let config = config + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![analyze_codebase]); let session = client.create_session(config).await?; @@ -77,6 +73,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/tools/custom-agents/typescript/src/index.ts b/test/scenarios/tools/custom-agents/typescript/src/index.ts index db6dff214..4a48902f8 100644 --- a/test/scenarios/tools/custom-agents/typescript/src/index.ts +++ b/test/scenarios/tools/custom-agents/typescript/src/index.ts @@ -1,19 +1,17 @@ -import { CopilotClient, defineTool , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, defineTool } from "@github/copilot-sdk"; import { z } from "zod"; const analyzeCodebase = defineTool("analyze-codebase", { - description: "Performs deep analysis of the codebase, generating extensive context", - parameters: z.object({ query: z.string().describe("The analysis query") }), - handler: async ({ query }) => { - return `Analysis result for: ${query}`; - }, + description: + "Performs deep analysis of the codebase, generating extensive context", + parameters: z.object({ query: z.string().describe("The analysis query") }), + handler: async ({ query }) => { + return `Analysis result for: ${query}`; + }, }); async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -26,15 +24,18 @@ async function main() { { name: "researcher", displayName: "Research Agent", - description: "A research agent that can only read and search files, not modify them", + description: + "A research agent that can only read and search files, not modify them", tools: ["grep", "glob", "view", "analyze-codebase"], - prompt: "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.", + prompt: + "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.", }, ], }); const response = await session.sendAndWait({ - prompt: "What custom agents are available? Describe the researcher agent and its capabilities.", + prompt: + "What custom agents are available? Describe the researcher agent and its capabilities.", }); if (response) { diff --git a/test/scenarios/tools/mcp-servers/csharp/Program.cs b/test/scenarios/tools/mcp-servers/csharp/Program.cs index ed667825f..7d7fe4738 100644 --- a/test/scenarios/tools/mcp-servers/csharp/Program.cs +++ b/test/scenarios/tools/mcp-servers/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/mcp-servers/go/main.go b/test/scenarios/tools/mcp-servers/go/main.go index b1a1225f1..dd6ec5219 100644 --- a/test/scenarios/tools/mcp-servers/go/main.go +++ b/test/scenarios/tools/mcp-servers/go/main.go @@ -11,9 +11,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -63,10 +61,10 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } if len(mcpServers) > 0 { keys := make([]string, 0, len(mcpServers)) diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index 2fa81b82d..706094ac9 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -1,14 +1,11 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: # MCP server config — demonstrates the configuration pattern. @@ -16,7 +13,11 @@ async def main(): # Otherwise, runs without MCP tools as a build/integration test. mcp_servers = {} if os.environ.get("MCP_SERVER_CMD"): - args = os.environ.get("MCP_SERVER_ARGS", "").split() if os.environ.get("MCP_SERVER_ARGS") else [] + args = ( + os.environ.get("MCP_SERVER_ARGS", "").split() + if os.environ.get("MCP_SERVER_ARGS") + else [] + ) mcp_servers["example"] = { "type": "stdio", "command": os.environ["MCP_SERVER_CMD"], @@ -36,9 +37,7 @@ async def main(): session = await client.create_session(session_config) - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/tools/mcp-servers/rust/src/main.rs b/test/scenarios/tools/mcp-servers/rust/src/main.rs index fd76147a1..a1b043854 100644 --- a/test/scenarios/tools/mcp-servers/rust/src/main.rs +++ b/test/scenarios/tools/mcp-servers/rust/src/main.rs @@ -13,9 +13,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mcp_cmd = std::env::var("MCP_SERVER_CMD").ok(); let mcp_args_env = std::env::var("MCP_SERVER_ARGS").ok(); @@ -25,7 +23,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { .map(|s| s.split(' ').map(str::to_string).collect()) .unwrap_or_default(); let stdio = McpStdioServerConfig { - tools: vec!["*".to_string()], command: cmd.clone(), args, ..Default::default() @@ -45,7 +42,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { config.system_message = Some(sysmsg); config.available_tools = Some(Vec::new()); config.mcp_servers = mcp_servers; - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; @@ -63,6 +60,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { println!("\nNo MCP servers configured (set MCP_SERVER_CMD to test with a real server)"); } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/tools/mcp-servers/typescript/src/index.ts b/test/scenarios/tools/mcp-servers/typescript/src/index.ts index 5117d3a64..838094c8d 100644 --- a/test/scenarios/tools/mcp-servers/typescript/src/index.ts +++ b/test/scenarios/tools/mcp-servers/typescript/src/index.ts @@ -1,10 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { // MCP server config — demonstrates the configuration pattern. @@ -15,7 +12,9 @@ async function main() { mcpServers["example"] = { type: "stdio", command: process.env.MCP_SERVER_CMD, - args: process.env.MCP_SERVER_ARGS ? process.env.MCP_SERVER_ARGS.split(" ") : [], + args: process.env.MCP_SERVER_ARGS + ? process.env.MCP_SERVER_ARGS.split(" ") + : [], }; } @@ -38,9 +37,13 @@ async function main() { } if (Object.keys(mcpServers).length > 0) { - console.log("\nMCP servers configured: " + Object.keys(mcpServers).join(", ")); + console.log( + "\nMCP servers configured: " + Object.keys(mcpServers).join(", "), + ); } else { - console.log("\nNo MCP servers configured (set MCP_SERVER_CMD to test with a real server)"); + console.log( + "\nNo MCP servers configured (set MCP_SERVER_CMD to test with a real server)", + ); } await session.disconnect(); diff --git a/test/scenarios/tools/no-tools/csharp/Program.cs b/test/scenarios/tools/no-tools/csharp/Program.cs index a0ea0eefe..a9a2e0308 100644 --- a/test/scenarios/tools/no-tools/csharp/Program.cs +++ b/test/scenarios/tools/no-tools/csharp/Program.cs @@ -7,11 +7,7 @@ You can only respond with text based on your training data. If asked about your capabilities or tools, clearly state that you have no tools available. """; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/no-tools/go/main.go b/test/scenarios/tools/no-tools/go/main.go index 5d1aa872f..9698ebded 100644 --- a/test/scenarios/tools/no-tools/go/main.go +++ b/test/scenarios/tools/no-tools/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) @@ -15,9 +14,7 @@ You can only respond with text based on your training data. If asked about your capabilities or tools, clearly state that you have no tools available.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -46,8 +43,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index c3eeb6a17..35448e9a2 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -1,7 +1,6 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig SYSTEM_PROMPT = """You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -10,23 +9,16 @@ async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": [], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=[], ) - response = await session.send_and_wait( - "Use the bash tool to run 'echo hello'." - ) + response = await session.send_and_wait("Use the bash tool to run 'echo hello'.") if response: print(response.data.content) diff --git a/test/scenarios/tools/no-tools/rust/src/main.rs b/test/scenarios/tools/no-tools/rust/src/main.rs index 691ac47ed..c2e13339f 100644 --- a/test/scenarios/tools/no-tools/rust/src/main.rs +++ b/test/scenarios/tools/no-tools/rust/src/main.rs @@ -14,9 +14,7 @@ If asked about your capabilities or tools, clearly state that you have no tools #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); @@ -26,7 +24,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { config.model = Some("claude-haiku-4.5".to_string()); config.system_message = Some(sysmsg); config.available_tools = Some(Vec::new()); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; let response = session @@ -39,6 +37,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/tools/no-tools/typescript/src/index.ts b/test/scenarios/tools/no-tools/typescript/src/index.ts index 743aafe54..5756bb350 100644 --- a/test/scenarios/tools/no-tools/typescript/src/index.ts +++ b/test/scenarios/tools/no-tools/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; const SYSTEM_PROMPT = `You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -6,10 +6,7 @@ You can only respond with text based on your training data. If asked about your capabilities or tools, clearly state that you have no tools available.`; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/tools/skills/csharp/Program.cs b/test/scenarios/tools/skills/csharp/Program.cs index 81adf96a5..5e3f3c859 100644 --- a/test/scenarios/tools/skills/csharp/Program.cs +++ b/test/scenarios/tools/skills/csharp/Program.cs @@ -1,10 +1,7 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -17,7 +14,7 @@ Model = "claude-haiku-4.5", SkillDirectories = [skillsDir], OnPermissionRequest = (request, invocation) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), Hooks = new SessionHooks { OnPreToolUse = (input, invocation) => diff --git a/test/scenarios/tools/skills/go/main.go b/test/scenarios/tools/skills/go/main.go index 7b0ef8032..21d9604f7 100644 --- a/test/scenarios/tools/skills/go/main.go +++ b/test/scenarios/tools/skills/go/main.go @@ -4,17 +4,15 @@ import ( "context" "fmt" "log" - "os" "path/filepath" "runtime" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -28,8 +26,8 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", SkillDirectories: []string{skillsDir}, - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, Hooks: &copilot.SessionHooks{ OnPreToolUse: func(input copilot.PreToolUseHookInput, invocation copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index a6d6bf2c0..6b066bbf4 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -1,23 +1,18 @@ import asyncio -import os from pathlib import Path from copilot import CopilotClient -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionApproveOnce async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") session = await client.create_session( - on_permission_request=lambda _, __: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda _, __: PermissionDecisionApproveOnce(), model="claude-haiku-4.5", skill_directories=[skills_dir], hooks={ diff --git a/test/scenarios/tools/skills/rust/src/main.rs b/test/scenarios/tools/skills/rust/src/main.rs index 845704fac..64cf689a4 100644 --- a/test/scenarios/tools/skills/rust/src/main.rs +++ b/test/scenarios/tools/skills/rust/src/main.rs @@ -27,9 +27,7 @@ impl SessionHooks for AllowAllHooks { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; // CARGO_MANIFEST_DIR resolves to .../tools/skills/rust at compile time. let skills_dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "..", "sample-skills"] @@ -40,7 +38,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { config.model = Some("claude-haiku-4.5".to_string()); config.skill_directories = Some(vec![skills_dir]); let config = config - .with_handler(Arc::new(ApproveAllHandler)) + .with_permission_handler(Arc::new(ApproveAllHandler)) .with_hooks(Arc::new(AllowAllHooks)); let session = client.create_session(config).await?; @@ -57,6 +55,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { println!("\nSkill directories configured successfully"); - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/tools/skills/typescript/src/index.ts b/test/scenarios/tools/skills/typescript/src/index.ts index 740adc587..0a934eb39 100644 --- a/test/scenarios/tools/skills/typescript/src/index.ts +++ b/test/scenarios/tools/skills/typescript/src/index.ts @@ -1,14 +1,11 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const skillsDir = path.resolve(__dirname, "../../sample-skills"); diff --git a/test/scenarios/tools/tool-filtering/csharp/Program.cs b/test/scenarios/tools/tool-filtering/csharp/Program.cs index 72431c005..23f6bf4b4 100644 --- a/test/scenarios/tools/tool-filtering/csharp/Program.cs +++ b/test/scenarios/tools/tool-filtering/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/tool-filtering/go/main.go b/test/scenarios/tools/tool-filtering/go/main.go index e4a958be2..646582bdf 100644 --- a/test/scenarios/tools/tool-filtering/go/main.go +++ b/test/scenarios/tools/tool-filtering/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) @@ -12,9 +11,7 @@ import ( const systemPrompt = `You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -43,8 +40,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 9da4ca571..a38f73c78 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -1,24 +1,18 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.""" async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": ["grep", "glob", "view"], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=["grep", "glob", "view"], ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/tool-filtering/rust/src/main.rs b/test/scenarios/tools/tool-filtering/rust/src/main.rs index edc203550..bce9b3aba 100644 --- a/test/scenarios/tools/tool-filtering/rust/src/main.rs +++ b/test/scenarios/tools/tool-filtering/rust/src/main.rs @@ -12,9 +12,7 @@ of tools. When asked about your tools, list exactly which tools you have availab #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); @@ -28,7 +26,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { "glob".to_string(), "view".to_string(), ]); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; @@ -42,6 +40,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/tools/tool-filtering/typescript/src/index.ts b/test/scenarios/tools/tool-filtering/typescript/src/index.ts index 87a86062e..7ab2d2f93 100644 --- a/test/scenarios/tools/tool-filtering/typescript/src/index.ts +++ b/test/scenarios/tools/tool-filtering/typescript/src/index.ts @@ -1,17 +1,15 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ model: "claude-haiku-4.5", systemMessage: { mode: "replace", - content: "You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.", + content: + "You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.", }, availableTools: ["grep", "glob", "view"], }); diff --git a/test/scenarios/tools/tool-overrides/csharp/Program.cs b/test/scenarios/tools/tool-overrides/csharp/Program.cs index a8c7679de..c88b8dc2c 100644 --- a/test/scenarios/tools/tool-overrides/csharp/Program.cs +++ b/test/scenarios/tools/tool-overrides/csharp/Program.cs @@ -2,11 +2,7 @@ using GitHub.Copilot; using Microsoft.Extensions.AI; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/tool-overrides/go/main.go b/test/scenarios/tools/tool-overrides/go/main.go index 8d5f6a756..9f77fc56d 100644 --- a/test/scenarios/tools/tool-overrides/go/main.go +++ b/test/scenarios/tools/tool-overrides/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) @@ -14,9 +13,7 @@ type GrepParams struct { } func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -48,8 +45,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 687933973..aa31de170 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -1,10 +1,8 @@ import asyncio -import os from pydantic import BaseModel, Field from copilot import CopilotClient, define_tool -from copilot.client import SubprocessConfig from copilot.session import PermissionHandler @@ -12,25 +10,26 @@ class GrepParams(BaseModel): query: str = Field(description="Search query") -@define_tool("grep", description="A custom grep implementation that overrides the built-in", overrides_built_in_tool=True) +@define_tool( + "grep", + description="A custom grep implementation that overrides the built-in", + overrides_built_in_tool=True, +) def custom_grep(params: GrepParams) -> str: return f"CUSTOM_GREP_RESULT: {params.query}" async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5", tools=[custom_grep] + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + tools=[custom_grep], ) - response = await session.send_and_wait( - "Use grep to search for the word 'hello'" - ) + response = await session.send_and_wait("Use grep to search for the word 'hello'") if response: print(response.data.content) diff --git a/test/scenarios/tools/tool-overrides/rust/src/main.rs b/test/scenarios/tools/tool-overrides/rust/src/main.rs index ce002a27d..bfa46b1b3 100644 --- a/test/scenarios/tools/tool-overrides/rust/src/main.rs +++ b/test/scenarios/tools/tool-overrides/rust/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::tool::{ToolHandlerRouter, define_tool}; +use github_copilot_sdk::tool::define_tool; use github_copilot_sdk::types::{SessionConfig, ToolResult}; use github_copilot_sdk::{Client, ClientOptions}; use schemars::JsonSchema; @@ -19,30 +19,22 @@ struct GrepParams { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; - let grep_tool = define_tool( + let mut grep_tool = define_tool( "grep", "A custom grep implementation that overrides the built-in", |_inv, params: GrepParams| async move { Ok(ToolResult::Text(format!("CUSTOM_GREP_RESULT: {}", params.query))) }, ); - - let router = ToolHandlerRouter::new(vec![grep_tool], Arc::new(ApproveAllHandler)); - let mut tools = router.tools(); - for t in tools.iter_mut() { - if t.name == "grep" { - t.overrides_built_in_tool = true; - } - } + grep_tool.overrides_built_in_tool = true; let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); - config.tools = Some(tools); - let config = config.with_handler(Arc::new(router)); + let config = config + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![grep_tool]); let session = client.create_session(config).await?; @@ -56,6 +48,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/tools/tool-overrides/typescript/src/index.ts b/test/scenarios/tools/tool-overrides/typescript/src/index.ts index fe6ff874f..fa3fc457b 100644 --- a/test/scenarios/tools/tool-overrides/typescript/src/index.ts +++ b/test/scenarios/tools/tool-overrides/typescript/src/index.ts @@ -1,11 +1,8 @@ -import { CopilotClient, defineTool, approveAll , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, defineTool, approveAll } from "@github/copilot-sdk"; import { z } from "zod"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -13,7 +10,8 @@ async function main() { onPermissionRequest: approveAll, tools: [ defineTool("grep", { - description: "A custom grep implementation that overrides the built-in", + description: + "A custom grep implementation that overrides the built-in", parameters: z.object({ query: z.string().describe("Search query"), }), diff --git a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs index 93ad41f1e..64704ff3f 100644 --- a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs +++ b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs @@ -1,15 +1,12 @@ using System.ComponentModel; using GitHub.Copilot; +using GitHub.Copilot.Rpc; using Microsoft.Extensions.AI; // In-memory virtual filesystem var virtualFs = new Dictionary(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -49,7 +46,7 @@ "List all files in the virtual filesystem"), ], OnPermissionRequest = (request, invocation) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), Hooks = new SessionHooks { OnPreToolUse = (input, invocation) => diff --git a/test/scenarios/tools/virtual-filesystem/go/main.go b/test/scenarios/tools/virtual-filesystem/go/main.go index de4b50637..84dccf7f4 100644 --- a/test/scenarios/tools/virtual-filesystem/go/main.go +++ b/test/scenarios/tools/virtual-filesystem/go/main.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "log" - "os" "strings" "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) // In-memory virtual filesystem @@ -73,9 +73,7 @@ func main() { }, } - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -88,8 +86,8 @@ func main() { // Remove all built-in tools — only our custom virtual FS tools are available AvailableTools: []string{}, Tools: []copilot.Tool{createFile, readFile, listFiles}, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, Hooks: &copilot.SessionHooks{ OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 57b197509..eeafa22ce 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -1,10 +1,10 @@ import asyncio -import os -from copilot import CopilotClient, define_tool -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult + from pydantic import BaseModel, Field +from copilot import CopilotClient, define_tool +from copilot.generated.rpc import PermissionDecisionApproveOnce + # In-memory virtual filesystem virtual_fs: dict[str, str] = {} @@ -40,7 +40,7 @@ def list_files() -> str: async def auto_approve_permission(request, invocation): - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() async def auto_approve_tool(input_data, invocation): @@ -48,10 +48,7 @@ async def auto_approve_tool(input_data, invocation): async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts index 3fa21db00..432d91da9 100644 --- a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts +++ b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts @@ -1,11 +1,12 @@ -import { CopilotClient, defineTool , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, defineTool } from "@github/copilot-sdk"; import { z } from "zod"; // In-memory virtual filesystem const virtualFs = new Map(); const createFile = defineTool("create_file", { - description: "Create or overwrite a file at the given path with the provided content", + description: + "Create or overwrite a file at the given path with the provided content", parameters: z.object({ path: z.string().describe("File path"), content: z.string().describe("File content"), @@ -38,10 +39,7 @@ const listFiles = defineTool("list_files", { }); async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/transport/reconnect/go/main.go b/test/scenarios/transport/reconnect/go/main.go index fda142316..2efeaa2db 100644 --- a/test/scenarios/transport/reconnect/go/main.go +++ b/test/scenarios/transport/reconnect/go/main.go @@ -38,10 +38,10 @@ func main() { } if response1 != nil { -if d, ok := response1.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} else { + if d, ok := response1.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } else { log.Fatal("No response content received for session 1") } @@ -66,10 +66,10 @@ fmt.Println(d.Content) } if response2 != nil { -if d, ok := response2.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} else { + if d, ok := response2.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } else { log.Fatal("No response content received for session 2") } diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index d1d4505a8..cc79f9721 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -1,23 +1,23 @@ import asyncio import os import sys -from copilot import CopilotClient -from copilot.client import ExternalServerConfig + +from copilot import CopilotClient, RuntimeConnection async def main(): - client = CopilotClient(ExternalServerConfig( - url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - )) + client = CopilotClient( + connection=RuntimeConnection.for_uri( + os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + ), + ) try: # First session print("--- Session 1 ---") - session1 = await client.create_session({"model": "claude-haiku-4.5"}) + session1 = await client.create_session(model="claude-haiku-4.5") - response1 = await session1.send_and_wait( - "What is the capital of France?" - ) + response1 = await session1.send_and_wait("What is the capital of France?") if response1 and response1.data.content: print(response1.data.content) @@ -30,11 +30,9 @@ async def main(): # Second session — tests that the server accepts new sessions print("--- Session 2 ---") - session2 = await client.create_session({"model": "claude-haiku-4.5"}) + session2 = await client.create_session(model="claude-haiku-4.5") - response2 = await session2.send_and_wait( - "What is the capital of France?" - ) + response2 = await session2.send_and_wait("What is the capital of France?") if response2 and response2.data.content: print(response2.data.content) diff --git a/test/scenarios/transport/reconnect/typescript/src/index.ts b/test/scenarios/transport/reconnect/typescript/src/index.ts index 6fc1c417e..e1ea21fd1 100644 --- a/test/scenarios/transport/reconnect/typescript/src/index.ts +++ b/test/scenarios/transport/reconnect/typescript/src/index.ts @@ -2,7 +2,9 @@ import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - connection: RuntimeConnection.forUri(process.env.COPILOT_CLI_URL || "localhost:3000"), + connection: RuntimeConnection.forUri( + process.env.COPILOT_CLI_URL || "localhost:3000", + ), }); try { @@ -42,7 +44,9 @@ async function main() { await session2.disconnect(); console.log("Session 2 disconnected"); - console.log("\nReconnect test passed — both sessions completed successfully"); + console.log( + "\nReconnect test passed — both sessions completed successfully", + ); } finally { await client.stop(); } diff --git a/test/scenarios/transport/stdio/csharp/Program.cs b/test/scenarios/transport/stdio/csharp/Program.cs index e9dfbcccc..576ca5518 100644 --- a/test/scenarios/transport/stdio/csharp/Program.cs +++ b/test/scenarios/transport/stdio/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/transport/stdio/go/main.go b/test/scenarios/transport/stdio/go/main.go index 8fab8510d..51b592431 100644 --- a/test/scenarios/transport/stdio/go/main.go +++ b/test/scenarios/transport/stdio/go/main.go @@ -4,16 +4,13 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { // Go SDK auto-reads COPILOT_CLI_PATH from env - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -37,8 +34,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index 39ce2bb81..6be1d4294 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -1,21 +1,15 @@ import asyncio -import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( - github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/transport/stdio/rust/src/main.rs b/test/scenarios/transport/stdio/rust/src/main.rs index 156b3587d..b3f92eaf9 100644 --- a/test/scenarios/transport/stdio/rust/src/main.rs +++ b/test/scenarios/transport/stdio/rust/src/main.rs @@ -8,13 +8,11 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; let response = session.send_and_wait("What is the capital of France?").await?; @@ -25,6 +23,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/transport/stdio/typescript/src/index.ts b/test/scenarios/transport/stdio/typescript/src/index.ts index c80c1b074..7df9cd888 100644 --- a/test/scenarios/transport/stdio/typescript/src/index.ts +++ b/test/scenarios/transport/stdio/typescript/src/index.ts @@ -1,9 +1,10 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), }); try { diff --git a/test/scenarios/transport/tcp/go/main.go b/test/scenarios/transport/tcp/go/main.go index acdbaab76..95da9cf68 100644 --- a/test/scenarios/transport/tcp/go/main.go +++ b/test/scenarios/transport/tcp/go/main.go @@ -41,8 +41,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index b441bec51..1bf32b475 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -1,20 +1,20 @@ import asyncio import os -from copilot import CopilotClient -from copilot.client import ExternalServerConfig + +from copilot import CopilotClient, RuntimeConnection async def main(): - client = CopilotClient(ExternalServerConfig( - url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - )) + client = CopilotClient( + connection=RuntimeConnection.for_uri( + os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + ), + ) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/transport/tcp/rust/src/main.rs b/test/scenarios/transport/tcp/rust/src/main.rs index 49691c1b2..f9ccfe5f3 100644 --- a/test/scenarios/transport/tcp/rust/src/main.rs +++ b/test/scenarios/transport/tcp/rust/src/main.rs @@ -20,13 +20,13 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { opts.transport = Transport::External { host: host.to_string(), port, + connection_token: None, }; - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); let client = Client::start(opts).await?; let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); - let config = config.with_handler(Arc::new(ApproveAllHandler)); + let config = config.with_permission_handler(Arc::new(ApproveAllHandler)); let session = client.create_session(config).await?; @@ -38,6 +38,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { } } - session.destroy().await?; + session.disconnect().await?; Ok(()) } diff --git a/test/scenarios/transport/tcp/typescript/src/index.ts b/test/scenarios/transport/tcp/typescript/src/index.ts index e4775f545..9b6efd277 100644 --- a/test/scenarios/transport/tcp/typescript/src/index.ts +++ b/test/scenarios/transport/tcp/typescript/src/index.ts @@ -2,7 +2,9 @@ import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - connection: RuntimeConnection.forUri(process.env.COPILOT_CLI_URL || "localhost:3000"), + connection: RuntimeConnection.forUri( + process.env.COPILOT_CLI_URL || "localhost:3000", + ), }); try { 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 8c3e28542..f9918fa13 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 @@ -8,7 +8,3 @@ conversations: content: What is 1+1? - role: assistant content: 1+1 equals 2. - - role: user - content: What is 3+3? - - role: assistant - content: 3+3 equals 6. 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 82c9917c3..250402101 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 @@ -8,7 +8,3 @@ conversations: content: What is 1+1? - role: assistant content: 1 + 1 = 2 - - role: user - content: What is 3+3? - - role: assistant - content: 3 + 3 = 6