diff --git a/.github/actions/setup-copilot/action.yml b/.github/actions/setup-copilot/action.yml index dcc99b8a70..1288a1d89b 100644 --- a/.github/actions/setup-copilot/action.yml +++ b/.github/actions/setup-copilot/action.yml @@ -4,15 +4,36 @@ outputs: cli-path: description: "Path to the Copilot CLI executable" value: ${{ steps.cli-path.outputs.path }} + cli-version: + description: "Pinned @github/copilot version installed (read from pom.xml)" + value: ${{ steps.cli-version.outputs.version }} runs: using: "composite" steps: - - uses: actions/setup-node@v6 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v6 with: node-version: 22 - - name: Install Copilot CLI - run: npm install -g @github/copilot + - name: Read pinned @github/copilot version from pom.xml + id: cli-version shell: bash + # The version is the SINGLE SOURCE OF TRUTH for the Copilot CLI version + # used across all CI paths. It is kept in sync with the reference + # implementation pinned in .lastmerge by + # .github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh. + run: | + PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" + VERSION=$(sed -n "s|.*<${PROP}>\(.*\).*|\1|p" pom.xml | head -n 1 | tr -d '[:space:]') + if [[ -z "$VERSION" || "$VERSION" == "PRIMER_TO_REPLACE" ]]; then + echo "::error::Could not read pinned @github/copilot version from pom.xml property <${PROP}>" >&2 + exit 1 + fi + echo "Pinned @github/copilot version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Install Copilot CLI (pinned to pom.xml version) + shell: bash + env: + CLI_VERSION: ${{ steps.cli-version.outputs.version }} + run: npm install -g "@github/copilot@${CLI_VERSION}" - name: Set CLI path id: cli-path run: echo "path=$(which copilot)" >> $GITHUB_OUTPUT diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 11e06d9b9e..8bf544f4d1 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,5 +1,15 @@ { "entries": { + "actions/checkout@v6.0.2": { + "repo": "actions/checkout", + "version": "v6.0.2", + "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" + }, + "actions/download-artifact@v8.0.1": { + "repo": "actions/download-artifact", + "version": "v8.0.1", + "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" + }, "actions/github-script@v8": { "repo": "actions/github-script", "version": "v8", @@ -20,6 +30,11 @@ "version": "v4", "sha": "49933ea5288caeca8642d1e84afbd3f7d6820020" }, + "actions/upload-artifact@v7.0.1": { + "repo": "actions/upload-artifact", + "version": "v7.0.1", + "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" + }, "github/gh-aw-actions/setup-cli@v0.68.3": { "repo": "github/gh-aw-actions/setup-cli", "version": "v0.68.3", diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 620c8d52a2..16199628b9 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -12,7 +12,7 @@ coverage coverage - 82.7% - 82.7% + 81.5% + 81.5% diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d029784219..7b304f2677 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -222,6 +222,8 @@ Test method names are converted to lowercase snake_case for snapshot filenames t - **DO NOT** modify test snapshots in `target/copilot-sdk/test/snapshots/` - these come from reference implementation - **DO NOT** alter the Eclipse formatter configuration in `pom.xml` without team consensus - **DO NOT** remove or skip Checkstyle or Spotless checks +- **YOU MUST ALWAYS** run `gh aw compile ` after editing any `.github/workflows/*.md` agentic workflow source file to regenerate the corresponding `.lock.yml`. The lock file contains a content hash of the frontmatter — any edit to the `.md` without recompiling will cause the workflow to fail at runtime with a "lock file out of sync" error. +- **DO NOT** edit `.github/workflows/*.lock.yml` directly — these are auto-generated by `gh aw compile` from the `.md` source files. ### Security Guidelines diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 03f0f715ae..04d5ab5f47 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,5 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/scripts/codegen" - schedule: - interval: "daily" - open-pull-requests-limit: 1 - allow: - - dependency-name: "@github/copilot" - - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/prompts/coding-agent-merge-reference-impl-instructions.md b/.github/prompts/coding-agent-merge-reference-impl-instructions.md index c93af9a1a5..5ae1621db6 100644 --- a/.github/prompts/coding-agent-merge-reference-impl-instructions.md +++ b/.github/prompts/coding-agent-merge-reference-impl-instructions.md @@ -1,5 +1,5 @@ - - + + Follow the agentic-merge-reference-impl prompt at .github/prompts/agentic-merge-reference-impl.prompt.md to port reference implementation changes to the Java SDK. diff --git a/.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh b/.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh index 480d43bc0a..2dda8da07f 100755 --- a/.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh +++ b/.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh @@ -5,8 +5,12 @@ # Finalises a reference implementation merge: # 1. Runs format + test + build (via format-and-test.sh) # 2. Updates .lastmerge to reference implementation HEAD -# 3. Commits the .lastmerge update -# 4. Pushes the branch to origin +# 3. Syncs the @github/copilot version property in pom.xml from the +# cloned reference implementation's nodejs/package.json +# 4. Syncs scripts/codegen/package.json to the same @github/copilot +# version so the code generator uses matching schemas +# 5. Commits the .lastmerge + pom.xml + codegen package updates +# 6. Pushes the branch to origin # # Usage: ./.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh # ./.github/scripts/reference-impl-sync/merge-reference-impl-finish.sh --skip-tests @@ -48,8 +52,22 @@ echo "▸ Updating .lastmerge…" NEW_COMMIT=$(cd "$REFERENCE_IMPL_DIR" && git rev-parse origin/main) echo "$NEW_COMMIT" > "$ROOT_DIR/.lastmerge" -git add .lastmerge -git commit -m "Update .lastmerge to $NEW_COMMIT" +# ── 2b. Sync pom.xml @github/copilot version ───────────────── +# Keeps the canonical CLI version in pom.xml aligned with what the +# reference implementation pinned in .lastmerge depends on. +echo "▸ Syncing @github/copilot version in pom.xml from reference implementation…" +"$ROOT_DIR/.github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh" "$REFERENCE_IMPL_DIR" + +# ── 2c. Sync scripts/codegen @github/copilot version ───────── +# Keeps scripts/codegen/package.json in lockstep so the code generator +# uses schemas from the same CLI version that the tests run against. +# This eliminates the gap where Dependabot could race ahead of the +# reference implementation sync. +echo "▸ Syncing @github/copilot version in scripts/codegen/package.json…" +"$ROOT_DIR/.github/scripts/reference-impl-sync/sync-codegen-version.sh" "$REFERENCE_IMPL_DIR" + +git add .lastmerge pom.xml scripts/codegen/package.json scripts/codegen/package-lock.json +git commit -m "Update .lastmerge to $NEW_COMMIT, sync pom.xml CLI version, and update scripts/codegen @github/copilot version" # ── 3. Push branch ─────────────────────────────────────────── echo "▸ Pushing branch $BRANCH_NAME to origin…" diff --git a/.github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh b/.github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh new file mode 100755 index 0000000000..1d15d8aee9 --- /dev/null +++ b/.github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# ────────────────────────────────────────────────────────────── +# sync-cli-version-from-reference-impl.sh +# +# Reads the @github/copilot version specifier from the cloned +# reference implementation's nodejs/package.json, and updates the +# corresponding property in pom.xml: +# +# +# +# This keeps the canonical Copilot CLI version (declared in pom.xml) +# in sync with whatever the reference implementation pinned in +# .lastmerge depends on. All workflows that install the Copilot CLI +# (build-test.yml — implicitly via cloned SDK, run-smoke-test.yml and +# update-copilot-dependency.yml — via the setup-copilot action) read +# this single property so every CI path uses the same CLI version. +# +# Usage: +# ./sync-cli-version-from-reference-impl.sh +# +# Or, when invoked from merge-reference-impl-finish.sh, sources +# REFERENCE_IMPL_DIR from the .merge-env file. +# ────────────────────────────────────────────────────────────── +set -euo pipefail + +# Locate the repo root by walking up from this script until we find a pom.xml. +# This is resilient to the script being moved to a different depth under +# .github/scripts/ in the future. +find_repo_root() { + local dir + dir="$(cd "$(dirname "$0")" && pwd)" + while [[ "$dir" != "/" ]]; do + if [[ -f "$dir/pom.xml" ]]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + echo "❌ Could not locate repo root (no pom.xml found above $(dirname "$0"))" >&2 + return 1 +} +ROOT_DIR="$(find_repo_root)" + +REFERENCE_IMPL_DIR="${1:-${REFERENCE_IMPL_DIR:-}}" +if [[ -z "$REFERENCE_IMPL_DIR" ]]; then + echo "❌ Usage: $0 " >&2 + echo " or set REFERENCE_IMPL_DIR in the environment." >&2 + exit 1 +fi + +PKG_JSON="$REFERENCE_IMPL_DIR/nodejs/package.json" +if [[ ! -f "$PKG_JSON" ]]; then + echo "❌ Cannot find $PKG_JSON" >&2 + exit 1 +fi + +# node is always available since the reference implementation uses npm. +CLI_VERSION=$(node -e \ + "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const v=(p.dependencies&&p.dependencies['@github/copilot'])||(p.devDependencies&&p.devDependencies['@github/copilot']);process.stdout.write(v||'');" \ + "$PKG_JSON") + +if [[ -z "$CLI_VERSION" ]]; then + echo "❌ Could not extract @github/copilot version from $PKG_JSON" >&2 + exit 1 +fi + +POM="$ROOT_DIR/pom.xml" +PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" + +if ! grep -q "<${PROP}>" "$POM"; then + echo "❌ Property <${PROP}> not found in $POM" >&2 + exit 1 +fi + +# Use a portable sed invocation (works on both BSD/macOS and GNU/Linux). +TMP="$(mktemp)" +sed -E "s|<${PROP}>[^<]*|<${PROP}>${CLI_VERSION}|" "$POM" > "$TMP" +mv "$TMP" "$POM" + +echo "▸ Updated pom.xml: <${PROP}> = ${CLI_VERSION}" diff --git a/.github/scripts/reference-impl-sync/sync-codegen-version.sh b/.github/scripts/reference-impl-sync/sync-codegen-version.sh new file mode 100755 index 0000000000..09e182bb24 --- /dev/null +++ b/.github/scripts/reference-impl-sync/sync-codegen-version.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# ────────────────────────────────────────────────────────────── +# sync-codegen-version.sh +# +# Updates the @github/copilot dependency in scripts/codegen/package.json +# to match the version used by the reference implementation. This keeps +# the code generator schemas in lockstep with the CLI version used for +# testing, eliminating the gap where Dependabot could race ahead. +# +# Usage: +# ./sync-codegen-version.sh +# +# Or, when invoked from merge-reference-impl-finish.sh, the directory +# is passed as $1. +# ────────────────────────────────────────────────────────────── +set -euo pipefail + +# Locate the repo root by walking up from this script until we find a pom.xml. +find_repo_root() { + local dir + dir="$(cd "$(dirname "$0")" && pwd)" + while [[ "$dir" != "/" ]]; do + if [[ -f "$dir/pom.xml" ]]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + echo "❌ Could not locate repo root (no pom.xml found above $(dirname "$0"))" >&2 + return 1 +} +ROOT_DIR="$(find_repo_root)" + +REFERENCE_IMPL_DIR="${1:-${REFERENCE_IMPL_DIR:-}}" +if [[ -z "$REFERENCE_IMPL_DIR" ]]; then + echo "❌ Usage: $0 " >&2 + echo " or set REFERENCE_IMPL_DIR in the environment." >&2 + exit 1 +fi + +PKG_JSON="$REFERENCE_IMPL_DIR/nodejs/package.json" +if [[ ! -f "$PKG_JSON" ]]; then + echo "❌ Cannot find $PKG_JSON" >&2 + exit 1 +fi + +# Extract the @github/copilot version from the reference implementation. +CLI_VERSION=$(node -e \ + "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const v=(p.dependencies&&p.dependencies['@github/copilot'])||(p.devDependencies&&p.devDependencies['@github/copilot']);process.stdout.write(v||'');" \ + "$PKG_JSON") + +if [[ -z "$CLI_VERSION" ]]; then + echo "❌ Could not extract @github/copilot version from $PKG_JSON" >&2 + exit 1 +fi + +CODEGEN_DIR="$ROOT_DIR/scripts/codegen" +CODEGEN_PKG="$CODEGEN_DIR/package.json" + +if [[ ! -f "$CODEGEN_PKG" ]]; then + echo "❌ Cannot find $CODEGEN_PKG" >&2 + exit 1 +fi + +# Update scripts/codegen/package.json with the new version and regenerate the lock file. +# Intentionally omit --save-exact to preserve the version specifier used by the reference +# implementation (e.g. a caret range like '^1.0.36-0' rather than an exact pin '1.0.36-0'). +echo "▸ Updating scripts/codegen/package.json: @github/copilot → ${CLI_VERSION}" +cd "$CODEGEN_DIR" +npm install "@github/copilot@${CLI_VERSION}" +echo "▸ Updated scripts/codegen to @github/copilot@${CLI_VERSION}" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1143634f69..9154375a9a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -89,6 +89,39 @@ jobs: COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.path }} run: mvn verify + - name: Validate reference-impl-sync completeness + if: >- + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'reference-impl-sync') + run: | + git fetch origin main --depth=1 + CHANGED=$(git diff --name-only origin/main...HEAD) + + # 1. .lastmerge must be updated (proves finish script ran) + if echo "$CHANGED" | grep -q '^\\.lastmerge$'; then + echo "✅ .lastmerge was updated (finish script ran)" + else + echo "❌ .lastmerge was not updated. The merge-reference-impl-finish.sh script" + echo " must be run before this PR can merge. This script updates .lastmerge," + echo " syncs the CLI version in pom.xml, and syncs scripts/codegen/package.json." + exit 1 + fi + + # 2. If codegen inputs changed, generated output must also have changed + if echo "$CHANGED" | grep -q '^scripts/codegen/'; then + if echo "$CHANGED" | grep -q '^src/generated/java/'; then + echo "✅ Codegen inputs changed and generated files were regenerated" + else + echo "❌ scripts/codegen/ was updated but src/generated/java/ has no changes." + echo " The Codegen Check workflow should regenerate these files automatically." + echo " If it hasn't run yet, wait for it to complete and push regenerated files." + echo " Or run manually: cd scripts/codegen && npm ci && npm run generate" + exit 1 + fi + else + echo "✅ No codegen input changes — regeneration not needed" + fi + - name: Upload test results for site generation if: success() && github.ref == 'refs/heads/main' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/codegen-agentic-fix.lock.yml b/.github/workflows/codegen-agentic-fix.lock.yml index b44681eb74..958f3cd155 100644 --- a/.github/workflows/codegen-agentic-fix.lock.yml +++ b/.github/workflows/codegen-agentic-fix.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e9707f96f0d1dc7a2c4bbd2c193b917ff5793901b14d055a1fef0da62dd2b928","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"70880e5fb9ec78342cf56974b2414d8cf0a516533e201e437835164543ef58c0","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -178,19 +178,19 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_89262d074598e425_EOF' + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' - GH_AW_PROMPT_89262d074598e425_EOF + GH_AW_PROMPT_d94a435f1fbdc38f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_89262d074598e425_EOF' + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' - Tools: add_comment(max:5), push_to_pull_request_branch(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_89262d074598e425_EOF + Tools: add_comment(max:5), push_to_pull_request_branch, missing_tool, missing_data, noop + GH_AW_PROMPT_d94a435f1fbdc38f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_89262d074598e425_EOF' + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' The following GitHub context information is available for this workflow: @@ -220,12 +220,12 @@ jobs: {{/if}} - GH_AW_PROMPT_89262d074598e425_EOF + GH_AW_PROMPT_d94a435f1fbdc38f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_89262d074598e425_EOF' + cat << 'GH_AW_PROMPT_d94a435f1fbdc38f_EOF' {{#runtime-import .github/workflows/codegen-agentic-fix.md}} - GH_AW_PROMPT_89262d074598e425_EOF + GH_AW_PROMPT_d94a435f1fbdc38f_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -402,16 +402,15 @@ 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_e118843832e795e2_EOF' - {"add_comment":{"max":5,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","max":3,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"]},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_e118843832e795e2_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b6e7bb90508de076_EOF' + {"add_comment":{"max":5,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","labels":["dependencies"],"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_b6e7bb90508de076_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", - "push_to_pull_request_branch": " CONSTRAINTS: Maximum 3 push(es) can be made." + "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading." }, "repo_params": {}, "dynamic_tools": [] @@ -609,7 +608,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_1f06674d94d6899b_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_f8f308e27510535f_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -650,7 +649,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_1f06674d94d6899b_EOF + GH_AW_MCP_CONFIG_f8f308e27510535f_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -662,7 +661,7 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 + timeout-minutes: 60 run: | set -o pipefail touch /tmp/gh-aw/agent-step-summary.md @@ -967,7 +966,7 @@ jobs: GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" - GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_TIMEOUT_MINUTES: "60" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1233,7 +1232,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":3,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"]},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"labels\":[\"dependencies\"],\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codegen-agentic-fix.md b/.github/workflows/codegen-agentic-fix.md index eb93e1095d..b4aa0a8f20 100644 --- a/.github/workflows/codegen-agentic-fix.md +++ b/.github/workflows/codegen-agentic-fix.md @@ -23,6 +23,8 @@ permissions: contents: read actions: read +timeout-minutes: 60 + network: allowed: - defaults @@ -33,9 +35,10 @@ tools: toolsets: [context, repos] safe-outputs: - push_to_pull_request_branch: - max: 3 - add_comment: + push-to-pull-request-branch: + target: "*" + labels: [dependencies] + add-comment: target: "*" max: 5 noop: @@ -204,7 +207,7 @@ For each attempt: ### Step 5: Push fixes -After `mvn verify` passes, commit all changes and use the `push_to_pull_request_branch` safe-output tool to push to PR #${{ inputs.pr_number }}: +After `mvn verify` passes, commit all changes and use the `push-to-pull-request-branch` safe-output tool to push to PR #${{ inputs.pr_number }}: ```bash git add -A @@ -213,13 +216,13 @@ git commit -m "Fix codegen and build failures after @github/copilot update Automated fix applied by codegen-agentic-fix workflow." ``` -Then call the `push_to_pull_request_branch` tool to push your commits to the PR branch. +Then call the `push-to-pull-request-branch` tool to push your commits to the PR branch. ### Step 6: Failure handling If all 3 attempts fail: -1. Call the `add_comment` tool on PR #${{ inputs.pr_number }} explaining: +1. Call the `add-comment` tool on PR #${{ inputs.pr_number }} explaining: - What errors remain - What fixes were attempted - Whether the issue is in the code generator or handwritten code diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 5bc3f56182..917cd02e04 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -24,9 +24,9 @@ jobs: # Install GitHub CLI and gh-aw extension for Copilot Agent interaction - name: Install gh-aw extension - uses: github/gh-aw/actions/setup-cli@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3 + uses: github/gh-aw/actions/setup-cli@f01a9d118afa6e306f3645ca31e43f4ea8fb4d22 # v0.71.1 with: - version: v0.42.17 + version: v0.68.3 # Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 diff --git a/.github/workflows/weekly-reference-impl-sync.lock.yml b/.github/workflows/reference-impl-sync.lock.yml similarity index 95% rename from .github/workflows/weekly-reference-impl-sync.lock.yml rename to .github/workflows/reference-impl-sync.lock.yml index 49bfd3ac0f..5cd565b426 100644 --- a/.github/workflows/weekly-reference-impl-sync.lock.yml +++ b/.github/workflows/reference-impl-sync.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"54f733578d3011b148c7aff09a0e72085d787a777a996488f08aeffdf52ec93b","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e8c3534f6cb36bd08b5436922ff89339bc22c719895422c0526b8a7779186327","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]} # ___ _ _ # / _ \ | | (_) @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Weekly reference implementation sync workflow. Checks for new commits in the official +# Reference implementation sync workflow. Checks for new commits in the official # Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. # # Secrets used: @@ -47,11 +47,11 @@ # - ghcr.io/github/github-mcp-server:v0.32.0 # - node:lts-alpine -name: "Weekly Reference Implementation Sync" +name: "Reference Implementation Sync" "on": schedule: - - cron: "40 11 * * 0" - # Friendly format: weekly (scattered) + - cron: "47 22 * * *" + # Friendly format: daily (scattered) workflow_dispatch: inputs: aw_context: @@ -65,7 +65,7 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" -run-name: "Weekly Reference Implementation Sync" +run-name: "Reference Implementation Sync" jobs: activation: @@ -97,7 +97,7 @@ jobs: GH_AW_INFO_VERSION: "1.0.21" GH_AW_INFO_AGENT_VERSION: "1.0.21" GH_AW_INFO_CLI_VERSION: "v0.68.3" - GH_AW_INFO_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_INFO_WORKFLOW_NAME: "Reference Implementation Sync" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" @@ -132,7 +132,7 @@ jobs: id: check-lock-file uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: - GH_AW_WORKFLOW_FILE: "weekly-reference-impl-sync.lock.yml" + GH_AW_WORKFLOW_FILE: "reference-impl-sync.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | @@ -166,14 +166,14 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_9a049ffd9b204d97_EOF' + cat << 'GH_AW_PROMPT_8be4e91e77b39cd4_EOF' - GH_AW_PROMPT_9a049ffd9b204d97_EOF + GH_AW_PROMPT_8be4e91e77b39cd4_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_9a049ffd9b204d97_EOF' + cat << 'GH_AW_PROMPT_8be4e91e77b39cd4_EOF' Tools: add_comment(max:10), create_issue, close_issue(max:10), assign_to_agent, missing_tool, missing_data, noop @@ -205,12 +205,12 @@ jobs: {{/if}} - GH_AW_PROMPT_9a049ffd9b204d97_EOF + GH_AW_PROMPT_8be4e91e77b39cd4_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_9a049ffd9b204d97_EOF' + cat << 'GH_AW_PROMPT_8be4e91e77b39cd4_EOF' - {{#runtime-import .github/workflows/weekly-reference-impl-sync.md}} - GH_AW_PROMPT_9a049ffd9b204d97_EOF + {{#runtime-import .github/workflows/reference-impl-sync.md}} + GH_AW_PROMPT_8be4e91e77b39cd4_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -292,7 +292,7 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - GH_AW_WORKFLOW_ID_SANITIZED: weeklyreferenceimplsync + GH_AW_WORKFLOW_ID_SANITIZED: referenceimplsync outputs: agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} @@ -381,9 +381,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_2a317c5680ea1925_EOF' - {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"max":1,"model":"claude-opus-4.6","name":"copilot","target":"*"},"close_issue":{"max":10,"required_labels":["reference-impl-sync"],"target":"*"},"create_issue":{"assignees":["copilot-swe-agent"],"expires":144,"labels":["reference-impl-sync"],"max":1,"title_prefix":"[reference-impl-sync] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_2a317c5680ea1925_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_76ba061ad4465f81_EOF' + {"add_comment":{"max":10,"target":"*"},"assign_to_agent":{"max":1,"model":"claude-opus-4.6","name":"copilot","target":"*"},"close_issue":{"max":10,"required_labels":["reference-impl-sync"],"target":"*"},"create_issue":{"expires":144,"labels":["reference-impl-sync"],"max":1,"title_prefix":"[reference-impl-sync] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_76ba061ad4465f81_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -392,7 +392,7 @@ jobs: "add_comment": " CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", "assign_to_agent": " CONSTRAINTS: Maximum 1 issue(s) can be assigned to agent.", "close_issue": " CONSTRAINTS: Maximum 10 issue(s) can be closed. Target: *.", - "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[reference-impl-sync] \". Labels [\"reference-impl-sync\"] will be automatically added. Assignees [\"copilot-swe-agent\"] will be automatically assigned." + "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[reference-impl-sync] \". Labels [\"reference-impl-sync\"] will be automatically added." }, "repo_params": {}, "dynamic_tools": [] @@ -646,7 +646,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_ee30822602a32ffa_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_cf52d533554e8317_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -687,7 +687,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_ee30822602a32ffa_EOF + GH_AW_MCP_CONFIG_cf52d533554e8317_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -891,7 +891,7 @@ jobs: issues: write pull-requests: write concurrency: - group: "gh-aw-conclusion-weekly-reference-impl-sync" + group: "gh-aw-conclusion-reference-impl-sync" cancel-in-progress: false outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} @@ -926,7 +926,7 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_NOOP_REPORT_AS_ISSUE: "false" @@ -942,7 +942,7 @@ jobs: uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} @@ -959,7 +959,7 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -973,7 +973,7 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -987,10 +987,10 @@ jobs: uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_WORKFLOW_ID: "weekly-reference-impl-sync" + GH_AW_WORKFLOW_ID: "reference-impl-sync" GH_AW_ENGINE_ID: "copilot" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} @@ -1098,8 +1098,8 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: - WORKFLOW_NAME: "Weekly Reference Implementation Sync" - WORKFLOW_DESCRIPTION: "Weekly reference implementation sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk) and assigns to Copilot to port changes." + WORKFLOW_NAME: "Reference Implementation Sync" + WORKFLOW_DESCRIPTION: "Reference implementation sync workflow. Checks for new commits in the official\nCopilot SDK (github/copilot-sdk) and assigns to Copilot to port changes." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | @@ -1184,14 +1184,14 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/weekly-reference-impl-sync" + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/reference-impl-sync" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_WORKFLOW_ID: "weekly-reference-impl-sync" - GH_AW_WORKFLOW_NAME: "Weekly Reference Implementation Sync" + GH_AW_WORKFLOW_ID: "reference-impl-sync" + GH_AW_WORKFLOW_NAME: "Reference Implementation Sync" outputs: assign_to_agent_assigned: ${{ steps.process_safe_outputs.outputs.assign_to_agent_assigned }} assign_to_agent_assignment_error_count: ${{ steps.process_safe_outputs.outputs.assign_to_agent_assignment_error_count }} @@ -1245,7 +1245,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"assign_to_agent\":{\"max\":1,\"model\":\"claude-opus-4.6\",\"name\":\"copilot\",\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"reference-impl-sync\"],\"target\":\"*\"},\"create_issue\":{\"assignees\":[\"copilot-swe-agent\"],\"expires\":144,\"labels\":[\"reference-impl-sync\"],\"max\":1,\"title_prefix\":\"[reference-impl-sync] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"assign_to_agent\":{\"max\":1,\"model\":\"claude-opus-4.6\",\"name\":\"copilot\",\"target\":\"*\"},\"close_issue\":{\"max\":10,\"required_labels\":[\"reference-impl-sync\"],\"target\":\"*\"},\"create_issue\":{\"expires\":144,\"labels\":[\"reference-impl-sync\"],\"max\":1,\"title_prefix\":\"[reference-impl-sync] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" GH_AW_ASSIGN_TO_AGENT_TOKEN: ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/weekly-reference-impl-sync.md b/.github/workflows/reference-impl-sync.md similarity index 85% rename from .github/workflows/weekly-reference-impl-sync.md rename to .github/workflows/reference-impl-sync.md index 1d1f9a4fd5..8e77f1a187 100644 --- a/.github/workflows/weekly-reference-impl-sync.md +++ b/.github/workflows/reference-impl-sync.md @@ -1,10 +1,10 @@ --- description: | - Weekly reference implementation sync workflow. Checks for new commits in the official + Reference implementation sync workflow. Checks for new commits in the official Copilot SDK (github/copilot-sdk) and assigns to Copilot to port changes. on: - schedule: weekly + schedule: daily workflow_dispatch: permissions: @@ -24,7 +24,6 @@ tools: safe-outputs: create-issue: title-prefix: "[reference-impl-sync] " - assignees: [copilot-swe-agent] labels: [reference-impl-sync] expires: 6 close-issue: @@ -41,7 +40,7 @@ safe-outputs: noop: report-as-issue: false --- -# Weekly Reference Implementation Sync +# Reference Implementation Sync You are an automation agent that detects new reference implementation changes and creates GitHub issues. You do **NOT** perform any code merges, edits, or pushes. Do **NOT** invoke any skills (especially `agentic-merge-reference-impl`). Your only job is to check for changes and use safe-output tools to create or close issues. @@ -105,7 +104,13 @@ Go to Step 3b. ### Instructions - Follow the [agentic-merge-reference-impl](.github/prompts/agentic-merge-reference-impl.prompt.md) prompt to port these changes to the Java SDK. + **You MUST follow these steps in order. Do NOT improvise or skip scripts.** + + 1. ✅✅Read [.github/prompts/agentic-merge-reference-impl.prompt.md](.github/prompts/agentic-merge-reference-impl.prompt.md) in full before starting✅✅ + + ❌❌Do NOT clone the reference implementation manually — the start script does this.❌❌ + ❌❌Do NOT update .lastmerge manually — the finish script does this.❌❌ + ❌❌Do NOT skip the finish script — it syncs codegen versions and updates .lastmerge.❌❌ ``` 4. After creating the issue, use the `assign_to_agent` safe-output tool to assign Copilot to the newly created issue. diff --git a/.github/workflows/weekly-reference-impl-sync.yml b/.github/workflows/reference-impl-sync.yml similarity index 93% rename from .github/workflows/weekly-reference-impl-sync.yml rename to .github/workflows/reference-impl-sync.yml index aa3fc971ac..ab8225a2af 100644 --- a/.github/workflows/weekly-reference-impl-sync.yml +++ b/.github/workflows/reference-impl-sync.yml @@ -1,9 +1,9 @@ -name: "Weekly Reference Implementation Sync" +name: "Reference Implementation Sync" on: schedule: - # Every Monday at 10:00 UTC (5:00 AM EST / 6:00 AM EDT) - - cron: '0 10 * * 1' + # Tuesday and Thursday at 10:00 UTC (5:00 AM EST / 6:00 AM EDT) + - cron: '0 10 * * 2,4' workflow_dispatch: permissions: @@ -129,16 +129,16 @@ jobs: Follow the [agentic-merge-reference-impl](.github/prompts/agentic-merge-reference-impl.prompt.md) prompt to port these changes to the Java SDK." - # Create the issue and assign to Copilot coding agent + # Create the issue with the label; the Copilot coding agent picks up + # unassigned issues that carry the configured label. ISSUE_URL=$(gh issue create \ --repo "$REPO" \ --title "Reference implementation sync: ${COMMIT_COUNT} new commits (${DATE})" \ --body "$BODY" \ - --label "reference-impl-sync" \ - --assignee "copilot-swe-agent") + --label "reference-impl-sync") echo "issue_url=$ISSUE_URL" >> "$GITHUB_OUTPUT" - echo "✅ Issue created and assigned to Copilot coding agent: $ISSUE_URL" + echo "✅ Issue created with label for Copilot coding agent: $ISSUE_URL" - name: Summary if: always() @@ -152,7 +152,7 @@ jobs: ISSUE_URL="iframe.php?url=https%3A%2F%2Fgithub.com%2F%24%7B%7B+steps.create-issue.outputs.issue_url+%7D%7D" { - echo "## Weekly Reference Implementation Sync" + echo "## Reference Implementation Sync" echo "" if [ "$HAS_CHANGES" = "true" ]; then echo "### ✅ New reference implementation changes detected" diff --git a/.gitignore b/.gitignore index 35ea546f09..654d3278f8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ changebundle.txt* .settings scripts/codegen/node_modules/ *~ +*.sln diff --git a/.lastmerge b/.lastmerge index caffac28be..4a35fe8da3 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -dd2dcbc439256acfb9feb2cff07c0b9c820091b8 +c063458ecc3d606766f04cf203b11b08de672cc8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d198ee218..b898011140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -> **Reference implementation sync:** [`github/copilot-sdk@dd2dcbc`](https://github.com/github/copilot-sdk/commit/dd2dcbc439256acfb9feb2cff07c0b9c820091b8) +> **Reference implementation sync:** [`github/copilot-sdk@c063458`](https://github.com/github/copilot-sdk/commit/c063458ecc3d606766f04cf203b11b08de672cc8) + +## [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) @@ -503,7 +506,10 @@ New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` [0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 [Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.1...HEAD [0.3.0-java-preview.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.1 -[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java.2...HEAD +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.1...HEAD +[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 +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.1...HEAD +[1.0.0-beta-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java.2...v1.0.0-beta-java.1 [0.3.0-java.2]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java.2 [0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 [Unreleased]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java-preview.0...HEAD diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f86976c01f..38f2def77c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,9 @@ If you have ideas for entirely new features, please post an issue or start a dis 1. Push to your fork and [submit a pull request][pr] 1. Pat yourself on the back and wait for your pull request to be reviewed and merged. -### Running tests and linters +### Running locally, including tests and linters + +The POM has logic to ensure a known correct installation of Copilot CLI is used when executing the tests. If you want to test against a different installation of Copilot CLI, set the value of the `copilot.cli.path` maven property to the fully qualified path to the Copilot CLI. ```bash # Build and run all tests @@ -54,6 +56,22 @@ mvn spotless:apply mvn spotless:check ``` +## Running the known correct Copilot CLI installation + +### POSIX + +```bash +export COPILOT_CLI_PATH="$(find "$PWD/target" -type f -path '*/nodejs/node_modules/@github/copilot/index.js' | head -n 1)" +node ${COPILOT_CLI_PATH} +``` + +### PowerShell + +```PowerShell +$env:COPILOT_CLI_PATH = (Get-ChildItem -Path "$PWD\target" -Recurse -Filter 'index.js' -File | Where-Object { $_.FullName -match '[\\/]nodejs[\\/]node_modules[\\/]@github[\\/]copilot[\\/]index\.js$' } | Select-Object -First 1 -ExpandProperty FullName) +node $env:COPILOT_CLI_PATH +``` + Here are a few things you can do that will increase the likelihood of your pull request being accepted: - Write tests. diff --git a/README.md b/README.md index f5658f3d6f..b1de3e2c97 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A com.github copilot-sdk-java - 0.3.0-java.2 + 1.0.0-beta-java.1 ``` @@ -53,14 +53,14 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 0.3.1-java.1-SNAPSHOT + 1.0.0-java.1-SNAPSHOT ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:0.3.0-java.2' +implementation 'com.github:copilot-sdk-java:1.0.0-beta-java.1' ``` ## Quick Start @@ -165,7 +165,7 @@ Contributions are welcome! Please see the [Contributing Guide](CONTRIBUTING.md) This SDK tracks the official [Copilot SDK](https://github.com/github/copilot-sdk) (.NET reference implementation) and ports changes to Java. The reference implementation merge process is automated with AI assistance: -**Weekly automated sync** — A [scheduled GitHub Actions workflow](.github/workflows/weekly-reference-impl-sync.yml) runs every Monday at 5 AM ET. It checks for new reference implementation commits since the last merge (tracked in [`.lastmerge`](.lastmerge)), and if changes are found, creates an issue labeled `reference-impl-sync` and assigns it to the GitHub Copilot coding agent. Any previously open `reference-impl-sync` issues are automatically closed. +**Automated sync** — A [scheduled GitHub Actions workflow](.github/workflows/reference-impl-sync.yml) runs on the schedule specified in that file. It checks for new reference implementation commits since the last merge (tracked in [`.lastmerge`](.lastmerge)), and if changes are found, creates an issue labeled `reference-impl-sync` and assigns it to the GitHub Copilot coding agent. Any previously open `reference-impl-sync` issues are automatically closed. The sync also updates the `@github/copilot` version in both `pom.xml` and `scripts/codegen/package.json` to keep schemas and test CLI in lockstep. **Reusable prompt** — The merge workflow is defined in [`agentic-merge-reference-impl.prompt.md`](.github/prompts/agentic-merge-reference-impl.prompt.md). It can be triggered manually from: - **VS Code Copilot Chat** — type `/agentic-merge-reference-impl` diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md index 3df254890c..9dd3833d1b 100644 --- a/docs/WORKFLOWS.md +++ b/docs/WORKFLOWS.md @@ -6,11 +6,11 @@ |----------|-------------|----------|----------| | [Build & Test](workflows/build-test.yml) | Builds, lints, and tests the Java SDK | `push` (main), `pull_request`, `merge_group`, `workflow_dispatch` | Sundays at 00:00 UTC | | [Codegen Check](workflows/codegen-check.yml) | Verifies that generated Java files are up-to-date with the JSON schemas | `push` (main), `pull_request`, `workflow_dispatch` | — | -| [Update @github/copilot Dependency](workflows/update-copilot-dependency.yml) | Updates the `@github/copilot` npm package, re-runs code generation, and opens a PR | `workflow_dispatch` | — | +| [Update @github/copilot Dependency](workflows/update-copilot-dependency.yml) | _(Codegen-only)_ Updates the `@github/copilot` npm package in `scripts/codegen`, re-runs code generation, and opens a PR. Does **not** update `pom.xml`; use the Reference Implementation Sync for full version alignment. | `workflow_dispatch` | — | | [Deploy Documentation](workflows/deploy-site.yml) | Generates and deploys versioned docs to GitHub Pages | `workflow_run` (after Build & Test), `release`, `workflow_dispatch` | — | | [Publish to Maven Central](workflows/publish-maven.yml) | Releases the SDK to Maven Central and creates a GitHub Release | `workflow_dispatch` | — | -| [Weekly Reference Implementation Sync](workflows/weekly-reference-impl-sync.yml) | Checks for new reference implementation commits and creates an issue for Copilot to merge | `workflow_dispatch` | Mondays at 10:00 UTC | -| [Weekly Reference Implementation Sync (Agentic)](workflows/weekly-reference-impl-sync.lock.yml) | Compiled agentic workflow that executes the reference implementation sync via `gh-aw` | `workflow_dispatch` | Tuesdays at 08:39 UTC (scattered) | +| [Reference Implementation Sync](workflows/reference-impl-sync.yml) | Checks for new reference implementation commits and creates an issue for Copilot to merge | `workflow_dispatch` | [See workflow](workflows/reference-impl-sync.yml) | +| [Reference Implementation Sync (Agentic)](workflows/reference-impl-sync.lock.yml) | Compiled agentic workflow that executes the reference implementation sync via `gh-aw` | `workflow_dispatch` | [See workflow](workflows/reference-impl-sync.lock.yml) | | [Copilot Setup Steps](workflows/copilot-setup-steps.yml) | Configures the environment for the GitHub Copilot coding agent | `push` (on self-change), `workflow_dispatch` | — | --- @@ -64,11 +64,11 @@ Manual-only workflow that performs a full release: --- -## Weekly Reference Implementation Sync +## Reference Implementation Sync -**File:** [`weekly-reference-impl-sync.yml`](workflows/weekly-reference-impl-sync.yml) +**File:** [`reference-impl-sync.yml`](workflows/reference-impl-sync.yml) -Runs every Monday at 10:00 UTC. Clones the official `github/copilot-sdk` repository and compares `HEAD` against the commit hash stored in `.lastmerge`. +Runs on the schedule specified in [`.github/workflows/reference-impl-sync.yml`](workflows/reference-impl-sync.yml). Clones the official `github/copilot-sdk` repository and compares `HEAD` against the commit hash stored in `.lastmerge`. If new commits are found: 1. Closes any previously open `reference-impl-sync` issues @@ -79,9 +79,9 @@ If no changes are found, any stale open `reference-impl-sync` issues are closed. --- -## Weekly Reference Implementation Sync (Agentic Workflow: Experimental) +## Reference Implementation Sync (Agentic Workflow: Experimental) -**File:** [`weekly-reference-impl-sync.lock.yml`](workflows/weekly-reference-impl-sync.lock.yml) +**File:** [`reference-impl-sync.lock.yml`](workflows/reference-impl-sync.lock.yml) Auto-generated compiled workflow produced by `gh aw compile` from the corresponding `.md` source. This is the agentic counterpart that actually executes the reference implementation merge using the `gh-aw` MCP server and Copilot coding agent. @@ -113,6 +113,8 @@ If changes appear, commit the updated generated files. **File:** [`update-copilot-dependency.yml`](workflows/update-copilot-dependency.yml) +> **Note:** This workflow is for codegen-only updates to `scripts/codegen`. It does **not** update `pom.xml`. For full version alignment (both CLI and codegen in lockstep), use the **Reference Implementation Sync** workflow instead. + Manual workflow triggered when a new version of the `@github/copilot` npm package is published. Accepts a `version` input (e.g. `1.0.25`). Steps: diff --git a/jbang-example.java b/jbang-example.java index 4b711ba735..0fb94c9c08 100644 --- a/jbang-example.java +++ b/jbang-example.java @@ -1,5 +1,5 @@ -! -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.generated.SessionUsageInfoEvent; diff --git a/pom.xml b/pom.xml index 5abdefcbff..91686a2709 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java - 0.3.0-java.2 + 1.0.0-beta-java.1 jar GitHub Copilot SDK :: Java @@ -33,7 +33,7 @@ scm:git:https://github.com/github/copilot-sdk-java.git scm:git:https://github.com/github/copilot-sdk-java.git https://github.com/github/copilot-sdk-java - v0.3.0-java.2 + v1.0.0-beta-java.1 @@ -49,10 +49,53 @@ ${project.build.directory}/copilot-sdk ${copilot.sdk.clone.dir}/test + + ${copilot.sdk.clone.dir}/nodejs/node_modules/@github/copilot/index.js false + + ${skip.test.harness} + + ^1.0.41-0 + @@ -60,7 +103,7 @@ com.fasterxml.jackson.core jackson-databind - 2.21.2 + 2.21.3 com.fasterxml.jackson.core @@ -70,7 +113,7 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.21.2 + 2.21.3 @@ -85,7 +128,7 @@ org.junit.jupiter junit-jupiter - 5.14.3 + 5.14.4 test @@ -240,6 +283,35 @@ + + + install-nodejs-cli-dependencies + generate-test-resources + + exec + + + ${skip.cli.install} + npm + ${copilot.sdk.clone.dir}/nodejs + + ci + --ignore-scripts + + + @@ -253,6 +325,15 @@ ${copilot.tests.dir} ${copilot.sdk.clone.dir} + + + ${copilot.cli.path} + @@ -587,6 +668,39 @@ true + + + skip-cli-install-when-tests-skipped + + + skipTests + true + + + + true + + + + + skip-cli-install-when-maven-test-skip + + + maven.test.skip + true + + + + true + + debug diff --git a/scripts/codegen/package-lock.json b/scripts/codegen/package-lock.json index 682b30bc66..b45b9c668a 100644 --- a/scripts/codegen/package-lock.json +++ b/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.36", + "@github/copilot": "^1.0.41-0", "json-schema": "^0.4.0", "tsx": "^4.20.6" } @@ -428,26 +428,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.36.tgz", - "integrity": "sha512-x0N5wLzw+tANzb+vCFYLHn3BV3qii2oyn14wC20RO7SsS8/YeBH8olvwlDLJ4PB0mL17QOiytNCdkvjvprm28w==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.41-0.tgz", + "integrity": "sha512-gLyCadBZdJeJtHJI3XdN8wAmLMEUdXfCa3EcVnbdbV1NHZDAJhr7h41l7a49pqRAmJyLUKlk1Lokk7U+OD3tgw==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.36", - "@github/copilot-darwin-x64": "1.0.36", - "@github/copilot-linux-arm64": "1.0.36", - "@github/copilot-linux-x64": "1.0.36", - "@github/copilot-win32-arm64": "1.0.36", - "@github/copilot-win32-x64": "1.0.36" + "@github/copilot-darwin-arm64": "1.0.41-0", + "@github/copilot-darwin-x64": "1.0.41-0", + "@github/copilot-linux-arm64": "1.0.41-0", + "@github/copilot-linux-x64": "1.0.41-0", + "@github/copilot-win32-arm64": "1.0.41-0", + "@github/copilot-win32-x64": "1.0.41-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.36.tgz", - "integrity": "sha512-5qkb7frTS4K/LdTDLrzKo78VR4aw/EZ6JzLz4KfmaW4UYyPiNirExDFXa/By22X0o8YMfOp4MCA2KSCAxKdgTg==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.41-0.tgz", + "integrity": "sha512-lrrH1oMbTOF1W/YxH6rvoEHOymxmXaMx4aDzm190hU0Yh6Cuu0BJGFvgG8nE9bqcv5O8W7eEBr26jDlGtnZiwg==", "cpu": [ "arm64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.36.tgz", - "integrity": "sha512-AdsM8QtM5QSzMLpavLREh8HALO5G+VWzGNQqIHu4f0YQC/s1cGoiwo3wsgkpxRcLGBykFc+bDX3yK3MDQ8XvSw==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.41-0.tgz", + "integrity": "sha512-4418VtSSkEgn4BcwCFg+0UDhGCfQgGTx16r/PiWbuUOgIBzts3FfVzWMWTuXyxk7kl2Ib8k7KSd/7rNpjcrzBw==", "cpu": [ "x64" ], @@ -477,9 +477,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.36.tgz", - "integrity": "sha512-n7K1I6r0ggOJ4A9uAMS11USTvn6BKtAwvrOkzEaeRK89VNUJzpTe6p0mE13ItzRe5eot9WLBQOxvXLtL9f6E+g==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.41-0.tgz", + "integrity": "sha512-5xjgp3Ak5QJ68byNbsgBpdK1V6T5t8EGu0pUwEJMNMMXxqvL9f7gPcnCGdTtV2DS4Q3adkziV/gpBSSQ5HY8hg==", "cpu": [ "arm64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.36.tgz", - "integrity": "sha512-wBtCdR3ITZcq07BJbkwHfwI6ayiwbH5pF1ex+Ycl4UI+Lf1vP9eQD6wJppPgsrjwFcdeWRThaYTPCRTkSGHv5g==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.41-0.tgz", + "integrity": "sha512-oWPkj0bSjBjtAqonMEZD7EuSByBNXwtceMw8y7uGOfs6jQXfhDGzCCB6NGb+lcftVNtWDKFCUtx+x8Fbt4O37w==", "cpu": [ "x64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.36.tgz", - "integrity": "sha512-0GzZUZQn07alI8BgbzK0NlR5+ta/Rd0sWmd8kbRCns7oybAIkSALy6BKVwJmVHtXUi6h4iUE8oiFhkn0spymvw==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.41-0.tgz", + "integrity": "sha512-MaPg4tFWTiRuyv+j0ymJbZp8UPK+RIXNMpekR7FRf8/Uz+NiJgTTxTDjFi4ytRJU5UNrUezkVAk5Xduq/CaIew==", "cpu": [ "arm64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.36.tgz", - "integrity": "sha512-UBX9qj0McCK/SLq93XIr1i80fj3b3XmE3befVFrzxQuTeOoxLURN35vi7W+4x+4ZfsDHQpRTlJNjZw9w0fPr+Q==", + "version": "1.0.41-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.41-0.tgz", + "integrity": "sha512-ykRuDWjJEgSywMFJl1yaefssaklCVSVhprx2NcSVh6tIGupvvzVAM6nL6Mj6nyKpG6FKGHanedBeL6SJc935cw==", "cpu": [ "x64" ], diff --git a/scripts/codegen/package.json b/scripts/codegen/package.json index 1b540376e1..d146fd945b 100644 --- a/scripts/codegen/package.json +++ b/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.36", + "@github/copilot": "^1.0.41-0", "json-schema": "^0.4.0", "tsx": "^4.20.6" } diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java index a6d75a8f65..2ff43e67a8 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java @@ -56,6 +56,8 @@ public record AssistantMessageEventData( @JsonProperty("interactionId") String interactionId, /** GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ @JsonProperty("requestId") String requestId, + /** Identifier for the agent loop turn that produced this message, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java new file mode 100644 index 0000000000..8cf1a9c8e7 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code assistant.message_start} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantMessageStartEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.message_start"; } + + @JsonProperty("data") + private AssistantMessageStartEventData data; + + public AssistantMessageStartEventData getData() { return data; } + public void setData(AssistantMessageStartEventData data) { this.data = data; } + + /** Data payload for {@link AssistantMessageStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantMessageStartEventData( + /** Message ID this start event belongs to, matching subsequent deltas and assistant.message */ + @JsonProperty("messageId") String messageId, + /** Generation phase this message belongs to for phased-output models */ + @JsonProperty("phase") String phase + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java index 4bb8aa92c6..ee3e9f9cfb 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java @@ -24,7 +24,7 @@ public record AssistantUsageCopilotUsage( /** Itemized token usage breakdown */ @JsonProperty("tokenDetails") List tokenDetails, - /** Total cost in nano-AIU (AI Units) for this request */ + /** Total cost in nano-AI units for this request */ @JsonProperty("totalNanoAiu") Double totalNanoAiu ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java b/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java index 8ad1a2da82..e234ccd287 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java @@ -38,7 +38,9 @@ public record AutoModeSwitchRequestedEventData( /** Unique identifier for this request; used to respond via session.respondToAutoModeSwitch() */ @JsonProperty("requestId") String requestId, /** The rate limit error code that triggered this request */ - @JsonProperty("errorCode") String errorCode + @JsonProperty("errorCode") String errorCode, + /** Seconds until the rate limit resets, when known. Lets clients render a humanized reset time alongside the prompt. */ + @JsonProperty("retryAfterSeconds") Double retryAfterSeconds ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java b/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java index 7e02e4f778..76e5a0ed8f 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java +++ b/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java @@ -24,7 +24,7 @@ public record CompactionCompleteCompactionTokensUsedCopilotUsage( /** Itemized token usage breakdown */ @JsonProperty("tokenDetails") List tokenDetails, - /** Total cost in nano-AIU (AI Units) for this request */ + /** Total cost in nano-AI units for this request */ @JsonProperty("totalNanoAiu") Double totalNanoAiu ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java b/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java index b8c0dd93d5..b037b82ac5 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java +++ b/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java @@ -24,6 +24,8 @@ public record McpOauthRequiredStaticClientConfig( /** OAuth client ID for the server */ @JsonProperty("clientId") String clientId, /** Whether this is a public OAuth client */ - @JsonProperty("publicClient") Boolean publicClient + @JsonProperty("publicClient") Boolean publicClient, + /** Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ + @JsonProperty("grantType") String grantType ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java b/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java new file mode 100644 index 0000000000..939e49ef55 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The {@code model.call_failure} session event. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ModelCallFailureEvent extends SessionEvent { + + @Override + public String getType() { return "model.call_failure"; } + + @JsonProperty("data") + private ModelCallFailureEventData data; + + public ModelCallFailureEventData getData() { return data; } + public void setData(ModelCallFailureEventData data) { this.data = data; } + + /** Data payload for {@link ModelCallFailureEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ModelCallFailureEventData( + /** Model identifier used for the failed API call */ + @JsonProperty("model") String model, + /** What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ + @JsonProperty("initiator") String initiator, + /** Completion ID from the model provider (e.g., chatcmpl-abc123) */ + @JsonProperty("apiCallId") String apiCallId, + /** GitHub request tracing ID (x-github-request-id header) for server-side log correlation */ + @JsonProperty("providerCallId") String providerCallId, + /** HTTP status code from the failed request */ + @JsonProperty("statusCode") Long statusCode, + /** Duration of the failed API call in milliseconds */ + @JsonProperty("durationMs") Double durationMs, + /** Where the failed model call originated */ + @JsonProperty("source") ModelCallFailureSource source, + /** Raw provider/runtime error message for restricted telemetry */ + @JsonProperty("errorMessage") String errorMessage + ) { + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java b/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java new file mode 100644 index 0000000000..469adaab43 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Where the failed model call originated + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelCallFailureSource { + /** The {@code top_level} variant. */ + TOP_LEVEL("top_level"), + /** The {@code subagent} variant. */ + SUBAGENT("subagent"), + /** The {@code mcp_sampling} variant. */ + MCP_SAMPLING("mcp_sampling"); + + private final String value; + ModelCallFailureSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelCallFailureSource fromValue(String value) { + for (ModelCallFailureSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelCallFailureSource value: " + value); + } +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java b/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java index 0045393c1a..0b8da105e2 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java @@ -40,7 +40,7 @@ public record PermissionCompletedEventData( /** Optional tool call ID associated with this permission prompt; clients may use it to correlate UI created from tool-scoped prompts */ @JsonProperty("toolCallId") String toolCallId, /** The result of the permission request */ - @JsonProperty("result") PermissionCompletedResult result + @JsonProperty("result") Object result ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java index 33dc688348..ecb85aacf0 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java @@ -37,6 +37,10 @@ public final class SessionErrorEvent extends SessionEvent { public record SessionErrorEventData( /** Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "context_limit", "query") */ @JsonProperty("errorType") String errorType, + /** Fine-grained error code from the upstream provider, when available. For `errorType: "rate_limit"`, this is one of the `RateLimitErrorCode` values (e.g., `"user_weekly_rate_limited"`, `"user_global_rate_limited"`, `"rate_limited"`, `"user_model_rate_limited"`, `"integration_rate_limited"`). */ + @JsonProperty("errorCode") String errorCode, + /** Only set on `errorType: "rate_limit"`. When `true`, the runtime will follow this error with an `auto_mode_switch.requested` event (or silently switch if `continueOnAutoMode` is enabled). UI clients can use this flag to suppress duplicate rendering of the rate-limit error when they show their own auto-mode-switch prompt. */ + @JsonProperty("eligibleForAutoSwitch") Boolean eligibleForAutoSwitch, /** Human-readable error message */ @JsonProperty("message") String message, /** Error stack trace, when available */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java index 85c62a2418..02364db4f3 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java @@ -54,9 +54,11 @@ @JsonSubTypes.Type(value = AssistantReasoningDeltaEvent.class, name = "assistant.reasoning_delta"), @JsonSubTypes.Type(value = AssistantStreamingDeltaEvent.class, name = "assistant.streaming_delta"), @JsonSubTypes.Type(value = AssistantMessageEvent.class, name = "assistant.message"), + @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), @JsonSubTypes.Type(value = AssistantMessageDeltaEvent.class, name = "assistant.message_delta"), @JsonSubTypes.Type(value = AssistantTurnEndEvent.class, name = "assistant.turn_end"), @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), + @JsonSubTypes.Type(value = ModelCallFailureEvent.class, name = "model.call_failure"), @JsonSubTypes.Type(value = AbortEvent.class, name = "abort"), @JsonSubTypes.Type(value = ToolUserRequestedEvent.class, name = "tool.user_requested"), @JsonSubTypes.Type(value = ToolExecutionStartEvent.class, name = "tool.execution_start"), @@ -133,9 +135,11 @@ public abstract sealed class SessionEvent permits AssistantReasoningDeltaEvent, AssistantStreamingDeltaEvent, AssistantMessageEvent, + AssistantMessageStartEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, + ModelCallFailureEvent, AbortEvent, ToolUserRequestedEvent, ToolExecutionStartEvent, diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java index 4dee36ba5c..01c1e0efbe 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java @@ -40,7 +40,9 @@ public record SessionInfoEventData( /** Human-readable informational message for display in the timeline */ @JsonProperty("message") String message, /** Optional URL associated with this message that the user can open in a browser */ - @JsonProperty("url") String url + @JsonProperty("url") String url, + /** Optional actionable tip displayed with this message */ + @JsonProperty("tip") String tip ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java index c23c8a5f50..812cff0e81 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java @@ -42,7 +42,9 @@ public record SessionModelChangeEventData( /** Reasoning effort level before the model change, if applicable */ @JsonProperty("previousReasoningEffort") String previousReasoningEffort, /** Reasoning effort level after the model change, if applicable */ - @JsonProperty("reasoningEffort") String reasoningEffort + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Reason the change happened, when not user-initiated. Currently `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path. UI clients can use this to render contextual copy. */ + @JsonProperty("cause") String cause ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java index 7973e7d17c..31bd3b4c36 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java @@ -48,8 +48,12 @@ public record SessionResumeEventData( @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at resume time */ @JsonProperty("alreadyInUse") Boolean alreadyInUse, + /** True when this resume attached to a session that the runtime already had running in-memory (for example, an extension joining a session another client was actively driving). False (or omitted) for cold resumes — the runtime had to reconstitute the session from its persisted event log. */ + @JsonProperty("sessionWasActive") Boolean sessionWasActive, /** Whether this session supports remote steering via Mission Control */ - @JsonProperty("remoteSteerable") Boolean remoteSteerable + @JsonProperty("remoteSteerable") Boolean remoteSteerable, + /** When true, tool calls and permission requests left in flight by the previous session lifetime remain pending after resume and the agentic loop awaits their results. User sends are queued behind the pending work until all such requests reach a terminal state. When false (the default), any such tool calls and permission requests are immediately marked as interrupted on resume. */ + @JsonProperty("continuePendingWork") Boolean continuePendingWork ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java b/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java index 39b6cfcfab..97d5060d32 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java @@ -42,6 +42,10 @@ public record SessionShutdownEventData( @JsonProperty("errorReason") String errorReason, /** Total number of premium API requests used during the session */ @JsonProperty("totalPremiumRequests") Double totalPremiumRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, /** Cumulative time spent in API calls during the session, in milliseconds */ @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, /** Unix timestamp (milliseconds) when the session started */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java index e5cae14764..e7a831ca67 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; import javax.annotation.processing.Generated; @javax.annotation.processing.Generated("copilot-sdk-codegen") @@ -19,6 +20,10 @@ public record ShutdownModelMetric( /** Request count and cost metrics */ @JsonProperty("requests") ShutdownModelMetricRequests requests, /** Token usage breakdown */ - @JsonProperty("usage") ShutdownModelMetricUsage usage + @JsonProperty("usage") ShutdownModelMetricUsage usage, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java new file mode 100644 index 0000000000..9f7cde6301 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownModelMetricTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Double tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java new file mode 100644 index 0000000000..ec51804886 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Double tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java index 6481374d1b..a1deeeab56 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java @@ -52,6 +52,8 @@ public record ToolExecutionCompleteEventData( @JsonProperty("error") ToolExecutionCompleteError error, /** Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) */ @JsonProperty("toolTelemetry") Map toolTelemetry, + /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java index 7da7572864..5a48cdd977 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java @@ -45,6 +45,8 @@ public record ToolExecutionStartEventData( @JsonProperty("mcpServerName") String mcpServerName, /** Original tool name on the MCP server, when the tool is an MCP tool */ @JsonProperty("mcpToolName") String mcpToolName, + /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId ) { diff --git a/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java b/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java index 5d156082db..ec5f382463 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java +++ b/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java @@ -51,7 +51,9 @@ public record UserMessageEventData( /** The agent mode that was active when this message was sent */ @JsonProperty("agentMode") UserMessageAgentMode agentMode, /** CAPI interaction ID for correlating this user message with its turn */ - @JsonProperty("interactionId") String interactionId + @JsonProperty("interactionId") String interactionId, + /** Parent agent task ID for background telemetry correlated to this user turn */ + @JsonProperty("parentAgentTaskId") String parentAgentTaskId ) { } } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java index edd34eb947..4695ff9fb9 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java @@ -21,6 +21,8 @@ public record AgentInfo( /** Human-readable display name */ @JsonProperty("displayName") String displayName, /** Description of the agent's purpose */ - @JsonProperty("description") String description + @JsonProperty("description") String description, + /** Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. */ + @JsonProperty("path") String path ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java new file mode 100644 index 0000000000..665cd9694c --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code connect} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectParams( + /** Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ + @JsonProperty("token") String token +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java new file mode 100644 index 0000000000..56666b2523 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code connect} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectResult( + /** Always true on success */ + @JsonProperty("ok") Boolean ok, + /** Server protocol version number */ + @JsonProperty("protocolVersion") Long protocolVersion, + /** Server package version */ + @JsonProperty("version") String version +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java index d2e3161000..b82c49ec23 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java @@ -63,4 +63,12 @@ public CompletableFuture ping(PingParams params) { return caller.invoke("ping", params, PingResult.class); } + /** + * Invokes {@code connect}. + * @since 1.0.0 + */ + public CompletableFuture connect(ConnectParams params) { + return caller.invoke("connect", params, ConnectResult.class); + } + } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java index 9e516b45aa..82ba816531 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionNameGetResult( - /** The session name, falling back to the auto-generated summary, or null if neither exists */ + /** The session name (user-set or auto-generated), or null if not yet set */ @JsonProperty("name") String name ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java index 39ab121b6d..d4934fc08a 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java @@ -46,6 +46,8 @@ public final class SessionRpc { public final SessionFleetApi fleet; /** API methods for the {@code agent} namespace. */ public final SessionAgentApi agent; + /** API methods for the {@code tasks} namespace. */ + public final SessionTasksApi tasks; /** API methods for the {@code skills} namespace. */ public final SessionSkillsApi skills; /** API methods for the {@code mcp} namespace. */ @@ -87,6 +89,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.instructions = new SessionInstructionsApi(caller, sessionId); this.fleet = new SessionFleetApi(caller, sessionId); this.agent = new SessionAgentApi(caller, sessionId); + this.tasks = new SessionTasksApi(caller, sessionId); this.skills = new SessionSkillsApi(caller, sessionId); this.mcp = new SessionMcpApi(caller, sessionId); this.plugins = new SessionPluginsApi(caller, sessionId); @@ -100,6 +103,14 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.usage = new SessionUsageApi(caller, sessionId); } + /** + * Invokes {@code session.suspend}. + * @since 1.0.0 + */ + public CompletableFuture suspend() { + return caller.invoke("session.suspend", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + /** * Invokes {@code session.log}. *

diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java new file mode 100644 index 0000000000..300b1e4cba --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.suspend} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSuspendParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java new file mode 100644 index 0000000000..8e5f0ddeb3 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.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 tasks} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionTasksApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionTasksApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Invokes {@code session.tasks.startAgent}. + *

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

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

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

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture remove(SessionTasksRemoveParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.remove", _p, SessionTasksRemoveResult.class); + } + +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java new file mode 100644 index 0000000000..7f3ba8a5a8 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.cancel} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksCancelParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java new file mode 100644 index 0000000000..976e75d646 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.cancel} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksCancelResult( + /** Whether the task was successfully cancelled */ + @JsonProperty("cancelled") Boolean cancelled +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java new file mode 100644 index 0000000000..379221deb1 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.list} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java new file mode 100644 index 0000000000..47c5b1bec1 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.list} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksListResult( + /** Currently tracked tasks */ + @JsonProperty("tasks") List tasks +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java new file mode 100644 index 0000000000..14aca99e38 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.promoteToBackground} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksPromoteToBackgroundParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java new file mode 100644 index 0000000000..87f09638f6 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.promoteToBackground} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksPromoteToBackgroundResult( + /** Whether the task was successfully promoted to background mode */ + @JsonProperty("promoted") Boolean promoted +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java new file mode 100644 index 0000000000..16e3bbf413 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.remove} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRemoveParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java new file mode 100644 index 0000000000..c2f10222d1 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.remove} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRemoveResult( + /** Whether the task was removed. Returns false if the task does not exist or is still running/idle (cancel it first). */ + @JsonProperty("removed") Boolean removed +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java new file mode 100644 index 0000000000..74a2ba3097 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.tasks.startAgent} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksStartAgentParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Type of agent to start (e.g., 'explore', 'task', 'general-purpose') */ + @JsonProperty("agentType") String agentType, + /** Task prompt for the agent */ + @JsonProperty("prompt") String prompt, + /** Short name for the agent, used to generate a human-readable ID */ + @JsonProperty("name") String name, + /** Short description of the task */ + @JsonProperty("description") String description, + /** Optional model override */ + @JsonProperty("model") String model +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java new file mode 100644 index 0000000000..34a5dd4921 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.tasks.startAgent} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksStartAgentResult( + /** Generated agent ID for the background task */ + @JsonProperty("agentId") String agentId +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java index 9cfdbcfc8d..cb1897d279 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java @@ -26,6 +26,10 @@ public record SessionUsageGetMetricsResult( @JsonProperty("totalPremiumRequestCost") Double totalPremiumRequestCost, /** Raw count of user-initiated API requests */ @JsonProperty("totalUserRequests") Long totalUserRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Long totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, /** Total time spent in model API calls (milliseconds) */ @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, /** Session start timestamp (epoch milliseconds) */ diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java index 2a2260db8a..c2afa210bd 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java @@ -36,8 +36,9 @@ public record SessionWorkspacesGetWorkspaceResultWorkspace( @JsonProperty("repository") String repository, @JsonProperty("host_type") SessionWorkspacesGetWorkspaceResultWorkspaceHostType hostType, @JsonProperty("branch") String branch, - @JsonProperty("summary") String summary, @JsonProperty("name") String name, + @JsonProperty("user_named") Boolean userNamed, + @JsonProperty("summary") String summary, @JsonProperty("summary_count") Long summaryCount, @JsonProperty("created_at") OffsetDateTime createdAt, @JsonProperty("updated_at") OffsetDateTime updatedAt, diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java index 8334872cbb..844d4a1bf8 100644 --- a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; import javax.annotation.processing.Generated; @javax.annotation.processing.Generated("copilot-sdk-codegen") @@ -19,6 +20,10 @@ public record UsageMetricsModelMetric( /** Request count and cost metrics for this model */ @JsonProperty("requests") UsageMetricsModelMetricRequests requests, /** Token usage metrics for this model */ - @JsonProperty("usage") UsageMetricsModelMetricUsage usage + @JsonProperty("usage") UsageMetricsModelMetricUsage usage, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Long totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails ) { } diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java new file mode 100644 index 0000000000..51ef4f3ee2 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetricTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Long tokenCount +) { +} diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java new file mode 100644 index 0000000000..1f763f0281 --- /dev/null +++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Long tokenCount +) { +} diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index 07b5eefecc..ebf6a96a64 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -30,14 +30,28 @@ final class CliServerManager { private static final Logger LOG = Logger.getLogger(CliServerManager.class.getName()); + private static final int STDERR_READER_JOIN_TIMEOUT_MS = 5000; private final CopilotClientOptions options; private final StringBuilder stderrBuffer = new StringBuilder(); + private volatile Thread stderrThread; + private String connectionToken; CliServerManager(CopilotClientOptions options) { this.options = options; } + /** + * Sets the connection token to pass to the CLI process via environment + * variable. + * + * @param connectionToken + * the token, or {@code null} if not applicable + */ + void setConnectionToken(String connectionToken) { + this.connectionToken = connectionToken; + } + /** * Starts the CLI server process. * @@ -115,6 +129,16 @@ ProcessInfo startCliServer() throws IOException, InterruptedException { pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); } + // Set Copilot home directory if configured + if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) { + pb.environment().put("COPILOT_HOME", options.getCopilotHome()); + } + + // Set connection token for TCP mode + if (connectionToken != null && !connectionToken.isEmpty()) { + pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken); + } + // Set telemetry environment variables if configured if (options.getTelemetry() != null) { var telemetry = options.getTelemetry(); @@ -177,7 +201,7 @@ JsonRpcClient connectToServer(Process process, String tcpHost, Integer tcpPort) } private void startStderrReader(Process process) { - var stderrThread = new Thread(() -> { + var thread = new Thread(() -> { try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { String line; @@ -191,8 +215,9 @@ private void startStderrReader(Process process) { LOG.log(Level.FINE, "Error reading stderr", e); } }, "cli-stderr-reader"); - stderrThread.setDaemon(true); - stderrThread.start(); + thread.setDaemon(true); + thread.start(); + this.stderrThread = thread; } private Integer waitForPortAnnouncement(Process process) throws IOException { @@ -204,11 +229,9 @@ private Integer waitForPortAnnouncement(Process process) throws IOException { while (System.currentTimeMillis() < deadline) { String line = reader.readLine(); if (line == null) { + awaitStderrReader(); String stderr = getStderrOutput(); - if (!stderr.isEmpty()) { - throw new IOException("CLI process exited unexpectedly. stderr: " + stderr); - } - throw new IOException("CLI process exited unexpectedly"); + throw new IOException(formatCliExitedMessage("CLI process exited unexpectedly.", stderr)); } Matcher matcher = portPattern.matcher(line); @@ -228,12 +251,30 @@ String getStderrOutput() { } } + private void awaitStderrReader() { + Thread t = this.stderrThread; + if (t != null) { + try { + t.join(STDERR_READER_JOIN_TIMEOUT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + private void clearStderrBuffer() { synchronized (stderrBuffer) { stderrBuffer.setLength(0); } } + static String formatCliExitedMessage(String message, String stderrOutput) { + if (stderrOutput == null || stderrOutput.isEmpty()) { + return message; + } + return message + "\nstderr: " + stderrOutput; + } + private List resolveCliCommand(String cliPath, List args) { boolean isJsFile = cliPath.toLowerCase().endsWith(".js"); diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index 4b3bb0904f..4d0522162e 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -21,6 +21,7 @@ import com.github.copilot.sdk.json.CopilotClientOptions; import com.github.copilot.sdk.json.CreateSessionResponse; +import com.github.copilot.sdk.generated.rpc.ConnectParams; import com.github.copilot.sdk.generated.rpc.ServerRpc; import com.github.copilot.sdk.json.DeleteSessionResponse; import com.github.copilot.sdk.json.GetAuthStatusResponse; @@ -75,6 +76,7 @@ public final class CopilotClient implements AutoCloseable { * shutdown via {@link #stop()}. */ public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10; + private static final int FORCE_KILL_TIMEOUT_SECONDS = 10; private final CopilotClientOptions options; private final CliServerManager serverManager; private final LifecycleEventManager lifecycleManager = new LifecycleEventManager(); @@ -83,6 +85,7 @@ public final class CopilotClient implements AutoCloseable { private volatile boolean disposed = false; private final String optionsHost; private final Integer optionsPort; + private final String effectiveConnectionToken; private volatile List modelsCache; private final Object modelsCacheLock = new Object(); @@ -122,6 +125,24 @@ public CopilotClient(CopilotClientOptions options) { "GitHubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)"); } + // Validate tcpConnectionToken + if (this.options.getTcpConnectionToken() != null) { + if (this.options.getTcpConnectionToken().isEmpty()) { + throw new IllegalArgumentException("TcpConnectionToken must be a non-empty string"); + } + if (this.options.isUseStdio()) { + throw new IllegalArgumentException("TcpConnectionToken cannot be used with UseStdio = true"); + } + } + + // Compute effective connection token: use provided, or auto-generate for + // SDK-spawned TCP mode, or null for stdio/external server + boolean sdkSpawnsCli = !this.options.isUseStdio() + && (this.options.getCliUrl() == null || this.options.getCliUrl().isEmpty()); + this.effectiveConnectionToken = this.options.getTcpConnectionToken() != null + ? this.options.getTcpConnectionToken() + : (sdkSpawnsCli ? java.util.UUID.randomUUID().toString() : null); + // Parse CliUrl if provided if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { URI uri = CliServerManager.parseCliUrl(this.options.getCliUrl()); @@ -133,6 +154,7 @@ public CopilotClient(CopilotClientOptions options) { } this.serverManager = new CliServerManager(this.options); + this.serverManager.setConnectionToken(this.effectiveConnectionToken); } /** @@ -195,34 +217,65 @@ private Connection startCoreBody() { } catch (Exception e) { String stderr = serverManager.getStderrOutput(); if (!stderr.isEmpty()) { - throw new CompletionException(new IOException("CLI process exited unexpectedly. stderr: " + stderr, e)); + throw new CompletionException(new IOException( + CliServerManager.formatCliExitedMessage("CLI process exited unexpectedly.", stderr), e)); } throw new CompletionException(e); } } private static final int MIN_PROTOCOL_VERSION = 2; + private static final int METHOD_NOT_FOUND_ERROR_CODE = -32601; private void verifyProtocolVersion(Connection connection) throws Exception { int expectedVersion = SdkProtocolVersion.get(); - var params = new HashMap(); - params.put("message", null); - PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, TimeUnit.SECONDS); + Integer serverVersion; + + try { + // Try the new 'connect' RPC which supports connection tokens + var connectParams = new ConnectParams(effectiveConnectionToken); + var connectResponse = connection.rpc + .invoke("connect", connectParams, com.github.copilot.sdk.generated.rpc.ConnectResult.class) + .get(30, TimeUnit.SECONDS); + serverVersion = connectResponse.protocolVersion() != null + ? connectResponse.protocolVersion().intValue() + : null; + } catch (Exception e) { + // Unwrap CompletionException/ExecutionException to check inner cause + Throwable cause = e; + while (cause instanceof java.util.concurrent.ExecutionException || cause instanceof CompletionException) { + cause = cause.getCause(); + } + if (cause instanceof JsonRpcException rpcEx && isUnsupportedConnectMethod(rpcEx)) { + // Legacy server without 'connect'; fall back to 'ping'. + // A token, if any, is silently dropped — the legacy server can't enforce one. + var params = new HashMap(); + params.put("message", null); + PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, + TimeUnit.SECONDS); + serverVersion = pingResponse.protocolVersion(); + } else { + throw e; + } + } - if (pingResponse.protocolVersion() == null) { - throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion - + ", but server does not report a protocol version. " + if (serverVersion == null) { + throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION + + "-" + expectedVersion + ", but server does not report a protocol version. " + "Please update your server to ensure compatibility."); } - int serverVersion = pingResponse.protocolVersion(); if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > expectedVersion) { - throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion - + " (minimum " + MIN_PROTOCOL_VERSION + "), but server reports version " + serverVersion + ". " + throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION + + "-" + expectedVersion + ", but server reports version " + serverVersion + ". " + "Please update your SDK or server to ensure compatibility."); } } + private static boolean isUnsupportedConnectMethod(JsonRpcException ex) { + return ex.getCode() == METHOD_NOT_FOUND_ERROR_CODE || "Unhandled method connect".equals(ex.getMessage()); + } + /** * Disconnects from the Copilot server and closes all active sessions. *

@@ -301,8 +354,14 @@ private CompletableFuture cleanupConnection() { if (connection.process != null) { try { if (connection.process.isAlive()) { - connection.process.destroyForcibly(); + Process destroyedProcess = connection.process.destroyForcibly(); + if (!destroyedProcess.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + LOG.fine("Process did not terminate within force kill timeout"); + } } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.log(Level.FINE, "Interrupted while killing process", e); } catch (Exception e) { LOG.log(Level.FINE, "Error killing process", e); } @@ -744,18 +803,15 @@ public CompletableFuture getForegroundSessionId() { * if the operation fails */ public CompletableFuture setForegroundSessionId(String sessionId) { - return ensureConnected() - .thenCompose( - connection -> connection.rpc - .invoke("session.setForeground", Map.of("sessionId", sessionId), - com.github.copilot.sdk.json.SetForegroundSessionResponse.class) - .thenAccept(response -> { - if (!response.success()) { - throw new RuntimeException(response.error() != null - ? response.error() - : "Failed to set foreground session"); - } - })); + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("session.setForeground", new com.github.copilot.sdk.json.SetForegroundSessionRequest(sessionId), + com.github.copilot.sdk.json.SetForegroundSessionResponse.class) + .thenAccept(response -> { + if (!response.success()) { + throw new RuntimeException( + response.error() != null ? response.error() : "Failed to set foreground session"); + } + })); } /** diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index de6977ea8d..91dbd46424 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -122,6 +122,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setAgent(config.getAgent()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setSkillDirectories(config.getSkillDirectories()); + request.setInstructionDirectories(config.getInstructionDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setConfigDir(config.getConfigDir()); request.setEnableConfigDiscovery(config.getEnableConfigDiscovery()); @@ -199,6 +200,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setDefaultAgent(config.getDefaultAgent()); request.setAgent(config.getAgent()); request.setSkillDirectories(config.getSkillDirectories()); + request.setInstructionDirectories(config.getInstructionDirectories()); request.setDisabledSkills(config.getDisabledSkills()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setModelCapabilities(config.getModelCapabilities()); diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java index 3a14d17335..dff72581a9 100644 --- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java +++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -44,6 +44,7 @@ public class CopilotClientOptions { private String[] cliArgs; private String cliPath; private String cliUrl; + private String copilotHome; private String cwd; private Map environment; private Executor executor; @@ -53,6 +54,7 @@ public class CopilotClientOptions { private int port; private TelemetryConfig telemetry; private Integer sessionIdleTimeoutSeconds; + private String tcpConnectionToken; private Boolean useLoggedInUser; private boolean useStdio = true; @@ -191,6 +193,37 @@ public CopilotClientOptions setCliUrl(String cliUrl) { return this; } + /** + * Gets the base directory for Copilot data (session state, config, etc.). + * + * @return the Copilot home directory path, or {@code null} to use the CLI + * default ({@code ~/.copilot}) + */ + public String getCopilotHome() { + return copilotHome; + } + + /** + * Sets the base directory for Copilot data (session state, config, etc.). + *

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

+ * This option is only used when the SDK spawns the CLI process; it is ignored + * when connecting to an external server via {@link #setCliUrl(String)}. + * + * @param copilotHome + * the Copilot home directory path, or {@code null} to use the CLI + * default + * @return this options instance for method chaining + */ + public CopilotClientOptions setCopilotHome(String copilotHome) { + this.copilotHome = copilotHome; + return this; + } + /** * Gets the working directory for the CLI process. * @@ -462,6 +495,31 @@ public CopilotClientOptions setSessionIdleTimeoutSeconds(Integer sessionIdleTime return this; } + /** + * Gets the connection token for the headless CLI server (TCP only). + * + * @return the connection token, or {@code null} if not set + */ + public String getTcpConnectionToken() { + return tcpConnectionToken; + } + + /** + * Sets the connection token for the headless CLI server (TCP only). + *

+ * When the SDK spawns its own CLI in TCP mode and this is omitted, a UUID is + * generated automatically so the loopback listener is safe by default. Cannot + * be combined with {@link #setUseStdio(boolean)} = {@code true}. + * + * @param tcpConnectionToken + * the connection token (must not be {@code null} or empty) + * @return this options instance for method chaining + */ + public CopilotClientOptions setTcpConnectionToken(String tcpConnectionToken) { + this.tcpConnectionToken = Objects.requireNonNull(tcpConnectionToken, "tcpConnectionToken must not be null"); + return this; + } + /** * Returns whether to use the logged-in user for authentication. * @@ -533,6 +591,7 @@ public CopilotClientOptions clone() { copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null; copy.cliPath = this.cliPath; copy.cliUrl = this.cliUrl; + copy.copilotHome = this.copilotHome; copy.cwd = this.cwd; copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; copy.executor = this.executor; @@ -541,6 +600,7 @@ public CopilotClientOptions clone() { copy.onListModels = this.onListModels; copy.port = this.port; copy.sessionIdleTimeoutSeconds = this.sessionIdleTimeoutSeconds; + copy.tcpConnectionToken = this.tcpConnectionToken; copy.telemetry = this.telemetry; copy.useLoggedInUser = this.useLoggedInUser; copy.useStdio = this.useStdio; diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index ef8d5fda2d..5243f99ec2 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -91,6 +91,9 @@ public final class CreateSessionRequest { @JsonProperty("skillDirectories") private List skillDirectories; + @JsonProperty("instructionDirectories") + private List instructionDirectories; + @JsonProperty("disabledSkills") private List disabledSkills; @@ -326,6 +329,18 @@ public void setSkillDirectories(List skillDirectories) { this.skillDirectories = skillDirectories; } + /** Gets instruction directories. @return the instruction directories */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets instruction directories. @param instructionDirectories the directories + */ + public void setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + } + /** Gets disabled skills. @return the disabled skill names */ public List getDisabledSkills() { return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index b4dacf3702..3f0a0706d6 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -59,6 +59,7 @@ public class ResumeSessionConfig { private DefaultAgentConfig defaultAgent; private String agent; private List skillDirectories; + private List instructionDirectories; private List disabledSkills; private InfiniteSessionConfig infiniteSessions; private Consumer onEvent; @@ -591,6 +592,27 @@ public ResumeSessionConfig setSkillDirectories(List skillDirectories) { return this; } + /** + * Gets the additional directories to search for custom instruction files. + * + * @return the list of instruction directory paths + */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets additional directories to search for custom instruction files. + * + * @param instructionDirectories + * the list of instruction directory paths + * @return this config for method chaining + */ + public ResumeSessionConfig setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + return this; + } + /** * Gets the disabled skills. * @@ -775,6 +797,9 @@ public ResumeSessionConfig clone() { copy.defaultAgent = this.defaultAgent; copy.agent = this.agent; copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.instructionDirectories = this.instructionDirectories != null + ? new ArrayList<>(this.instructionDirectories) + : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.infiniteSessions = this.infiniteSessions; copy.onEvent = this.onEvent; diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index e36e90b675..c5931c275f 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -98,6 +98,9 @@ public final class ResumeSessionRequest { @JsonProperty("skillDirectories") private List skillDirectories; + @JsonProperty("instructionDirectories") + private List instructionDirectories; + @JsonProperty("disabledSkills") private List disabledSkills; @@ -366,6 +369,18 @@ public void setSkillDirectories(List skillDirectories) { this.skillDirectories = skillDirectories; } + /** Gets instruction directories. @return the instruction directories */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets instruction directories. @param instructionDirectories the directories + */ + public void setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + } + /** Gets disabled skills. @return the disabled skill names */ public List getDisabledSkills() { return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index 09661346a2..4d571990c6 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -57,6 +57,7 @@ public class SessionConfig { private String agent; private InfiniteSessionConfig infiniteSessions; private List skillDirectories; + private List instructionDirectories; private List disabledSkills; private String configDir; private Boolean enableConfigDiscovery; @@ -550,6 +551,27 @@ public SessionConfig setSkillDirectories(List skillDirectories) { return this; } + /** + * Gets the additional directories to search for custom instruction files. + * + * @return the list of instruction directory paths + */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets additional directories to search for custom instruction files. + * + * @param instructionDirectories + * the list of instruction directory paths + * @return this config instance for method chaining + */ + public SessionConfig setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + return this; + } + /** * Gets the disabled skill names. * @@ -825,6 +847,9 @@ public SessionConfig clone() { copy.agent = this.agent; copy.infiniteSessions = this.infiniteSessions; copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.instructionDirectories = this.instructionDirectories != null + ? new ArrayList<>(this.instructionDirectories) + : null; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; copy.configDir = this.configDir; copy.enableConfigDiscovery = this.enableConfigDiscovery; diff --git a/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java new file mode 100644 index 0000000000..d3944871a0 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request body for session.setForeground RPC call. + *

+ * Using an explicit record type (rather than an ad-hoc map) ensures correct + * JSON serialization in all execution environments. + * + * @since 1.0.0 + */ +public record SetForegroundSessionRequest( + /** The session ID to bring to the foreground. */ + @JsonProperty("sessionId") String sessionId) { +} diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index a4c4d830ad..e1e08a2753 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -626,6 +626,31 @@ var session = client.createSession( --- +## Instruction Directories + +Provide additional directories containing custom instruction files. These instructions are automatically included in the system message for all conversations in the session. + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setInstructionDirectories(List.of("/path/to/instructions")) +).get(); +``` + +Instruction files are discovered from `.github/instructions/` subdirectories within each specified path and should use the `.instructions.md` extension. + +This is also supported on session resume: + +```java +var session = client.resumeSession(sessionId, + new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setInstructionDirectories(List.of("/path/to/instructions")) +); +``` + +--- + ## Custom Configuration Directory Use a custom configuration directory for session settings: diff --git a/src/site/markdown/cookbook/error-handling.md b/src/site/markdown/cookbook/error-handling.md index 4fffd2ba0d..c192a33653 100644 --- a/src/site/markdown/cookbook/error-handling.md +++ b/src/site/markdown/cookbook/error-handling.md @@ -30,7 +30,7 @@ jbang BasicErrorHandling.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -64,7 +64,7 @@ public class BasicErrorHandling { ## Handling specific error types ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import java.util.concurrent.ExecutionException; @@ -99,7 +99,7 @@ public class SpecificErrorHandling { ## Timeout handling ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotSession; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -130,7 +130,7 @@ public class TimeoutHandling { ## Aborting a request ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotSession; import com.github.copilot.sdk.json.MessageOptions; import java.util.concurrent.Executors; @@ -162,7 +162,7 @@ public class AbortRequest { ## Graceful shutdown ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; public class GracefulShutdown { @@ -192,7 +192,7 @@ public class GracefulShutdown { ## Try-with-resources pattern ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -224,7 +224,7 @@ public class TryWithResources { ## Handling tool errors ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; diff --git a/src/site/markdown/cookbook/managing-local-files.md b/src/site/markdown/cookbook/managing-local-files.md index 723d9b1059..b8ab60314b 100644 --- a/src/site/markdown/cookbook/managing-local-files.md +++ b/src/site/markdown/cookbook/managing-local-files.md @@ -34,7 +34,7 @@ jbang ManagingLocalFiles.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.generated.SessionIdleEvent; @@ -161,7 +161,7 @@ session.send(new MessageOptions().setPrompt(prompt)); ## Interactive file organization ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import java.io.BufferedReader; import java.io.InputStreamReader; diff --git a/src/site/markdown/cookbook/multiple-sessions.md b/src/site/markdown/cookbook/multiple-sessions.md index c7ac909f18..de0777ec51 100644 --- a/src/site/markdown/cookbook/multiple-sessions.md +++ b/src/site/markdown/cookbook/multiple-sessions.md @@ -30,7 +30,7 @@ jbang MultipleSessions.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -123,7 +123,7 @@ try { ## Managing session lifecycle with CompletableFuture ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import java.util.concurrent.CompletableFuture; import java.util.List; diff --git a/src/site/markdown/cookbook/persisting-sessions.md b/src/site/markdown/cookbook/persisting-sessions.md index de50001fcf..ed2c62bb0c 100644 --- a/src/site/markdown/cookbook/persisting-sessions.md +++ b/src/site/markdown/cookbook/persisting-sessions.md @@ -30,7 +30,7 @@ jbang PersistingSessions.java **Code:** ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.json.MessageOptions; @@ -127,7 +127,7 @@ public class DeleteSession { ## Getting session history ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.generated.UserMessageEvent; @@ -162,7 +162,7 @@ public class SessionHistory { ## Complete example with session management ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import java.util.Scanner; public class SessionManager { diff --git a/src/site/markdown/cookbook/pr-visualization.md b/src/site/markdown/cookbook/pr-visualization.md index 40d7cde768..7d51c826a1 100644 --- a/src/site/markdown/cookbook/pr-visualization.md +++ b/src/site/markdown/cookbook/pr-visualization.md @@ -34,7 +34,7 @@ jbang PRVisualization.java github/copilot-sdk ## Full example: PRVisualization.java ```java -//DEPS com.github:copilot-sdk-java:0.3.0-java.2 +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.1 import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.generated.AssistantMessageEvent; import com.github.copilot.sdk.generated.ToolExecutionStartEvent; diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md index 10c1cb3d80..224fc1948c 100644 --- a/src/site/markdown/setup.md +++ b/src/site/markdown/setup.md @@ -157,12 +157,28 @@ try (var client = new CopilotClient(options)) { Multiple application instances can share a single CLI server: ```java -// In different parts of your application or different containers -var client1 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321")); -var client2 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321")); +// Use an explicit connection token so all clients can authenticate +var token = "my-shared-secret"; +var client1 = new CopilotClient(new CopilotClientOptions() + .setCliUrl("cli-server:4321").setTcpConnectionToken(token)); +var client2 = new CopilotClient(new CopilotClientOptions() + .setCliUrl("cli-server:4321").setTcpConnectionToken(token)); // Both connect to the same CLI server ``` +### Connection Token (TCP Security) + +When the SDK spawns the CLI in TCP mode, a random connection token is generated automatically +to protect the loopback listener. You can also provide an explicit token: + +```java +var options = new CopilotClientOptions() + .setUseStdio(false) + .setTcpConnectionToken("my-secret-token"); +``` + +> **Note:** `tcpConnectionToken` cannot be used with `useStdio = true`. + ### Deployment Patterns **Container deployment:** @@ -340,10 +356,12 @@ Complete list of `CopilotClientOptions` settings: | `cliPath` | String | Path to CLI executable | `"copilot"` from PATH | | `cliUrl` | String | External CLI server URL | `null` (spawn process) | | `cliArgs` | String[] | Extra CLI arguments | `null` | +| `copilotHome` | String | Base directory for Copilot data | `null` (~/.copilot) | | `gitHubToken` | String | GitHub OAuth token | `null` | | `useLoggedInUser` | Boolean | Use system credentials | `true` | | `useStdio` | boolean | Use stdio transport | `true` | | `port` | int | TCP port for CLI | `0` (random) | +| `tcpConnectionToken` | String | Connection token for TCP mode | `null` (auto-generated) | | `autoStart` | boolean | Auto-start server | `true` | | `autoRestart` | boolean | Auto-restart on crash | `true` | | `logLevel` | String | CLI log level | `"info"` | diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index f4f702f9ee..74bee7b503 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -36,6 +36,9 @@ void copilotClientOptionsCloneBasic() { original.setPort(9000); original.setGitHubToken("ghp_test"); original.setUseLoggedInUser(false); + original.setCopilotHome("/custom/copilot/home"); + original.setUseStdio(false); + original.setTcpConnectionToken("my-token-123"); CopilotClientOptions cloned = original.clone(); @@ -44,6 +47,8 @@ void copilotClientOptionsCloneBasic() { assertEquals(original.getPort(), cloned.getPort()); assertEquals(original.getGitHubToken(), cloned.getGitHubToken()); assertEquals(original.getUseLoggedInUser(), cloned.getUseLoggedInUser()); + assertEquals(original.getCopilotHome(), cloned.getCopilotHome()); + assertEquals(original.getTcpConnectionToken(), cloned.getTcpConnectionToken()); } @Test @@ -120,6 +125,7 @@ void sessionConfigListIndependence() { toolList.add("grep"); toolList.add("bash"); original.setAvailableTools(toolList); + original.setInstructionDirectories(new ArrayList<>(List.of("/path/a", "/path/b"))); SessionConfig cloned = original.clone(); @@ -129,6 +135,7 @@ void sessionConfigListIndependence() { // The cloned config should be unaffected by mutations to the original list assertEquals(2, cloned.getAvailableTools().size()); assertEquals(3, original.getAvailableTools().size()); + assertEquals(List.of("/path/a", "/path/b"), cloned.getInstructionDirectories()); } @Test diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java index 1cc067587e..b0430959b9 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -199,6 +199,30 @@ void testSessionIdleTimeoutSecondsOptionAccepted() { assertEquals(600, options.getSessionIdleTimeoutSeconds()); } + @Test + void testTcpConnectionTokenWithUseStdioThrows() { + var options = new CopilotClientOptions().setUseStdio(true).setTcpConnectionToken("my-token"); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testTcpConnectionTokenAcceptedInTcpMode() { + var options = new CopilotClientOptions().setUseStdio(false).setTcpConnectionToken("my-token"); + + // Should not throw + try (var client = new CopilotClient(options)) { + assertNotNull(client); + } + } + + @Test + void testCopilotHomeOptionSetOnOptions() { + var options = new CopilotClientOptions().setCopilotHome("/custom/home"); + + assertEquals("/custom/home", options.getCopilotHome()); + } + // ===== onLifecycle tests ===== /** diff --git a/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java b/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java index 203f5faed6..60849981d7 100644 --- a/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java +++ b/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java @@ -18,6 +18,7 @@ import com.github.copilot.sdk.json.PreToolUseHookInput; import com.github.copilot.sdk.json.PreToolUseHookOutput; import com.github.copilot.sdk.json.SectionOverride; +import com.github.copilot.sdk.json.SetForegroundSessionRequest; import com.github.copilot.sdk.json.SetForegroundSessionResponse; import com.github.copilot.sdk.json.ToolBinaryResult; import com.github.copilot.sdk.json.ToolResultObject; @@ -86,6 +87,14 @@ void getForegroundSessionResponseRecord() { assertEquals("/home/user/project", response.workspacePath()); } + // ===== SetForegroundSessionRequest record ===== + + @Test + void setForegroundSessionRequestRecord() { + var request = new SetForegroundSessionRequest("session-123"); + assertEquals("session-123", request.sessionId()); + } + // ===== SetForegroundSessionResponse record ===== @Test diff --git a/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/src/test/java/com/github/copilot/sdk/E2ETestContext.java index 45fcbc0a5d..3d9180bf97 100644 --- a/src/test/java/com/github/copilot/sdk/E2ETestContext.java +++ b/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -249,6 +249,7 @@ public List> getExchanges() throws IOException, InterruptedE public Map getEnvironment() { Map env = new HashMap<>(System.getenv()); env.put("COPILOT_API_URL", proxyUrl); + env.put("COPILOT_HOME", homeDir.toString()); env.put("XDG_CONFIG_HOME", homeDir.toString()); env.put("XDG_STATE_HOME", homeDir.toString()); return env; diff --git a/src/test/java/com/github/copilot/sdk/EventFidelityTest.java b/src/test/java/com/github/copilot/sdk/EventFidelityTest.java new file mode 100644 index 0000000000..60b8e13275 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/EventFidelityTest.java @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.AssistantUsageEvent; +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.SessionUsageInfoEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for event fidelity — verifying the shape, ordering, and presence of + * key events emitted from the runtime. + * + *

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

+ */ +public class EventFidelityTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that an {@code assistant.usage} event is emitted after the model + * processes a prompt. + * + * @see Snapshot: + * event_fidelity/should_emit_assistant_usage_event_after_model_call + */ + @Test + void testShouldEmitAssistantUsageEventAfterModelCall() throws Exception { + ctx.configureForTest("event_fidelity", "should_emit_assistant_usage_event_after_model_call"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 5+5? Reply with just the number.")).get(60, + TimeUnit.SECONDS); + + List usageEvents = events.stream().filter(e -> e instanceof AssistantUsageEvent) + .map(e -> (AssistantUsageEvent) e).toList(); + + assertFalse(usageEvents.isEmpty(), "Should have received an assistant.usage event after model call"); + + AssistantUsageEvent lastUsage = usageEvents.get(usageEvents.size() - 1); + assertNotNull(lastUsage.getData().model(), "Usage event should have a model field"); + assertFalse(lastUsage.getData().model().isEmpty(), "Model field should not be empty"); + + session.close(); + } + } + + /** + * Verifies that a {@code session.usage_info} event is emitted after the model + * processes a prompt. + * + * @see Snapshot: + * event_fidelity/should_emit_session_usage_info_event_after_model_call + */ + @Test + void testShouldEmitSessionUsageInfoEventAfterModelCall() throws Exception { + ctx.configureForTest("event_fidelity", "should_emit_session_usage_info_event_after_model_call"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 5+5? Reply with just the number.")).get(60, + TimeUnit.SECONDS); + + List usageInfoEvents = events.stream() + .filter(e -> e instanceof SessionUsageInfoEvent).map(e -> (SessionUsageInfoEvent) e).toList(); + + assertFalse(usageInfoEvents.isEmpty(), "Should have received a session.usage_info event after model call"); + + session.close(); + } + } +} diff --git a/src/test/java/com/github/copilot/sdk/PermissionsTest.java b/src/test/java/com/github/copilot/sdk/PermissionsTest.java index b498c1ce76..041d8181cb 100644 --- a/src/test/java/com/github/copilot/sdk/PermissionsTest.java +++ b/src/test/java/com/github/copilot/sdk/PermissionsTest.java @@ -363,4 +363,115 @@ void testShouldDenyToolOperationsWhenHandlerExplicitlyDeniesAfterResume(TestInfo session2.close(); } } + + /** + * Verifies that a permission handler returning {@code noResult} is handled + * correctly — the handler is called, and the session can be aborted afterward. + * + * @see Snapshot: permissions/should_deny_permission_with_noresult_kind + */ + @Test + void testShouldDenyPermissionWithNoResultKind() throws Exception { + ctx.configureForTest("permissions", "should_deny_permission_with_noresult_kind"); + + var permissionCalled = new CompletableFuture(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + permissionCalled.complete(true); + return CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.NO_RESULT)); + })).get(); + + session.send(new MessageOptions().setPrompt("Run 'node --version'")); + + assertTrue(permissionCalled.get(30, TimeUnit.SECONDS), + "Expected the no-result permission handler to be called."); + + session.abort().get(10, TimeUnit.SECONDS); + session.close(); + } + } + + /** + * Verifies that the runtime short-circuits the permission handler when + * {@code session.permissions.setApproveAll(true)} has been called. + * + * @see Snapshot: + * permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled + */ + @Test + void testShouldShortCircuitPermissionHandlerWhenSetApproveAllEnabled() throws Exception { + ctx.configureForTest("permissions", "should_short_circuit_permission_handler_when_set_approve_all_enabled"); + + var handlerCallCount = new int[]{0}; + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + handlerCallCount[0]++; + return CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + })).get(); + + // Set approve-all so the runtime short-circuits + var setResult = session.getRpc().permissions + .setApproveAll(new com.github.copilot.sdk.generated.rpc.SessionPermissionsSetApproveAllParams( + session.getSessionId(), true)) + .get(10, TimeUnit.SECONDS); + assertTrue(setResult.success(), "setApproveAll should succeed"); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo test' and tell me what happens")) + .get(60, TimeUnit.SECONDS); + assertNotNull(response); + + // Handler should not have been called since runtime approves all + assertEquals(0, handlerCallCount[0], + "Permission handler should not be called when setApproveAll is enabled"); + + session.close(); + } + } + + /** + * Verifies that the SDK correctly waits for a slow permission handler before + * completing tool execution. + * + * @see Snapshot: permissions/should_wait_for_slow_permission_handler + */ + @Test + void testShouldWaitForSlowPermissionHandler() throws Exception { + ctx.configureForTest("permissions", "should_wait_for_slow_permission_handler"); + + var handlerEntered = new CompletableFuture(); + var releaseHandler = new CompletableFuture(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + handlerEntered.complete(null); + return releaseHandler.thenApply( + v -> new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + })).get(); + + // Capture the sendAndWait future before awaiting it so we can interact with the + // handler + CompletableFuture responseFuture = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo slow_handler_test'")); + + // Wait for permission handler to be entered + handlerEntered.get(30, TimeUnit.SECONDS); + + // Release the handler + releaseHandler.complete(null); + + // Session should complete successfully + AssistantMessageEvent message = responseFuture.get(60, TimeUnit.SECONDS); + assertNotNull(message); + + session.close(); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java new file mode 100644 index 0000000000..5dcb366048 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for session configuration features. + */ +public class SessionConfigE2ETest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testShouldApplyInstructionDirectoriesOnCreate() throws Exception { + ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_create"); + + // Set up instruction directory with a custom instruction file + Path projectDir = ctx.getWorkDir().resolve("instruction-create-project"); + Path instructionDir = ctx.getWorkDir().resolve("extra-create-instructions"); + Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions"); + String sentinel = "JAVA_CREATE_INSTRUCTION_DIRECTORIES_SENTINEL"; + Files.createDirectories(projectDir); + Files.createDirectories(instructionFilesDir); + Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + "."); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setWorkingDirectory(projectDir.toString()) + .setInstructionDirectories(List.of(instructionDir.toString())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + String systemMessage = getSystemMessage(exchanges.get(0)); + assertNotNull(systemMessage, "System message should not be null"); + assertTrue(systemMessage.contains(sentinel), + "System message should contain the instruction sentinel: " + sentinel); + } + } + + @Test + void testShouldApplyInstructionDirectoriesOnResume() throws Exception { + ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_resume"); + + // Set up instruction directory with a custom instruction file + Path projectDir = ctx.getWorkDir().resolve("instruction-resume-project"); + Path instructionDir = ctx.getWorkDir().resolve("extra-resume-instructions"); + Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions"); + String sentinel = "JAVA_RESUME_INSTRUCTION_DIRECTORIES_SENTINEL"; + Files.createDirectories(projectDir); + Files.createDirectories(instructionFilesDir); + Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + "."); + + try (CopilotClient client = ctx.createClient()) { + // Create a session first + CopilotSession session1 = client.createSession(new SessionConfig() + .setWorkingDirectory(projectDir.toString()).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + // Resume with instructionDirectories + CopilotSession session2 = client.resumeSession(session1.getSessionId(), + new ResumeSessionConfig().setWorkingDirectory(projectDir.toString()) + .setInstructionDirectories(List.of(instructionDir.toString())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + session2.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + String systemMessage = getSystemMessage(exchanges.get(0)); + assertNotNull(systemMessage, "System message should not be null"); + assertTrue(systemMessage.contains(sentinel), + "System message should contain the instruction sentinel: " + sentinel); + } + } + + @SuppressWarnings("unchecked") + private static String getSystemMessage(Map exchange) { + // The exchange structure is: { request: { messages: [...] }, response: ..., + // requestHeaders: ... } + Object requestObj = exchange.get("request"); + if (!(requestObj instanceof Map request)) { + return null; + } + Object messagesObj = request.get("messages"); + if (messagesObj instanceof List messages) { + for (Object msg : messages) { + if (msg instanceof Map msgMap) { + if ("system".equals(msgMap.get("role"))) { + Object content = msgMap.get("content"); + return content != null ? content.toString() : null; + } + } + } + } + return null; + } +} diff --git a/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java b/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java index 2da7783587..5a9362f8d4 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java @@ -2275,7 +2275,10 @@ void testParsePermissionCompletedEvent() throws Exception { assertNotNull(event); assertEquals("permission.completed", event.getType()); assertEquals("perm-req-456", event.getData().requestId()); - assertEquals(PermissionCompletedKind.APPROVED, event.getData().result().kind()); + assertNotNull(event.getData().result()); + @SuppressWarnings("unchecked") + var result = (java.util.Map) event.getData().result(); + assertEquals("approved", result.get("kind")); } @Test diff --git a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java index 73ed6fffa3..f31ca2d2d8 100644 --- a/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java @@ -865,7 +865,7 @@ private SessionStartEvent createSessionStartEvent(String sessionId) { private AssistantMessageEvent createAssistantMessageEvent(String content) { var event = new AssistantMessageEvent(); var data = new AssistantMessageEvent.AssistantMessageEventData(null, content, null, null, null, null, null, - null, null, null, null); + null, null, null, null, null); event.setData(data); return event; } diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index abb4447f52..a79c02334c 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -441,4 +441,28 @@ void testBuildResumeRequestWithGitHubToken() { assertEquals("ghp_per_session_token", request.getGitHubToken()); } + + // ========================================================================= + // instructionDirectories propagation + // ========================================================================= + + @Test + void testBuildCreateRequestPropagatesInstructionDirectories() { + var dirs = List.of("/path/to/instructions", "/another/path"); + var config = new SessionConfig().setInstructionDirectories(dirs); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals(dirs, request.getInstructionDirectories()); + } + + @Test + void testBuildResumeRequestPropagatesInstructionDirectories() { + var dirs = List.of("/resume/instructions", "/other/dir"); + var config = new ResumeSessionConfig().setInstructionDirectories(dirs); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-inst", config); + + assertEquals(dirs, request.getInstructionDirectories()); + } } diff --git a/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java b/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java index f3f5976523..e922f4bf06 100644 --- a/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java +++ b/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java @@ -182,4 +182,97 @@ void testShouldProduceDeltasAfterSessionResume() throws Exception { } } } + + /** + * Verifies that no delta events are produced after resuming a session with + * streaming disabled (even though it was originally created with streaming + * enabled). + * + * @see Snapshot: + * streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled + */ + @Test + void testShouldNotProduceDeltasAfterSessionResumeWithStreamingDisabled() throws Exception { + ctx.configureForTest("streaming_fidelity", + "should_not_produce_deltas_after_session_resume_with_streaming_disabled"); + + try (CopilotClient client = ctx.createClient()) { + // Create a streaming session and send an initial message + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); + session.sendAndWait(new MessageOptions().setPrompt("What is 3 + 6?")).get(60, TimeUnit.SECONDS); + String sessionId = session.getSessionId(); + session.close(); + + // Resume using a new client with streaming DISABLED + try (CopilotClient newClient = ctx.createClient()) { + CopilotSession session2 = newClient.resumeSession(sessionId, new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(false)).get(); + + List events = new ArrayList<>(); + session2.on(events::add); + + AssistantMessageEvent answer = session2 + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("18"), + "Follow-up response should contain 18: " + answer.getData().content()); + + // No deltas when streaming is toggled off + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertTrue(deltaEvents.isEmpty(), + "Should not receive delta events when streaming is disabled on resume"); + + // But should still have a final assistant.message + List assistantEvents = events.stream() + .filter(e -> e instanceof AssistantMessageEvent).map(e -> (AssistantMessageEvent) e).toList(); + assertFalse(assistantEvents.isEmpty(), + "Should still have a final assistant.message when streaming is disabled"); + + session2.close(); + } + } + } + + /** + * Verifies that setting reasoningEffort alongside streaming=true does not break + * the streaming pipeline — deltas still arrive and complete successfully. + * + * @see Snapshot: + * streaming_fidelity/should_emit_streaming_deltas_with_reasoning_effort_configured + */ + @Test + void testShouldEmitStreamingDeltasWithReasoningEffortConfigured() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_emit_streaming_deltas_with_reasoning_effort_configured"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setStreaming(true).setReasoningEffort("high")) + .get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 15 * 17?")).get(60, TimeUnit.SECONDS); + + // With streaming + reasoning effort, we should still get content deltas + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertFalse(deltaEvents.isEmpty(), "Should have received delta events with reasoning effort configured"); + + // And a final assistant.message with the answer + List assistantEvents = events.stream() + .filter(e -> e instanceof AssistantMessageEvent).map(e -> (AssistantMessageEvent) e).toList(); + assertFalse(assistantEvents.isEmpty(), "Should have received assistant message events"); + assertTrue(assistantEvents.get(assistantEvents.size() - 1).getData().content().contains("255"), + "Response should contain 255"); + + session.close(); + } + } } diff --git a/src/test/java/com/github/copilot/sdk/ToolResultsTest.java b/src/test/java/com/github/copilot/sdk/ToolResultsTest.java new file mode 100644 index 0000000000..31f9d1b07d --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ToolResultsTest.java @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.ToolExecutionCompleteEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolResultObject; + +/** + * E2E tests for tool result types — verifying that rejected and denied result + * types are handled correctly by the runtime. + * + *

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

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