diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index db0fd0fec5..fff64bcdbe 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -3,7 +3,7 @@
{
"name": "Python 3",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
- "image": "mcr.microsoft.com/devcontainers/python:2-3.14-trixie",
+ "image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie",
"features": {
"ghcr.io/devcontainers/features/copilot-cli:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
diff --git a/.github/agents/docs-maintenance.agent.md b/.github/agents/docs-maintenance.agent.md
index 9b97fecf4d..c5363e3699 100644
--- a/.github/agents/docs-maintenance.agent.md
+++ b/.github/agents/docs-maintenance.agent.md
@@ -122,7 +122,6 @@ Every major SDK feature should be documented. Core features include:
- Client initialization and configuration
- Connection modes (stdio vs TCP)
- Authentication options
-- Auto-start and auto-restart behavior
**Session Management:**
- Creating sessions
@@ -342,7 +341,7 @@ cat nodejs/src/types.ts | grep -A 10 "export interface ExportSessionOptions"
```
**Must match:**
-- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `autoRestart`, `env`, `githubToken`, `useLoggedInUser`
+- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `env`, `githubToken`, `useLoggedInUser`
- `createSession()` config: `model`, `tools`, `hooks`, `systemMessage`, `mcpServers`, `availableTools`, `excludedTools`, `streaming`, `reasoningEffort`, `provider`, `infiniteSessions`, `customAgents`, `workingDirectory`
- `CopilotSession` methods: `send()`, `sendAndWait()`, `getMessages()`, `disconnect()`, `abort()`, `on()`, `once()`, `off()`
- Hook names: `onPreToolUse`, `onPostToolUse`, `onUserPromptSubmitted`, `onSessionStart`, `onSessionEnd`, `onErrorOccurred`
@@ -360,7 +359,7 @@ cat python/copilot/types.py | grep -A 15 "class SessionHooks"
```
**Must match (snake_case):**
-- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `auto_restart`, `env`, `github_token`, `use_logged_in_user`
+- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `env`, `github_token`, `use_logged_in_user`
- `create_session()` config keys: `model`, `tools`, `hooks`, `system_message`, `mcp_servers`, `available_tools`, `excluded_tools`, `streaming`, `reasoning_effort`, `provider`, `infinite_sessions`, `custom_agents`, `working_directory`
- `CopilotSession` methods: `send()`, `send_and_wait()`, `get_messages()`, `disconnect()`, `abort()`, `export_session()`
- Hook names: `on_pre_tool_use`, `on_post_tool_use`, `on_user_prompt_submitted`, `on_session_start`, `on_session_end`, `on_error_occurred`
@@ -378,7 +377,7 @@ cat go/types.go | grep -A 15 "type SessionHooks struct"
```
**Must match (PascalCase for exported):**
-- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Env`, `GithubToken`, `UseLoggedInUser`
+- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `Env`, `GithubToken`, `UseLoggedInUser`
- `SessionConfig` fields: `Model`, `Tools`, `Hooks`, `SystemMessage`, `MCPServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory`
- `Session` methods: `Send()`, `SendAndWait()`, `GetMessages()`, `Disconnect()`, `Abort()`, `ExportSession()`
- Hook fields: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred`
@@ -396,7 +395,7 @@ cat dotnet/src/Types.cs | grep -A 15 "public class SessionHooks"
```
**Must match (PascalCase):**
-- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Environment`, `GithubToken`, `UseLoggedInUser`
+- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `Environment`, `GithubToken`, `UseLoggedInUser`
- `SessionConfig` properties: `Model`, `Tools`, `Hooks`, `SystemMessage`, `McpServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory`
- `CopilotSession` methods: `SendAsync()`, `SendAndWaitAsync()`, `GetMessagesAsync()`, `DisposeAsync()`, `AbortAsync()`, `ExportSessionAsync()`
- Hook properties: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred`
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 7c362ab537..0133053997 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -25,6 +25,7 @@
- Python: `cd python && uv pip install -e ".[dev]"` → `uv run pytest` (E2E tests use the test harness)
- Go: `cd go && go test ./...`
- .NET: `cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj`
+ - **.NET testing note:** Never add `InternalsVisibleTo` to any project file when writing tests. Tests must only access public APIs.
## Testing & E2E tips ⚙️
diff --git a/.github/workflows/cross-repo-issue-analysis.lock.yml b/.github/workflows/cross-repo-issue-analysis.lock.yml
index c7cd9f4de5..05b2f23cb2 100644
--- a/.github/workflows/cross-repo-issue-analysis.lock.yml
+++ b/.github/workflows/cross-repo-issue-analysis.lock.yml
@@ -13,7 +13,7 @@
# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
#
-# This file was automatically generated by gh-aw (v0.50.5). DO NOT EDIT.
+# This file was automatically generated by gh-aw (v0.52.1). DO NOT EDIT.
#
# To update this file, edit the corresponding .md file and run:
# gh aw compile
@@ -21,9 +21,9 @@
#
# For more information: https://github.github.com/gh-aw/introduction/overview/
#
-# Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue and suggested-fix PR there
+# Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there
#
-# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"553bdce55a05e3f846f312d711680323ba79effef8a001bd23cb72c1c0459413","compiler_version":"v0.50.5"}
+# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"bbe407b2d324d84d7c6653015841817713551b010318cee1ec12dd5c1c077977","compiler_version":"v0.52.1"}
name: "SDK Runtime Triage"
"on":
@@ -40,7 +40,7 @@ name: "SDK Runtime Triage"
permissions: {}
concurrency:
- group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}"
run-name: "SDK Runtime Triage"
@@ -56,33 +56,50 @@ jobs:
body: ${{ steps.sanitized.outputs.body }}
comment_id: ""
comment_repo: ""
+ model: ${{ steps.generate_aw_info.outputs.model }}
secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
text: ${{ steps.sanitized.outputs.text }}
title: ${{ steps.sanitized.outputs.title }}
steps:
- name: Setup Scripts
- uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5
+ uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1
with:
destination: /opt/gh-aw/actions
+ - name: Generate agentic run info
+ id: generate_aw_info
+ env:
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
+ GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }}
+ GH_AW_INFO_VERSION: ""
+ GH_AW_INFO_AGENT_VERSION: "0.0.420"
+ GH_AW_INFO_CLI_VERSION: "v0.52.1"
+ GH_AW_INFO_WORKFLOW_NAME: "SDK Runtime Triage"
+ GH_AW_INFO_EXPERIMENTAL: "false"
+ GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
+ GH_AW_INFO_STAGED: "false"
+ GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]'
+ GH_AW_INFO_FIREWALL_ENABLED: "true"
+ GH_AW_INFO_AWF_VERSION: "v0.23.0"
+ GH_AW_INFO_AWMG_VERSION: ""
+ GH_AW_INFO_FIREWALL_TYPE: "squid"
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { main } = require('/opt/gh-aw/actions/generate_aw_info.cjs');
+ await main(core, context);
- name: Validate COPILOT_GITHUB_TOKEN secret
id: validate-secret
run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- - name: Validate context variables
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/validate_context_variables.cjs');
- await main();
- name: Checkout .github and .agents folders
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github
.agents
+ sparse-checkout-cone-mode: true
fetch-depth: 1
persist-credentials: false
- name: Check workflow file timestamps
@@ -130,10 +147,7 @@ jobs:
cat "/opt/gh-aw/prompts/safe_outputs_prompt.md"
cat << 'GH_AW_PROMPT_EOF'
- Tools: create_issue, create_pull_request, add_labels, missing_tool, missing_data
- GH_AW_PROMPT_EOF
- cat "/opt/gh-aw/prompts/safe_outputs_create_pull_request.md"
- cat << 'GH_AW_PROMPT_EOF'
+ Tools: create_issue, add_labels, missing_tool, missing_data, noop
The following GitHub context information is available for this workflow:
@@ -231,12 +245,14 @@ jobs:
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
run: bash /opt/gh-aw/actions/print_prompt_summary.sh
- - name: Upload prompt artifact
+ - name: Upload activation artifact
if: success()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
- name: prompt
- path: /tmp/gh-aw/aw-prompts/prompt.txt
+ name: activation
+ path: |
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/aw-prompts/prompt.txt
retention-days: 1
agent:
@@ -245,7 +261,6 @@ jobs:
permissions:
contents: read
issues: read
- pull-requests: read
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GH_AW_ASSETS_ALLOWED_EXTS: ""
@@ -261,12 +276,13 @@ jobs:
detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
detection_success: ${{ steps.detection_conclusion.outputs.success }}
has_patch: ${{ steps.collect_output.outputs.has_patch }}
- model: ${{ steps.generate_aw_info.outputs.model }}
+ inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }}
+ model: ${{ needs.activation.outputs.model }}
output: ${{ steps.collect_output.outputs.output }}
output_types: ${{ steps.collect_output.outputs.output_types }}
steps:
- name: Setup Scripts
- uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5
+ uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1
with:
destination: /opt/gh-aw/actions
- name: Checkout repository
@@ -293,7 +309,7 @@ jobs:
- name: Checkout PR branch
id: checkout-pr
if: |
- github.event.pull_request
+ (github.event.pull_request) || (github.event.issue.pull_request)
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }}
@@ -304,52 +320,8 @@ jobs:
setupGlobals(core, github, context, exec, io);
const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs');
await main();
- - name: Generate agentic run info
- id: generate_aw_info
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const fs = require('fs');
-
- const awInfo = {
- engine_id: "copilot",
- engine_name: "GitHub Copilot CLI",
- model: process.env.GH_AW_MODEL_AGENT_COPILOT || "",
- version: "",
- agent_version: "0.0.418",
- cli_version: "v0.50.5",
- workflow_name: "SDK Runtime Triage",
- experimental: false,
- supports_tools_allowlist: true,
- run_id: context.runId,
- run_number: context.runNumber,
- run_attempt: process.env.GITHUB_RUN_ATTEMPT,
- repository: context.repo.owner + '/' + context.repo.repo,
- ref: context.ref,
- sha: context.sha,
- actor: context.actor,
- event_name: context.eventName,
- staged: false,
- allowed_domains: ["defaults"],
- firewall_enabled: true,
- awf_version: "v0.23.0",
- awmg_version: "v0.1.5",
- steps: {
- firewall: "squid"
- },
- created_at: new Date().toISOString()
- };
-
- // Write to /tmp/gh-aw directory to avoid inclusion in PR
- const tmpPath = '/tmp/gh-aw/aw_info.json';
- fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
- console.log('Generated aw_info.json at:', tmpPath);
- console.log(JSON.stringify(awInfo, null, 2));
-
- // Set model as output for reuse in other steps/jobs
- core.setOutput('model', awInfo.model);
- name: Install GitHub Copilot CLI
- run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.418
+ run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.420
- name: Install awf binary
run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.23.0
- name: Determine automatic lockdown mode for GitHub MCP Server
@@ -364,14 +336,14 @@ jobs:
const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs');
await determineAutomaticLockdown(github, context, core);
- name: Download container images
- run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.5 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine
+ run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.7 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine
- name: Write Safe Outputs Config
run: |
mkdir -p /opt/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
- {"add_labels":{"allowed":["runtime","sdk-fix-only","needs-investigation"],"max":3,"target":"triggering"},"create_issue":{"max":1},"create_pull_request":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}}
+ {"add_labels":{"allowed":["runtime","sdk-fix-only","needs-investigation"],"max":3,"target":"triggering"},"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}}
GH_AW_SAFE_OUTPUTS_CONFIG_EOF
cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF'
[
@@ -416,43 +388,6 @@ jobs:
},
"name": "create_issue"
},
- {
- "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[copilot-sdk] \". Labels [upstream-from-sdk ai-suggested-fix] will be automatically added. PRs will be created as drafts.",
- "inputSchema": {
- "additionalProperties": false,
- "properties": {
- "body": {
- "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.",
- "type": "string"
- },
- "branch": {
- "description": "Source branch name containing the changes. If omitted, uses the current working branch.",
- "type": "string"
- },
- "draft": {
- "description": "Whether to create the PR as a draft. Draft PRs cannot be merged until marked as ready for review. Use mark_pull_request_as_ready_for_review to convert a draft PR. Default: true.",
- "type": "boolean"
- },
- "labels": {
- "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.",
- "items": {
- "type": "string"
- },
- "type": "array"
- },
- "title": {
- "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.",
- "type": "string"
- }
- },
- "required": [
- "title",
- "body"
- ],
- "type": "object"
- },
- "name": "create_pull_request"
- },
{
"description": "Add labels to an existing GitHub issue or pull request for categorization and filtering. Labels must already exist in the repository. For creating new issues with labels, use create_issue with the labels property instead. CONSTRAINTS: Maximum 3 label(s) can be added. Only these labels are allowed: [runtime sdk-fix-only needs-investigation]. Target: triggering.",
"inputSchema": {
@@ -599,42 +534,6 @@ jobs:
}
}
},
- "create_pull_request": {
- "defaultMax": 1,
- "fields": {
- "body": {
- "required": true,
- "type": "string",
- "sanitize": true,
- "maxLength": 65000
- },
- "branch": {
- "required": true,
- "type": "string",
- "sanitize": true,
- "maxLength": 256
- },
- "draft": {
- "type": "boolean"
- },
- "labels": {
- "type": "array",
- "itemType": "string",
- "itemSanitize": true,
- "itemMaxLength": 128
- },
- "repo": {
- "type": "string",
- "maxLength": 256
- },
- "title": {
- "required": true,
- "type": "string",
- "sanitize": true,
- "maxLength": 128
- }
- }
- },
"missing_data": {
"defaultMax": 20,
"fields": {
@@ -752,10 +651,11 @@ jobs:
export MCP_GATEWAY_API_KEY
export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288"
export DEBUG="*"
export GH_AW_ENGINE="copilot"
- export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e 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_LOCKDOWN -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.1.5'
+ 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_LOCKDOWN -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.1.7'
mkdir -p /home/runner/.copilot
cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
@@ -787,17 +687,11 @@ jobs:
}
}
GH_AW_MCP_CONFIG_EOF
- - name: Generate workflow overview
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
- await generateWorkflowOverview(core);
- - name: Download prompt artifact
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
+ - name: Download activation artifact
+ uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
- name: prompt
- path: /tmp/gh-aw/aw-prompts
+ name: activation
+ path: /tmp/gh-aw
- name: Clean git credentials
run: bash /opt/gh-aw/actions/clean_git_credentials.sh
- name: Execute GitHub Copilot CLI
@@ -810,14 +704,6 @@ jobs:
# --allow-tool shell(date)
# --allow-tool shell(echo)
# --allow-tool shell(find:*)
- # --allow-tool shell(git add:*)
- # --allow-tool shell(git branch:*)
- # --allow-tool shell(git checkout:*)
- # --allow-tool shell(git commit:*)
- # --allow-tool shell(git merge:*)
- # --allow-tool shell(git rm:*)
- # --allow-tool shell(git status)
- # --allow-tool shell(git switch:*)
# --allow-tool shell(grep)
# --allow-tool shell(grep:*)
# --allow-tool shell(head)
@@ -838,7 +724,7 @@ jobs:
set -o pipefail
# shellcheck disable=SC1003
sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "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,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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --skip-pull --enable-api-proxy \
- -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find:*)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep:*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head:*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls:*)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail:*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(wc:*)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
+ -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep:*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head:*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls:*)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail:*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(wc:*)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
env:
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
@@ -854,6 +740,11 @@ jobs:
GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
GITHUB_WORKSPACE: ${{ github.workspace }}
XDG_CONFIG_HOME: /home/runner
+ - name: Detect inference access error
+ id: detect-inference-error
+ if: always()
+ continue-on-error: true
+ run: bash /opt/gh-aw/actions/detect_inference_access_error.sh
- name: Configure Git credentials
env:
REPO_NAME: ${{ github.repository }}
@@ -909,7 +800,7 @@ jobs:
SECRET_RUNTIME_TRIAGE_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }}
- name: Upload Safe Outputs
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: safe-output
path: ${{ env.GH_AW_SAFE_OUTPUTS }}
@@ -932,13 +823,13 @@ jobs:
await main();
- name: Upload sanitized agent output
if: always() && env.GH_AW_AGENT_OUTPUT
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: agent-output
path: ${{ env.GH_AW_AGENT_OUTPUT }}
if-no-files-found: warn
- name: Upload engine output files
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: agent_outputs
path: |
@@ -983,17 +874,15 @@ jobs:
- name: Upload agent artifacts
if: always()
continue-on-error: true
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: agent-artifacts
path: |
/tmp/gh-aw/aw-prompts/prompt.txt
- /tmp/gh-aw/aw_info.json
/tmp/gh-aw/mcp-logs/
/tmp/gh-aw/sandbox/firewall/logs/
/tmp/gh-aw/agent-stdio.log
/tmp/gh-aw/agent/
- /tmp/gh-aw/aw-*.patch
if-no-files-found: ignore
# --- Threat Detection (inline) ---
- name: Check if detection needed
@@ -1032,7 +921,7 @@ jobs:
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
WORKFLOW_NAME: "SDK Runtime Triage"
- WORKFLOW_DESCRIPTION: "Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue and suggested-fix PR there"
+ WORKFLOW_DESCRIPTION: "Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there"
HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }}
with:
script: |
@@ -1086,7 +975,7 @@ jobs:
await main();
- name: Upload threat detection log
if: always() && steps.detection_guard.outputs.run_detection == 'true'
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: threat-detection.log
path: /tmp/gh-aw/threat-detection/detection.log
@@ -1120,7 +1009,7 @@ jobs:
if: (always()) && (needs.agent.result != 'skipped')
runs-on: ubuntu-slim
permissions:
- contents: write
+ contents: read
issues: write
pull-requests: write
outputs:
@@ -1129,12 +1018,12 @@ jobs:
total_count: ${{ steps.missing_tool.outputs.total_count }}
steps:
- name: Setup Scripts
- uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5
+ uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1
with:
destination: /opt/gh-aw/actions
- name: Download agent output artifact
continue-on-error: true
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
+ uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: agent-output
path: /tmp/gh-aw/safeoutputs/
@@ -1181,9 +1070,9 @@ jobs:
GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis"
GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }}
GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
- GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }}
- GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }}
+ GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
GH_AW_GROUP_REPORTS: "false"
+ GH_AW_TIMEOUT_MINUTES: "20"
with:
github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }}
script: |
@@ -1208,20 +1097,6 @@ jobs:
setupGlobals(core, github, context, exec, io);
const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs');
await main();
- - name: Handle Create Pull Request Error
- id: handle_create_pr_error
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
- GH_AW_WORKFLOW_NAME: "SDK Runtime Triage"
- GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- with:
- github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs');
- await main();
pre_activation:
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'runtime triage'
@@ -1231,7 +1106,7 @@ jobs:
matched_command: ''
steps:
- name: Setup Scripts
- uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5
+ uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1
with:
destination: /opt/gh-aw/actions
- name: Check team membership for workflow
@@ -1248,17 +1123,16 @@ jobs:
await main();
safe_outputs:
- needs:
- - activation
- - agent
+ needs: agent
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
runs-on: ubuntu-slim
permissions:
- contents: write
+ contents: read
issues: write
pull-requests: write
timeout-minutes: 15
env:
+ GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/cross-repo-issue-analysis"
GH_AW_ENGINE_ID: "copilot"
GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis"
GH_AW_WORKFLOW_NAME: "SDK Runtime Triage"
@@ -1267,16 +1141,18 @@ jobs:
code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }}
create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }}
create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }}
+ created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }}
+ created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }}
process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }}
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
steps:
- name: Setup Scripts
- uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5
+ uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1
with:
destination: /opt/gh-aw/actions
- name: Download agent output artifact
continue-on-error: true
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
+ uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: agent-output
path: /tmp/gh-aw/safeoutputs/
@@ -1285,42 +1161,15 @@ jobs:
mkdir -p /tmp/gh-aw/safeoutputs/
find "/tmp/gh-aw/safeoutputs/" -type f -print
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV"
- - name: Download patch artifact
- continue-on-error: true
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
- with:
- name: agent-artifacts
- path: /tmp/gh-aw/
- - name: Checkout repository
- if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request'))
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- repository: github/copilot-agent-runtime
- ref: ${{ github.base_ref || github.ref_name }}
- token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }}
- persist-credentials: false
- fetch-depth: 1
- - name: Configure Git credentials
- if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request'))
- env:
- REPO_NAME: "github/copilot-agent-runtime"
- SERVER_URL: ${{ github.server_url }}
- GIT_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }}
- run: |
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global user.name "github-actions[bot]"
- git config --global am.keepcr true
- # Re-authenticate git with GitHub token
- SERVER_URL_STRIPPED="${SERVER_URL#https://}"
- git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
- echo "Git configured with standard GitHub Actions identity"
- name: Process Safe Outputs
id: process_safe_outputs
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
- GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"create_pull_request\":{\"base_branch\":\"${{ github.base_ref || github.ref_name }}\",\"draft\":true,\"labels\":[\"upstream-from-sdk\",\"ai-suggested-fix\"],\"max\":1,\"max_patch_size\":1024,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"missing_data\":{},\"missing_tool\":{}}"
- GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
+ GH_AW_ALLOWED_DOMAINS: "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,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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.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"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"missing_data\":{},\"missing_tool\":{}}"
with:
github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }}
script: |
@@ -1330,7 +1179,7 @@ jobs:
await main();
- name: Upload safe output items manifest
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: safe-output-items
path: /tmp/safe-output-items.jsonl
diff --git a/.github/workflows/cross-repo-issue-analysis.md b/.github/workflows/cross-repo-issue-analysis.md
index 8a0218427a..61b19f4913 100644
--- a/.github/workflows/cross-repo-issue-analysis.md
+++ b/.github/workflows/cross-repo-issue-analysis.md
@@ -1,5 +1,5 @@
---
-description: Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue and suggested-fix PR there
+description: Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there
on:
issues:
types: [labeled]
@@ -13,7 +13,6 @@ if: "github.event_name == 'workflow_dispatch' || github.event.label.name == 'run
permissions:
contents: read
issues: read
- pull-requests: read
steps:
- name: Clone copilot-agent-runtime
run: git clone --depth 1 https://x-access-token:${{ secrets.RUNTIME_TRIAGE_TOKEN }}@github.com/github/copilot-agent-runtime.git ${{ github.workspace }}/copilot-agent-runtime
@@ -21,7 +20,6 @@ tools:
github:
toolsets: [default]
github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }}
- edit:
bash:
- "grep:*"
- "find:*"
@@ -42,12 +40,6 @@ safe-outputs:
labels: [upstream-from-sdk, ai-triaged]
target-repo: "github/copilot-agent-runtime"
max: 1
- create-pull-request:
- title-prefix: "[copilot-sdk] "
- labels: [upstream-from-sdk, ai-suggested-fix]
- draft: true
- target-repo: "github/copilot-agent-runtime"
-
timeout-minutes: 20
---
@@ -106,10 +98,6 @@ Classify the issue into one of these categories:
- References the original SDK issue (e.g., `github/copilot-sdk#123`)
- Includes the specific files and code paths involved
- Suggests a fix approach
- - Create a draft PR in `github/copilot-agent-runtime` with a suggested fix:
- - Make the minimal, targeted code changes needed
- - Include a clear PR description linking back to both issues
- - If you're uncertain about the fix, still create the PR as a starting point for discussion
3. **Needs-investigation**: You cannot confidently determine the root cause. Label the issue `needs-investigation`.
@@ -117,7 +105,6 @@ Classify the issue into one of these categories:
1. **Be thorough but focused**: Read enough code to be confident in your analysis, but don't read every file in both repos
2. **Err on the side of creating the runtime issue**: If there's a reasonable chance the fix is in the runtime, create the issue. False positives are better than missed upstream bugs.
-3. **Make actionable PRs**: Even if the fix isn't perfect, a draft PR with a concrete starting point is more useful than just an issue description
-4. **Link everything**: Always cross-reference between the SDK issue, runtime issue, and runtime PR so maintainers can follow the trail
-5. **Be specific**: When describing the root cause, point to specific files, functions, and line numbers in both repos
-6. **Don't duplicate**: Before creating a runtime issue, search existing open issues in `github/copilot-agent-runtime` to avoid duplicates. If a related issue exists, reference it instead of creating a new one.
+3. **Link everything**: Always cross-reference between the SDK issue and runtime issue so maintainers can follow the trail
+4. **Be specific**: When describing the root cause, point to specific files, functions, and line numbers in both repos
+5. **Don't duplicate**: Before creating a runtime issue, search existing open issues in `github/copilot-agent-runtime` to avoid duplicates. If a related issue exists, reference it instead of creating a new one.
diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml
index 9e978a22fa..9dec016675 100644
--- a/.github/workflows/nodejs-sdk-tests.yml
+++ b/.github/workflows/nodejs-sdk-tests.yml
@@ -62,6 +62,9 @@ jobs:
- name: Typecheck SDK
run: npm run typecheck
+ - name: Build SDK
+ run: npm run build
+
- name: Install test harness dependencies
working-directory: ./test/harness
run: npm ci --ignore-scripts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5abbfefc43..ac5712aa58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,78 @@ All notable changes to the Copilot SDK are documented in this file.
This changelog is automatically generated by an AI agent when stable releases are published.
See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the full list.
+## [v0.1.32](https://github.com/github/copilot-sdk/releases/tag/v0.1.32) (2026-03-07)
+
+### Feature: backward compatibility with v2 CLI servers
+
+SDK applications written against the v3 API now also work when connected to a v2 CLI server, with no code changes required. The SDK detects the server's protocol version and automatically adapts v2 `tool.call` and `permission.request` messages into the same user-facing handlers used by v3. ([#706](https://github.com/github/copilot-sdk/pull/706))
+
+```ts
+const session = await client.createSession({
+ tools: [myTool], // unchanged — works with v2 and v3 servers
+ onPermissionRequest: approveAll,
+});
+```
+
+```cs
+var session = await client.CreateSessionAsync(new SessionConfig {
+ Tools = [myTool], // unchanged — works with v2 and v3 servers
+ OnPermissionRequest = approveAll,
+});
+```
+
+## [v0.1.31](https://github.com/github/copilot-sdk/releases/tag/v0.1.31) (2026-03-07)
+
+### Feature: multi-client tool and permission broadcasts (protocol v3)
+
+The SDK now uses protocol version 3, where the runtime broadcasts `external_tool.requested` and `permission.requested` as session events to all connected clients. This enables multi-client architectures where different clients contribute different tools, or where multiple clients observe the same permission prompts — if one client approves, all clients see the result. Your existing tool and permission handler code is unchanged. ([#686](https://github.com/github/copilot-sdk/pull/686))
+
+```ts
+// Two clients each register different tools; the agent can use both
+const session1 = await client1.createSession({
+ tools: [defineTool("search", { handler: doSearch })],
+ onPermissionRequest: approveAll,
+});
+const session2 = await client2.resumeSession(session1.id, {
+ tools: [defineTool("analyze", { handler: doAnalyze })],
+ onPermissionRequest: approveAll,
+});
+```
+
+```cs
+var session1 = await client1.CreateSessionAsync(new SessionConfig {
+ Tools = [AIFunctionFactory.Create(DoSearch, "search")],
+ OnPermissionRequest = PermissionHandlers.ApproveAll,
+});
+var session2 = await client2.ResumeSessionAsync(session1.Id, new ResumeSessionConfig {
+ Tools = [AIFunctionFactory.Create(DoAnalyze, "analyze")],
+ OnPermissionRequest = PermissionHandlers.ApproveAll,
+});
+```
+
+### Feature: strongly-typed `PermissionRequestResultKind` for .NET and Go
+
+Rather than comparing `result.Kind` against undiscoverable magic strings like `"approved"` or `"denied-interactively-by-user"`, .NET and Go now provide typed constants. Node and Python already had typed unions for this; this brings full parity. ([#631](https://github.com/github/copilot-sdk/pull/631))
+
+```cs
+session.OnPermissionCompleted += (e) => {
+ if (e.Result.Kind == PermissionRequestResultKind.Approved) { /* ... */ }
+ if (e.Result.Kind == PermissionRequestResultKind.DeniedInteractivelyByUser) { /* ... */ }
+};
+```
+
+```go
+// Go: PermissionKindApproved, PermissionKindDeniedByRules,
+// PermissionKindDeniedCouldNotRequestFromUser, PermissionKindDeniedInteractivelyByUser
+if result.Kind == copilot.PermissionKindApproved { /* ... */ }
+```
+
+### Other changes
+
+- feature: **[Python]** **[Go]** add `get_last_session_id()` / `GetLastSessionID()` for SDK-wide parity (was already available in Node and .NET) ([#671](https://github.com/github/copilot-sdk/pull/671))
+- improvement: **[Python]** add `timeout` parameter to generated RPC methods, allowing callers to override the default 30s timeout for long-running operations ([#681](https://github.com/github/copilot-sdk/pull/681))
+- bugfix: **[Go]** `PermissionRequest` fields are now properly typed (`ToolName`, `Diff`, `Path`, etc.) instead of a generic `Extra map[string]any` catch-all ([#685](https://github.com/github/copilot-sdk/pull/685))
+
## [v0.1.30](https://github.com/github/copilot-sdk/releases/tag/v0.1.30) (2026-03-03)
### Feature: support overriding built-in tools
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4650ee04e3..7dbe1b4923 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,60 +1,71 @@
-## Contributing
+# Contributing
-[fork]: https://github.com/github/copilot-sdk/fork
-[pr]: https://github.com/github/copilot-sdk/compare
+Thanks for your interest in contributing!
-Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
+This repository contains the Copilot SDK, a set of multi-language SDKs (Node/TypeScript, Python, Go, .NET) for building applications with the GitHub Copilot agent, maintained by the GitHub Copilot team.
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
-## What kinds of contributions we're looking for
+## Before You Submit a PR
-We'd love your help with:
+**Please discuss any feature work with us before writing code.**
- * Fixing any bugs in the existing feature set
- * Making the SDKs more idiomatic and nice to use for each supported language
- * Improving documentation
+The team already has a committed product roadmap, and features must be maintained in sync across all supported languages. Pull requests that introduce features not previously aligned with the team are unlikely to be accepted, regardless of their quality or scope.
-If you have ideas for entirely new features, please post an issue or start a discussion. We're very open to new features but need to make sure they align with the direction of the underlying Copilot CLI and can be maintained in sync across all our supported languages.
+If you submit a PR, **be sure to link to an associated issue describing the bug or agreed feature**. No PRs without context :)
-Currently **we are not looking to add SDKs for other languages**. If you want to create a Copilot SDK for another language, we'd love to hear from you, and we may offer to link to your SDK from our repo. However we do not plan to add further language-specific SDKs to this repo in the short term, since we need to retain our maintenance capacity for moving forwards quickly with the existing language set. So, for any other languages, please consider running your own external project.
+## What We're Looking For
-## Prerequisites for running and testing code
+We welcome:
+
+- Bug fixes with clear reproduction steps
+- Improvements to documentation
+- Making the SDKs more idiomatic and nice to use for each supported language
+- Bug reports and feature suggestions on [our issue tracker](https://github.com/github/copilot-sdk/issues) — especially for bugs with repro steps
+
+We are generally **not** looking for:
+
+- New features, capabilities, or UX changes that haven't been discussed and agreed with the team
+- Refactors or architectural changes
+- Integrations with external tools or services
+- Additional documentation
+- **SDKs for other languages** — if you want to create a Copilot SDK for another language, we'd love to hear from you and may offer to link to your SDK from our repo. However we do not plan to add further language-specific SDKs to this repo in the short term, since we need to retain our maintenance capacity for moving forwards quickly with the existing language set. For other languages, please consider running your own external project.
+
+## Prerequisites for Running and Testing Code
This is a multi-language SDK repository. Install the tools for the SDK(s) you plan to work on:
### All SDKs
-1. (Optional) Install [just](https://github.com/casey/just) command runner for convenience
+
+1. The end-to-end tests across all languages use a shared test harness written in Node.js. Before running tests in any language, `cd test/harness && npm ci`.
### Node.js/TypeScript SDK
+
1. Install [Node.js](https://nodejs.org/) (v18+)
1. Install dependencies: `cd nodejs && npm ci`
### Python SDK
+
1. Install [Python 3.8+](https://www.python.org/downloads/)
1. Install [uv](https://github.com/astral-sh/uv)
1. Install dependencies: `cd python && uv pip install -e ".[dev]"`
### Go SDK
+
1. Install [Go 1.24+](https://go.dev/doc/install)
1. Install [golangci-lint](https://golangci-lint.run/welcome/install/#local-installation)
1. Install dependencies: `cd go && go mod download`
### .NET SDK
+
1. Install [.NET 8.0+](https://dotnet.microsoft.com/download)
-1. Install [Node.js](https://nodejs.org/) (v18+) (the .NET tests depend on a TypeScript-based test harness)
-1. Install npm dependencies (from the repository root):
- ```bash
- cd nodejs && npm ci
- cd test/harness && npm ci
- ```
1. Install .NET dependencies: `cd dotnet && dotnet restore`
-## Submitting a pull request
+## Submitting a Pull Request
-1. [Fork][fork] and clone the repository
+1. Fork and clone the repository
1. Install dependencies for the SDK(s) you're modifying (see above)
1. Make sure the tests pass on your machine (see commands below)
1. Make sure linter passes on your machine (see commands below)
@@ -63,29 +74,7 @@ This is a multi-language SDK repository. Install the tools for the SDK(s) you pl
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
-
-If you installed `just`, you can use it to run tests and linters across all SDKs or for specific languages:
-
-```bash
-# All SDKs
-just test # Run all tests
-just lint # Run all linters
-just format # Format all code
-
-# Individual SDKs
-just test-nodejs # Node.js tests
-just test-python # Python tests
-just test-go # Go tests
-just test-dotnet # .NET tests
-
-just lint-nodejs # Node.js linting
-just lint-python # Python linting
-just lint-go # Go linting
-just lint-dotnet # .NET linting
-```
-
-Or run commands directly in each SDK directory:
+### Running Tests and Linters
```bash
# Node.js
diff --git a/README.md b/README.md
index be9b4694b6..65a2339c85 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
Agents for every app.
-Embed Copilot's agentic workflows in your application—now available in Technical preview as a programmable SDK for Python, TypeScript, Go, and .NET.
+Embed Copilot's agentic workflows in your application—now available in Technical preview as a programmable SDK for Python, TypeScript, Go, .NET, and Java.
The GitHub Copilot SDK exposes the same engine behind Copilot CLI: a production-tested agent runtime you can invoke programmatically. No need to build your own orchestration—you define agent behavior, Copilot handles planning, tool invocation, file edits, and more.
@@ -20,6 +20,7 @@ The GitHub Copilot SDK exposes the same engine behind Copilot CLI: a production-
| **Python** | [`python/`](./python/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/python/README.md) | `pip install github-copilot-sdk` |
| **Go** | [`go/`](./go/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/go/README.md) | `go get github.com/github/copilot-sdk/go` |
| **.NET** | [`dotnet/`](./dotnet/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/dotnet/README.md) | `dotnet add package GitHub.Copilot.SDK` |
+| **Java** | [`github/copilot-sdk-java`](https://github.com/github/copilot-sdk-java) | WIP | Maven coordinates
`com.github:copilot-sdk-java`
See instructions for [Maven](https://github.com/github/copilot-sdk-java?tab=readme-ov-file#maven) and [Gradle](https://github.com/github/copilot-sdk-java?tab=readme-ov-file#gradle) |
See the individual SDK READMEs for installation, usage examples, and API reference.
@@ -91,7 +92,14 @@ Yes, the GitHub Copilot SDK allows you to define custom agents, skills, and tool
### Are there instructions for Copilot to speed up development with the SDK?
-Yes, check out the custom instructions at [`github/awesome-copilot`](https://github.com/github/awesome-copilot/blob/main/collections/copilot-sdk.md).
+Yes, check out the custom instructions for each SDK:
+
+- **[Node.js / TypeScript](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-nodejs.instructions.md)**
+- **[Python](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-python.instructions.md)**
+- **[.NET](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-csharp.instructions.md)**
+- **[Go](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-go.instructions.md)**
+- **[Java](https://github.com/github/copilot-sdk-java/blob/main/instructions/copilot-sdk-java.instructions.md)**
+
### What models are supported?
@@ -107,8 +115,12 @@ Please use the [GitHub Issues](https://github.com/github/copilot-sdk/issues) pag
## Quick Links
+- **[Documentation](./docs/index.md)** – Full documentation index
- **[Getting Started](./docs/getting-started.md)** – Tutorial to get up and running
+- **[Setup Guides](./docs/setup/index.md)** – Architecture, deployment, and scaling
- **[Authentication](./docs/auth/index.md)** – GitHub OAuth, BYOK, and more
+- **[Features](./docs/features/index.md)** – Hooks, custom agents, MCP, skills, and more
+- **[Troubleshooting](./docs/troubleshooting/debugging.md)** – Common issues and solutions
- **[Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk)** – Practical recipes for common tasks across all languages
- **[More Resources](https://github.com/github/awesome-copilot/blob/main/collections/copilot-sdk.md)** – Additional examples, tutorials, and community resources
@@ -118,12 +130,10 @@ Please use the [GitHub Issues](https://github.com/github/copilot-sdk/issues) pag
| SDK | Location |
| --------------| ----------------------------------------------------------------- |
-| **Java** | [copilot-community-sdk/copilot-sdk-java][sdk-java] |
| **Rust** | [copilot-community-sdk/copilot-sdk-rust][sdk-rust] |
| **Clojure** | [copilot-community-sdk/copilot-sdk-clojure][sdk-clojure] |
| **C++** | [0xeb/copilot-sdk-cpp][sdk-cpp] |
-[sdk-java]: https://github.com/copilot-community-sdk/copilot-sdk-java
[sdk-rust]: https://github.com/copilot-community-sdk/copilot-sdk-rust
[sdk-cpp]: https://github.com/0xeb/copilot-sdk-cpp
[sdk-clojure]: https://github.com/copilot-community-sdk/copilot-sdk-clojure
diff --git a/assets/copilot.png b/assets/copilot.png
new file mode 100644
index 0000000000..e71958c947
Binary files /dev/null and b/assets/copilot.png differ
diff --git a/docs/auth/byok.md b/docs/auth/byok.md
index ca7861c160..8d96502802 100644
--- a/docs/auth/byok.md
+++ b/docs/auth/byok.md
@@ -23,7 +23,7 @@ Azure AI Foundry (formerly Azure OpenAI) is a common BYOK deployment target for
```python
import asyncio
import os
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
FOUNDRY_MODEL_URL = "https://your-resource.openai.azure.com/openai/v1/"
# Set FOUNDRY_API_KEY environment variable
@@ -32,14 +32,11 @@ async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session({
- "model": "gpt-5.2-codex", # Your deployment name
- "provider": {
- "type": "openai",
- "base_url": FOUNDRY_MODEL_URL,
- "wire_api": "responses", # Use "completions" for older models
- "api_key": os.environ["FOUNDRY_API_KEY"],
- },
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.2-codex", provider={
+ "type": "openai",
+ "base_url": FOUNDRY_MODEL_URL,
+ "wire_api": "responses", # Use "completions" for older models
+ "api_key": os.environ["FOUNDRY_API_KEY"],
})
done = asyncio.Event()
@@ -306,6 +303,116 @@ provider: {
> **Note:** The `bearerToken` option accepts a **static token string** only. The SDK does not refresh this token automatically. If your token expires, requests will fail and you'll need to create a new session with a fresh token.
+## Custom Model Listing
+
+When using BYOK, the CLI server may not know which models your provider supports. You can supply a custom `onListModels` handler at the client level so that `client.listModels()` returns your provider's models in the standard `ModelInfo` format. This lets downstream consumers discover available models without querying the CLI.
+
+
+Node.js / TypeScript
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+import type { ModelInfo } from "@github/copilot-sdk";
+
+const client = new CopilotClient({
+ onListModels: () => [
+ {
+ id: "my-custom-model",
+ name: "My Custom Model",
+ capabilities: {
+ supports: { vision: false, reasoningEffort: false },
+ limits: { max_context_window_tokens: 128000 },
+ },
+ },
+ ],
+});
+```
+
+
+
+
+Python
+
+```python
+from copilot import CopilotClient
+from copilot.types import ModelInfo, ModelCapabilities, ModelSupports, ModelLimits
+
+client = CopilotClient({
+ "on_list_models": lambda: [
+ ModelInfo(
+ id="my-custom-model",
+ name="My Custom Model",
+ capabilities=ModelCapabilities(
+ supports=ModelSupports(vision=False, reasoning_effort=False),
+ limits=ModelLimits(max_context_window_tokens=128000),
+ ),
+ )
+ ],
+})
+```
+
+
+
+
+Go
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ client := copilot.NewClient(&copilot.ClientOptions{
+ OnListModels: func(ctx context.Context) ([]copilot.ModelInfo, error) {
+ return []copilot.ModelInfo{
+ {
+ ID: "my-custom-model",
+ Name: "My Custom Model",
+ Capabilities: copilot.ModelCapabilities{
+ Supports: copilot.ModelSupports{Vision: false, ReasoningEffort: false},
+ Limits: copilot.ModelLimits{MaxContextWindowTokens: 128000},
+ },
+ },
+ }, nil
+ },
+ })
+ _ = client
+}
+```
+
+
+
+
+.NET
+
+```csharp
+using GitHub.Copilot.SDK;
+
+var client = new CopilotClient(new CopilotClientOptions
+{
+ OnListModels = (ct) => Task.FromResult(new List
+ {
+ new()
+ {
+ Id = "my-custom-model",
+ Name = "My Custom Model",
+ Capabilities = new ModelCapabilities
+ {
+ Supports = new ModelSupports { Vision = false, ReasoningEffort = false },
+ Limits = new ModelLimits { MaxContextWindowTokens = 128000 }
+ }
+ }
+ })
+});
+```
+
+
+
+Results are cached after the first call, just like the default behavior. The handler completely replaces the CLI's `models.list` RPC — no fallback to the server occurs.
+
## Limitations
When using BYOK, be aware of these limitations:
@@ -363,7 +470,21 @@ const session = await client.createSession({
For Azure OpenAI endpoints (`*.openai.azure.com`), use the correct type:
-
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+const session = await client.createSession({
+ model: "gpt-4.1",
+ provider: {
+ type: "azure",
+ baseUrl: "https://my-resource.openai.azure.com",
+ },
+});
+```
+
+
```typescript
// ❌ Wrong: Using "openai" type with native Azure endpoint
provider: {
@@ -380,7 +501,21 @@ provider: {
However, if your Azure AI Foundry deployment provides an OpenAI-compatible endpoint path (e.g., `/openai/v1/`), use `type: "openai"`:
-
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+const session = await client.createSession({
+ model: "gpt-4.1",
+ provider: {
+ type: "openai",
+ baseUrl: "https://your-resource.openai.azure.com/openai/v1/",
+ },
+});
+```
+
+
```typescript
// ✅ Correct: OpenAI-compatible Azure AI Foundry endpoint
provider: {
diff --git a/docs/auth/index.md b/docs/auth/index.md
index 9fc65fe28b..2f36d8b21d 100644
--- a/docs/auth/index.md
+++ b/docs/auth/index.md
@@ -50,7 +50,20 @@ await client.start()
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+func main() {
+ // Default: uses logged-in user credentials
+ client := copilot.NewClient(nil)
+ _ = client
+}
+```
+
+
```go
import copilot "github.com/github/copilot-sdk/go"
@@ -120,7 +133,23 @@ await client.start()
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+func main() {
+ userAccessToken := "token"
+ client := copilot.NewClient(&copilot.ClientOptions{
+ GitHubToken: userAccessToken,
+ UseLoggedInUser: copilot.Bool(false),
+ })
+ _ = client
+}
+```
+
+
```go
import copilot "github.com/github/copilot-sdk/go"
@@ -135,7 +164,19 @@ client := copilot.NewClient(&copilot.ClientOptions{
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+var userAccessToken = "token";
+await using var client = new CopilotClient(new CopilotClientOptions
+{
+ GithubToken = userAccessToken,
+ UseLoggedInUser = false,
+});
+```
+
+
```csharp
using GitHub.Copilot.SDK;
@@ -254,7 +295,16 @@ const client = new CopilotClient({
Python
-
+
+```python
+from copilot import CopilotClient
+
+client = CopilotClient({
+ "use_logged_in_user": False,
+})
+```
+
+
```python
client = CopilotClient({
"use_logged_in_user": False, # Only use explicit tokens
@@ -266,7 +316,21 @@ client = CopilotClient({
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+func main() {
+ client := copilot.NewClient(&copilot.ClientOptions{
+ UseLoggedInUser: copilot.Bool(false),
+ })
+ _ = client
+}
+```
+
+
```go
client := copilot.NewClient(&copilot.ClientOptions{
UseLoggedInUser: copilot.Bool(false), // Only use explicit tokens
@@ -291,4 +355,4 @@ await using var client = new CopilotClient(new CopilotClientOptions
- [BYOK Documentation](./byok.md) - Learn how to use your own API keys
- [Getting Started Guide](../getting-started.md) - Build your first Copilot-powered app
-- [MCP Servers](../mcp) - Connect to external tools
+- [MCP Servers](../features/mcp.md) - Connect to external tools
diff --git a/docs/guides/custom-agents.md b/docs/features/custom-agents.md
similarity index 76%
rename from docs/guides/custom-agents.md
rename to docs/features/custom-agents.md
index 16f7a37a0c..47712d9cf0 100644
--- a/docs/guides/custom-agents.md
+++ b/docs/features/custom-agents.md
@@ -65,13 +65,15 @@ const session = await client.createSession({
```python
from copilot import CopilotClient
+from copilot.types import PermissionRequestResult
client = CopilotClient()
await client.start()
-session = await client.create_session({
- "model": "gpt-4.1",
- "custom_agents": [
+session = await client.create_session(
+ on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"),
+ model="gpt-4.1",
+ custom_agents=[
{
"name": "researcher",
"display_name": "Research Agent",
@@ -87,8 +89,7 @@ session = await client.create_session({
"prompt": "You are a code editor. Make minimal, surgical changes to files as requested.",
},
],
- "on_permission_request": lambda req: {"kind": "approved"},
-})
+)
```
@@ -96,7 +97,47 @@ session = await client.create_session({
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+ client.Start(ctx)
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Model: "gpt-4.1",
+ CustomAgents: []copilot.CustomAgentConfig{
+ {
+ Name: "researcher",
+ DisplayName: "Research Agent",
+ Description: "Explores codebases and answers questions using read-only tools",
+ Tools: []string{"grep", "glob", "view"},
+ Prompt: "You are a research assistant. Analyze code and answer questions. Do not modify any files.",
+ },
+ {
+ Name: "editor",
+ DisplayName: "Editor Agent",
+ Description: "Makes targeted code changes",
+ Tools: []string{"view", "edit", "bash"},
+ Prompt: "You are a code editor. Make minimal, surgical changes to files as requested.",
+ },
+ },
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+ _ = session
+}
+```
+
+
```go
ctx := context.Background()
client := copilot.NewClient(nil)
@@ -178,6 +219,103 @@ await using var session = await client.CreateSessionAsync(new SessionConfig
> **Tip:** A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities.
+In addition to per-agent configuration above, you can set `agent` on the **session config** itself to pre-select which custom agent is active when the session starts. See [Selecting an Agent at Session Creation](#selecting-an-agent-at-session-creation) below.
+
+| Session Config Property | Type | Description |
+|-------------------------|------|-------------|
+| `agent` | `string` | Name of the custom agent to pre-select at session creation. Must match a `name` in `customAgents`. |
+
+## Selecting an Agent at Session Creation
+
+You can pass `agent` in the session config to pre-select which custom agent should be active when the session starts. The value must match the `name` of one of the agents defined in `customAgents`.
+
+This is equivalent to calling `session.rpc.agent.select()` after creation, but avoids the extra API call and ensures the agent is active from the very first prompt.
+
+
+Node.js / TypeScript
+
+
+```typescript
+const session = await client.createSession({
+ customAgents: [
+ {
+ name: "researcher",
+ prompt: "You are a research assistant. Analyze code and answer questions.",
+ },
+ {
+ name: "editor",
+ prompt: "You are a code editor. Make minimal, surgical changes.",
+ },
+ ],
+ agent: "researcher", // Pre-select the researcher agent
+});
+```
+
+
+
+
+Python
+
+
+```python
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ custom_agents=[
+ {
+ "name": "researcher",
+ "prompt": "You are a research assistant. Analyze code and answer questions.",
+ },
+ {
+ "name": "editor",
+ "prompt": "You are a code editor. Make minimal, surgical changes.",
+ },
+ ],
+ agent="researcher", # Pre-select the researcher agent
+)
+```
+
+
+
+
+Go
+
+
+```go
+session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ CustomAgents: []copilot.CustomAgentConfig{
+ {
+ Name: "researcher",
+ Prompt: "You are a research assistant. Analyze code and answer questions.",
+ },
+ {
+ Name: "editor",
+ Prompt: "You are a code editor. Make minimal, surgical changes.",
+ },
+ },
+ Agent: "researcher", // Pre-select the researcher agent
+})
+```
+
+
+
+
+.NET
+
+
+```csharp
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ CustomAgents = new List
+ {
+ new() { Name = "researcher", Prompt = "You are a research assistant. Analyze code and answer questions." },
+ new() { Name = "editor", Prompt = "You are a code editor. Make minimal, surgical changes." },
+ },
+ Agent = "researcher", // Pre-select the researcher agent
+});
+```
+
+
+
## How Sub-Agent Delegation Works
When you send a prompt to a session with custom agents, the runtime evaluates whether to delegate to a sub-agent:
@@ -286,7 +424,51 @@ response = await session.send_and_wait({
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+ client.Start(ctx)
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Model: "gpt-4.1",
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+
+ session.On(func(event copilot.SessionEvent) {
+ switch event.Type {
+ case "subagent.started":
+ fmt.Printf("▶ Sub-agent started: %s\n", *event.Data.AgentDisplayName)
+ fmt.Printf(" Description: %s\n", *event.Data.AgentDescription)
+ fmt.Printf(" Tool call ID: %s\n", *event.Data.ToolCallID)
+ case "subagent.completed":
+ fmt.Printf("✅ Sub-agent completed: %s\n", *event.Data.AgentDisplayName)
+ case "subagent.failed":
+ fmt.Printf("❌ Sub-agent failed: %s — %v\n", *event.Data.AgentDisplayName, event.Data.Error)
+ case "subagent.selected":
+ fmt.Printf("🎯 Agent selected: %s\n", *event.Data.AgentDisplayName)
+ }
+ })
+
+ _, err := session.SendAndWait(ctx, copilot.MessageOptions{
+ Prompt: "Research how authentication works in this codebase",
+ })
+ _ = err
+}
+```
+
+
```go
session.On(func(event copilot.SessionEvent) {
switch event.Type {
diff --git a/docs/features/hooks.md b/docs/features/hooks.md
new file mode 100644
index 0000000000..1a01c5f1a4
--- /dev/null
+++ b/docs/features/hooks.md
@@ -0,0 +1,988 @@
+# Working with Hooks
+
+Hooks let you plug custom logic into every stage of a Copilot session — from the moment it starts, through each user prompt and tool call, to the moment it ends. This guide walks through practical use cases so you can ship permissions, auditing, notifications, and more without modifying the core agent behavior.
+
+## Overview
+
+A hook is a callback you register once when creating a session. The SDK invokes it at a well-defined point in the conversation lifecycle, passes contextual input, and optionally accepts output that modifies the session's behavior.
+
+```mermaid
+flowchart LR
+ A[Session starts] -->|onSessionStart| B[User sends prompt]
+ B -->|onUserPromptSubmitted| C[Agent picks a tool]
+ C -->|onPreToolUse| D[Tool executes]
+ D -->|onPostToolUse| E{More work?}
+ E -->|yes| C
+ E -->|no| F[Session ends]
+ F -->|onSessionEnd| G((Done))
+ C -.->|error| H[onErrorOccurred]
+ D -.->|error| H
+```
+
+| Hook | When it fires | What you can do |
+|------|---------------|-----------------|
+| [`onSessionStart`](../hooks/session-lifecycle.md#session-start) | Session begins (new or resumed) | Inject context, load preferences |
+| [`onUserPromptSubmitted`](../hooks/user-prompt-submitted.md) | User sends a message | Rewrite prompts, add context, filter input |
+| [`onPreToolUse`](../hooks/pre-tool-use.md) | Before a tool executes | Allow / deny / modify the call |
+| [`onPostToolUse`](../hooks/post-tool-use.md) | After a tool returns | Transform results, redact secrets, audit |
+| [`onSessionEnd`](../hooks/session-lifecycle.md#session-end) | Session ends | Clean up, record metrics |
+| [`onErrorOccurred`](../hooks/error-handling.md) | An error is raised | Custom logging, retry logic, alerts |
+
+All hooks are **optional** — register only the ones you need. Returning `null` (or the language equivalent) from any hook tells the SDK to continue with default behavior.
+
+## Registering Hooks
+
+Pass a `hooks` object when you create (or resume) a session. Every example below follows this pattern.
+
+
+Node.js / TypeScript
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+await client.start();
+
+const session = await client.createSession({
+ hooks: {
+ onSessionStart: async (input, invocation) => { /* ... */ },
+ onPreToolUse: async (input, invocation) => { /* ... */ },
+ onPostToolUse: async (input, invocation) => { /* ... */ },
+ // ... add only the hooks you need
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+
+
+
+Python
+
+```python
+from copilot import CopilotClient
+
+client = CopilotClient()
+await client.start()
+
+session = await client.create_session(
+ on_permission_request=lambda req, inv: {"kind": "approved"},
+ hooks={
+ "on_session_start": on_session_start,
+ "on_pre_tool_use": on_pre_tool_use,
+ "on_post_tool_use": on_post_tool_use,
+ # ... add only the hooks you need
+ },
+)
+```
+
+
+
+
+Go
+
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func onSessionStart(input copilot.SessionStartHookInput, inv copilot.HookInvocation) (*copilot.SessionStartHookOutput, error) {
+ return nil, nil
+}
+
+func onPreToolUse(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
+ return nil, nil
+}
+
+func onPostToolUse(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) {
+ return nil, nil
+}
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+
+ session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ Hooks: &copilot.SessionHooks{
+ OnSessionStart: onSessionStart,
+ OnPreToolUse: onPreToolUse,
+ OnPostToolUse: onPostToolUse,
+ },
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: "approved"}, nil
+ },
+ })
+ _ = session
+ _ = err
+}
+```
+
+
+```go
+client := copilot.NewClient(nil)
+
+session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ Hooks: &copilot.SessionHooks{
+ OnSessionStart: onSessionStart,
+ OnPreToolUse: onPreToolUse,
+ OnPostToolUse: onPostToolUse,
+ // ... add only the hooks you need
+ },
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: "approved"}, nil
+ },
+})
+```
+
+
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class HooksExample
+{
+ static Task onSessionStart(SessionStartHookInput input, HookInvocation invocation) =>
+ Task.FromResult(null);
+ static Task onPreToolUse(PreToolUseHookInput input, HookInvocation invocation) =>
+ Task.FromResult(null);
+ static Task onPostToolUse(PostToolUseHookInput input, HookInvocation invocation) =>
+ Task.FromResult(null);
+
+ public static async Task Main()
+ {
+ var client = new CopilotClient();
+
+ var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Hooks = new SessionHooks
+ {
+ OnSessionStart = onSessionStart,
+ OnPreToolUse = onPreToolUse,
+ OnPostToolUse = onPostToolUse,
+ },
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+ });
+ }
+}
+```
+
+
+```csharp
+var client = new CopilotClient();
+
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Hooks = new SessionHooks
+ {
+ OnSessionStart = onSessionStart,
+ OnPreToolUse = onPreToolUse,
+ OnPostToolUse = onPostToolUse,
+ // ... add only the hooks you need
+ },
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+});
+```
+
+
+
+> **Tip:** Every hook handler receives an `invocation` parameter containing the `sessionId`, which is useful for correlating logs and maintaining per-session state.
+
+---
+
+## Use Case: Permission Control
+
+Use `onPreToolUse` to build a permission layer that decides which tools the agent may run, what arguments are allowed, and whether the user should be prompted before execution.
+
+### Allow-list a safe set of tools
+
+
+Node.js / TypeScript
+
+```typescript
+const READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"];
+
+const session = await client.createSession({
+ hooks: {
+ onPreToolUse: async (input) => {
+ if (!READ_ONLY_TOOLS.includes(input.toolName)) {
+ return {
+ permissionDecision: "deny",
+ permissionDecisionReason:
+ `Only read-only tools are allowed. "${input.toolName}" was blocked.`,
+ };
+ }
+ return { permissionDecision: "allow" };
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+
+
+
+Python
+
+```python
+READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"]
+
+async def on_pre_tool_use(input_data, invocation):
+ if input_data["toolName"] not in READ_ONLY_TOOLS:
+ return {
+ "permissionDecision": "deny",
+ "permissionDecisionReason":
+ f'Only read-only tools are allowed. "{input_data["toolName"]}" was blocked.',
+ }
+ return {"permissionDecision": "allow"}
+
+session = await client.create_session(
+ on_permission_request=lambda req, inv: {"kind": "approved"},
+ hooks={"on_pre_tool_use": on_pre_tool_use},
+)
+```
+
+
+
+
+Go
+
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+
+ readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true}
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Hooks: &copilot.SessionHooks{
+ OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
+ if !readOnlyTools[input.ToolName] {
+ return &copilot.PreToolUseHookOutput{
+ PermissionDecision: "deny",
+ PermissionDecisionReason: fmt.Sprintf("Only read-only tools are allowed. %q was blocked.", input.ToolName),
+ }, nil
+ }
+ return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil
+ },
+ },
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+ _ = session
+}
+```
+
+
+```go
+readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true}
+
+session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Hooks: &copilot.SessionHooks{
+ OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
+ if !readOnlyTools[input.ToolName] {
+ return &copilot.PreToolUseHookOutput{
+ PermissionDecision: "deny",
+ PermissionDecisionReason: fmt.Sprintf("Only read-only tools are allowed. %q was blocked.", input.ToolName),
+ }, nil
+ }
+ return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil
+ },
+ },
+})
+```
+
+
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class PermissionControlExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+
+ var readOnlyTools = new HashSet { "read_file", "glob", "grep", "view" };
+
+ var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Hooks = new SessionHooks
+ {
+ OnPreToolUse = (input, invocation) =>
+ {
+ if (!readOnlyTools.Contains(input.ToolName))
+ {
+ return Task.FromResult(new PreToolUseHookOutput
+ {
+ PermissionDecision = "deny",
+ PermissionDecisionReason = $"Only read-only tools are allowed. \"{input.ToolName}\" was blocked.",
+ });
+ }
+ return Task.FromResult(
+ new PreToolUseHookOutput { PermissionDecision = "allow" });
+ },
+ },
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+ });
+ }
+}
+```
+
+
+```csharp
+var readOnlyTools = new HashSet { "read_file", "glob", "grep", "view" };
+
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Hooks = new SessionHooks
+ {
+ OnPreToolUse = (input, invocation) =>
+ {
+ if (!readOnlyTools.Contains(input.ToolName))
+ {
+ return Task.FromResult(new PreToolUseHookOutput
+ {
+ PermissionDecision = "deny",
+ PermissionDecisionReason = $"Only read-only tools are allowed. \"{input.ToolName}\" was blocked.",
+ });
+ }
+ return Task.FromResult(
+ new PreToolUseHookOutput { PermissionDecision = "allow" });
+ },
+ },
+});
+```
+
+
+
+### Restrict file access to specific directories
+
+```typescript
+const ALLOWED_DIRS = ["/home/user/projects", "/tmp"];
+
+const session = await client.createSession({
+ hooks: {
+ onPreToolUse: async (input) => {
+ if (["read_file", "write_file", "edit"].includes(input.toolName)) {
+ const filePath = (input.toolArgs as { path: string }).path;
+ const allowed = ALLOWED_DIRS.some((dir) => filePath.startsWith(dir));
+
+ if (!allowed) {
+ return {
+ permissionDecision: "deny",
+ permissionDecisionReason:
+ `Access to "${filePath}" is outside the allowed directories.`,
+ };
+ }
+ }
+ return { permissionDecision: "allow" };
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+### Ask the user before destructive operations
+
+```typescript
+const DESTRUCTIVE_TOOLS = ["delete_file", "shell", "bash"];
+
+const session = await client.createSession({
+ hooks: {
+ onPreToolUse: async (input) => {
+ if (DESTRUCTIVE_TOOLS.includes(input.toolName)) {
+ return { permissionDecision: "ask" };
+ }
+ return { permissionDecision: "allow" };
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+Returning `"ask"` delegates the decision to the user at runtime — useful for destructive actions where you want a human in the loop.
+
+---
+
+## Use Case: Auditing & Compliance
+
+Combine `onPreToolUse`, `onPostToolUse`, and the session lifecycle hooks to build a complete audit trail that records every action the agent takes.
+
+### Structured audit log
+
+
+Node.js / TypeScript
+
+```typescript
+interface AuditEntry {
+ timestamp: number;
+ sessionId: string;
+ event: string;
+ toolName?: string;
+ toolArgs?: unknown;
+ toolResult?: unknown;
+ prompt?: string;
+}
+
+const auditLog: AuditEntry[] = [];
+
+const session = await client.createSession({
+ hooks: {
+ onSessionStart: async (input, invocation) => {
+ auditLog.push({
+ timestamp: input.timestamp,
+ sessionId: invocation.sessionId,
+ event: "session_start",
+ });
+ return null;
+ },
+ onUserPromptSubmitted: async (input, invocation) => {
+ auditLog.push({
+ timestamp: input.timestamp,
+ sessionId: invocation.sessionId,
+ event: "user_prompt",
+ prompt: input.prompt,
+ });
+ return null;
+ },
+ onPreToolUse: async (input, invocation) => {
+ auditLog.push({
+ timestamp: input.timestamp,
+ sessionId: invocation.sessionId,
+ event: "tool_call",
+ toolName: input.toolName,
+ toolArgs: input.toolArgs,
+ });
+ return { permissionDecision: "allow" };
+ },
+ onPostToolUse: async (input, invocation) => {
+ auditLog.push({
+ timestamp: input.timestamp,
+ sessionId: invocation.sessionId,
+ event: "tool_result",
+ toolName: input.toolName,
+ toolResult: input.toolResult,
+ });
+ return null;
+ },
+ onSessionEnd: async (input, invocation) => {
+ auditLog.push({
+ timestamp: input.timestamp,
+ sessionId: invocation.sessionId,
+ event: "session_end",
+ });
+
+ // Persist the log — swap this with your own storage backend
+ await fs.promises.writeFile(
+ `audit-${invocation.sessionId}.json`,
+ JSON.stringify(auditLog, null, 2),
+ );
+ return null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+
+
+
+Python
+
+
+```python
+import json, aiofiles
+
+audit_log = []
+
+async def on_session_start(input_data, invocation):
+ audit_log.append({
+ "timestamp": input_data["timestamp"],
+ "session_id": invocation["session_id"],
+ "event": "session_start",
+ })
+ return None
+
+async def on_user_prompt_submitted(input_data, invocation):
+ audit_log.append({
+ "timestamp": input_data["timestamp"],
+ "session_id": invocation["session_id"],
+ "event": "user_prompt",
+ "prompt": input_data["prompt"],
+ })
+ return None
+
+async def on_pre_tool_use(input_data, invocation):
+ audit_log.append({
+ "timestamp": input_data["timestamp"],
+ "session_id": invocation["session_id"],
+ "event": "tool_call",
+ "tool_name": input_data["toolName"],
+ "tool_args": input_data["toolArgs"],
+ })
+ return {"permissionDecision": "allow"}
+
+async def on_post_tool_use(input_data, invocation):
+ audit_log.append({
+ "timestamp": input_data["timestamp"],
+ "session_id": invocation["session_id"],
+ "event": "tool_result",
+ "tool_name": input_data["toolName"],
+ "tool_result": input_data["toolResult"],
+ })
+ return None
+
+async def on_session_end(input_data, invocation):
+ audit_log.append({
+ "timestamp": input_data["timestamp"],
+ "session_id": invocation["session_id"],
+ "event": "session_end",
+ })
+ async with aiofiles.open(f"audit-{invocation['session_id']}.json", "w") as f:
+ await f.write(json.dumps(audit_log, indent=2))
+ return None
+
+session = await client.create_session(
+ on_permission_request=lambda req, inv: {"kind": "approved"},
+ hooks={
+ "on_session_start": on_session_start,
+ "on_user_prompt_submitted": on_user_prompt_submitted,
+ "on_pre_tool_use": on_pre_tool_use,
+ "on_post_tool_use": on_post_tool_use,
+ "on_session_end": on_session_end,
+ },
+)
+```
+
+
+
+### Redact secrets from tool results
+
+```typescript
+const SECRET_PATTERNS = [
+ /(?:api[_-]?key|token|secret|password)\s*[:=]\s*["']?[\w\-\.]+["']?/gi,
+];
+
+const session = await client.createSession({
+ hooks: {
+ onPostToolUse: async (input) => {
+ if (typeof input.toolResult !== "string") return null;
+
+ let redacted = input.toolResult;
+ for (const pattern of SECRET_PATTERNS) {
+ redacted = redacted.replace(pattern, "[REDACTED]");
+ }
+
+ return redacted !== input.toolResult
+ ? { modifiedResult: redacted }
+ : null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+---
+
+## Use Case: Notifications & Sounds
+
+Hooks fire in your application's process, so you can trigger any side-effect — desktop notifications, sounds, Slack messages, or webhook calls.
+
+### Desktop notification on session events
+
+
+Node.js / TypeScript
+
+```typescript
+import notifier from "node-notifier"; // npm install node-notifier
+
+const session = await client.createSession({
+ hooks: {
+ onSessionEnd: async (input, invocation) => {
+ notifier.notify({
+ title: "Copilot Session Complete",
+ message: `Session ${invocation.sessionId.slice(0, 8)} finished (${input.reason}).`,
+ });
+ return null;
+ },
+ onErrorOccurred: async (input) => {
+ notifier.notify({
+ title: "Copilot Error",
+ message: input.error.slice(0, 200),
+ });
+ return null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+
+
+
+Python
+
+```python
+import subprocess
+
+async def on_session_end(input_data, invocation):
+ sid = invocation["session_id"][:8]
+ reason = input_data["reason"]
+ subprocess.Popen([
+ "notify-send", "Copilot Session Complete",
+ f"Session {sid} finished ({reason}).",
+ ])
+ return None
+
+async def on_error_occurred(input_data, invocation):
+ subprocess.Popen([
+ "notify-send", "Copilot Error",
+ input_data["error"][:200],
+ ])
+ return None
+
+session = await client.create_session(
+ on_permission_request=lambda req, inv: {"kind": "approved"},
+ hooks={
+ "on_session_end": on_session_end,
+ "on_error_occurred": on_error_occurred,
+ },
+)
+```
+
+
+
+### Play a sound when a tool finishes
+
+```typescript
+import { exec } from "node:child_process";
+
+const session = await client.createSession({
+ hooks: {
+ onPostToolUse: async (input) => {
+ // macOS: play a system sound after every tool call
+ exec("afplay /System/Library/Sounds/Pop.aiff");
+ return null;
+ },
+ onErrorOccurred: async () => {
+ exec("afplay /System/Library/Sounds/Basso.aiff");
+ return null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+### Post to Slack on errors
+
+```typescript
+const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;
+
+const session = await client.createSession({
+ hooks: {
+ onErrorOccurred: async (input, invocation) => {
+ if (!input.recoverable) {
+ await fetch(SLACK_WEBHOOK_URL, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ text: `🚨 Unrecoverable error in session \`${invocation.sessionId.slice(0, 8)}\`:\n\`\`\`${input.error}\`\`\``,
+ }),
+ });
+ }
+ return null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+---
+
+## Use Case: Prompt Enrichment
+
+Use `onSessionStart` and `onUserPromptSubmitted` to automatically inject context so users don't have to repeat themselves.
+
+### Inject project metadata at session start
+
+```typescript
+const session = await client.createSession({
+ hooks: {
+ onSessionStart: async (input) => {
+ const pkg = JSON.parse(
+ await fs.promises.readFile("package.json", "utf-8"),
+ );
+ return {
+ additionalContext: [
+ `Project: ${pkg.name} v${pkg.version}`,
+ `Node: ${process.version}`,
+ `CWD: ${input.cwd}`,
+ ].join("\n"),
+ };
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+### Expand shorthand commands in prompts
+
+```typescript
+const SHORTCUTS: Record = {
+ "/fix": "Find and fix all errors in the current file",
+ "/test": "Write comprehensive unit tests for this code",
+ "/explain": "Explain this code in detail",
+ "/refactor": "Refactor this code to improve readability",
+};
+
+const session = await client.createSession({
+ hooks: {
+ onUserPromptSubmitted: async (input) => {
+ for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) {
+ if (input.prompt.startsWith(shortcut)) {
+ const rest = input.prompt.slice(shortcut.length).trim();
+ return { modifiedPrompt: rest ? `${expansion}: ${rest}` : expansion };
+ }
+ }
+ return null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+---
+
+## Use Case: Error Handling & Recovery
+
+The `onErrorOccurred` hook gives you a chance to react to failures — whether that means retrying, notifying a human, or gracefully shutting down.
+
+### Retry transient model errors
+
+```typescript
+const session = await client.createSession({
+ hooks: {
+ onErrorOccurred: async (input) => {
+ if (input.errorContext === "model_call" && input.recoverable) {
+ return {
+ errorHandling: "retry",
+ retryCount: 3,
+ userNotification: "Temporary model issue — retrying…",
+ };
+ }
+ return null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+### Friendly error messages
+
+```typescript
+const FRIENDLY_MESSAGES: Record = {
+ model_call: "The AI model is temporarily unavailable. Please try again.",
+ tool_execution: "A tool encountered an error. Check inputs and try again.",
+ system: "A system error occurred. Please try again later.",
+};
+
+const session = await client.createSession({
+ hooks: {
+ onErrorOccurred: async (input) => {
+ return {
+ userNotification: FRIENDLY_MESSAGES[input.errorContext] ?? input.error,
+ };
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+---
+
+## Use Case: Session Metrics
+
+Track how long sessions run, how many tools are invoked, and why sessions end — useful for dashboards and cost monitoring.
+
+
+Node.js / TypeScript
+
+```typescript
+const metrics = new Map();
+
+const session = await client.createSession({
+ hooks: {
+ onSessionStart: async (input, invocation) => {
+ metrics.set(invocation.sessionId, {
+ start: input.timestamp,
+ toolCalls: 0,
+ prompts: 0,
+ });
+ return null;
+ },
+ onUserPromptSubmitted: async (_input, invocation) => {
+ metrics.get(invocation.sessionId)!.prompts++;
+ return null;
+ },
+ onPreToolUse: async (_input, invocation) => {
+ metrics.get(invocation.sessionId)!.toolCalls++;
+ return { permissionDecision: "allow" };
+ },
+ onSessionEnd: async (input, invocation) => {
+ const m = metrics.get(invocation.sessionId)!;
+ const durationSec = (input.timestamp - m.start) / 1000;
+
+ console.log(
+ `Session ${invocation.sessionId.slice(0, 8)}: ` +
+ `${durationSec.toFixed(1)}s, ${m.prompts} prompts, ` +
+ `${m.toolCalls} tool calls, ended: ${input.reason}`,
+ );
+
+ metrics.delete(invocation.sessionId);
+ return null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+
+
+
+Python
+
+```python
+session_metrics = {}
+
+async def on_session_start(input_data, invocation):
+ session_metrics[invocation["session_id"]] = {
+ "start": input_data["timestamp"],
+ "tool_calls": 0,
+ "prompts": 0,
+ }
+ return None
+
+async def on_user_prompt_submitted(input_data, invocation):
+ session_metrics[invocation["session_id"]]["prompts"] += 1
+ return None
+
+async def on_pre_tool_use(input_data, invocation):
+ session_metrics[invocation["session_id"]]["tool_calls"] += 1
+ return {"permissionDecision": "allow"}
+
+async def on_session_end(input_data, invocation):
+ m = session_metrics.pop(invocation["session_id"])
+ duration = (input_data["timestamp"] - m["start"]) / 1000
+ sid = invocation["session_id"][:8]
+ print(
+ f"Session {sid}: {duration:.1f}s, {m['prompts']} prompts, "
+ f"{m['tool_calls']} tool calls, ended: {input_data['reason']}"
+ )
+ return None
+
+session = await client.create_session(
+ on_permission_request=lambda req, inv: {"kind": "approved"},
+ hooks={
+ "on_session_start": on_session_start,
+ "on_user_prompt_submitted": on_user_prompt_submitted,
+ "on_pre_tool_use": on_pre_tool_use,
+ "on_session_end": on_session_end,
+ },
+)
+```
+
+
+
+---
+
+## Combining Hooks
+
+Hooks compose naturally. A single `hooks` object can handle permissions **and** auditing **and** notifications — each hook does its own job.
+
+```typescript
+const session = await client.createSession({
+ hooks: {
+ onSessionStart: async (input) => {
+ console.log(`[audit] session started in ${input.cwd}`);
+ return { additionalContext: "Project uses TypeScript and Vitest." };
+ },
+ onPreToolUse: async (input) => {
+ console.log(`[audit] tool requested: ${input.toolName}`);
+ if (input.toolName === "shell") {
+ return { permissionDecision: "ask" };
+ }
+ return { permissionDecision: "allow" };
+ },
+ onPostToolUse: async (input) => {
+ console.log(`[audit] tool completed: ${input.toolName}`);
+ return null;
+ },
+ onErrorOccurred: async (input) => {
+ console.error(`[alert] ${input.errorContext}: ${input.error}`);
+ return null;
+ },
+ onSessionEnd: async (input, invocation) => {
+ console.log(`[audit] session ${invocation.sessionId.slice(0, 8)} ended: ${input.reason}`);
+ return null;
+ },
+ },
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+```
+
+## Best Practices
+
+1. **Keep hooks fast.** Every hook runs inline — slow hooks delay the conversation. Offload heavy work (database writes, HTTP calls) to a background queue when possible.
+
+2. **Return `null` when you have nothing to change.** This tells the SDK to proceed with defaults and avoids unnecessary object allocation.
+
+3. **Be explicit with permission decisions.** Returning `{ permissionDecision: "allow" }` is clearer than returning `null`, even though both allow the tool.
+
+4. **Don't swallow critical errors.** It's fine to suppress recoverable tool errors, but always log or alert on unrecoverable ones.
+
+5. **Use `additionalContext` instead of `modifiedPrompt` when possible.** Appending context preserves the user's original intent while still guiding the model.
+
+6. **Scope state by session ID.** If you track per-session data, key it on `invocation.sessionId` and clean up in `onSessionEnd`.
+
+## Reference
+
+For full type definitions, input/output field tables, and additional examples for every hook, see the API reference:
+
+- [Hooks Overview](../hooks/index.md)
+- [Pre-Tool Use](../hooks/pre-tool-use.md)
+- [Post-Tool Use](../hooks/post-tool-use.md)
+- [User Prompt Submitted](../hooks/user-prompt-submitted.md)
+- [Session Lifecycle](../hooks/session-lifecycle.md)
+- [Error Handling](../hooks/error-handling.md)
+
+## See Also
+
+- [Getting Started](../getting-started.md)
+- [Custom Agents & Sub-Agent Orchestration](./custom-agents.md)
+- [Streaming Session Events](./streaming-events.md)
+- [Debugging Guide](../troubleshooting/debugging.md)
diff --git a/docs/features/image-input.md b/docs/features/image-input.md
new file mode 100644
index 0000000000..8295b83d71
--- /dev/null
+++ b/docs/features/image-input.md
@@ -0,0 +1,480 @@
+# Image Input
+
+Send images to Copilot sessions as attachments. There are two ways to attach images:
+
+- **File attachment** (`type: "file"`) — provide an absolute path; the runtime reads the file from disk, converts it to base64, and sends it to the LLM.
+- **Blob attachment** (`type: "blob"`) — provide base64-encoded data directly; useful when the image is already in memory (e.g., screenshots, generated images, or data from an API).
+
+## Overview
+
+```mermaid
+sequenceDiagram
+ participant App as Your App
+ participant SDK as SDK Session
+ participant RT as Copilot Runtime
+ participant LLM as Vision Model
+
+ App->>SDK: send({ prompt, attachments: [{ type: "file", path }] })
+ SDK->>RT: JSON-RPC with file attachment
+ RT->>RT: Read file from disk
+ RT->>RT: Detect image, convert to base64
+ RT->>RT: Resize if needed (model-specific limits)
+ RT->>LLM: image_url content block (base64)
+ LLM-->>RT: Response referencing the image
+ RT-->>SDK: assistant.message events
+ SDK-->>App: event stream
+```
+
+| Concept | Description |
+|---------|-------------|
+| **File attachment** | An attachment with `type: "file"` and an absolute `path` to an image on disk |
+| **Blob attachment** | An attachment with `type: "blob"`, base64-encoded `data`, and a `mimeType` — no disk I/O needed |
+| **Automatic encoding** | For file attachments, the runtime reads the image and converts it to base64 automatically |
+| **Auto-resize** | The runtime automatically resizes or quality-reduces images that exceed model-specific limits |
+| **Vision capability** | The model must have `capabilities.supports.vision = true` to process images |
+
+## Quick Start — File Attachment
+
+Attach an image file to any message using the file attachment type. The path must be an absolute path to an image on disk.
+
+
+Node.js / TypeScript
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+await client.start();
+
+const session = await client.createSession({
+ model: "gpt-4.1",
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+
+await session.send({
+ prompt: "Describe what you see in this image",
+ attachments: [
+ {
+ type: "file",
+ path: "/absolute/path/to/screenshot.png",
+ },
+ ],
+});
+```
+
+
+
+
+Python
+
+```python
+from copilot import CopilotClient
+from copilot.types import PermissionRequestResult
+
+client = CopilotClient()
+await client.start()
+
+session = await client.create_session(
+ on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"),
+ model="gpt-4.1",
+)
+
+await session.send(
+ "Describe what you see in this image",
+ attachments=[
+ {
+ "type": "file",
+ "path": "/absolute/path/to/screenshot.png",
+ },
+ ],
+)
+```
+
+
+
+
+Go
+
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+ client.Start(ctx)
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Model: "gpt-4.1",
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+
+ path := "/absolute/path/to/screenshot.png"
+ session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Describe what you see in this image",
+ Attachments: []copilot.Attachment{
+ {
+ Type: copilot.AttachmentTypeFile,
+ Path: &path,
+ },
+ },
+ })
+}
+```
+
+
+```go
+ctx := context.Background()
+client := copilot.NewClient(nil)
+client.Start(ctx)
+
+session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Model: "gpt-4.1",
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+})
+
+path := "/absolute/path/to/screenshot.png"
+session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Describe what you see in this image",
+ Attachments: []copilot.Attachment{
+ {
+ Type: copilot.AttachmentTypeFile,
+ Path: &path,
+ },
+ },
+})
+```
+
+
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class ImageInputExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+ await using var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Model = "gpt-4.1",
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+ });
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Describe what you see in this image",
+ Attachments = new List
+ {
+ new UserMessageDataAttachmentsItemFile
+ {
+ Path = "/absolute/path/to/screenshot.png",
+ DisplayName = "screenshot.png",
+ },
+ },
+ });
+ }
+}
+```
+
+
+```csharp
+using GitHub.Copilot.SDK;
+
+await using var client = new CopilotClient();
+await using var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Model = "gpt-4.1",
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+});
+
+await session.SendAsync(new MessageOptions
+{
+ Prompt = "Describe what you see in this image",
+ Attachments = new List
+ {
+ new UserMessageDataAttachmentsItemFile
+ {
+ Path = "/absolute/path/to/screenshot.png",
+ DisplayName = "screenshot.png",
+ },
+ },
+});
+```
+
+
+
+## Quick Start — Blob Attachment
+
+When you already have image data in memory (e.g., a screenshot captured by your app, or an image fetched from an API), use a blob attachment to send it directly without writing to disk.
+
+
+Node.js / TypeScript
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+await client.start();
+
+const session = await client.createSession({
+ model: "gpt-4.1",
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+
+const base64ImageData = "..."; // your base64-encoded image
+await session.send({
+ prompt: "Describe what you see in this image",
+ attachments: [
+ {
+ type: "blob",
+ data: base64ImageData,
+ mimeType: "image/png",
+ displayName: "screenshot.png",
+ },
+ ],
+});
+```
+
+
+
+
+Python
+
+```python
+from copilot import CopilotClient
+from copilot.types import PermissionRequestResult
+
+client = CopilotClient()
+await client.start()
+
+session = await client.create_session({
+ "model": "gpt-4.1",
+ "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"),
+})
+
+base64_image_data = "..." # your base64-encoded image
+await session.send(
+ "Describe what you see in this image",
+ attachments=[
+ {
+ "type": "blob",
+ "data": base64_image_data,
+ "mimeType": "image/png",
+ "displayName": "screenshot.png",
+ },
+ ],
+)
+```
+
+
+
+
+Go
+
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+ client.Start(ctx)
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Model: "gpt-4.1",
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+
+ base64ImageData := "..."
+ mimeType := "image/png"
+ displayName := "screenshot.png"
+ session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Describe what you see in this image",
+ Attachments: []copilot.Attachment{
+ {
+ Type: copilot.AttachmentTypeBlob,
+ Data: &base64ImageData,
+ MIMEType: &mimeType,
+ DisplayName: &displayName,
+ },
+ },
+ })
+}
+```
+
+
+```go
+mimeType := "image/png"
+displayName := "screenshot.png"
+session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Describe what you see in this image",
+ Attachments: []copilot.Attachment{
+ {
+ Type: copilot.AttachmentTypeBlob,
+ Data: &base64ImageData, // base64-encoded string
+ MIMEType: &mimeType,
+ DisplayName: &displayName,
+ },
+ },
+})
+```
+
+
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class BlobAttachmentExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+ await using var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Model = "gpt-4.1",
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+ });
+
+ var base64ImageData = "...";
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Describe what you see in this image",
+ Attachments = new List
+ {
+ new UserMessageDataAttachmentsItemBlob
+ {
+ Data = base64ImageData,
+ MimeType = "image/png",
+ DisplayName = "screenshot.png",
+ },
+ },
+ });
+ }
+}
+```
+
+
+```csharp
+await session.SendAsync(new MessageOptions
+{
+ Prompt = "Describe what you see in this image",
+ Attachments = new List
+ {
+ new UserMessageDataAttachmentsItemBlob
+ {
+ Data = base64ImageData,
+ MimeType = "image/png",
+ DisplayName = "screenshot.png",
+ },
+ },
+});
+```
+
+
+
+## Supported Formats
+
+Supported image formats include JPG, PNG, GIF, and other common image types. For file attachments, the runtime reads the image from disk and converts it as needed. For blob attachments, you provide the base64 data and MIME type directly. Use PNG or JPEG for best results, as these are the most widely supported formats.
+
+The model's `capabilities.limits.vision.supported_media_types` field lists the exact MIME types it accepts.
+
+## Automatic Processing
+
+The runtime automatically processes images to fit within the model's constraints. No manual resizing is required.
+
+- Images that exceed the model's dimension or size limits are automatically resized (preserving aspect ratio) or quality-reduced.
+- If an image cannot be brought within limits after processing, it is skipped and not sent to the LLM.
+- The model's `capabilities.limits.vision.max_prompt_image_size` field indicates the maximum image size in bytes.
+
+You can check these limits at runtime via the model capabilities object. For the best experience, use reasonably-sized PNG or JPEG images.
+
+## Vision Model Capabilities
+
+Not all models support vision. Check the model's capabilities before sending images.
+
+### Capability fields
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `capabilities.supports.vision` | `boolean` | Whether the model can process image inputs |
+| `capabilities.limits.vision.supported_media_types` | `string[]` | MIME types the model accepts (e.g., `["image/png", "image/jpeg"]`) |
+| `capabilities.limits.vision.max_prompt_images` | `number` | Maximum number of images per prompt |
+| `capabilities.limits.vision.max_prompt_image_size` | `number` | Maximum image size in bytes |
+
+### Vision limits type
+
+
+```typescript
+interface VisionCapabilities {
+ vision?: {
+ supported_media_types: string[];
+ max_prompt_images: number;
+ max_prompt_image_size: number; // bytes
+ };
+}
+```
+
+```typescript
+vision?: {
+ supported_media_types: string[];
+ max_prompt_images: number;
+ max_prompt_image_size: number; // bytes
+};
+```
+
+## Receiving Image Results
+
+When tools return images (e.g., screenshots or generated charts), the result contains `"image"` content blocks with base64-encoded data.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `type` | `"image"` | Content block type discriminator |
+| `data` | `string` | Base64-encoded image data |
+| `mimeType` | `string` | MIME type (e.g., `"image/png"`) |
+
+These image blocks appear in `tool.execution_complete` event results. See the [Streaming Events](./streaming-events.md) guide for the full event lifecycle.
+
+## Tips & Limitations
+
+| Tip | Details |
+|-----|---------|
+| **Use PNG or JPEG directly** | Avoids conversion overhead — these are sent to the LLM as-is |
+| **Keep images reasonably sized** | Large images may be quality-reduced, which can lose important details |
+| **Use absolute paths for file attachments** | The runtime reads files from disk; relative paths may not resolve correctly |
+| **Use blob attachments for in-memory data** | When you already have base64 data (e.g., screenshots, API responses), blob avoids unnecessary disk I/O |
+| **Check vision support first** | Sending images to a non-vision model wastes tokens without visual understanding |
+| **Multiple images are supported** | Attach several attachments in one message, up to the model's `max_prompt_images` limit |
+| **SVG is not supported** | SVG files are text-based and excluded from image processing |
+
+## See Also
+
+- [Streaming Events](./streaming-events.md) — event lifecycle including tool result content blocks
+- [Steering & Queueing](./steering-and-queueing.md) — sending follow-up messages with attachments
diff --git a/docs/features/index.md b/docs/features/index.md
new file mode 100644
index 0000000000..3eb63a799a
--- /dev/null
+++ b/docs/features/index.md
@@ -0,0 +1,25 @@
+# Features
+
+These guides cover the capabilities you can add to your Copilot SDK application. Each guide includes examples in all supported languages (TypeScript, Python, Go, and .NET).
+
+> **New to the SDK?** Start with the [Getting Started tutorial](../getting-started.md) first, then come back here to add more capabilities.
+
+## Guides
+
+| Feature | Description |
+|---|---|
+| [Hooks](./hooks.md) | Intercept and customize session behavior — control tool execution, transform results, handle errors |
+| [Custom Agents](./custom-agents.md) | Define specialized sub-agents with scoped tools and instructions |
+| [MCP Servers](./mcp.md) | Integrate Model Context Protocol servers for external tool access |
+| [Skills](./skills.md) | Load reusable prompt modules from directories |
+| [Image Input](./image-input.md) | Send images to sessions as attachments |
+| [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) |
+| [Steering & Queueing](./steering-and-queueing.md) | Control message delivery — immediate steering vs. sequential queueing |
+| [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage |
+
+## Related
+
+- [Hooks Reference](../hooks/index.md) — detailed API reference for each hook type
+- [Integrations](../integrations/microsoft-agent-framework.md) — use the SDK with other platforms (MAF, etc.)
+- [Troubleshooting](../troubleshooting/debugging.md) — when things don't work as expected
+- [Compatibility](../troubleshooting/compatibility.md) — SDK vs CLI feature matrix
diff --git a/docs/mcp/overview.md b/docs/features/mcp.md
similarity index 89%
rename from docs/mcp/overview.md
rename to docs/features/mcp.md
index 5ad8b1df39..62465c0bd7 100644
--- a/docs/mcp/overview.md
+++ b/docs/features/mcp.md
@@ -59,32 +59,29 @@ const session = await client.createSession({
```python
import asyncio
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session({
- "model": "gpt-5",
- "mcp_servers": {
- # Local MCP server (stdio)
- "my-local-server": {
- "type": "local",
- "command": "python",
- "args": ["./mcp_server.py"],
- "env": {"DEBUG": "true"},
- "cwd": "./servers",
- "tools": ["*"],
- "timeout": 30000,
- },
- # Remote MCP server (HTTP)
- "github": {
- "type": "http",
- "url": "https://api.githubcopilot.com/mcp/",
- "headers": {"Authorization": "Bearer ${TOKEN}"},
- "tools": ["*"],
- },
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5", mcp_servers={
+ # Local MCP server (stdio)
+ "my-local-server": {
+ "type": "local",
+ "command": "python",
+ "args": ["./mcp_server.py"],
+ "env": {"DEBUG": "true"},
+ "cwd": "./servers",
+ "tools": ["*"],
+ "timeout": 30000,
+ },
+ # Remote MCP server (HTTP)
+ "github": {
+ "type": "http",
+ "url": "https://api.githubcopilot.com/mcp/",
+ "headers": {"Authorization": "Bearer ${TOKEN}"},
+ "tools": ["*"],
},
})
@@ -258,7 +255,7 @@ directories for different applications.
| "Timeout" errors | Increase the `timeout` value or check server performance |
| Tools work but aren't called | Ensure your prompt clearly requires the tool's functionality |
-For detailed debugging guidance, see the **[MCP Debugging Guide](./debugging.md)**.
+For detailed debugging guidance, see the **[MCP Debugging Guide](../troubleshooting/mcp-debugging.md)**.
## Related Resources
@@ -266,10 +263,10 @@ For detailed debugging guidance, see the **[MCP Debugging Guide](./debugging.md)
- [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - Community MCP servers
- [GitHub MCP Server](https://github.com/github/github-mcp-server) - Official GitHub MCP server
- [Getting Started Guide](../getting-started.md) - SDK basics and custom tools
-- [General Debugging Guide](../debugging.md) - SDK-wide debugging
+- [General Debugging Guide](.../troubleshooting/mcp-debugging.md) - SDK-wide debugging
## See Also
-- [MCP Debugging Guide](./debugging.md) - Detailed MCP troubleshooting
+- [MCP Debugging Guide](../troubleshooting/mcp-debugging.md) - Detailed MCP troubleshooting
- [Issue #9](https://github.com/github/copilot-sdk/issues/9) - Original MCP tools usage question
- [Issue #36](https://github.com/github/copilot-sdk/issues/36) - MCP documentation tracking issue
diff --git a/docs/guides/session-persistence.md b/docs/features/session-persistence.md
similarity index 87%
rename from docs/guides/session-persistence.md
rename to docs/features/session-persistence.md
index e2b736c1b5..3b0e9f69b9 100644
--- a/docs/guides/session-persistence.md
+++ b/docs/features/session-persistence.md
@@ -46,16 +46,13 @@ await session.sendAndWait({ prompt: "Analyze my codebase" });
### Python
```python
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
client = CopilotClient()
await client.start()
# Create a session with a meaningful ID
-session = await client.create_session({
- "session_id": "user-123-task-456",
- "model": "gpt-5.2-codex",
-})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.2-codex", session_id="user-123-task-456")
# Do some work...
await session.send_and_wait({"prompt": "Analyze my codebase"})
@@ -65,7 +62,33 @@ await session.send_and_wait({"prompt": "Analyze my codebase"})
### Go
-
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ SessionID: "user-123-task-456",
+ Model: "gpt-5.2-codex",
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+
+ session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Analyze my codebase"})
+ _ = session
+}
+```
+
+
```go
ctx := context.Background()
client := copilot.NewClient(nil)
@@ -134,7 +157,7 @@ await session.sendAndWait({ prompt: "What did we discuss earlier?" });
```python
# Resume from a different client instance (or after restart)
-session = await client.resume_session("user-123-task-456")
+session = await client.resume_session("user-123-task-456", on_permission_request=PermissionHandler.approve_all)
# Continue where you left off
await session.send_and_wait({"prompt": "What did we discuss earlier?"})
@@ -142,7 +165,27 @@ await session.send_and_wait({"prompt": "What did we discuss earlier?"})
### Go
-
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+
+ session, _ := client.ResumeSession(ctx, "user-123-task-456", nil)
+
+ session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What did we discuss earlier?"})
+ _ = session
+}
+```
+
+
```go
ctx := context.Background()
@@ -155,7 +198,28 @@ session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What did we discuss ear
### C# (.NET)
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class ResumeSessionExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+
+ var session = await client.ResumeSessionAsync("user-123-task-456", new ResumeSessionConfig
+ {
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+ });
+
+ await session.SendAndWaitAsync(new MessageOptions { Prompt = "What did we discuss earlier?" });
+ }
+}
+```
+
+
```csharp
// Resume from a different client instance (or after restart)
var session = await client.ResumeSessionAsync("user-123-task-456");
@@ -181,6 +245,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u
| `configDir` | Override configuration directory |
| `mcpServers` | Configure MCP servers |
| `customAgents` | Configure custom agents |
+| `agent` | Pre-select a custom agent by name |
| `skillDirectories` | Directories to load skills from |
| `disabledSkills` | Skills to disable |
| `infiniteSessions` | Configure infinite session behavior |
@@ -494,7 +559,7 @@ const session = await client.createSession({
});
```
-> **Note:** Thresholds are context utilization ratios (0.0-1.0), not absolute token counts. See the [Compatibility Guide](../compatibility.md) for details.
+> **Note:** Thresholds are context utilization ratios (0.0-1.0), not absolute token counts. See the [Compatibility Guide](../troubleshooting/compatibility.md) for details.
## Limitations & Considerations
@@ -554,6 +619,6 @@ await withSessionLock("user-123-task-456", async () => {
## Next Steps
-- [Hooks Overview](../hooks/overview.md) - Customize session behavior with hooks
-- [Compatibility Guide](../compatibility.md) - SDK vs CLI feature comparison
-- [Debugging Guide](../debugging.md) - Troubleshoot session issues
+- [Hooks Overview](../hooks/index.md) - Customize session behavior with hooks
+- [Compatibility Guide](../troubleshooting/compatibility.md) - SDK vs CLI feature comparison
+- [Debugging Guide](../troubleshooting/debugging.md) - Troubleshoot session issues
diff --git a/docs/guides/skills.md b/docs/features/skills.md
similarity index 82%
rename from docs/guides/skills.md
rename to docs/features/skills.md
index 5e085e3b29..466c637ffe 100644
--- a/docs/guides/skills.md
+++ b/docs/features/skills.md
@@ -43,19 +43,20 @@ await session.sendAndWait({ prompt: "Review this code for security issues" });
```python
from copilot import CopilotClient
+from copilot.types import PermissionRequestResult
async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session({
- "model": "gpt-4.1",
- "skill_directories": [
+ session = await client.create_session(
+ on_permission_request=lambda req, inv: {"kind": "approved"},
+ model="gpt-4.1",
+ skill_directories=[
"./skills/code-review",
"./skills/documentation",
],
- "on_permission_request": lambda req: {"kind": "approved"},
- })
+ )
# Copilot now has access to skills in those directories
await session.send_and_wait({"prompt": "Review this code for security issues"})
@@ -159,10 +160,13 @@ const session = await client.createSession({
Python
```python
-session = await client.create_session({
- "skill_directories": ["./skills"],
- "disabled_skills": ["experimental-feature", "deprecated-tool"],
-})
+from copilot import PermissionHandler
+
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ skill_directories=["./skills"],
+ disabled_skills=["experimental-feature", "deprecated-tool"],
+)
```
@@ -170,7 +174,31 @@ session = await client.create_session({
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ SkillDirectories: []string{"./skills"},
+ DisabledSkills: []string{"experimental-feature", "deprecated-tool"},
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+ _ = session
+}
+```
+
+
```go
session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
SkillDirectories: []string{"./skills"},
@@ -183,7 +211,28 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class SkillsExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+
+ var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ SkillDirectories = new List { "./skills" },
+ DisabledSkills = new List { "experimental-feature", "deprecated-tool" },
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+ });
+ }
+}
+```
+
+
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
@@ -319,4 +368,4 @@ If multiple skills provide conflicting instructions:
- [Custom Agents](../getting-started.md#create-custom-agents) - Define specialized AI personas
- [Custom Tools](../getting-started.md#step-4-add-a-custom-tool) - Build your own tools
-- [MCP Servers](../mcp/overview.md) - Connect external tool providers
+- [MCP Servers](./mcp.md) - Connect external tool providers
diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md
new file mode 100644
index 0000000000..7da349e1c2
--- /dev/null
+++ b/docs/features/steering-and-queueing.md
@@ -0,0 +1,579 @@
+# Steering & Queueing
+
+Two interaction patterns let users send messages while the agent is already working: **steering** redirects the agent mid-turn, and **queueing** buffers messages for sequential processing after the current turn completes.
+
+## Overview
+
+When a session is actively processing a turn, incoming messages can be delivered in one of two modes via the `mode` field on `MessageOptions`:
+
+| Mode | Behavior | Use case |
+|------|----------|----------|
+| `"immediate"` (steering) | Injected into the **current** LLM turn | "Actually, don't create that file — use a different approach" |
+| `"enqueue"` (queueing) | Queued and processed **after** the current turn finishes | "After this, also fix the tests" |
+
+```mermaid
+sequenceDiagram
+ participant U as User
+ participant S as Session
+ participant LLM as Agent
+
+ U->>S: send({ prompt: "Refactor auth" })
+ S->>LLM: Turn starts
+
+ Note over U,LLM: Agent is busy...
+
+ U->>S: send({ prompt: "Use JWT instead", mode: "immediate" })
+ S-->>LLM: Injected into current turn (steering)
+
+ U->>S: send({ prompt: "Then update the docs", mode: "enqueue" })
+ S-->>S: Queued for next turn
+
+ LLM->>S: Turn completes (incorporates steering)
+ S->>LLM: Processes queued message
+ LLM->>S: Turn completes
+```
+
+## Steering (Immediate Mode)
+
+Steering sends a message that is injected directly into the agent's current turn. The agent sees the message in real time and adjusts its response accordingly — useful for course-correcting without aborting the turn.
+
+
+Node.js / TypeScript
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+await client.start();
+
+const session = await client.createSession({
+ model: "gpt-4.1",
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+
+// Start a long-running task
+const msgId = await session.send({
+ prompt: "Refactor the authentication module to use sessions",
+});
+
+// While the agent is working, steer it
+await session.send({
+ prompt: "Actually, use JWT tokens instead of sessions",
+ mode: "immediate",
+});
+```
+
+
+
+
+Python
+
+```python
+from copilot import CopilotClient
+from copilot.types import PermissionRequestResult
+
+async def main():
+ client = CopilotClient()
+ await client.start()
+
+ session = await client.create_session(
+ on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"),
+ model="gpt-4.1",
+ )
+
+ # Start a long-running task
+ msg_id = await session.send({
+ "prompt": "Refactor the authentication module to use sessions",
+ })
+
+ # While the agent is working, steer it
+ await session.send({
+ "prompt": "Actually, use JWT tokens instead of sessions",
+ "mode": "immediate",
+ })
+
+ await client.stop()
+```
+
+
+
+
+Go
+
+```go
+package main
+
+import (
+ "context"
+ "log"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+ if err := client.Start(ctx); err != nil {
+ log.Fatal(err)
+ }
+ defer client.Stop()
+
+ session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ Model: "gpt-4.1",
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // Start a long-running task
+ _, err = session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Refactor the authentication module to use sessions",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // While the agent is working, steer it
+ _, err = session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Actually, use JWT tokens instead of sessions",
+ Mode: "immediate",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+
+
+
+.NET
+
+```csharp
+using GitHub.Copilot.SDK;
+
+await using var client = new CopilotClient();
+await using var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Model = "gpt-4.1",
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+});
+
+// Start a long-running task
+var msgId = await session.SendAsync(new MessageOptions
+{
+ Prompt = "Refactor the authentication module to use sessions"
+});
+
+// While the agent is working, steer it
+await session.SendAsync(new MessageOptions
+{
+ Prompt = "Actually, use JWT tokens instead of sessions",
+ Mode = "immediate"
+});
+```
+
+
+
+### How Steering Works Internally
+
+1. The message is added to the runtime's `ImmediatePromptProcessor` queue
+2. Before the next LLM request within the current turn, the processor injects the message into the conversation
+3. The agent sees the steering message as a new user message and adjusts its response
+4. If the turn completes before the steering message is processed, it is automatically moved to the regular queue for the next turn
+
+> **Note:** Steering messages are best-effort within the current turn. If the agent has already committed to a tool call, the steering takes effect after that call completes but still within the same turn.
+
+## Queueing (Enqueue Mode)
+
+Queueing buffers messages to be processed sequentially after the current turn finishes. Each queued message starts its own full turn. This is the default mode — if you omit `mode`, the SDK uses `"enqueue"`.
+
+
+Node.js / TypeScript
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+await client.start();
+
+const session = await client.createSession({
+ model: "gpt-4.1",
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+
+// Send an initial task
+await session.send({ prompt: "Set up the project structure" });
+
+// Queue follow-up tasks while the agent is busy
+await session.send({
+ prompt: "Add unit tests for the auth module",
+ mode: "enqueue",
+});
+
+await session.send({
+ prompt: "Update the README with setup instructions",
+ mode: "enqueue",
+});
+
+// Messages are processed in FIFO order after each turn completes
+```
+
+
+
+
+Python
+
+```python
+from copilot import CopilotClient
+from copilot.types import PermissionRequestResult
+
+async def main():
+ client = CopilotClient()
+ await client.start()
+
+ session = await client.create_session(
+ on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"),
+ model="gpt-4.1",
+ )
+
+ # Send an initial task
+ await session.send({"prompt": "Set up the project structure"})
+
+ # Queue follow-up tasks while the agent is busy
+ await session.send({
+ "prompt": "Add unit tests for the auth module",
+ "mode": "enqueue",
+ })
+
+ await session.send({
+ "prompt": "Update the README with setup instructions",
+ "mode": "enqueue",
+ })
+
+ # Messages are processed in FIFO order after each turn completes
+ await client.stop()
+```
+
+
+
+
+Go
+
+
+```go
+package main
+
+import (
+ "context"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+ client.Start(ctx)
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Model: "gpt-4.1",
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+
+ session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Set up the project structure",
+ })
+
+ session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Add unit tests for the auth module",
+ Mode: "enqueue",
+ })
+
+ session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Update the README with setup instructions",
+ Mode: "enqueue",
+ })
+}
+```
+
+
+```go
+// Send an initial task
+session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Set up the project structure",
+})
+
+// Queue follow-up tasks while the agent is busy
+session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Add unit tests for the auth module",
+ Mode: "enqueue",
+})
+
+session.Send(ctx, copilot.MessageOptions{
+ Prompt: "Update the README with setup instructions",
+ Mode: "enqueue",
+})
+
+// Messages are processed in FIFO order after each turn completes
+```
+
+
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class QueueingExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+ await using var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Model = "gpt-4.1",
+ OnPermissionRequest = (req, inv) =>
+ Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
+ });
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Set up the project structure"
+ });
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Add unit tests for the auth module",
+ Mode = "enqueue"
+ });
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Update the README with setup instructions",
+ Mode = "enqueue"
+ });
+ }
+}
+```
+
+
+```csharp
+// Send an initial task
+await session.SendAsync(new MessageOptions
+{
+ Prompt = "Set up the project structure"
+});
+
+// Queue follow-up tasks while the agent is busy
+await session.SendAsync(new MessageOptions
+{
+ Prompt = "Add unit tests for the auth module",
+ Mode = "enqueue"
+});
+
+await session.SendAsync(new MessageOptions
+{
+ Prompt = "Update the README with setup instructions",
+ Mode = "enqueue"
+});
+
+// Messages are processed in FIFO order after each turn completes
+```
+
+
+
+### How Queueing Works Internally
+
+1. The message is added to the session's `itemQueue` as a `QueuedItem`
+2. When the current turn completes and the session becomes idle, `processQueuedItems()` runs
+3. Items are dequeued in FIFO order — each message triggers a full agentic turn
+4. If a steering message was pending when the turn ended, it is moved to the front of the queue
+5. Processing continues until the queue is empty, then the session emits an idle event
+
+## Combining Steering and Queueing
+
+You can use both patterns together in a single session. Steering affects the current turn while queued messages wait for their own turns:
+
+
+Node.js / TypeScript
+
+```typescript
+const session = await client.createSession({
+ model: "gpt-4.1",
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+
+// Start a task
+await session.send({ prompt: "Refactor the database layer" });
+
+// Steer the current work
+await session.send({
+ prompt: "Make sure to keep backwards compatibility with the v1 API",
+ mode: "immediate",
+});
+
+// Queue a follow-up for after this turn
+await session.send({
+ prompt: "Now add migration scripts for the schema changes",
+ mode: "enqueue",
+});
+```
+
+
+
+
+Python
+
+```python
+session = await client.create_session(
+ on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"),
+ model="gpt-4.1",
+)
+
+# Start a task
+await session.send({"prompt": "Refactor the database layer"})
+
+# Steer the current work
+await session.send({
+ "prompt": "Make sure to keep backwards compatibility with the v1 API",
+ "mode": "immediate",
+})
+
+# Queue a follow-up for after this turn
+await session.send({
+ "prompt": "Now add migration scripts for the schema changes",
+ "mode": "enqueue",
+})
+```
+
+
+
+## Choosing Between Steering and Queueing
+
+| Scenario | Pattern | Why |
+|----------|---------|-----|
+| Agent is going down the wrong path | **Steering** | Redirects the current turn without losing progress |
+| You thought of something the agent should also do | **Queueing** | Doesn't disrupt current work; runs next |
+| Agent is about to make a mistake | **Steering** | Intervenes before the mistake is committed |
+| You want to chain multiple tasks | **Queueing** | FIFO ordering ensures predictable execution |
+| You want to add context to the current task | **Steering** | Agent incorporates it into its current reasoning |
+| You want to batch unrelated requests | **Queueing** | Each gets its own full turn with clean context |
+
+## Building a UI with Steering & Queueing
+
+Here's a pattern for building an interactive UI that supports both modes:
+
+```typescript
+import { CopilotClient, CopilotSession } from "@github/copilot-sdk";
+
+interface PendingMessage {
+ prompt: string;
+ mode: "immediate" | "enqueue";
+ sentAt: Date;
+}
+
+class InteractiveChat {
+ private session: CopilotSession;
+ private isProcessing = false;
+ private pendingMessages: PendingMessage[] = [];
+
+ constructor(session: CopilotSession) {
+ this.session = session;
+
+ session.on((event) => {
+ if (event.type === "session.idle") {
+ this.isProcessing = false;
+ this.onIdle();
+ }
+ if (event.type === "assistant.message") {
+ this.renderMessage(event);
+ }
+ });
+ }
+
+ async sendMessage(prompt: string): Promise {
+ if (!this.isProcessing) {
+ this.isProcessing = true;
+ await this.session.send({ prompt });
+ return;
+ }
+
+ // Session is busy — let the user choose how to deliver
+ // Your UI would present this choice (e.g., buttons, keyboard shortcuts)
+ }
+
+ async steer(prompt: string): Promise {
+ this.pendingMessages.push({
+ prompt,
+ mode: "immediate",
+ sentAt: new Date(),
+ });
+ await this.session.send({ prompt, mode: "immediate" });
+ }
+
+ async enqueue(prompt: string): Promise {
+ this.pendingMessages.push({
+ prompt,
+ mode: "enqueue",
+ sentAt: new Date(),
+ });
+ await this.session.send({ prompt, mode: "enqueue" });
+ }
+
+ private onIdle(): void {
+ this.pendingMessages = [];
+ // Update UI to show session is ready for new input
+ }
+
+ private renderMessage(event: unknown): void {
+ // Render assistant message in your UI
+ }
+}
+```
+
+## API Reference
+
+### MessageOptions
+
+| Language | Field | Type | Default | Description |
+|----------|-------|------|---------|-------------|
+| Node.js | `mode` | `"enqueue" \| "immediate"` | `"enqueue"` | Message delivery mode |
+| Python | `mode` | `Literal["enqueue", "immediate"]` | `"enqueue"` | Message delivery mode |
+| Go | `Mode` | `string` | `"enqueue"` | Message delivery mode |
+| .NET | `Mode` | `string?` | `"enqueue"` | Message delivery mode |
+
+### Delivery Modes
+
+| Mode | Effect | During active turn | During idle |
+|------|--------|-------------------|-------------|
+| `"enqueue"` | Queue for next turn | Waits in FIFO queue | Starts a new turn immediately |
+| `"immediate"` | Inject into current turn | Injected before next LLM call | Starts a new turn immediately |
+
+> **Note:** When the session is idle (not processing), both modes behave identically — the message starts a new turn immediately.
+
+## Best Practices
+
+1. **Default to queueing** — Use `"enqueue"` (or omit `mode`) for most messages. It's predictable and doesn't risk disrupting in-progress work.
+
+2. **Reserve steering for corrections** — Use `"immediate"` when the agent is actively doing the wrong thing and you need to redirect it before it goes further.
+
+3. **Keep steering messages concise** — The agent needs to quickly understand the course correction. Long, complex steering messages may confuse the current context.
+
+4. **Don't over-steer** — Multiple rapid steering messages can degrade turn quality. If you need to change direction significantly, consider aborting the turn and starting fresh.
+
+5. **Show queue state in your UI** — Display the number of queued messages so users know what's pending. Listen for idle events to clear the display.
+
+6. **Handle the steering-to-queue fallback** — If a steering message arrives after the turn completes, it's automatically moved to the queue. Design your UI to reflect this transition.
+
+## See Also
+
+- [Getting Started](../getting-started.md) — Set up a session and send messages
+- [Custom Agents](./custom-agents.md) — Define specialized agents with scoped tools
+- [Session Hooks](../hooks/index.md) — React to session lifecycle events
+- [Session Persistence](./session-persistence.md) — Resume sessions across restarts
diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md
new file mode 100644
index 0000000000..d03ed95fa7
--- /dev/null
+++ b/docs/features/streaming-events.md
@@ -0,0 +1,791 @@
+# Streaming Session Events
+
+Every action the Copilot agent takes — thinking, writing code, running tools — is emitted as a **session event** you can subscribe to. This guide is a field-level reference for each event type so you know exactly what data to expect without reading the SDK source.
+
+## Overview
+
+When `streaming: true` is set on a session, the SDK emits **ephemeral** events in real time (deltas, progress updates) alongside **persisted** events (complete messages, tool results). All events share a common envelope and carry a `data` payload whose shape depends on the event `type`.
+
+```mermaid
+sequenceDiagram
+ participant App as Your App
+ participant SDK as SDK Session
+ participant Agent as Copilot Agent
+
+ App->>SDK: send({ prompt })
+ SDK->>Agent: JSON-RPC
+
+ Agent-->>SDK: assistant.turn_start
+ SDK-->>App: event
+
+ loop Streaming response
+ Agent-->>SDK: assistant.message_delta (ephemeral)
+ SDK-->>App: event
+ end
+
+ Agent-->>SDK: assistant.message
+ SDK-->>App: event
+
+ loop Tool execution
+ Agent-->>SDK: tool.execution_start
+ SDK-->>App: event
+ Agent-->>SDK: tool.execution_complete
+ SDK-->>App: event
+ end
+
+ Agent-->>SDK: assistant.turn_end
+ SDK-->>App: event
+
+ Agent-->>SDK: session.idle (ephemeral)
+ SDK-->>App: event
+```
+
+| Concept | Description |
+|---------|-------------|
+| **Ephemeral event** | Transient; streamed in real time but **not** persisted to the session log. Not replayed on session resume. |
+| **Persisted event** | Saved to the session event log on disk. Replayed when resuming a session. |
+| **Delta event** | An ephemeral streaming chunk (text or reasoning). Accumulate deltas to build the complete content. |
+| **`parentId` chain** | Each event's `parentId` points to the previous event, forming a linked list you can walk. |
+
+## Event Envelope
+
+Every session event, regardless of type, includes these fields:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | `string` (UUID v4) | Unique event identifier |
+| `timestamp` | `string` (ISO 8601) | When the event was created |
+| `parentId` | `string \| null` | ID of the previous event in the chain; `null` for the first event |
+| `ephemeral` | `boolean?` | `true` for transient events; absent or `false` for persisted events |
+| `type` | `string` | Event type discriminator (see tables below) |
+| `data` | `object` | Event-specific payload |
+
+## Subscribing to Events
+
+
+Node.js / TypeScript
+
+```typescript
+// All events
+session.on((event) => {
+ console.log(event.type, event.data);
+});
+
+// Specific event type — data is narrowed automatically
+session.on("assistant.message_delta", (event) => {
+ process.stdout.write(event.data.deltaContent);
+});
+```
+
+
+
+
+Python
+
+
+```python
+from copilot import CopilotClient
+from copilot.generated.session_events import SessionEventType
+
+client = CopilotClient()
+
+session = None # assume session is created elsewhere
+
+def handle(event):
+ if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
+ print(event.data.delta_content, end="", flush=True)
+
+# session.on(handle)
+```
+
+
+```python
+from copilot.generated.session_events import SessionEventType
+
+def handle(event):
+ if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
+ print(event.data.delta_content, end="", flush=True)
+
+session.on(handle)
+```
+
+
+
+
+Go
+
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ client := copilot.NewClient(nil)
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ Model: "gpt-4.1",
+ Streaming: true,
+ OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+ })
+
+ session.On(func(event copilot.SessionEvent) {
+ if event.Type == "assistant.message_delta" {
+ fmt.Print(*event.Data.DeltaContent)
+ }
+ })
+ _ = session
+}
+```
+
+
+```go
+session.On(func(event copilot.SessionEvent) {
+ if event.Type == "assistant.message_delta" {
+ fmt.Print(*event.Data.DeltaContent)
+ }
+})
+```
+
+
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class StreamingEventsExample
+{
+ public static async Task Example(CopilotSession session)
+ {
+ session.On(evt =>
+ {
+ if (evt is AssistantMessageDeltaEvent delta)
+ {
+ Console.Write(delta.Data.DeltaContent);
+ }
+ });
+ }
+}
+```
+
+
+```csharp
+session.On(evt =>
+{
+ if (evt is AssistantMessageDeltaEvent delta)
+ {
+ Console.Write(delta.Data.DeltaContent);
+ }
+});
+```
+
+
+
+> **Tip (Python / Go):** These SDKs use a single `Data` class/struct with all possible fields as optional/nullable. Only the fields listed in the tables below are populated for each event type — the rest will be `None` / `nil`.
+>
+> **Tip (.NET):** The .NET SDK uses separate, strongly-typed data classes per event (e.g., `AssistantMessageDeltaData`), so only the relevant fields exist on each type.
+>
+> **Tip (TypeScript):** The TypeScript SDK uses a discriminated union — when you match on `event.type`, the `data` payload is automatically narrowed to the correct shape.
+
+---
+
+## Assistant Events
+
+These events track the agent's response lifecycle — from turn start through streaming chunks to the final message.
+
+### `assistant.turn_start`
+
+Emitted when the agent begins processing a turn.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `turnId` | `string` | ✅ | Turn identifier (typically a stringified turn number) |
+| `interactionId` | `string` | | CAPI interaction ID for telemetry correlation |
+
+### `assistant.intent`
+
+Ephemeral. Short description of what the agent is currently doing, updated as it works.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `intent` | `string` | ✅ | Human-readable intent (e.g., "Exploring codebase") |
+
+### `assistant.reasoning`
+
+Complete extended thinking block from the model. Emitted after reasoning is finished.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `reasoningId` | `string` | ✅ | Unique identifier for this reasoning block |
+| `content` | `string` | ✅ | The complete extended thinking text |
+
+### `assistant.reasoning_delta`
+
+Ephemeral. Incremental chunk of the model's extended thinking, streamed in real time.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `reasoningId` | `string` | ✅ | Matches the corresponding `assistant.reasoning` event |
+| `deltaContent` | `string` | ✅ | Text chunk to append to reasoning content |
+
+### `assistant.message`
+
+The assistant's complete response for this LLM call. May include tool invocation requests.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `messageId` | `string` | ✅ | Unique identifier for this message |
+| `content` | `string` | ✅ | The assistant's text response |
+| `toolRequests` | `ToolRequest[]` | | Tool calls the assistant wants to make (see below) |
+| `reasoningOpaque` | `string` | | Encrypted extended thinking (Anthropic models); session-bound |
+| `reasoningText` | `string` | | Readable reasoning text from extended thinking |
+| `encryptedContent` | `string` | | Encrypted reasoning content (OpenAI models); session-bound |
+| `phase` | `string` | | Generation phase (e.g., `"thinking"` vs `"response"`) |
+| `outputTokens` | `number` | | Actual output token count from the API response |
+| `interactionId` | `string` | | CAPI interaction ID for telemetry |
+| `parentToolCallId` | `string` | | Set when this message originates from a sub-agent |
+
+**`ToolRequest` fields:**
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Unique ID for this tool call |
+| `name` | `string` | ✅ | Tool name (e.g., `"bash"`, `"edit"`, `"grep"`) |
+| `arguments` | `object` | | Parsed arguments for the tool |
+| `type` | `"function" \| "custom"` | | Call type; defaults to `"function"` when absent |
+
+### `assistant.message_delta`
+
+Ephemeral. Incremental chunk of the assistant's text response, streamed in real time.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `messageId` | `string` | ✅ | Matches the corresponding `assistant.message` event |
+| `deltaContent` | `string` | ✅ | Text chunk to append to the message |
+| `parentToolCallId` | `string` | | Set when originating from a sub-agent |
+
+### `assistant.turn_end`
+
+Emitted when the agent finishes a turn (all tool executions complete, final response delivered).
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `turnId` | `string` | ✅ | Matches the corresponding `assistant.turn_start` event |
+
+### `assistant.usage`
+
+Ephemeral. Token usage and cost information for an individual API call.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `model` | `string` | ✅ | Model identifier (e.g., `"gpt-4.1"`) |
+| `inputTokens` | `number` | | Input tokens consumed |
+| `outputTokens` | `number` | | Output tokens produced |
+| `cacheReadTokens` | `number` | | Tokens read from prompt cache |
+| `cacheWriteTokens` | `number` | | Tokens written to prompt cache |
+| `cost` | `number` | | Model multiplier cost for billing |
+| `duration` | `number` | | API call duration in milliseconds |
+| `initiator` | `string` | | What triggered this call (e.g., `"sub-agent"`); absent for user-initiated |
+| `apiCallId` | `string` | | Completion ID from the provider (e.g., `chatcmpl-abc123`) |
+| `providerCallId` | `string` | | GitHub request tracing ID (`x-github-request-id`) |
+| `parentToolCallId` | `string` | | Set when usage originates from a sub-agent |
+| `quotaSnapshots` | `Record` | | Per-quota resource usage, keyed by quota identifier |
+| `copilotUsage` | `CopilotUsage` | | Itemized token cost breakdown from the API |
+
+### `assistant.streaming_delta`
+
+Ephemeral. Low-level network progress indicator — total bytes received from the streaming API response.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `totalResponseSizeBytes` | `number` | ✅ | Cumulative bytes received so far |
+
+---
+
+## Tool Execution Events
+
+These events track the full lifecycle of each tool invocation — from the model requesting a tool call through execution to completion.
+
+### `tool.execution_start`
+
+Emitted when a tool begins executing.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Unique identifier for this tool call |
+| `toolName` | `string` | ✅ | Name of the tool (e.g., `"bash"`, `"edit"`, `"grep"`) |
+| `arguments` | `object` | | Parsed arguments passed to the tool |
+| `mcpServerName` | `string` | | MCP server name, when the tool is provided by an MCP server |
+| `mcpToolName` | `string` | | Original tool name on the MCP server |
+| `parentToolCallId` | `string` | | Set when invoked by a sub-agent |
+
+### `tool.execution_partial_result`
+
+Ephemeral. Incremental output from a running tool (e.g., streaming bash output).
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` |
+| `partialOutput` | `string` | ✅ | Incremental output chunk |
+
+### `tool.execution_progress`
+
+Ephemeral. Human-readable progress status from a running tool (e.g., MCP server progress notifications).
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` |
+| `progressMessage` | `string` | ✅ | Progress status message |
+
+### `tool.execution_complete`
+
+Emitted when a tool finishes executing — successfully or with an error.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` |
+| `success` | `boolean` | ✅ | Whether execution succeeded |
+| `model` | `string` | | Model that generated this tool call |
+| `interactionId` | `string` | | CAPI interaction ID |
+| `isUserRequested` | `boolean` | | `true` when the user explicitly requested this tool call |
+| `result` | `Result` | | Present on success (see below) |
+| `error` | `{ message, code? }` | | Present on failure |
+| `toolTelemetry` | `object` | | Tool-specific telemetry (e.g., CodeQL check counts) |
+| `parentToolCallId` | `string` | | Set when invoked by a sub-agent |
+
+**`Result` fields:**
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `content` | `string` | ✅ | Concise result sent to the LLM (may be truncated for token efficiency) |
+| `detailedContent` | `string` | | Full result for display, preserving complete content like diffs |
+| `contents` | `ContentBlock[]` | | Structured content blocks (text, terminal, image, audio, resource) |
+
+### `tool.user_requested`
+
+Emitted when the user explicitly requests a tool invocation (rather than the model choosing to call one).
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Unique identifier for this tool call |
+| `toolName` | `string` | ✅ | Name of the tool the user wants to invoke |
+| `arguments` | `object` | | Arguments for the invocation |
+
+---
+
+## Session Lifecycle Events
+
+### `session.idle`
+
+Ephemeral. The agent has finished all processing and is ready for the next message. This is the signal that a turn is fully complete.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `backgroundTasks` | `BackgroundTasks` | | Background agents/shells still running when the agent became idle |
+
+### `session.error`
+
+An error occurred during session processing.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `errorType` | `string` | ✅ | Error category (e.g., `"authentication"`, `"quota"`, `"rate_limit"`) |
+| `message` | `string` | ✅ | Human-readable error message |
+| `stack` | `string` | | Error stack trace |
+| `statusCode` | `number` | | HTTP status code from the upstream request |
+| `providerCallId` | `string` | | GitHub request tracing ID for server-side log correlation |
+
+### `session.compaction_start`
+
+Context window compaction has begun. **Data payload is empty (`{}`)**.
+
+### `session.compaction_complete`
+
+Context window compaction finished.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `success` | `boolean` | ✅ | Whether compaction succeeded |
+| `error` | `string` | | Error message if compaction failed |
+| `preCompactionTokens` | `number` | | Tokens before compaction |
+| `postCompactionTokens` | `number` | | Tokens after compaction |
+| `preCompactionMessagesLength` | `number` | | Message count before compaction |
+| `messagesRemoved` | `number` | | Messages removed |
+| `tokensRemoved` | `number` | | Tokens removed |
+| `summaryContent` | `string` | | LLM-generated summary of compacted history |
+| `checkpointNumber` | `number` | | Checkpoint snapshot number created for recovery |
+| `checkpointPath` | `string` | | File path where the checkpoint was stored |
+| `compactionTokensUsed` | `{ input, output, cachedInput }` | | Token usage for the compaction LLM call |
+| `requestId` | `string` | | GitHub request tracing ID for the compaction call |
+
+### `session.title_changed`
+
+Ephemeral. The session's auto-generated title was updated.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `title` | `string` | ✅ | New session title |
+
+### `session.context_changed`
+
+The session's working directory or repository context changed.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `cwd` | `string` | ✅ | Current working directory |
+| `gitRoot` | `string` | | Git repository root |
+| `repository` | `string` | | Repository in `"owner/name"` format |
+| `branch` | `string` | | Current git branch |
+
+### `session.usage_info`
+
+Ephemeral. Context window utilization snapshot.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `tokenLimit` | `number` | ✅ | Maximum tokens for the model's context window |
+| `currentTokens` | `number` | ✅ | Current tokens in the context window |
+| `messagesLength` | `number` | ✅ | Current message count in the conversation |
+
+### `session.task_complete`
+
+The agent has completed its assigned task.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `summary` | `string` | | Summary of the completed task |
+
+### `session.shutdown`
+
+The session has ended.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `shutdownType` | `"routine" \| "error"` | ✅ | Normal shutdown or crash |
+| `errorReason` | `string` | | Error description when `shutdownType` is `"error"` |
+| `totalPremiumRequests` | `number` | ✅ | Total premium API requests used |
+| `totalApiDurationMs` | `number` | ✅ | Cumulative API call time in milliseconds |
+| `sessionStartTime` | `number` | ✅ | Unix timestamp (ms) when the session started |
+| `codeChanges` | `{ linesAdded, linesRemoved, filesModified }` | ✅ | Aggregate code change metrics |
+| `modelMetrics` | `Record` | ✅ | Per-model usage breakdown |
+| `currentModel` | `string` | | Model selected at shutdown time |
+
+---
+
+## Permission & User Input Events
+
+These events are emitted when the agent needs approval or input from the user before continuing.
+
+### `permission.requested`
+
+Ephemeral. The agent needs permission to perform an action (run a command, write a file, etc.).
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Use this to respond via `session.respondToPermission()` |
+| `permissionRequest` | `PermissionRequest` | ✅ | Details of the permission being requested |
+
+The `permissionRequest` is a discriminated union on `kind`:
+
+| `kind` | Key Fields | Description |
+|--------|------------|-------------|
+| `"shell"` | `fullCommandText`, `intention`, `commands[]`, `possiblePaths[]` | Execute a shell command |
+| `"write"` | `fileName`, `diff`, `intention`, `newFileContents?` | Write/modify a file |
+| `"read"` | `path`, `intention` | Read a file or directory |
+| `"mcp"` | `serverName`, `toolName`, `toolTitle`, `args?`, `readOnly` | Call an MCP tool |
+| `"url"` | `url`, `intention` | Fetch a URL |
+| `"memory"` | `subject`, `fact`, `citations` | Store a memory |
+| `"custom-tool"` | `toolName`, `toolDescription`, `args?` | Call a custom tool |
+
+All `kind` variants also include an optional `toolCallId` linking back to the tool call that triggered the request.
+
+### `permission.completed`
+
+Ephemeral. A permission request was resolved.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Matches the corresponding `permission.requested` |
+| `result.kind` | `string` | ✅ | One of: `"approved"`, `"denied-by-rules"`, `"denied-interactively-by-user"`, `"denied-no-approval-rule-and-could-not-request-from-user"`, `"denied-by-content-exclusion-policy"` |
+
+### `user_input.requested`
+
+Ephemeral. The agent is asking the user a question.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Use this to respond via `session.respondToUserInput()` |
+| `question` | `string` | ✅ | The question to present to the user |
+| `choices` | `string[]` | | Predefined choices for the user |
+| `allowFreeform` | `boolean` | | Whether free-form text input is allowed |
+
+### `user_input.completed`
+
+Ephemeral. A user input request was resolved.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Matches the corresponding `user_input.requested` |
+
+### `elicitation.requested`
+
+Ephemeral. The agent needs structured form input from the user (MCP elicitation protocol).
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Use this to respond via `session.respondToElicitation()` |
+| `message` | `string` | ✅ | Description of what information is needed |
+| `mode` | `"form"` | | Elicitation mode (currently only `"form"`) |
+| `requestedSchema` | `{ type: "object", properties, required? }` | ✅ | JSON Schema describing the form fields |
+
+### `elicitation.completed`
+
+Ephemeral. An elicitation request was resolved.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Matches the corresponding `elicitation.requested` |
+
+---
+
+## Sub-Agent & Skill Events
+
+### `subagent.started`
+
+A custom agent was invoked as a sub-agent.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Parent tool call that spawned this sub-agent |
+| `agentName` | `string` | ✅ | Internal name of the sub-agent |
+| `agentDisplayName` | `string` | ✅ | Human-readable display name |
+| `agentDescription` | `string` | ✅ | Description of what the sub-agent does |
+
+### `subagent.completed`
+
+A sub-agent finished successfully.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Matches the corresponding `subagent.started` |
+| `agentName` | `string` | ✅ | Internal name |
+| `agentDisplayName` | `string` | ✅ | Display name |
+
+### `subagent.failed`
+
+A sub-agent encountered an error.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `toolCallId` | `string` | ✅ | Matches the corresponding `subagent.started` |
+| `agentName` | `string` | ✅ | Internal name |
+| `agentDisplayName` | `string` | ✅ | Display name |
+| `error` | `string` | ✅ | Error message |
+
+### `subagent.selected`
+
+A custom agent was selected (inferred) to handle the current request.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `agentName` | `string` | ✅ | Internal name of the selected agent |
+| `agentDisplayName` | `string` | ✅ | Display name |
+| `tools` | `string[] \| null` | ✅ | Tool names available to this agent; `null` for all tools |
+
+### `subagent.deselected`
+
+A custom agent was deselected, returning to the default agent. **Data payload is empty (`{}`)**.
+
+### `skill.invoked`
+
+A skill was activated for the current conversation.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `name` | `string` | ✅ | Skill name |
+| `path` | `string` | ✅ | File path to the SKILL.md definition |
+| `content` | `string` | ✅ | Full skill content injected into the conversation |
+| `allowedTools` | `string[]` | | Tools auto-approved while this skill is active |
+| `pluginName` | `string` | | Plugin the skill originated from |
+| `pluginVersion` | `string` | | Plugin version |
+
+---
+
+## Other Events
+
+### `abort`
+
+The current turn was aborted.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `reason` | `string` | ✅ | Why the turn was aborted (e.g., `"user initiated"`) |
+
+### `user.message`
+
+The user sent a message. Recorded for the session timeline.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `content` | `string` | ✅ | The user's message text |
+| `transformedContent` | `string` | | Transformed version after preprocessing |
+| `attachments` | `Attachment[]` | | File, directory, selection, blob, or GitHub reference attachments |
+| `source` | `string` | | Message source identifier |
+| `agentMode` | `string` | | Agent mode: `"interactive"`, `"plan"`, `"autopilot"`, or `"shell"` |
+| `interactionId` | `string` | | CAPI interaction ID |
+
+### `system.message`
+
+A system or developer prompt was injected into the conversation.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `content` | `string` | ✅ | The prompt text |
+| `role` | `"system" \| "developer"` | ✅ | Message role |
+| `name` | `string` | | Source identifier |
+| `metadata` | `{ promptVersion?, variables? }` | | Prompt template metadata |
+
+### `external_tool.requested`
+
+Ephemeral. The agent wants to invoke an external tool (one provided by the SDK consumer).
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Use this to respond via `session.respondToExternalTool()` |
+| `sessionId` | `string` | ✅ | Session this request belongs to |
+| `toolCallId` | `string` | ✅ | Tool call ID for this invocation |
+| `toolName` | `string` | ✅ | Name of the external tool |
+| `arguments` | `object` | | Arguments for the tool |
+
+### `external_tool.completed`
+
+Ephemeral. An external tool request was resolved.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Matches the corresponding `external_tool.requested` |
+
+### `exit_plan_mode.requested`
+
+Ephemeral. The agent has created a plan and wants to exit plan mode.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Use this to respond via `session.respondToExitPlanMode()` |
+| `summary` | `string` | ✅ | Summary of the plan |
+| `planContent` | `string` | ✅ | Full plan file content |
+| `actions` | `string[]` | ✅ | Available user actions (e.g., approve, edit, reject) |
+| `recommendedAction` | `string` | ✅ | Suggested action |
+
+### `exit_plan_mode.completed`
+
+Ephemeral. An exit plan mode request was resolved.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Matches the corresponding `exit_plan_mode.requested` |
+
+### `command.queued`
+
+Ephemeral. A slash command was queued for execution.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Use this to respond via `session.respondToQueuedCommand()` |
+| `command` | `string` | ✅ | The slash command text (e.g., `/help`, `/clear`) |
+
+### `command.completed`
+
+Ephemeral. A queued command was resolved.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Matches the corresponding `command.queued` |
+
+---
+
+## Quick Reference: Agentic Turn Flow
+
+A typical agentic turn emits events in this order:
+
+```
+assistant.turn_start → Turn begins
+├── assistant.intent → What the agent plans to do (ephemeral)
+├── assistant.reasoning_delta → Streaming thinking chunks (ephemeral, repeated)
+├── assistant.reasoning → Complete thinking block
+├── assistant.message_delta → Streaming response chunks (ephemeral, repeated)
+├── assistant.message → Complete response (may include toolRequests)
+├── assistant.usage → Token usage for this API call (ephemeral)
+│
+├── [If tools were requested:]
+│ ├── permission.requested → Needs user approval (ephemeral)
+│ ├── permission.completed → Approval result (ephemeral)
+│ ├── tool.execution_start → Tool begins
+│ ├── tool.execution_partial_result → Streaming tool output (ephemeral, repeated)
+│ ├── tool.execution_progress → Progress updates (ephemeral, repeated)
+│ ├── tool.execution_complete → Tool finished
+│ │
+│ └── [Agent loops: more reasoning → message → tool calls...]
+│
+assistant.turn_end → Turn complete
+session.idle → Ready for next message (ephemeral)
+```
+
+## All Event Types at a Glance
+
+| Event Type | Ephemeral | Category | Key Data Fields |
+|------------|-----------|----------|-----------------|
+| `assistant.turn_start` | | Assistant | `turnId`, `interactionId?` |
+| `assistant.intent` | ✅ | Assistant | `intent` |
+| `assistant.reasoning` | | Assistant | `reasoningId`, `content` |
+| `assistant.reasoning_delta` | ✅ | Assistant | `reasoningId`, `deltaContent` |
+| `assistant.streaming_delta` | ✅ | Assistant | `totalResponseSizeBytes` |
+| `assistant.message` | | Assistant | `messageId`, `content`, `toolRequests?`, `outputTokens?`, `phase?` |
+| `assistant.message_delta` | ✅ | Assistant | `messageId`, `deltaContent`, `parentToolCallId?` |
+| `assistant.turn_end` | | Assistant | `turnId` |
+| `assistant.usage` | ✅ | Assistant | `model`, `inputTokens?`, `outputTokens?`, `cost?`, `duration?` |
+| `tool.user_requested` | | Tool | `toolCallId`, `toolName`, `arguments?` |
+| `tool.execution_start` | | Tool | `toolCallId`, `toolName`, `arguments?`, `mcpServerName?` |
+| `tool.execution_partial_result` | ✅ | Tool | `toolCallId`, `partialOutput` |
+| `tool.execution_progress` | ✅ | Tool | `toolCallId`, `progressMessage` |
+| `tool.execution_complete` | | Tool | `toolCallId`, `success`, `result?`, `error?` |
+| `session.idle` | ✅ | Session | `backgroundTasks?` |
+| `session.error` | | Session | `errorType`, `message`, `statusCode?` |
+| `session.compaction_start` | | Session | *(empty)* |
+| `session.compaction_complete` | | Session | `success`, `preCompactionTokens?`, `summaryContent?` |
+| `session.title_changed` | ✅ | Session | `title` |
+| `session.context_changed` | | Session | `cwd`, `gitRoot?`, `repository?`, `branch?` |
+| `session.usage_info` | ✅ | Session | `tokenLimit`, `currentTokens`, `messagesLength` |
+| `session.task_complete` | | Session | `summary?` |
+| `session.shutdown` | | Session | `shutdownType`, `codeChanges`, `modelMetrics` |
+| `permission.requested` | ✅ | Permission | `requestId`, `permissionRequest` |
+| `permission.completed` | ✅ | Permission | `requestId`, `result.kind` |
+| `user_input.requested` | ✅ | User Input | `requestId`, `question`, `choices?` |
+| `user_input.completed` | ✅ | User Input | `requestId` |
+| `elicitation.requested` | ✅ | User Input | `requestId`, `message`, `requestedSchema` |
+| `elicitation.completed` | ✅ | User Input | `requestId` |
+| `subagent.started` | | Sub-Agent | `toolCallId`, `agentName`, `agentDisplayName` |
+| `subagent.completed` | | Sub-Agent | `toolCallId`, `agentName`, `agentDisplayName` |
+| `subagent.failed` | | Sub-Agent | `toolCallId`, `agentName`, `error` |
+| `subagent.selected` | | Sub-Agent | `agentName`, `agentDisplayName`, `tools` |
+| `subagent.deselected` | | Sub-Agent | *(empty)* |
+| `skill.invoked` | | Skill | `name`, `path`, `content`, `allowedTools?` |
+| `abort` | | Control | `reason` |
+| `user.message` | | User | `content`, `attachments?`, `agentMode?` |
+| `system.message` | | System | `content`, `role` |
+| `external_tool.requested` | ✅ | External Tool | `requestId`, `toolName`, `arguments?` |
+| `external_tool.completed` | ✅ | External Tool | `requestId` |
+| `command.queued` | ✅ | Command | `requestId`, `command` |
+| `command.completed` | ✅ | Command | `requestId` |
+| `exit_plan_mode.requested` | ✅ | Plan Mode | `requestId`, `summary`, `planContent`, `actions` |
+| `exit_plan_mode.completed` | ✅ | Plan Mode | `requestId` |
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 05bbde8dc6..9d4189f561 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -129,15 +129,16 @@ Create `main.py`:
```python
import asyncio
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session({"model": "gpt-4.1"})
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1")
response = await session.send_and_wait({"prompt": "What is 2 + 2?"})
+ response = await session.send_and_wait({"prompt": "What is 2 + 2?"})
print(response.data.content)
await client.stop()
@@ -274,17 +275,14 @@ Update `main.py`:
```python
import asyncio
import sys
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
from copilot.generated.session_events import SessionEventType
async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session({
- "model": "gpt-4.1",
- "streaming": True,
- })
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True)
# Listen for response chunks
def handle_event(event):
@@ -433,7 +431,7 @@ from copilot.generated.session_events import SessionEvent, SessionEventType
client = CopilotClient()
-session = client.create_session({"on_permission_request": lambda req, inv: {"kind": "approved"}})
+session = client.create_session(on_permission_request=lambda req, inv: {"kind": "approved"})
# Subscribe to all events
unsubscribe = session.on(lambda event: print(f"Event: {event.type}"))
@@ -653,7 +651,7 @@ Update `main.py`:
import asyncio
import random
import sys
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
from copilot.tools import define_tool
from copilot.generated.session_events import SessionEventType
from pydantic import BaseModel, Field
@@ -676,11 +674,7 @@ async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session({
- "model": "gpt-4.1",
- "streaming": True,
- "tools": [get_weather],
- })
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True, tools=[get_weather])
def handle_event(event):
if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
@@ -925,7 +919,7 @@ Create `weather_assistant.py`:
import asyncio
import random
import sys
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
from copilot.tools import define_tool
from copilot.generated.session_events import SessionEventType
from pydantic import BaseModel, Field
@@ -945,11 +939,7 @@ async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session({
- "model": "gpt-4.1",
- "streaming": True,
- "tools": [get_weather],
- })
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True, tools=[get_weather])
def handle_event(event):
if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
@@ -1224,7 +1214,7 @@ const session = await client.createSession({
});
```
-📖 **[Full MCP documentation →](./mcp/overview.md)** - Learn about local vs remote servers, all configuration options, and troubleshooting.
+📖 **[Full MCP documentation →](./features/mcp.md)** - Learn about local vs remote servers, all configuration options, and troubleshooting.
### Create Custom Agents
@@ -1241,9 +1231,11 @@ const session = await client.createSession({
});
```
+> **Tip:** You can also set `agent: "pr-reviewer"` in the session config to pre-select this agent from the start. See the [Custom Agents guide](./guides/custom-agents.md#selecting-an-agent-at-session-creation) for details.
+
### Customize the System Message
-Control the AI's behavior and personality:
+Control the AI's behavior and personality by appending instructions:
```typescript
const session = await client.createSession({
@@ -1253,6 +1245,28 @@ const session = await client.createSession({
});
```
+For more fine-grained control, use `mode: "customize"` to override individual sections of the system prompt while preserving the rest:
+
+```typescript
+const session = await client.createSession({
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." },
+ code_change_rules: { action: "remove" },
+ guidelines: { action: "append", content: "\n* Always cite data sources" },
+ },
+ content: "Focus on financial analysis and reporting.",
+ },
+});
+```
+
+Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`.
+
+Each override supports four actions: `replace`, `remove`, `append`, and `prepend`. Unknown section IDs are handled gracefully — content is appended to additional instructions and a warning is emitted; `remove` on unknown sections is silently ignored.
+
+See the language-specific SDK READMEs for examples in [TypeScript](../nodejs/README.md), [Python](../python/README.md), [Go](../go/README.md), and [C#](../dotnet/README.md).
+
---
## Connecting to an External CLI Server
@@ -1306,7 +1320,7 @@ client = CopilotClient({
await client.start()
# Use the client normally
-session = await client.create_session({"on_permission_request": PermissionHandler.approve_all})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all)
# ...
```
@@ -1393,6 +1407,119 @@ await using var session = await client.CreateSessionAsync(new()
---
+## Telemetry & Observability
+
+The Copilot SDK supports [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. Provide a `telemetry` configuration to the client to enable trace export from the CLI process and automatic [W3C Trace Context](https://www.w3.org/TR/trace-context/) propagation between the SDK and CLI.
+
+### Enabling Telemetry
+
+Pass a `telemetry` (or `Telemetry`) config when creating the client. This is the opt-in — no separate "enabled" flag is needed.
+
+
+Node.js / TypeScript
+
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient({
+ telemetry: {
+ otlpEndpoint: "http://localhost:4318",
+ },
+});
+```
+
+Optional peer dependency: `@opentelemetry/api`
+
+
+
+
+Python
+
+
+```python
+from copilot import CopilotClient, SubprocessConfig
+
+client = CopilotClient(SubprocessConfig(
+ telemetry={
+ "otlp_endpoint": "http://localhost:4318",
+ },
+))
+```
+
+Install with telemetry extras: `pip install copilot-sdk[telemetry]` (provides `opentelemetry-api`)
+
+
+
+
+Go
+
+
+```go
+client, err := copilot.NewClient(copilot.ClientOptions{
+ Telemetry: &copilot.TelemetryConfig{
+ OTLPEndpoint: "http://localhost:4318",
+ },
+})
+```
+
+Dependency: `go.opentelemetry.io/otel`
+
+
+
+
+.NET
+
+
+```csharp
+var client = new CopilotClient(new CopilotClientOptions
+{
+ Telemetry = new TelemetryConfig
+ {
+ OtlpEndpoint = "http://localhost:4318",
+ },
+});
+```
+
+No extra dependencies — uses built-in `System.Diagnostics.Activity`.
+
+
+
+### TelemetryConfig Options
+
+| Option | Node.js | Python | Go | .NET | Description |
+|---|---|---|---|---|---|
+| OTLP endpoint | `otlpEndpoint` | `otlp_endpoint` | `OTLPEndpoint` | `OtlpEndpoint` | OTLP HTTP endpoint URL |
+| File path | `filePath` | `file_path` | `FilePath` | `FilePath` | File path for JSON-lines trace output |
+| Exporter type | `exporterType` | `exporter_type` | `ExporterType` | `ExporterType` | `"otlp-http"` or `"file"` |
+| Source name | `sourceName` | `source_name` | `SourceName` | `SourceName` | Instrumentation scope name |
+| Capture content | `captureContent` | `capture_content` | `CaptureContent` | `CaptureContent` | Whether to capture message content |
+
+### File Export
+
+To write traces to a local file instead of an OTLP endpoint:
+
+
+```typescript
+const client = new CopilotClient({
+ telemetry: {
+ filePath: "./traces.jsonl",
+ exporterType: "file",
+ },
+});
+```
+
+### Trace Context Propagation
+
+Trace context is propagated automatically — no manual instrumentation is needed:
+
+- **SDK → CLI**: `traceparent` and `tracestate` headers from the current span/activity are included in `session.create`, `session.resume`, and `session.send` RPC calls.
+- **CLI → SDK**: When the CLI invokes tool handlers, the trace context from the CLI's span is propagated so your tool code runs under the correct parent span.
+
+📖 **[OpenTelemetry Instrumentation Guide →](./observability/opentelemetry.md)** — TelemetryConfig options, trace context propagation, and per-language dependencies.
+
+---
+
## Learn More
- [Authentication Guide](./auth/index.md) - GitHub OAuth, environment variables, and BYOK
@@ -1401,9 +1528,10 @@ await using var session = await client.CreateSessionAsync(new()
- [Python SDK Reference](../python/README.md)
- [Go SDK Reference](../go/README.md)
- [.NET SDK Reference](../dotnet/README.md)
-- [Using MCP Servers](./mcp) - Integrate external tools via Model Context Protocol
+- [Using MCP Servers](./features/mcp.md) - Integrate external tools via Model Context Protocol
- [GitHub MCP Server Documentation](https://github.com/github/github-mcp-server)
- [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - Explore more MCP servers
+- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) - TelemetryConfig, trace context propagation, and per-language dependencies
---
diff --git a/docs/guides/setup/byok.md b/docs/guides/setup/byok.md
deleted file mode 100644
index 5b8b8a460c..0000000000
--- a/docs/guides/setup/byok.md
+++ /dev/null
@@ -1,360 +0,0 @@
-# BYOK (Bring Your Own Key) Setup
-
-Use your own model provider API keys instead of GitHub Copilot authentication. You control the identity layer, the model provider, and the billing — the SDK provides the agent runtime.
-
-**Best for:** Apps where users don't have GitHub accounts, enterprise deployments with existing model provider contracts, apps needing full control over identity and billing.
-
-## How It Works
-
-With BYOK, the SDK uses the Copilot CLI as an agent runtime only — it doesn't call GitHub's Copilot API. Instead, model requests go directly to your configured provider (OpenAI, Azure AI Foundry, Anthropic, etc.).
-
-```mermaid
-flowchart LR
- subgraph App["Your Application"]
- SDK["SDK Client"]
- IdP["Your Identity
Provider"]
- end
-
- subgraph CLI["Copilot CLI"]
- Runtime["Agent Runtime"]
- end
-
- subgraph Provider["Your Model Provider"]
- API["OpenAI / Azure /
Anthropic / Ollama"]
- end
-
- IdP -.->|"authenticates
users"| SDK
- SDK --> Runtime
- Runtime -- "API key" --> API
-
- style App fill:#0d1117,stroke:#58a6ff,color:#c9d1d9
- style CLI fill:#0d1117,stroke:#3fb950,color:#c9d1d9
- style Provider fill:#161b22,stroke:#f0883e,color:#c9d1d9
-```
-
-**Key characteristics:**
-- No GitHub Copilot subscription needed
-- No GitHub account needed for end users
-- You manage authentication and identity yourself
-- Model requests go to your provider, billed to your account
-- Full agent runtime capabilities (tools, sessions, streaming) still work
-
-## Architecture: GitHub Auth vs. BYOK
-
-```mermaid
-flowchart TB
- subgraph GitHub["GitHub Auth Path"]
- direction LR
- G1["User"] --> G2["GitHub OAuth"]
- G2 --> G3["SDK + CLI"]
- G3 --> G4["☁️ Copilot API"]
- end
-
- subgraph BYOK["BYOK Path"]
- direction LR
- B1["User"] --> B2["Your Auth"]
- B2 --> B3["SDK + CLI"]
- B3 --> B4["☁️ Your Provider"]
- end
-
- style GitHub fill:#161b22,stroke:#8b949e,color:#c9d1d9
- style BYOK fill:#0d1117,stroke:#3fb950,color:#c9d1d9
-```
-
-## Quick Start
-
-
-Node.js / TypeScript
-
-```typescript
-import { CopilotClient } from "@github/copilot-sdk";
-
-const client = new CopilotClient();
-
-const session = await client.createSession({
- model: "gpt-4.1",
- provider: {
- type: "openai",
- baseUrl: "https://api.openai.com/v1",
- apiKey: process.env.OPENAI_API_KEY,
- },
-});
-
-const response = await session.sendAndWait({ prompt: "Hello!" });
-console.log(response?.data.content);
-
-await client.stop();
-```
-
-
-
-
-Python
-
-```python
-import os
-from copilot import CopilotClient
-
-client = CopilotClient()
-await client.start()
-
-session = await client.create_session({
- "model": "gpt-4.1",
- "provider": {
- "type": "openai",
- "base_url": "https://api.openai.com/v1",
- "api_key": os.environ["OPENAI_API_KEY"],
- },
-})
-
-response = await session.send_and_wait({"prompt": "Hello!"})
-print(response.data.content)
-
-await client.stop()
-```
-
-
-
-
-Go
-
-
-```go
-client := copilot.NewClient(nil)
-client.Start(ctx)
-defer client.Stop()
-
-session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
- Model: "gpt-4.1",
- Provider: &copilot.ProviderConfig{
- Type: "openai",
- BaseURL: "https://api.openai.com/v1",
- APIKey: os.Getenv("OPENAI_API_KEY"),
- },
-})
-
-response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"})
-fmt.Println(*response.Data.Content)
-```
-
-
-
-
-.NET
-
-```csharp
-await using var client = new CopilotClient();
-await using var session = await client.CreateSessionAsync(new SessionConfig
-{
- Model = "gpt-4.1",
- Provider = new ProviderConfig
- {
- Type = "openai",
- BaseUrl = "https://api.openai.com/v1",
- ApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"),
- },
-});
-
-var response = await session.SendAndWaitAsync(
- new MessageOptions { Prompt = "Hello!" });
-Console.WriteLine(response?.Data.Content);
-```
-
-
-
-## Provider Configurations
-
-### OpenAI
-
-```typescript
-provider: {
- type: "openai",
- baseUrl: "https://api.openai.com/v1",
- apiKey: process.env.OPENAI_API_KEY,
-}
-```
-
-### Azure AI Foundry
-
-```typescript
-provider: {
- type: "openai",
- baseUrl: "https://your-resource.openai.azure.com/openai/v1/",
- apiKey: process.env.FOUNDRY_API_KEY,
- wireApi: "responses", // For GPT-5 series models
-}
-```
-
-### Azure OpenAI (Native)
-
-```typescript
-provider: {
- type: "azure",
- baseUrl: "https://your-resource.openai.azure.com",
- apiKey: process.env.AZURE_OPENAI_KEY,
- azure: { apiVersion: "2024-10-21" },
-}
-```
-
-### Anthropic
-
-```typescript
-provider: {
- type: "anthropic",
- baseUrl: "https://api.anthropic.com",
- apiKey: process.env.ANTHROPIC_API_KEY,
-}
-```
-
-### Ollama (Local)
-
-```typescript
-provider: {
- type: "openai",
- baseUrl: "http://localhost:11434/v1",
- // No API key needed for local Ollama
-}
-```
-
-## Managing Identity Yourself
-
-With BYOK, you're responsible for authentication. Here are common patterns:
-
-### Pattern 1: Your Own Identity Provider
-
-```mermaid
-sequenceDiagram
- participant User
- participant App as Your App
- participant IdP as Your Identity Provider
- participant SDK as SDK + CLI
- participant LLM as Model Provider
-
- User->>App: Login
- App->>IdP: Authenticate user
- IdP-->>App: User identity + permissions
-
- App->>App: Look up API key for user's tier
- App->>SDK: Create session (with provider config)
- SDK->>LLM: Model request (your API key)
- LLM-->>SDK: Response
- SDK-->>App: Result
- App-->>User: Display
-```
-
-```typescript
-// Your app handles auth, then creates sessions with your API key
-app.post("/chat", authMiddleware, async (req, res) => {
- const user = req.user; // From your auth middleware
-
- // Use your API key — not the user's
- const session = await getOrCreateSession(user.id, {
- model: getModelForTier(user.tier), // "gpt-4.1" for pro, etc.
- provider: {
- type: "openai",
- baseUrl: "https://api.openai.com/v1",
- apiKey: process.env.OPENAI_API_KEY, // Your key, your billing
- },
- });
-
- const response = await session.sendAndWait({ prompt: req.body.message });
- res.json({ content: response?.data.content });
-});
-```
-
-### Pattern 2: Per-Customer API Keys
-
-For B2B apps where each customer brings their own model provider keys:
-
-```mermaid
-flowchart TB
- subgraph Customers
- C1["Customer A
(OpenAI key)"]
- C2["Customer B
(Azure key)"]
- C3["Customer C
(Anthropic key)"]
- end
-
- subgraph App["Your App"]
- Router["Request Router"]
- KS["Key Store
(encrypted)"]
- end
-
- C1 --> Router
- C2 --> Router
- C3 --> Router
-
- Router --> KS
- KS --> SDK1["SDK → OpenAI"]
- KS --> SDK2["SDK → Azure"]
- KS --> SDK3["SDK → Anthropic"]
-
- style App fill:#0d1117,stroke:#58a6ff,color:#c9d1d9
-```
-
-```typescript
-async function createSessionForCustomer(customerId: string) {
- const config = await keyStore.getProviderConfig(customerId);
-
- return client.createSession({
- sessionId: `customer-${customerId}-${Date.now()}`,
- model: config.model,
- provider: {
- type: config.providerType,
- baseUrl: config.baseUrl,
- apiKey: config.apiKey,
- },
- });
-}
-```
-
-## Session Persistence with BYOK
-
-When resuming BYOK sessions, you **must** re-provide the provider configuration. API keys are never persisted to disk for security.
-
-```typescript
-// Create session
-const session = await client.createSession({
- sessionId: "task-123",
- model: "gpt-4.1",
- provider: {
- type: "openai",
- baseUrl: "https://api.openai.com/v1",
- apiKey: process.env.OPENAI_API_KEY,
- },
-});
-
-// Resume later — must re-provide provider config
-const resumed = await client.resumeSession("task-123", {
- provider: {
- type: "openai",
- baseUrl: "https://api.openai.com/v1",
- apiKey: process.env.OPENAI_API_KEY, // Required again
- },
-});
-```
-
-## Limitations
-
-| Limitation | Details |
-|------------|---------|
-| **Static credentials only** | API keys or bearer tokens — no native Entra ID, OIDC, or managed identity support. See [Azure Managed Identity workaround](./azure-managed-identity.md) for using `DefaultAzureCredential` with short-lived tokens. |
-| **No auto-refresh** | If a bearer token expires, you must create a new session |
-| **Your billing** | All model usage is billed to your provider account |
-| **Model availability** | Limited to what your provider offers |
-| **Keys not persisted** | Must re-provide on session resume |
-
-For the full BYOK reference, see the **[BYOK documentation](../../auth/byok.md)**.
-
-## When to Move On
-
-| Need | Next Guide |
-|------|-----------|
-| Run the SDK on a server | [Backend Services](./backend-services.md) |
-| Multiple users with GitHub accounts | [GitHub OAuth](./github-oauth.md) |
-| Handle many concurrent users | [Scaling & Multi-Tenancy](./scaling.md) |
-
-## Next Steps
-
-- **[BYOK reference](../../auth/byok.md)** — Full provider config details and troubleshooting
-- **[Backend Services](./backend-services.md)** — Deploy the SDK server-side
-- **[Scaling & Multi-Tenancy](./scaling.md)** — Serve many customers at scale
diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md
index 0f705868df..a67906ac94 100644
--- a/docs/hooks/error-handling.md
+++ b/docs/hooks/error-handling.md
@@ -12,7 +12,15 @@ The `onErrorOccurred` hook is called when errors occur during session execution.
Node.js / TypeScript
-
+
+```ts
+import type { ErrorOccurredHookInput, HookInvocation, ErrorOccurredHookOutput } from "@github/copilot-sdk";
+type ErrorOccurredHandler = (
+ input: ErrorOccurredHookInput,
+ invocation: HookInvocation
+) => Promise;
+```
+
```typescript
type ErrorOccurredHandler = (
input: ErrorOccurredHookInput,
@@ -25,7 +33,17 @@ type ErrorOccurredHandler = (
Python
-
+
+```python
+from copilot.types import ErrorOccurredHookInput, HookInvocation, ErrorOccurredHookOutput
+from typing import Callable, Awaitable
+
+ErrorOccurredHandler = Callable[
+ [ErrorOccurredHookInput, HookInvocation],
+ Awaitable[ErrorOccurredHookOutput | None]
+]
+```
+
```python
ErrorOccurredHandler = Callable[
[ErrorOccurredHookInput, HookInvocation],
@@ -38,7 +56,20 @@ ErrorOccurredHandler = Callable[
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+type ErrorOccurredHandler func(
+ input copilot.ErrorOccurredHookInput,
+ invocation copilot.HookInvocation,
+) (*copilot.ErrorOccurredHookOutput, error)
+
+func main() {}
+```
+
```go
type ErrorOccurredHandler func(
input ErrorOccurredHookInput,
@@ -51,7 +82,15 @@ type ErrorOccurredHandler func(
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public delegate Task ErrorOccurredHandler(
+ ErrorOccurredHookInput input,
+ HookInvocation invocation);
+```
+
```csharp
public delegate Task ErrorOccurredHandler(
ErrorOccurredHookInput input,
@@ -107,15 +146,15 @@ const session = await client.createSession({
Python
```python
+from copilot import PermissionHandler
+
async def on_error_occurred(input_data, invocation):
print(f"[{invocation['session_id']}] Error: {input_data['error']}")
print(f" Context: {input_data['errorContext']}")
print(f" Recoverable: {input_data['recoverable']}")
return None
-session = await client.create_session({
- "hooks": {"on_error_occurred": on_error_occurred}
-})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_error_occurred": on_error_occurred})
```
@@ -123,7 +162,33 @@ session = await client.create_session({
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ client := copilot.NewClient(nil)
+ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ Hooks: &copilot.SessionHooks{
+ OnErrorOccurred: func(input copilot.ErrorOccurredHookInput, inv copilot.HookInvocation) (*copilot.ErrorOccurredHookOutput, error) {
+ fmt.Printf("[%s] Error: %s\n", inv.SessionID, input.Error)
+ fmt.Printf(" Context: %s\n", input.ErrorContext)
+ fmt.Printf(" Recoverable: %v\n", input.Recoverable)
+ return nil, nil
+ },
+ },
+ })
+ _ = session
+}
+```
+
```go
session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
Hooks: &copilot.SessionHooks{
@@ -142,7 +207,32 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class ErrorHandlingExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+ var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Hooks = new SessionHooks
+ {
+ OnErrorOccurred = (input, invocation) =>
+ {
+ Console.Error.WriteLine($"[{invocation.SessionId}] Error: {input.Error}");
+ Console.Error.WriteLine($" Context: {input.ErrorContext}");
+ Console.Error.WriteLine($" Recoverable: {input.Recoverable}");
+ return Task.FromResult(null);
+ },
+ },
+ });
+ }
+}
+```
+
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
@@ -381,6 +471,6 @@ const session = await client.createSession({
## See Also
-- [Hooks Overview](./overview.md)
+- [Hooks Overview](./index.md)
- [Session Lifecycle Hooks](./session-lifecycle.md)
-- [Debugging Guide](../debugging.md)
+- [Debugging Guide](../troubleshooting/debugging.md)
diff --git a/docs/hooks/overview.md b/docs/hooks/index.md
similarity index 96%
rename from docs/hooks/overview.md
rename to docs/hooks/index.md
index a51ef04640..d83b11b2f9 100644
--- a/docs/hooks/overview.md
+++ b/docs/hooks/index.md
@@ -53,7 +53,7 @@ const session = await client.createSession({
Python
```python
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
async def main():
client = CopilotClient()
@@ -70,13 +70,11 @@ async def main():
async def on_session_start(input_data, invocation):
return {"additionalContext": "User prefers concise answers."}
- session = await client.create_session({
- "hooks": {
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={
"on_pre_tool_use": on_pre_tool_use,
"on_post_tool_use": on_post_tool_use,
"on_session_start": on_session_start,
- }
- })
+ })
```
@@ -233,4 +231,4 @@ const session = await client.createSession({
- [Getting Started Guide](../getting-started.md)
- [Custom Tools](../getting-started.md#step-4-add-a-custom-tool)
-- [Debugging Guide](../debugging.md)
+- [Debugging Guide](../troubleshooting/debugging.md)
diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md
index 0021e20a0d..029e9eb2f7 100644
--- a/docs/hooks/post-tool-use.md
+++ b/docs/hooks/post-tool-use.md
@@ -12,7 +12,15 @@ The `onPostToolUse` hook is called **after** a tool executes. Use it to:
Node.js / TypeScript
-
+
+```ts
+import type { PostToolUseHookInput, HookInvocation, PostToolUseHookOutput } from "@github/copilot-sdk";
+type PostToolUseHandler = (
+ input: PostToolUseHookInput,
+ invocation: HookInvocation
+) => Promise;
+```
+
```typescript
type PostToolUseHandler = (
input: PostToolUseHookInput,
@@ -25,7 +33,17 @@ type PostToolUseHandler = (
Python
-
+
+```python
+from copilot.types import PostToolUseHookInput, HookInvocation, PostToolUseHookOutput
+from typing import Callable, Awaitable
+
+PostToolUseHandler = Callable[
+ [PostToolUseHookInput, HookInvocation],
+ Awaitable[PostToolUseHookOutput | None]
+]
+```
+
```python
PostToolUseHandler = Callable[
[PostToolUseHookInput, HookInvocation],
@@ -38,7 +56,20 @@ PostToolUseHandler = Callable[
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+type PostToolUseHandler func(
+ input copilot.PostToolUseHookInput,
+ invocation copilot.HookInvocation,
+) (*copilot.PostToolUseHookOutput, error)
+
+func main() {}
+```
+
```go
type PostToolUseHandler func(
input PostToolUseHookInput,
@@ -51,7 +82,15 @@ type PostToolUseHandler func(
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public delegate Task PostToolUseHandler(
+ PostToolUseHookInput input,
+ HookInvocation invocation);
+```
+
```csharp
public delegate Task PostToolUseHandler(
PostToolUseHookInput input,
@@ -106,15 +145,15 @@ const session = await client.createSession({
Python
```python
+from copilot import PermissionHandler
+
async def on_post_tool_use(input_data, invocation):
print(f"[{invocation['session_id']}] Tool: {input_data['toolName']}")
print(f" Args: {input_data['toolArgs']}")
print(f" Result: {input_data['toolResult']}")
return None # Pass through unchanged
-session = await client.create_session({
- "hooks": {"on_post_tool_use": on_post_tool_use}
-})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_post_tool_use": on_post_tool_use})
```
@@ -122,7 +161,33 @@ session = await client.create_session({
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ client := copilot.NewClient(nil)
+ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ Hooks: &copilot.SessionHooks{
+ OnPostToolUse: func(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) {
+ fmt.Printf("[%s] Tool: %s\n", inv.SessionID, input.ToolName)
+ fmt.Printf(" Args: %v\n", input.ToolArgs)
+ fmt.Printf(" Result: %v\n", input.ToolResult)
+ return nil, nil
+ },
+ },
+ })
+ _ = session
+}
+```
+
```go
session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
Hooks: &copilot.SessionHooks{
@@ -141,7 +206,32 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class PostToolUseExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+ var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Hooks = new SessionHooks
+ {
+ OnPostToolUse = (input, invocation) =>
+ {
+ Console.WriteLine($"[{invocation.SessionId}] Tool: {input.ToolName}");
+ Console.WriteLine($" Args: {input.ToolArgs}");
+ Console.WriteLine($" Result: {input.ToolResult}");
+ return Task.FromResult(null);
+ },
+ },
+ });
+ }
+}
+```
+
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
@@ -338,6 +428,6 @@ const session = await client.createSession({
## See Also
-- [Hooks Overview](./overview.md)
+- [Hooks Overview](./index.md)
- [Pre-Tool Use Hook](./pre-tool-use.md)
- [Error Handling Hook](./error-handling.md)
diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md
index ac12df4fab..e1bb974951 100644
--- a/docs/hooks/pre-tool-use.md
+++ b/docs/hooks/pre-tool-use.md
@@ -12,7 +12,15 @@ The `onPreToolUse` hook is called **before** a tool executes. Use it to:
Node.js / TypeScript
-
+
+```ts
+import type { PreToolUseHookInput, HookInvocation, PreToolUseHookOutput } from "@github/copilot-sdk";
+type PreToolUseHandler = (
+ input: PreToolUseHookInput,
+ invocation: HookInvocation
+) => Promise;
+```
+
```typescript
type PreToolUseHandler = (
input: PreToolUseHookInput,
@@ -25,7 +33,17 @@ type PreToolUseHandler = (
Python
-
+
+```python
+from copilot.types import PreToolUseHookInput, HookInvocation, PreToolUseHookOutput
+from typing import Callable, Awaitable
+
+PreToolUseHandler = Callable[
+ [PreToolUseHookInput, HookInvocation],
+ Awaitable[PreToolUseHookOutput | None]
+]
+```
+
```python
PreToolUseHandler = Callable[
[PreToolUseHookInput, HookInvocation],
@@ -38,7 +56,20 @@ PreToolUseHandler = Callable[
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+type PreToolUseHandler func(
+ input copilot.PreToolUseHookInput,
+ invocation copilot.HookInvocation,
+) (*copilot.PreToolUseHookOutput, error)
+
+func main() {}
+```
+
```go
type PreToolUseHandler func(
input PreToolUseHookInput,
@@ -51,7 +82,15 @@ type PreToolUseHandler func(
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public delegate Task PreToolUseHandler(
+ PreToolUseHookInput input,
+ HookInvocation invocation);
+```
+
```csharp
public delegate Task PreToolUseHandler(
PreToolUseHookInput input,
@@ -114,14 +153,14 @@ const session = await client.createSession({
Python
```python
+from copilot import PermissionHandler
+
async def on_pre_tool_use(input_data, invocation):
print(f"[{invocation['session_id']}] Calling {input_data['toolName']}")
print(f" Args: {input_data['toolArgs']}")
return {"permissionDecision": "allow"}
-session = await client.create_session({
- "hooks": {"on_pre_tool_use": on_pre_tool_use}
-})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_pre_tool_use": on_pre_tool_use})
```
@@ -129,7 +168,34 @@ session = await client.create_session({
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ client := copilot.NewClient(nil)
+ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ Hooks: &copilot.SessionHooks{
+ OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
+ fmt.Printf("[%s] Calling %s\n", inv.SessionID, input.ToolName)
+ fmt.Printf(" Args: %v\n", input.ToolArgs)
+ return &copilot.PreToolUseHookOutput{
+ PermissionDecision: "allow",
+ }, nil
+ },
+ },
+ })
+ _ = session
+}
+```
+
```go
session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
Hooks: &copilot.SessionHooks{
@@ -149,7 +215,33 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class PreToolUseExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+ var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Hooks = new SessionHooks
+ {
+ OnPreToolUse = (input, invocation) =>
+ {
+ Console.WriteLine($"[{invocation.SessionId}] Calling {input.ToolName}");
+ Console.WriteLine($" Args: {input.ToolArgs}");
+ return Task.FromResult(
+ new PreToolUseHookOutput { PermissionDecision = "allow" }
+ );
+ },
+ },
+ });
+ }
+}
+```
+
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
@@ -294,6 +386,6 @@ const session = await client.createSession({
## See Also
-- [Hooks Overview](./overview.md)
+- [Hooks Overview](./index.md)
- [Post-Tool Use Hook](./post-tool-use.md)
-- [Debugging Guide](../debugging.md)
+- [Debugging Guide](../troubleshooting/debugging.md)
diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md
index 74f4666f45..4efd33ccc0 100644
--- a/docs/hooks/session-lifecycle.md
+++ b/docs/hooks/session-lifecycle.md
@@ -16,7 +16,15 @@ The `onSessionStart` hook is called when a session begins (new or resumed).
Node.js / TypeScript
-
+
+```ts
+import type { SessionStartHookInput, HookInvocation, SessionStartHookOutput } from "@github/copilot-sdk";
+type SessionStartHandler = (
+ input: SessionStartHookInput,
+ invocation: HookInvocation
+) => Promise;
+```
+
```typescript
type SessionStartHandler = (
input: SessionStartHookInput,
@@ -29,7 +37,17 @@ type SessionStartHandler = (
Python
-
+
+```python
+from copilot.types import SessionStartHookInput, HookInvocation, SessionStartHookOutput
+from typing import Callable, Awaitable
+
+SessionStartHandler = Callable[
+ [SessionStartHookInput, HookInvocation],
+ Awaitable[SessionStartHookOutput | None]
+]
+```
+
```python
SessionStartHandler = Callable[
[SessionStartHookInput, HookInvocation],
@@ -42,7 +60,20 @@ SessionStartHandler = Callable[
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+type SessionStartHandler func(
+ input copilot.SessionStartHookInput,
+ invocation copilot.HookInvocation,
+) (*copilot.SessionStartHookOutput, error)
+
+func main() {}
+```
+
```go
type SessionStartHandler func(
input SessionStartHookInput,
@@ -55,7 +86,15 @@ type SessionStartHandler func(
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public delegate Task SessionStartHandler(
+ SessionStartHookInput input,
+ HookInvocation invocation);
+```
+
```csharp
public delegate Task SessionStartHandler(
SessionStartHookInput input,
@@ -113,6 +152,8 @@ Package manager: ${projectInfo.packageManager}
Python
```python
+from copilot import PermissionHandler
+
async def on_session_start(input_data, invocation):
print(f"Session {invocation['session_id']} started ({input_data['source']})")
@@ -126,9 +167,7 @@ Package manager: {project_info['packageManager']}
""".strip()
}
-session = await client.create_session({
- "hooks": {"on_session_start": on_session_start}
-})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_session_start": on_session_start})
```
@@ -208,7 +247,17 @@ type SessionEndHandler = (
Python
-
+
+```python
+from copilot.types import SessionEndHookInput, HookInvocation
+from typing import Callable, Awaitable
+
+SessionEndHandler = Callable[
+ [SessionEndHookInput, HookInvocation],
+ Awaitable[None]
+]
+```
+
```python
SessionEndHandler = Callable[
[SessionEndHookInput, HookInvocation],
@@ -221,7 +270,20 @@ SessionEndHandler = Callable[
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+type SessionEndHandler func(
+ input copilot.SessionEndHookInput,
+ invocation copilot.HookInvocation,
+) error
+
+func main() {}
+```
+
```go
type SessionEndHandler func(
input SessionEndHookInput,
@@ -309,6 +371,8 @@ const session = await client.createSession({
Python
```python
+from copilot import PermissionHandler
+
session_start_times = {}
async def on_session_start(input_data, invocation):
@@ -328,12 +392,10 @@ async def on_session_end(input_data, invocation):
session_start_times.pop(invocation["session_id"], None)
return None
-session = await client.create_session({
- "hooks": {
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={
"on_session_start": on_session_start,
"on_session_end": on_session_end,
- }
-})
+ })
```
@@ -442,6 +504,6 @@ Session Summary:
## See Also
-- [Hooks Overview](./overview.md)
+- [Hooks Overview](./index.md)
- [Error Handling Hook](./error-handling.md)
-- [Debugging Guide](../debugging.md)
+- [Debugging Guide](../troubleshooting/debugging.md)
diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md
index 3205b95cd4..2aca7f1cec 100644
--- a/docs/hooks/user-prompt-submitted.md
+++ b/docs/hooks/user-prompt-submitted.md
@@ -12,7 +12,15 @@ The `onUserPromptSubmitted` hook is called when a user submits a message. Use it
Node.js / TypeScript
-
+
+```ts
+import type { UserPromptSubmittedHookInput, HookInvocation, UserPromptSubmittedHookOutput } from "@github/copilot-sdk";
+type UserPromptSubmittedHandler = (
+ input: UserPromptSubmittedHookInput,
+ invocation: HookInvocation
+) => Promise;
+```
+
```typescript
type UserPromptSubmittedHandler = (
input: UserPromptSubmittedHookInput,
@@ -25,7 +33,17 @@ type UserPromptSubmittedHandler = (
Python
-
+
+```python
+from copilot.types import UserPromptSubmittedHookInput, HookInvocation, UserPromptSubmittedHookOutput
+from typing import Callable, Awaitable
+
+UserPromptSubmittedHandler = Callable[
+ [UserPromptSubmittedHookInput, HookInvocation],
+ Awaitable[UserPromptSubmittedHookOutput | None]
+]
+```
+
```python
UserPromptSubmittedHandler = Callable[
[UserPromptSubmittedHookInput, HookInvocation],
@@ -38,7 +56,20 @@ UserPromptSubmittedHandler = Callable[
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+type UserPromptSubmittedHandler func(
+ input copilot.UserPromptSubmittedHookInput,
+ invocation copilot.HookInvocation,
+) (*copilot.UserPromptSubmittedHookOutput, error)
+
+func main() {}
+```
+
```go
type UserPromptSubmittedHandler func(
input UserPromptSubmittedHookInput,
@@ -51,7 +82,15 @@ type UserPromptSubmittedHandler func(
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public delegate Task UserPromptSubmittedHandler(
+ UserPromptSubmittedHookInput input,
+ HookInvocation invocation);
+```
+
```csharp
public delegate Task UserPromptSubmittedHandler(
UserPromptSubmittedHookInput input,
@@ -102,13 +141,13 @@ const session = await client.createSession({
Python
```python
+from copilot import PermissionHandler
+
async def on_user_prompt_submitted(input_data, invocation):
print(f"[{invocation['session_id']}] User: {input_data['prompt']}")
return None
-session = await client.create_session({
- "hooks": {"on_user_prompt_submitted": on_user_prompt_submitted}
-})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_user_prompt_submitted": on_user_prompt_submitted})
```
@@ -116,7 +155,31 @@ session = await client.create_session({
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ client := copilot.NewClient(nil)
+ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ Hooks: &copilot.SessionHooks{
+ OnUserPromptSubmitted: func(input copilot.UserPromptSubmittedHookInput, inv copilot.HookInvocation) (*copilot.UserPromptSubmittedHookOutput, error) {
+ fmt.Printf("[%s] User: %s\n", inv.SessionID, input.Prompt)
+ return nil, nil
+ },
+ },
+ })
+ _ = session
+}
+```
+
```go
session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
Hooks: &copilot.SessionHooks{
@@ -133,7 +196,30 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class UserPromptSubmittedExample
+{
+ public static async Task Main()
+ {
+ await using var client = new CopilotClient();
+ var session = await client.CreateSessionAsync(new SessionConfig
+ {
+ Hooks = new SessionHooks
+ {
+ OnUserPromptSubmitted = (input, invocation) =>
+ {
+ Console.WriteLine($"[{invocation.SessionId}] User: {input.Prompt}");
+ return Task.FromResult(null);
+ },
+ },
+ });
+ }
+}
+```
+
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
@@ -360,6 +446,6 @@ const session = await client.createSession({
## See Also
-- [Hooks Overview](./overview.md)
+- [Hooks Overview](./index.md)
- [Session Lifecycle Hooks](./session-lifecycle.md)
- [Pre-Tool Use Hook](./pre-tool-use.md)
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000000..04ef99bd88
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,76 @@
+# GitHub Copilot SDK Documentation
+
+Welcome to the GitHub Copilot SDK docs. Whether you're building your first Copilot-powered app or deploying to production, you'll find what you need here.
+
+## Where to Start
+
+| I want to... | Go to |
+|---|---|
+| **Build my first app** | [Getting Started](./getting-started.md) — end-to-end tutorial with streaming & custom tools |
+| **Set up for production** | [Setup Guides](./setup/index.md) — architecture, deployment patterns, scaling |
+| **Configure authentication** | [Authentication](./auth/index.md) — GitHub OAuth, environment variables, BYOK |
+| **Add features to my app** | [Features](./features/index.md) — hooks, custom agents, MCP, skills, and more |
+| **Debug an issue** | [Troubleshooting](./troubleshooting/debugging.md) — common problems and solutions |
+
+## Documentation Map
+
+### [Getting Started](./getting-started.md)
+
+Step-by-step tutorial that takes you from zero to a working Copilot app with streaming responses and custom tools.
+
+### [Setup](./setup/index.md)
+
+How to configure and deploy the SDK for your use case.
+
+- [Local CLI](./setup/local-cli.md) — simplest path, uses your signed-in CLI
+- [Bundled CLI](./setup/bundled-cli.md) — ship the CLI with your app
+- [Backend Services](./setup/backend-services.md) — server-side with headless CLI over TCP
+- [GitHub OAuth](./setup/github-oauth.md) — implement the OAuth flow
+- [Azure Managed Identity](./setup/azure-managed-identity.md) — BYOK with Azure AI Foundry
+- [Scaling & Multi-Tenancy](./setup/scaling.md) — horizontal scaling, isolation patterns
+
+### [Authentication](./auth/index.md)
+
+Configuring how users and services authenticate with Copilot.
+
+- [Authentication Overview](./auth/index.md) — methods, priority order, and examples
+- [Bring Your Own Key (BYOK)](./auth/byok.md) — use your own API keys from OpenAI, Azure, Anthropic, and more
+
+### [Features](./features/index.md)
+
+Guides for building with the SDK's capabilities.
+
+- [Hooks](./features/hooks.md) — intercept and customize session behavior
+- [Custom Agents](./features/custom-agents.md) — define specialized sub-agents
+- [MCP Servers](./features/mcp.md) — integrate Model Context Protocol servers
+- [Skills](./features/skills.md) — load reusable prompt modules
+- [Image Input](./features/image-input.md) — send images as attachments
+- [Streaming Events](./features/streaming-events.md) — real-time event reference
+- [Steering & Queueing](./features/steering-and-queueing.md) — message delivery modes
+- [Session Persistence](./features/session-persistence.md) — resume sessions across restarts
+
+### [Hooks Reference](./hooks/index.md)
+
+Detailed API reference for each session hook.
+
+- [Pre-Tool Use](./hooks/pre-tool-use.md) — approve, deny, or modify tool calls
+- [Post-Tool Use](./hooks/post-tool-use.md) — transform tool results
+- [User Prompt Submitted](./hooks/user-prompt-submitted.md) — modify or filter user messages
+- [Session Lifecycle](./hooks/session-lifecycle.md) — session start and end
+- [Error Handling](./hooks/error-handling.md) — custom error handling
+
+### [Troubleshooting](./troubleshooting/debugging.md)
+
+- [Debugging Guide](./troubleshooting/debugging.md) — common issues and solutions
+- [MCP Debugging](./troubleshooting/mcp-debugging.md) — MCP-specific troubleshooting
+- [Compatibility](./troubleshooting/compatibility.md) — SDK vs CLI feature matrix
+
+### [Observability](./observability/opentelemetry.md)
+
+- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) — built-in TelemetryConfig and trace context propagation
+
+### [Integrations](./integrations/microsoft-agent-framework.md)
+
+Guides for using the SDK with other platforms and frameworks.
+
+- [Microsoft Agent Framework](./integrations/microsoft-agent-framework.md) — MAF multi-agent workflows
diff --git a/docs/integrations/microsoft-agent-framework.md b/docs/integrations/microsoft-agent-framework.md
new file mode 100644
index 0000000000..8e794759b7
--- /dev/null
+++ b/docs/integrations/microsoft-agent-framework.md
@@ -0,0 +1,456 @@
+# Microsoft Agent Framework Integration
+
+Use the Copilot SDK as an agent provider inside the [Microsoft Agent Framework](https://devblogs.microsoft.com/semantic-kernel/build-ai-agents-with-github-copilot-sdk-and-microsoft-agent-framework/) (MAF) to compose multi-agent workflows alongside Azure OpenAI, Anthropic, and other providers.
+
+## Overview
+
+The Microsoft Agent Framework is the unified successor to Semantic Kernel and AutoGen. It provides a standard interface for building, orchestrating, and deploying AI agents. Dedicated integration packages let you wrap a Copilot SDK client as a first-class MAF agent — interchangeable with any other agent provider in the framework.
+
+| Concept | Description |
+|---------|-------------|
+| **Microsoft Agent Framework** | Open-source framework for single- and multi-agent orchestration in .NET and Python |
+| **Agent provider** | A backend that powers an agent (Copilot, Azure OpenAI, Anthropic, etc.) |
+| **Orchestrator** | A MAF component that coordinates agents in sequential, concurrent, or handoff workflows |
+| **A2A protocol** | Agent-to-Agent communication standard supported by the framework |
+
+> **Note:** MAF integration packages are available for **.NET** and **Python**. For TypeScript and Go, use the Copilot SDK directly — the standard SDK APIs already provide tool calling, streaming, and custom agents.
+
+## Prerequisites
+
+Before you begin, ensure you have:
+
+- A working [Copilot SDK setup](../getting-started.md) in your language of choice
+- A GitHub Copilot subscription (Individual, Business, or Enterprise)
+- The Copilot CLI installed or available via the SDK's bundled CLI
+
+## Installation
+
+Install the Copilot SDK alongside the MAF integration package for your language:
+
+
+.NET
+
+```shell
+dotnet add package GitHub.Copilot.SDK
+dotnet add package Microsoft.Agents.AI.GitHub.Copilot --prerelease
+```
+
+
+
+
+Python
+
+```shell
+pip install copilot-sdk agent-framework-github-copilot
+```
+
+
+
+## Basic Usage
+
+Wrap the Copilot SDK client as a MAF agent with a single method call. The resulting agent conforms to the framework's standard interface and can be used anywhere a MAF agent is expected.
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+using Microsoft.Agents.AI;
+
+await using var copilotClient = new CopilotClient();
+await copilotClient.StartAsync();
+
+// Wrap as a MAF agent
+AIAgent agent = copilotClient.AsAIAgent();
+
+// Use the standard MAF interface
+string response = await agent.RunAsync("Explain how dependency injection works in ASP.NET Core");
+Console.WriteLine(response);
+```
+
+
+
+
+Python
+
+
+```python
+from agent_framework.github import GitHubCopilotAgent
+
+async def main():
+ agent = GitHubCopilotAgent(
+ default_options={
+ "instructions": "You are a helpful coding assistant.",
+ }
+ )
+
+ async with agent:
+ result = await agent.run("Explain how dependency injection works in FastAPI")
+ print(result)
+```
+
+
+
+## Adding Custom Tools
+
+Extend your Copilot agent with custom function tools. Tools defined through the standard Copilot SDK are automatically available when the agent runs inside MAF.
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+using Microsoft.Extensions.AI;
+using Microsoft.Agents.AI;
+
+// Define a custom tool
+AIFunction weatherTool = AIFunctionFactory.Create(
+ (string location) => $"The weather in {location} is sunny with a high of 25°C.",
+ "GetWeather",
+ "Get the current weather for a given location."
+);
+
+await using var copilotClient = new CopilotClient();
+await copilotClient.StartAsync();
+
+// Create agent with tools
+AIAgent agent = copilotClient.AsAIAgent(new AIAgentOptions
+{
+ Tools = new[] { weatherTool },
+});
+
+string response = await agent.RunAsync("What's the weather like in Seattle?");
+Console.WriteLine(response);
+```
+
+
+
+
+Python
+
+
+```python
+from agent_framework.github import GitHubCopilotAgent
+
+def get_weather(location: str) -> str:
+ """Get the current weather for a given location."""
+ return f"The weather in {location} is sunny with a high of 25°C."
+
+async def main():
+ agent = GitHubCopilotAgent(
+ default_options={
+ "instructions": "You are a helpful assistant with access to weather data.",
+ },
+ tools=[get_weather],
+ )
+
+ async with agent:
+ result = await agent.run("What's the weather like in Seattle?")
+ print(result)
+```
+
+
+
+You can also use Copilot SDK's native tool definition alongside MAF tools:
+
+
+Node.js / TypeScript (standalone SDK)
+
+```typescript
+import { CopilotClient, DefineTool } from "@github/copilot-sdk";
+
+const getWeather = DefineTool({
+ name: "GetWeather",
+ description: "Get the current weather for a given location.",
+ parameters: { location: { type: "string", description: "City name" } },
+ execute: async ({ location }) => `The weather in ${location} is sunny, 25°C.`,
+});
+
+const client = new CopilotClient();
+const session = await client.createSession({
+ model: "gpt-4.1",
+ tools: [getWeather],
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+
+await session.sendAndWait({ prompt: "What's the weather like in Seattle?" });
+```
+
+
+
+## Multi-Agent Workflows
+
+The primary benefit of MAF integration is composing Copilot alongside other agent providers in orchestrated workflows. Use the framework's built-in orchestrators to create pipelines where different agents handle different steps.
+
+### Sequential Workflow
+
+Run agents one after another, passing output from one to the next:
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Orchestration;
+
+await using var copilotClient = new CopilotClient();
+await copilotClient.StartAsync();
+
+// Copilot agent for code review
+AIAgent reviewer = copilotClient.AsAIAgent(new AIAgentOptions
+{
+ Instructions = "You review code for bugs, security issues, and best practices. Be thorough.",
+});
+
+// Azure OpenAI agent for generating documentation
+AIAgent documentor = AIAgent.FromOpenAI(new OpenAIAgentOptions
+{
+ Model = "gpt-4.1",
+ Instructions = "You write clear, concise documentation for code changes.",
+});
+
+// Compose in a sequential pipeline
+var pipeline = new SequentialOrchestrator(new[] { reviewer, documentor });
+
+string result = await pipeline.RunAsync(
+ "Review and document this pull request: added retry logic to the HTTP client"
+);
+Console.WriteLine(result);
+```
+
+
+
+
+Python
+
+
+```python
+from agent_framework.github import GitHubCopilotAgent
+from agent_framework.openai import OpenAIAgent
+from agent_framework.orchestration import SequentialOrchestrator
+
+async def main():
+ # Copilot agent for code review
+ reviewer = GitHubCopilotAgent(
+ default_options={
+ "instructions": "You review code for bugs, security issues, and best practices.",
+ }
+ )
+
+ # OpenAI agent for documentation
+ documentor = OpenAIAgent(
+ model="gpt-4.1",
+ instructions="You write clear, concise documentation for code changes.",
+ )
+
+ # Compose in a sequential pipeline
+ pipeline = SequentialOrchestrator(agents=[reviewer, documentor])
+
+ async with pipeline:
+ result = await pipeline.run(
+ "Review and document this PR: added retry logic to the HTTP client"
+ )
+ print(result)
+```
+
+
+
+### Concurrent Workflow
+
+Run multiple agents in parallel and aggregate their results:
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Orchestration;
+
+await using var copilotClient = new CopilotClient();
+await copilotClient.StartAsync();
+
+AIAgent securityReviewer = copilotClient.AsAIAgent(new AIAgentOptions
+{
+ Instructions = "Focus exclusively on security vulnerabilities and risks.",
+});
+
+AIAgent performanceReviewer = copilotClient.AsAIAgent(new AIAgentOptions
+{
+ Instructions = "Focus exclusively on performance bottlenecks and optimization opportunities.",
+});
+
+// Run both reviews concurrently
+var concurrent = new ConcurrentOrchestrator(new[] { securityReviewer, performanceReviewer });
+
+string combinedResult = await concurrent.RunAsync(
+ "Analyze this database query module for issues"
+);
+Console.WriteLine(combinedResult);
+```
+
+
+
+## Streaming Responses
+
+When building interactive applications, stream agent responses to show real-time output. The MAF integration preserves the Copilot SDK's streaming capabilities.
+
+
+.NET
+
+
+```csharp
+using GitHub.Copilot.SDK;
+using Microsoft.Agents.AI;
+
+await using var copilotClient = new CopilotClient();
+await copilotClient.StartAsync();
+
+AIAgent agent = copilotClient.AsAIAgent(new AIAgentOptions
+{
+ Streaming = true,
+});
+
+await foreach (var chunk in agent.RunStreamingAsync("Write a quicksort implementation in C#"))
+{
+ Console.Write(chunk);
+}
+Console.WriteLine();
+```
+
+
+
+
+Python
+
+
+```python
+from agent_framework.github import GitHubCopilotAgent
+
+async def main():
+ agent = GitHubCopilotAgent(
+ default_options={"streaming": True}
+ )
+
+ async with agent:
+ async for chunk in agent.run_streaming("Write a quicksort in Python"):
+ print(chunk, end="", flush=True)
+ print()
+```
+
+
+
+You can also stream directly through the Copilot SDK without MAF:
+
+
+Node.js / TypeScript (standalone SDK)
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+const session = await client.createSession({
+ model: "gpt-4.1",
+ streaming: true,
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+
+session.on("assistant.message_delta", (event) => {
+ process.stdout.write(event.data.delta ?? "");
+});
+
+await session.sendAndWait({ prompt: "Write a quicksort implementation in TypeScript" });
+```
+
+
+
+## Configuration Reference
+
+### MAF Agent Options
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Instructions` / `instructions` | `string` | System prompt for the agent |
+| `Tools` / `tools` | `AIFunction[]` / `list` | Custom function tools available to the agent |
+| `Streaming` / `streaming` | `bool` | Enable streaming responses |
+| `Model` / `model` | `string` | Override the default model |
+
+### Copilot SDK Options (Passed Through)
+
+All standard [SessionConfig](../getting-started.md) options are still available when creating the underlying Copilot client. The MAF wrapper delegates to the SDK under the hood:
+
+| SDK Feature | MAF Support |
+|-------------|-------------|
+| Custom tools (`DefineTool` / `AIFunctionFactory`) | ✅ Merged with MAF tools |
+| MCP servers | ✅ Configured on the SDK client |
+| Custom agents / sub-agents | ✅ Available within the Copilot agent |
+| Infinite sessions | ✅ Configured on the SDK client |
+| Model selection | ✅ Overridable per agent or per call |
+| Streaming | ✅ Full delta event support |
+
+## Best Practices
+
+### Choose the right level of integration
+
+Use the MAF wrapper when you need to compose Copilot with other providers in orchestrated workflows. If your application only uses Copilot, the standalone SDK is simpler and gives you full control:
+
+```typescript
+// Standalone SDK — full control, simpler setup
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient();
+const session = await client.createSession({
+ model: "gpt-4.1",
+ onPermissionRequest: async () => ({ kind: "approved" }),
+});
+const response = await session.sendAndWait({ prompt: "Explain this code" });
+```
+
+### Keep agents focused
+
+When building multi-agent workflows, give each agent a specific role with clear instructions. Avoid overlapping responsibilities:
+
+```typescript
+// ❌ Too vague — overlapping roles
+const agents = [
+ { instructions: "Help with code" },
+ { instructions: "Assist with programming" },
+];
+
+// ✅ Focused — clear separation of concerns
+const agents = [
+ { instructions: "Review code for security vulnerabilities. Flag SQL injection, XSS, and auth issues." },
+ { instructions: "Optimize code performance. Focus on algorithmic complexity and memory usage." },
+];
+```
+
+### Handle errors at the orchestration level
+
+Wrap agent calls in error handling, especially in multi-agent workflows where one agent's failure shouldn't block the entire pipeline:
+
+
+```csharp
+try
+{
+ string result = await pipeline.RunAsync("Analyze this module");
+ Console.WriteLine(result);
+}
+catch (AgentException ex)
+{
+ Console.Error.WriteLine($"Agent {ex.AgentName} failed: {ex.Message}");
+ // Fall back to single-agent mode or retry
+}
+```
+
+## See Also
+
+- [Getting Started](../getting-started.md) — initial Copilot SDK setup
+- [Custom Agents](../features/custom-agents.md) — define specialized sub-agents within the SDK
+- [Custom Skills](../features/skills.md) — reusable prompt modules
+- [Microsoft Agent Framework documentation](https://learn.microsoft.com/en-us/agent-framework/agents/providers/github-copilot) — official MAF docs for the Copilot provider
+- [Blog: Build AI Agents with GitHub Copilot SDK and Microsoft Agent Framework](https://devblogs.microsoft.com/semantic-kernel/build-ai-agents-with-github-copilot-sdk-and-microsoft-agent-framework/)
diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md
new file mode 100644
index 0000000000..b59e61a4cd
--- /dev/null
+++ b/docs/observability/opentelemetry.md
@@ -0,0 +1,158 @@
+# OpenTelemetry Instrumentation for Copilot SDK
+
+This guide shows how to add OpenTelemetry tracing to your Copilot SDK applications.
+
+## Built-in Telemetry Support
+
+The SDK has built-in support for configuring OpenTelemetry on the CLI process and propagating W3C Trace Context between the SDK and CLI. Provide a `TelemetryConfig` when creating the client to opt in:
+
+
+Node.js / TypeScript
+
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+
+const client = new CopilotClient({
+ telemetry: {
+ otlpEndpoint: "http://localhost:4318",
+ },
+});
+```
+
+
+
+
+Python
+
+
+```python
+from copilot import CopilotClient, SubprocessConfig
+
+client = CopilotClient(SubprocessConfig(
+ telemetry={
+ "otlp_endpoint": "http://localhost:4318",
+ },
+))
+```
+
+
+
+
+Go
+
+
+```go
+client, err := copilot.NewClient(copilot.ClientOptions{
+ Telemetry: &copilot.TelemetryConfig{
+ OTLPEndpoint: "http://localhost:4318",
+ },
+})
+```
+
+
+
+
+.NET
+
+
+```csharp
+var client = new CopilotClient(new CopilotClientOptions
+{
+ Telemetry = new TelemetryConfig
+ {
+ OtlpEndpoint = "http://localhost:4318",
+ },
+});
+```
+
+
+
+### TelemetryConfig Options
+
+| Option | Node.js | Python | Go | .NET | Description |
+|---|---|---|---|---|---|
+| OTLP endpoint | `otlpEndpoint` | `otlp_endpoint` | `OTLPEndpoint` | `OtlpEndpoint` | OTLP HTTP endpoint URL |
+| File path | `filePath` | `file_path` | `FilePath` | `FilePath` | File path for JSON-lines trace output |
+| Exporter type | `exporterType` | `exporter_type` | `ExporterType` | `ExporterType` | `"otlp-http"` or `"file"` |
+| Source name | `sourceName` | `source_name` | `SourceName` | `SourceName` | Instrumentation scope name |
+| Capture content | `captureContent` | `capture_content` | `CaptureContent` | `CaptureContent` | Whether to capture message content |
+
+### Trace Context Propagation
+
+> **Most users don't need this.** The `TelemetryConfig` above is all you need to collect traces from the CLI. The trace context propagation described in this section is an **advanced feature** for applications that create their own OpenTelemetry spans and want them to appear in the **same distributed trace** as the CLI's spans.
+
+The SDK can propagate W3C Trace Context (`traceparent`/`tracestate`) on JSON-RPC payloads so that your application's spans and the CLI's spans are linked in one distributed trace. This is useful when, for example, you want to see a "handle tool call" span in your app nested inside the CLI's "execute tool" span, or show the SDK call as a child of your request-handling span.
+
+#### SDK → CLI (outbound)
+
+For **Node.js**, provide an `onGetTraceContext` callback on the client options. This is only needed if your application already uses `@opentelemetry/api` and you want to link your spans with the CLI's spans. The SDK calls this callback before `session.create`, `session.resume`, and `session.send` RPCs:
+
+
+```typescript
+import { CopilotClient } from "@github/copilot-sdk";
+import { propagation, context } from "@opentelemetry/api";
+
+const client = new CopilotClient({
+ telemetry: { otlpEndpoint: "http://localhost:4318" },
+ onGetTraceContext: () => {
+ const carrier: Record = {};
+ propagation.inject(context.active(), carrier);
+ return carrier; // { traceparent: "00-...", tracestate: "..." }
+ },
+});
+```
+
+For **Python**, **Go**, and **.NET**, trace context injection is automatic when the respective OpenTelemetry/Activity API is configured — no callback is needed.
+
+#### CLI → SDK (inbound)
+
+When the CLI invokes a tool handler, the `traceparent` and `tracestate` from the CLI's span are available in all languages:
+
+- **Go**: The `ToolInvocation.TraceContext` field is a `context.Context` with the trace already restored — use it directly as the parent for your spans.
+- **Python**: Trace context is automatically restored around the handler via `trace_context()` — child spans are parented to the CLI's span automatically.
+- **.NET**: Trace context is automatically restored via `RestoreTraceContext()` — child `Activity` instances are parented to the CLI's span automatically.
+- **Node.js**: Since the SDK has no OpenTelemetry dependency, `traceparent` and `tracestate` are passed as raw strings on the `ToolInvocation` object. Restore the context manually if needed:
+
+
+```typescript
+import { propagation, context, trace } from "@opentelemetry/api";
+
+session.registerTool(myTool, async (args, invocation) => {
+ // Restore the CLI's trace context as the active context
+ const carrier = {
+ traceparent: invocation.traceparent,
+ tracestate: invocation.tracestate,
+ };
+ const parentCtx = propagation.extract(context.active(), carrier);
+
+ // Create a child span under the CLI's span
+ const tracer = trace.getTracer("my-app");
+ return context.with(parentCtx, () =>
+ tracer.startActiveSpan("my-tool", async (span) => {
+ try {
+ const result = await doWork(args);
+ return result;
+ } finally {
+ span.end();
+ }
+ })
+ );
+});
+```
+
+### Per-Language Dependencies
+
+| Language | Dependency | Notes |
+|---|---|---|
+| Node.js | — | No dependency; provide `onGetTraceContext` callback for outbound propagation |
+| Python | `opentelemetry-api` | Install with `pip install copilot-sdk[telemetry]` |
+| Go | `go.opentelemetry.io/otel` | Required dependency |
+| .NET | — | Uses built-in `System.Diagnostics.Activity` |
+
+## References
+
+- [OpenTelemetry GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
+- [OpenTelemetry MCP Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/)
+- [OpenTelemetry Python SDK](https://opentelemetry.io/docs/instrumentation/python/)
+- [Copilot SDK Documentation](https://github.com/github/copilot-sdk)
diff --git a/docs/opentelemetry-instrumentation.md b/docs/opentelemetry-instrumentation.md
deleted file mode 100644
index f0e1b2556c..0000000000
--- a/docs/opentelemetry-instrumentation.md
+++ /dev/null
@@ -1,570 +0,0 @@
-# OpenTelemetry Instrumentation for Copilot SDK
-
-This guide shows how to add OpenTelemetry tracing to your Copilot SDK applications using GenAI semantic conventions.
-
-## Overview
-
-The Copilot SDK emits session events as your agent processes requests. You can instrument your application to convert these events into OpenTelemetry spans and attributes following the [OpenTelemetry GenAI Semantic Conventions v1.34.0](https://opentelemetry.io/docs/specs/semconv/gen-ai/).
-
-## Installation
-
-```bash
-pip install opentelemetry-sdk opentelemetry-api
-```
-
-For exporting to observability backends:
-
-```bash
-# Console output
-pip install opentelemetry-sdk
-
-# Azure Monitor
-pip install azure-monitor-opentelemetry
-
-# OTLP (Jaeger, Prometheus, etc.)
-pip install opentelemetry-exporter-otlp
-```
-
-## Basic Setup
-
-### 1. Initialize OpenTelemetry
-
-```python
-from opentelemetry import trace
-from opentelemetry.sdk.trace import TracerProvider
-from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter
-
-# Setup tracer provider
-tracer_provider = TracerProvider()
-trace.set_tracer_provider(tracer_provider)
-
-# Add exporter (console example)
-span_exporter = ConsoleSpanExporter()
-tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
-
-# Get a tracer
-tracer = trace.get_tracer(__name__)
-```
-
-### 2. Create Spans Around Agent Operations
-
-```python
-from copilot import CopilotClient, PermissionHandler
-from copilot.generated.session_events import SessionEventType
-from opentelemetry import trace, context
-from opentelemetry.trace import SpanKind
-
-# Initialize client and start the CLI server
-client = CopilotClient()
-await client.start()
-
-tracer = trace.get_tracer(__name__)
-
-# Create a span for the agent invocation
-span_attrs = {
- "gen_ai.operation.name": "invoke_agent",
- "gen_ai.provider.name": "github.copilot",
- "gen_ai.agent.name": "my-agent",
- "gen_ai.request.model": "gpt-5",
-}
-
-span = tracer.start_span(
- name="invoke_agent my-agent",
- kind=SpanKind.CLIENT,
- attributes=span_attrs
-)
-token = context.attach(trace.set_span_in_context(span))
-
-try:
- # Create a session (model is set here, not on the client)
- session = await client.create_session({
- "model": "gpt-5",
- "on_permission_request": PermissionHandler.approve_all,
- })
-
- # Subscribe to events via callback
- def handle_event(event):
- if event.type == SessionEventType.ASSISTANT_USAGE:
- if event.data.model:
- span.set_attribute("gen_ai.response.model", event.data.model)
-
- unsubscribe = session.on(handle_event)
-
- # Send a message (returns a message ID)
- await session.send({"prompt": "Hello, world!"})
-
- # Or send and wait for the session to become idle
- response = await session.send_and_wait({"prompt": "Hello, world!"})
-finally:
- context.detach(token)
- span.end()
- await client.stop()
-```
-
-## Copilot SDK Event to GenAI Attribute Mapping
-
-The Copilot SDK emits `SessionEventType` events during agent execution. Subscribe to these events using `session.on(handler)`, which returns an unsubscribe function. Here's how to map these events to GenAI semantic convention attributes:
-
-### Core Session Events
-
-| SessionEventType | GenAI Attributes | Description |
-|------------------|------------------|-------------|
-| `SESSION_START` | - | Session initialization (mark span start) |
-| `SESSION_IDLE` | - | Session completed (mark span end) |
-| `SESSION_ERROR` | `error.type`, `error.message` | Error occurred |
-
-### Assistant Events
-
-| SessionEventType | GenAI Attributes | Description |
-|------------------|------------------|-------------|
-| `ASSISTANT_TURN_START` | - | Assistant begins processing |
-| `ASSISTANT_TURN_END` | - | Assistant finished processing |
-| `ASSISTANT_MESSAGE` | `gen_ai.output.messages` (event) | Final assistant message with complete content |
-| `ASSISTANT_MESSAGE_DELTA` | - | Streaming message chunk (optional to trace) |
-| `ASSISTANT_USAGE` | `gen_ai.usage.input_tokens`
`gen_ai.usage.output_tokens`
`gen_ai.response.model` | Token usage and model information |
-| `ASSISTANT_REASONING` | - | Reasoning content (optional to trace) |
-| `ASSISTANT_INTENT` | - | Assistant's understood intent |
-
-### Tool Execution Events
-
-| SessionEventType | GenAI Attributes / Span | Description |
-|------------------|-------------------------|-------------|
-| `TOOL_EXECUTION_START` | Create child span:
- `gen_ai.tool.name`
- `gen_ai.tool.call.id`
- `gen_ai.operation.name`: `execute_tool`
- `gen_ai.tool.call.arguments` (opt-in) | Tool execution begins |
-| `TOOL_EXECUTION_COMPLETE` | On child span:
- `gen_ai.tool.call.result` (opt-in)
- `error.type` (if failed)
End child span | Tool execution finished |
-| `TOOL_EXECUTION_PARTIAL_RESULT` | - | Streaming tool result |
-
-### Model and Context Events
-
-| SessionEventType | GenAI Attributes | Description |
-|------------------|------------------|-------------|
-| `SESSION_MODEL_CHANGE` | `gen_ai.request.model` | Model changed during session |
-| `SESSION_CONTEXT_CHANGED` | - | Context window modified |
-| `SESSION_TRUNCATION` | - | Context truncated |
-
-## Detailed Event Mapping Examples
-
-### ASSISTANT_USAGE Event
-
-When you receive an `ASSISTANT_USAGE` event, extract token usage:
-
-```python
-from copilot.generated.session_events import SessionEventType
-
-def handle_usage(event):
- if event.type == SessionEventType.ASSISTANT_USAGE:
- data = event.data
- if data.model:
- span.set_attribute("gen_ai.response.model", data.model)
- if data.input_tokens is not None:
- span.set_attribute("gen_ai.usage.input_tokens", int(data.input_tokens))
- if data.output_tokens is not None:
- span.set_attribute("gen_ai.usage.output_tokens", int(data.output_tokens))
-
-unsubscribe = session.on(handle_usage)
-await session.send({"prompt": "Hello"})
-```
-
-**Event Data Structure:**
-
-```python
-@dataclass
-class Usage:
- input_tokens: float
- output_tokens: float
- cache_read_tokens: float
- cache_write_tokens: float
-```
-
-**Maps to GenAI Attributes:**
-- `input_tokens` → `gen_ai.usage.input_tokens`
-- `output_tokens` → `gen_ai.usage.output_tokens`
-- Response model → `gen_ai.response.model`
-
-### TOOL_EXECUTION_START / COMPLETE Events
-
-Create child spans for each tool execution:
-
-```python
-from opentelemetry.trace import SpanKind
-import json
-
-# Dictionary to track active tool spans
-tool_spans = {}
-
-def handle_tool_events(event):
- data = event.data
-
- if event.type == SessionEventType.TOOL_EXECUTION_START and data:
- call_id = data.tool_call_id or str(uuid.uuid4())
- tool_name = data.tool_name or "unknown"
-
- tool_attrs = {
- "gen_ai.tool.name": tool_name,
- "gen_ai.operation.name": "execute_tool",
- }
-
- if call_id:
- tool_attrs["gen_ai.tool.call.id"] = call_id
-
- # Optional: include tool arguments (may contain sensitive data)
- if data.arguments is not None:
- try:
- tool_attrs["gen_ai.tool.call.arguments"] = json.dumps(data.arguments)
- except Exception:
- tool_attrs["gen_ai.tool.call.arguments"] = str(data.arguments)
-
- tool_span = tracer.start_span(
- name=f"execute_tool {tool_name}",
- kind=SpanKind.CLIENT,
- attributes=tool_attrs
- )
- tool_token = context.attach(trace.set_span_in_context(tool_span))
- tool_spans[call_id] = (tool_span, tool_token)
-
- elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data:
- call_id = data.tool_call_id
- entry = tool_spans.pop(call_id, None) if call_id else None
-
- if entry:
- tool_span, tool_token = entry
-
- # Optional: include tool result (may contain sensitive data)
- if data.result is not None:
- try:
- result_str = json.dumps(data.result)
- except Exception:
- result_str = str(data.result)
- # Truncate to 512 chars to avoid huge spans
- tool_span.set_attribute("gen_ai.tool.call.result", result_str[:512])
-
- # Mark as error if tool failed
- if hasattr(data, "success") and data.success is False:
- tool_span.set_attribute("error.type", "tool_error")
-
- context.detach(tool_token)
- tool_span.end()
-
-unsubscribe = session.on(handle_tool_events)
-await session.send({"prompt": "What's the weather?"})
-```
-
-**Tool Event Data:**
-- `tool_call_id` → `gen_ai.tool.call.id`
-- `tool_name` → `gen_ai.tool.name`
-- `arguments` → `gen_ai.tool.call.arguments` (opt-in)
-- `result` → `gen_ai.tool.call.result` (opt-in)
-
-### ASSISTANT_MESSAGE Event
-
-Capture the final message as a span event:
-
-```python
-def handle_message(event):
- if event.type == SessionEventType.ASSISTANT_MESSAGE and event.data:
- if event.data.content:
- # Add as a span event (opt-in for content recording)
- span.add_event(
- "gen_ai.output.messages",
- attributes={
- "gen_ai.event.content": json.dumps({
- "role": "assistant",
- "content": event.data.content
- })
- }
- )
-
-unsubscribe = session.on(handle_message)
-await session.send({"prompt": "Tell me a joke"})
-```
-
-## Complete Example
-
-```python
-import asyncio
-import json
-import uuid
-from copilot import CopilotClient, PermissionHandler
-from copilot.generated.session_events import SessionEventType
-from opentelemetry import trace, context
-from opentelemetry.trace import SpanKind
-from opentelemetry.sdk.trace import TracerProvider
-from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter
-
-# Setup OpenTelemetry
-tracer_provider = TracerProvider()
-trace.set_tracer_provider(tracer_provider)
-tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
-tracer = trace.get_tracer(__name__)
-
-async def invoke_agent(prompt: str):
- """Invoke agent with full OpenTelemetry instrumentation."""
-
- # Create main span
- span_attrs = {
- "gen_ai.operation.name": "invoke_agent",
- "gen_ai.provider.name": "github.copilot",
- "gen_ai.agent.name": "example-agent",
- "gen_ai.request.model": "gpt-5",
- }
-
- span = tracer.start_span(
- name="invoke_agent example-agent",
- kind=SpanKind.CLIENT,
- attributes=span_attrs
- )
- token = context.attach(trace.set_span_in_context(span))
- tool_spans = {}
-
- try:
- client = CopilotClient()
- await client.start()
-
- session = await client.create_session({
- "model": "gpt-5",
- "on_permission_request": PermissionHandler.approve_all,
- })
-
- # Subscribe to events via callback
- def handle_event(event):
- data = event.data
-
- # Handle usage events
- if event.type == SessionEventType.ASSISTANT_USAGE and data:
- if data.model:
- span.set_attribute("gen_ai.response.model", data.model)
- if data.input_tokens is not None:
- span.set_attribute("gen_ai.usage.input_tokens", int(data.input_tokens))
- if data.output_tokens is not None:
- span.set_attribute("gen_ai.usage.output_tokens", int(data.output_tokens))
-
- # Handle tool execution
- elif event.type == SessionEventType.TOOL_EXECUTION_START and data:
- call_id = data.tool_call_id or str(uuid.uuid4())
- tool_name = data.tool_name or "unknown"
-
- tool_attrs = {
- "gen_ai.tool.name": tool_name,
- "gen_ai.operation.name": "execute_tool",
- "gen_ai.tool.call.id": call_id,
- }
-
- tool_span = tracer.start_span(
- name=f"execute_tool {tool_name}",
- kind=SpanKind.CLIENT,
- attributes=tool_attrs
- )
- tool_token = context.attach(trace.set_span_in_context(tool_span))
- tool_spans[call_id] = (tool_span, tool_token)
-
- elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data:
- call_id = data.tool_call_id
- entry = tool_spans.pop(call_id, None) if call_id else None
- if entry:
- tool_span, tool_token = entry
- context.detach(tool_token)
- tool_span.end()
-
- # Capture final message
- elif event.type == SessionEventType.ASSISTANT_MESSAGE and data:
- if data.content:
- print(f"Assistant: {data.content}")
-
- unsubscribe = session.on(handle_event)
-
- # Send message and wait for completion
- response = await session.send_and_wait({"prompt": prompt})
-
- span.set_attribute("gen_ai.response.finish_reasons", ["stop"])
- unsubscribe()
-
- except Exception as e:
- span.set_attribute("error.type", type(e).__name__)
- raise
- finally:
- # Clean up any unclosed tool spans
- for call_id, (tool_span, tool_token) in tool_spans.items():
- tool_span.set_attribute("error.type", "stream_aborted")
- context.detach(tool_token)
- tool_span.end()
-
- context.detach(token)
- span.end()
- await client.stop()
-
-# Run
-asyncio.run(invoke_agent("What's 2+2?"))
-```
-
-## Required Span Attributes
-
-According to OpenTelemetry GenAI semantic conventions, these attributes are **required** for agent invocation spans:
-
-| Attribute | Description | Example |
-|-----------|-------------|---------|
-| `gen_ai.operation.name` | Operation type | `invoke_agent`, `chat`, `execute_tool` |
-| `gen_ai.provider.name` | Provider identifier | `github.copilot` |
-| `gen_ai.request.model` | Model used for request | `gpt-5`, `gpt-4.1` |
-
-## Recommended Span Attributes
-
-These attributes are **recommended** for better observability:
-
-| Attribute | Description |
-|-----------|-------------|
-| `gen_ai.agent.id` | Unique agent identifier |
-| `gen_ai.agent.name` | Human-readable agent name |
-| `gen_ai.response.model` | Actual model used in response |
-| `gen_ai.usage.input_tokens` | Input tokens consumed |
-| `gen_ai.usage.output_tokens` | Output tokens generated |
-| `gen_ai.response.finish_reasons` | Completion reasons (e.g., `["stop"]`) |
-
-## Content Recording
-
-Recording message content and tool arguments/results is **optional** and should be opt-in since it may contain sensitive data.
-
-### Environment Variable Control
-
-```bash
-# Enable content recording
-export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
-```
-
-### Checking at Runtime
-
-
-```python
-import os
-
-def should_record_content():
- return os.getenv("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false").lower() == "true"
-
-# Only add content if enabled
-if should_record_content() and event.data.content:
- span.add_event("gen_ai.output.messages", ...)
-```
-
-## MCP (Model Context Protocol) Tool Conventions
-
-For MCP-based tools, add these additional attributes following the [OpenTelemetry MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):
-
-
-```python
-tool_attrs = {
- # Required
- "mcp.method.name": "tools/call",
-
- # Recommended
- "mcp.server.name": data.mcp_server_name,
- "mcp.session.id": session.session_id,
-
- # GenAI attributes
- "gen_ai.tool.name": data.mcp_tool_name,
- "gen_ai.operation.name": "execute_tool",
- "network.transport": "pipe", # Copilot SDK uses stdio
-}
-```
-
-## Span Naming Conventions
-
-Follow these patterns for span names:
-
-| Operation | Span Name Pattern | Example |
-|-----------|-------------------|---------|
-| Agent invocation | `invoke_agent {agent_name}` | `invoke_agent weather-bot` |
-| Chat | `chat` | `chat` |
-| Tool execution | `execute_tool {tool_name}` | `execute_tool fetch_weather` |
-| MCP tool | `tools/call {tool_name}` | `tools/call read_file` |
-
-## Metrics
-
-You can also export metrics for token usage and operation duration:
-
-```python
-from opentelemetry import metrics
-from opentelemetry.sdk.metrics import MeterProvider
-from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
-
-# Setup metrics
-reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
-provider = MeterProvider(metric_readers=[reader])
-metrics.set_meter_provider(provider)
-
-meter = metrics.get_meter(__name__)
-
-# Create metrics
-operation_duration = meter.create_histogram(
- name="gen_ai.client.operation.duration",
- description="Duration of GenAI operations",
- unit="ms"
-)
-
-token_usage = meter.create_counter(
- name="gen_ai.client.token.usage",
- description="Token usage count"
-)
-
-# Record metrics
-operation_duration.record(123.45, attributes={
- "gen_ai.operation.name": "invoke_agent",
- "gen_ai.request.model": "gpt-5",
-})
-
-token_usage.add(150, attributes={
- "gen_ai.token.type": "input",
- "gen_ai.operation.name": "invoke_agent",
-})
-```
-
-## Azure Monitor Integration
-
-For production observability with Azure Monitor:
-
-```python
-from azure.monitor.opentelemetry import configure_azure_monitor
-
-# Enable Azure Monitor
-connection_string = "InstrumentationKey=..."
-configure_azure_monitor(connection_string=connection_string)
-
-# Your instrumented code here
-```
-
-View traces in the Azure Portal under your Application Insights resource → Tracing.
-
-## Best Practices
-
-1. **Always close spans**: Use try/finally blocks to ensure spans are ended even on errors
-2. **Set error attributes**: On exceptions, set `error.type` and optionally `error.message`
-3. **Use child spans for tools**: Create separate spans for each tool execution
-4. **Opt-in for content**: Only record message content and tool arguments when explicitly enabled
-5. **Truncate large values**: Limit tool results and arguments to reasonable sizes (e.g., 512 chars)
-6. **Set finish reasons**: Always set `gen_ai.response.finish_reasons` when the operation completes successfully
-7. **Include model info**: Capture both request and response model names
-
-## Troubleshooting
-
-### No spans appearing
-
-1. Verify tracer provider is set: `trace.set_tracer_provider(provider)`
-2. Add a span processor: `provider.add_span_processor(SimpleSpanProcessor(exporter))`
-3. Ensure spans are ended: Check for missing `span.end()` calls
-
-### Tool spans not showing as children
-
-Make sure to attach the tool span to the parent context:
-
-```python
-tool_token = context.attach(trace.set_span_in_context(tool_span))
-```
-
-### Context warnings in async code
-
-You may see "Failed to detach context" warnings in async streaming code. These are expected and don't affect tracing correctness.
-
-## References
-
-- [OpenTelemetry GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
-- [OpenTelemetry MCP Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/)
-- [OpenTelemetry Python SDK](https://opentelemetry.io/docs/instrumentation/python/)
-- [GenAI Semantic Conventions v1.34.0](https://opentelemetry.io/schemas/1.34.0)
-- [Copilot SDK Documentation](https://github.com/github/copilot-sdk)
diff --git a/docs/guides/setup/azure-managed-identity.md b/docs/setup/azure-managed-identity.md
similarity index 82%
rename from docs/guides/setup/azure-managed-identity.md
rename to docs/setup/azure-managed-identity.md
index 9ad1ddb157..40d87c5ba1 100644
--- a/docs/guides/setup/azure-managed-identity.md
+++ b/docs/setup/azure-managed-identity.md
@@ -1,6 +1,6 @@
# Azure Managed Identity with BYOK
-The Copilot SDK's [BYOK mode](./byok.md) accepts static API keys, but Azure deployments often use **Managed Identity** (Entra ID) instead of long-lived keys. Since the SDK doesn't natively support Entra ID authentication, you can use a short-lived bearer token via the `bearer_token` provider config field.
+The Copilot SDK's [BYOK mode](../auth/byok.md) accepts static API keys, but Azure deployments often use **Managed Identity** (Entra ID) instead of long-lived keys. Since the SDK doesn't natively support Entra ID authentication, you can use a short-lived bearer token via the `bearer_token` provider config field.
This guide shows how to use `DefaultAzureCredential` from the [Azure Identity](https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential) library to authenticate with Azure AI Foundry models through the Copilot SDK.
@@ -42,7 +42,7 @@ import asyncio
import os
from azure.identity import DefaultAzureCredential
-from copilot import CopilotClient, ProviderConfig, SessionConfig
+from copilot import CopilotClient, PermissionHandler
COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default"
@@ -58,15 +58,14 @@ async def main():
await client.start()
session = await client.create_session(
- SessionConfig(
- model="gpt-4.1",
- provider=ProviderConfig(
- type="openai",
- base_url=f"{foundry_url.rstrip('/')}/openai/v1/",
- bearer_token=token, # Short-lived bearer token
- wire_api="responses",
- ),
- )
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-4.1",
+ provider={
+ "type": "openai",
+ "base_url": f"{foundry_url.rstrip('/')}/openai/v1/",
+ "bearer_token": token, # Short-lived bearer token
+ "wire_api": "responses",
+ },
)
response = await session.send_and_wait({"prompt": "Hello from Managed Identity!"})
@@ -84,7 +83,7 @@ Bearer tokens expire (typically after ~1 hour). For servers or long-running agen
```python
from azure.identity import DefaultAzureCredential
-from copilot import CopilotClient, ProviderConfig, SessionConfig
+from copilot import CopilotClient, PermissionHandler
COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default"
@@ -98,24 +97,21 @@ class ManagedIdentityCopilotAgent:
self.credential = DefaultAzureCredential()
self.client = CopilotClient()
- def _get_session_config(self) -> SessionConfig:
- """Build a SessionConfig with a fresh bearer token."""
+ def _get_provider_config(self) -> dict:
+ """Build a provider config dict with a fresh bearer token."""
token = self.credential.get_token(COGNITIVE_SERVICES_SCOPE).token
- return SessionConfig(
- model=self.model,
- provider=ProviderConfig(
- type="openai",
- base_url=f"{self.foundry_url}/openai/v1/",
- bearer_token=token,
- wire_api="responses",
- ),
- )
+ return {
+ "type": "openai",
+ "base_url": f"{self.foundry_url}/openai/v1/",
+ "bearer_token": token,
+ "wire_api": "responses",
+ }
async def chat(self, prompt: str) -> str:
"""Send a prompt and return the response text."""
# Fresh token for each session
- config = self._get_session_config()
- session = await self.client.create_session(config)
+ provider = self._get_provider_config()
+ session = await self.client.create_session(on_permission_request=PermissionHandler.approve_all, model=self.model, provider=provider)
response = await session.send_and_wait({"prompt": prompt})
await session.disconnect()
@@ -207,11 +203,11 @@ See the [DefaultAzureCredential documentation](https://learn.microsoft.com/pytho
| Azure-hosted app with Managed Identity | ✅ Use this pattern |
| App with existing Azure AD service principal | ✅ Use this pattern |
| Local development with `az login` | ✅ Use this pattern |
-| Non-Azure environment with static API key | Use [standard BYOK](./byok.md) |
+| Non-Azure environment with static API key | Use [standard BYOK](../auth/byok.md) |
| GitHub Copilot subscription available | Use [GitHub OAuth](./github-oauth.md) |
## See Also
-- [BYOK Setup Guide](./byok.md) — Static API key configuration
+- [BYOK Setup Guide](../auth/byok.md) — Static API key configuration
- [Backend Services](./backend-services.md) — Server-side deployment
- [Azure Identity documentation](https://learn.microsoft.com/python/api/overview/azure/identity-readme)
diff --git a/docs/guides/setup/backend-services.md b/docs/setup/backend-services.md
similarity index 86%
rename from docs/guides/setup/backend-services.md
rename to docs/setup/backend-services.md
index e0d0975dbf..735adf4ff3 100644
--- a/docs/guides/setup/backend-services.md
+++ b/docs/setup/backend-services.md
@@ -111,17 +111,14 @@ res.json({ content: response?.data.content });
Python
```python
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
client = CopilotClient({
"cli_url": "localhost:4321",
})
await client.start()
-session = await client.create_session({
- "session_id": f"user-{user_id}-{int(time.time())}",
- "model": "gpt-4.1",
-})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-{int(time.time())}")
response = await session.send_and_wait({"prompt": message})
```
@@ -131,7 +128,39 @@ response = await session.send_and_wait({"prompt": message})
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+ userID := "user1"
+ message := "Hello"
+
+ client := copilot.NewClient(&copilot.ClientOptions{
+ CLIUrl: "localhost:4321",
+ })
+ client.Start(ctx)
+ defer client.Stop()
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ SessionID: fmt.Sprintf("user-%s-%d", userID, time.Now().Unix()),
+ Model: "gpt-4.1",
+ })
+
+ response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: message})
+ _ = response
+}
+```
+
+
```go
client := copilot.NewClient(&copilot.ClientOptions{
CLIUrl:"localhost:4321",
@@ -152,7 +181,30 @@ response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: message})
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+var userId = "user1";
+var message = "Hello";
+
+var client = new CopilotClient(new CopilotClientOptions
+{
+ CliUrl = "localhost:4321",
+ UseStdio = false,
+});
+
+await using var session = await client.CreateSessionAsync(new SessionConfig
+{
+ SessionId = $"user-{userId}-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}",
+ Model = "gpt-4.1",
+});
+
+var response = await session.SendAndWaitAsync(
+ new MessageOptions { Prompt = message });
+```
+
+
```csharp
var client = new CopilotClient(new CopilotClientOptions
{
@@ -225,7 +277,7 @@ app.post("/chat", authMiddleware, async (req, res) => {
### BYOK (No GitHub Auth)
-Use your own API keys for the model provider. See [BYOK](./byok.md) for details.
+Use your own API keys for the model provider. See [BYOK](../auth/byok.md) for details.
```typescript
const client = new CopilotClient({
@@ -423,10 +475,10 @@ setInterval(() => cleanupSessions(24 * 60 * 60 * 1000), 60 * 60 * 1000);
|------|-----------|
| Multiple CLI servers / high availability | [Scaling & Multi-Tenancy](./scaling.md) |
| GitHub account auth for users | [GitHub OAuth](./github-oauth.md) |
-| Your own model keys | [BYOK](./byok.md) |
+| Your own model keys | [BYOK](../auth/byok.md) |
## Next Steps
- **[Scaling & Multi-Tenancy](./scaling.md)** — Handle more users, add redundancy
-- **[Session Persistence](../session-persistence.md)** — Resume sessions across restarts
+- **[Session Persistence](../features/session-persistence.md)** — Resume sessions across restarts
- **[GitHub OAuth](./github-oauth.md)** — Add user authentication
diff --git a/docs/guides/setup/bundled-cli.md b/docs/setup/bundled-cli.md
similarity index 88%
rename from docs/guides/setup/bundled-cli.md
rename to docs/setup/bundled-cli.md
index 6daf57b56e..cdfe6df812 100644
--- a/docs/guides/setup/bundled-cli.md
+++ b/docs/setup/bundled-cli.md
@@ -85,7 +85,7 @@ await client.stop();
Python
```python
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
from pathlib import Path
client = CopilotClient({
@@ -93,7 +93,7 @@ client = CopilotClient({
})
await client.start()
-session = await client.create_session({"model": "gpt-4.1"})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1")
response = await session.send_and_wait({"prompt": "Hello!"})
print(response.data.content)
@@ -105,7 +105,35 @@ await client.stop()
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+
+ client := copilot.NewClient(&copilot.ClientOptions{
+ CLIPath: "./vendor/copilot",
+ })
+ if err := client.Start(ctx); err != nil {
+ log.Fatal(err)
+ }
+ defer client.Stop()
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"})
+ response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"})
+ fmt.Println(*response.Data.Content)
+}
+```
+
+
```go
client := copilot.NewClient(&copilot.ClientOptions{
CLIPath:"./vendor/copilot",
@@ -203,7 +231,7 @@ const session = await client.createSession({
});
```
-See the **[BYOK guide](./byok.md)** for full details.
+See the **[BYOK guide](../auth/byok.md)** for full details.
## Session Management
@@ -318,10 +346,10 @@ const client = new CopilotClient({
|------|-----------|
| Users signing in with GitHub accounts | [GitHub OAuth](./github-oauth.md) |
| Run on a server instead of user machines | [Backend Services](./backend-services.md) |
-| Use your own model keys | [BYOK](./byok.md) |
+| Use your own model keys | [BYOK](../auth/byok.md) |
## Next Steps
-- **[BYOK guide](./byok.md)** — Use your own model provider keys
-- **[Session Persistence](../session-persistence.md)** — Advanced session management
-- **[Getting Started tutorial](../../getting-started.md)** — Build a complete app
+- **[BYOK guide](../auth/byok.md)** — Use your own model provider keys
+- **[Session Persistence](../features/session-persistence.md)** — Advanced session management
+- **[Getting Started tutorial](../getting-started.md)** — Build a complete app
diff --git a/docs/guides/setup/github-oauth.md b/docs/setup/github-oauth.md
similarity index 86%
rename from docs/guides/setup/github-oauth.md
rename to docs/setup/github-oauth.md
index 07251c8fbe..81d2b25a27 100644
--- a/docs/guides/setup/github-oauth.md
+++ b/docs/setup/github-oauth.md
@@ -145,7 +145,7 @@ const response = await session.sendAndWait({ prompt: "Hello!" });
Python
```python
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
def create_client_for_user(user_token: str) -> CopilotClient:
return CopilotClient({
@@ -157,10 +157,7 @@ def create_client_for_user(user_token: str) -> CopilotClient:
client = create_client_for_user("gho_user_access_token")
await client.start()
-session = await client.create_session({
- "session_id": f"user-{user_id}-session",
- "model": "gpt-4.1",
-})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-session")
response = await session.send_and_wait({"prompt": "Hello!"})
```
@@ -170,7 +167,41 @@ response = await session.send_and_wait({"prompt": "Hello!"})
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func createClientForUser(userToken string) *copilot.Client {
+ return copilot.NewClient(&copilot.ClientOptions{
+ GitHubToken: userToken,
+ UseLoggedInUser: copilot.Bool(false),
+ })
+}
+
+func main() {
+ ctx := context.Background()
+ userID := "user1"
+
+ client := createClientForUser("gho_user_access_token")
+ client.Start(ctx)
+ defer client.Stop()
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ SessionID: fmt.Sprintf("user-%s-session", userID),
+ Model: "gpt-4.1",
+ })
+ response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"})
+ _ = response
+}
+```
+
+
```go
func createClientForUser(userToken string) *copilot.Client {
return copilot.NewClient(&copilot.ClientOptions{
@@ -196,7 +227,31 @@ response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}
.NET
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+CopilotClient CreateClientForUser(string userToken) =>
+ new CopilotClient(new CopilotClientOptions
+ {
+ GithubToken = userToken,
+ UseLoggedInUser = false,
+ });
+
+var userId = "user1";
+
+await using var client = CreateClientForUser("gho_user_access_token");
+await using var session = await client.CreateSessionAsync(new SessionConfig
+{
+ SessionId = $"user-{userId}-session",
+ Model = "gpt-4.1",
+});
+
+var response = await session.SendAndWaitAsync(
+ new MessageOptions { Prompt = "Hello!" });
+```
+
+
```csharp
CopilotClient CreateClientForUser(string userToken) =>
new CopilotClient(new CopilotClientOptions
@@ -374,12 +429,12 @@ For a lighter resource footprint, you can run a single external CLI server and p
| Need | Next Guide |
|------|-----------|
-| Users without GitHub accounts | [BYOK](./byok.md) |
+| Users without GitHub accounts | [BYOK](../auth/byok.md) |
| Run the SDK on servers | [Backend Services](./backend-services.md) |
| Handle many concurrent users | [Scaling & Multi-Tenancy](./scaling.md) |
## Next Steps
-- **[Authentication docs](../../auth/index.md)** — Full auth method reference
+- **[Authentication docs](../auth/index.md)** — Full auth method reference
- **[Backend Services](./backend-services.md)** — Run the SDK server-side
- **[Scaling & Multi-Tenancy](./scaling.md)** — Handle many users at scale
diff --git a/docs/guides/setup/index.md b/docs/setup/index.md
similarity index 94%
rename from docs/guides/setup/index.md
rename to docs/setup/index.md
index 2613fe29df..268e266882 100644
--- a/docs/guides/setup/index.md
+++ b/docs/setup/index.md
@@ -58,7 +58,7 @@ You're building a product for customers. You need to handle authentication for y
**Start with:**
1. **[GitHub OAuth](./github-oauth.md)** — Let customers sign in with GitHub
-2. **[BYOK](./byok.md)** — Manage identity yourself with your own model keys
+2. **[BYOK](../auth/byok.md)** — Manage identity yourself with your own model keys
3. **[Backend Services](./backend-services.md)** — Power your product from server-side code
**For production:**
@@ -74,7 +74,7 @@ You're embedding Copilot into a platform — APIs, developer tools, or infrastru
**Depending on your auth model:**
3. **[GitHub OAuth](./github-oauth.md)** — For GitHub-authenticated users
-4. **[BYOK](./byok.md)** — For self-managed identity and model access
+4. **[BYOK](../auth/byok.md)** — For self-managed identity and model access
## Decision Matrix
@@ -85,7 +85,7 @@ Use this table to find the right guides based on what you need to do:
| Simplest possible setup | [Local CLI](./local-cli.md) |
| Ship a standalone app with Copilot | [Bundled CLI](./bundled-cli.md) |
| Users sign in with GitHub | [GitHub OAuth](./github-oauth.md) |
-| Use your own model keys (OpenAI, Azure, etc.) | [BYOK](./byok.md) |
+| Use your own model keys (OpenAI, Azure, etc.) | [BYOK](../auth/byok.md) |
| Azure BYOK with Managed Identity (no API keys) | [Azure Managed Identity](./azure-managed-identity.md) |
| Run the SDK on a server | [Backend Services](./backend-services.md) |
| Serve multiple users / scale horizontally | [Scaling & Multi-Tenancy](./scaling.md) |
@@ -136,7 +136,7 @@ All guides assume you have:
- Go: `go get github.com/github/copilot-sdk/go`
- .NET: `dotnet add package GitHub.Copilot.SDK`
-If you're brand new, start with the **[Getting Started tutorial](../../getting-started.md)** first, then come back here for production configuration.
+If you're brand new, start with the **[Getting Started tutorial](../getting-started.md)** first, then come back here for production configuration.
## Next Steps
diff --git a/docs/guides/setup/local-cli.md b/docs/setup/local-cli.md
similarity index 82%
rename from docs/guides/setup/local-cli.md
rename to docs/setup/local-cli.md
index a5fa906b89..bb95a4d38c 100644
--- a/docs/guides/setup/local-cli.md
+++ b/docs/setup/local-cli.md
@@ -51,12 +51,12 @@ await client.stop();
Python
```python
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
client = CopilotClient()
await client.start()
-session = await client.create_session({"model": "gpt-4.1"})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1")
response = await session.send_and_wait({"prompt": "Hello!"})
print(response.data.content)
@@ -68,7 +68,33 @@ await client.stop()
Go
-
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func main() {
+ ctx := context.Background()
+
+ client := copilot.NewClient(nil)
+ if err := client.Start(ctx); err != nil {
+ log.Fatal(err)
+ }
+ defer client.Stop()
+
+ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"})
+ response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"})
+ fmt.Println(*response.Data.Content)
+}
+```
+
+
```go
client := copilot.NewClient(nil)
if err := client.Start(ctx); err != nil {
@@ -140,14 +166,11 @@ const client = new CopilotClient({
// Set log level for debugging
logLevel: "debug",
- // Pass extra CLI arguments
- cliArgs: ["--disable-telemetry"],
+ // Pass extra CLI arguments (example: set a custom log directory)
+ cliArgs: ["--log-dir=/tmp/copilot-logs"],
// Set working directory
cwd: "/path/to/project",
-
- // Auto-restart CLI if it crashes (default: true)
- autoRestart: true,
});
```
@@ -199,10 +222,10 @@ If you need any of these, it's time to pick a more advanced setup:
| Ship your app to others | [Bundled CLI](./bundled-cli.md) |
| Multiple users signing in | [GitHub OAuth](./github-oauth.md) |
| Run on a server | [Backend Services](./backend-services.md) |
-| Use your own model keys | [BYOK](./byok.md) |
+| Use your own model keys | [BYOK](../auth/byok.md) |
## Next Steps
-- **[Getting Started tutorial](../../getting-started.md)** — Build a complete interactive app
-- **[Authentication docs](../../auth/index.md)** — All auth methods in detail
-- **[Session Persistence](../session-persistence.md)** — Advanced session management
+- **[Getting Started tutorial](../getting-started.md)** — Build a complete interactive app
+- **[Authentication docs](../auth/index.md)** — All auth methods in detail
+- **[Session Persistence](../features/session-persistence.md)** — Advanced session management
diff --git a/docs/guides/setup/scaling.md b/docs/setup/scaling.md
similarity index 99%
rename from docs/guides/setup/scaling.md
rename to docs/setup/scaling.md
index 974276e5e1..325d9244dd 100644
--- a/docs/guides/setup/scaling.md
+++ b/docs/setup/scaling.md
@@ -629,7 +629,7 @@ flowchart TB
## Next Steps
-- **[Session Persistence](../session-persistence.md)** — Deep dive on resumable sessions
+- **[Session Persistence](../features/session-persistence.md)** — Deep dive on resumable sessions
- **[Backend Services](./backend-services.md)** — Core server-side setup
- **[GitHub OAuth](./github-oauth.md)** — Multi-user authentication
-- **[BYOK](./byok.md)** — Use your own model provider
+- **[BYOK](../auth/byok.md)** — Use your own model provider
diff --git a/docs/compatibility.md b/docs/troubleshooting/compatibility.md
similarity index 51%
rename from docs/compatibility.md
rename to docs/troubleshooting/compatibility.md
index bfd17915b5..1a322b88c2 100644
--- a/docs/compatibility.md
+++ b/docs/troubleshooting/compatibility.md
@@ -20,9 +20,15 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| Delete session | `deleteSession()` | Remove from storage |
| List sessions | `listSessions()` | All stored sessions |
| Get last session | `getLastSessionId()` | For quick resume |
+| Get foreground session | `getForegroundSessionId()` | Multi-session coordination |
+| Set foreground session | `setForegroundSessionId()` | Multi-session coordination |
| **Messaging** | | |
| Send message | `send()` | With attachments |
| Send and wait | `sendAndWait()` | Blocks until complete |
+| Steering (immediate mode) | `send({ mode: "immediate" })` | Inject mid-turn without aborting |
+| Queueing (enqueue mode) | `send({ mode: "enqueue" })` | Buffer for sequential processing (default) |
+| File attachments | `send({ attachments: [{ type: "file", path }] })` | Images auto-encoded and resized |
+| Directory attachments | `send({ attachments: [{ type: "directory", path }] })` | Attach directory context |
| Get history | `getMessages()` | All session events |
| Abort | `abort()` | Cancel in-flight request |
| **Tools** | | |
@@ -31,12 +37,28 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| Tool result modification | `onPostToolUse` hook | Transform results |
| Available/excluded tools | `availableTools`, `excludedTools` config | Filter tools |
| **Models** | | |
-| List models | `listModels()` | With capabilities |
-| Set model | `model` in session config | Per-session |
+| List models | `listModels()` | With capabilities, billing, policy |
+| Set model (at creation) | `model` in session config | Per-session |
+| Switch model (mid-session) | `session.setModel()` | Also via `session.rpc.model.switchTo()` |
+| Get current model | `session.rpc.model.getCurrent()` | Query active model |
| Reasoning effort | `reasoningEffort` config | For supported models |
+| **Agent Mode** | | |
+| Get current mode | `session.rpc.mode.get()` | Returns current mode |
+| Set mode | `session.rpc.mode.set()` | Switch between modes |
+| **Plan Management** | | |
+| Read plan | `session.rpc.plan.read()` | Get plan.md content and path |
+| Update plan | `session.rpc.plan.update()` | Write plan.md content |
+| Delete plan | `session.rpc.plan.delete()` | Remove plan.md |
+| **Workspace Files** | | |
+| List workspace files | `session.rpc.workspace.listFiles()` | Files in session workspace |
+| Read workspace file | `session.rpc.workspace.readFile()` | Read file content |
+| Create workspace file | `session.rpc.workspace.createFile()` | Create file in workspace |
| **Authentication** | | |
| Get auth status | `getAuthStatus()` | Check login state |
| Use token | `githubToken` option | Programmatic auth |
+| **Connectivity** | | |
+| Ping | `client.ping()` | Health check with server timestamp |
+| Get server status | `client.getStatus()` | Protocol version and server info |
| **MCP Servers** | | |
| Local/stdio servers | `mcpServers` config | Spawn processes |
| Remote HTTP/SSE | `mcpServers` config | Connect to services |
@@ -44,19 +66,27 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| Pre-tool use | `onPreToolUse` | Permission, modify args |
| Post-tool use | `onPostToolUse` | Modify results |
| User prompt | `onUserPromptSubmitted` | Modify prompts |
-| Session start/end | `onSessionStart`, `onSessionEnd` | Lifecycle |
+| Session start/end | `onSessionStart`, `onSessionEnd` | Lifecycle with source/reason |
| Error handling | `onErrorOccurred` | Custom handling |
| **Events** | | |
| All session events | `on()`, `once()` | 40+ event types |
| Streaming | `streaming: true` | Delta events |
-| **Advanced** | | |
-| Custom agents | `customAgents` config | Load agent definitions |
+| **Session Config** | | |
+| Custom agents | `customAgents` config | Define specialized agents |
| System message | `systemMessage` config | Append or replace |
| Custom provider | `provider` config | BYOK support |
| Infinite sessions | `infiniteSessions` config | Auto-compaction |
| Permission handler | `onPermissionRequest` | Approve/deny requests |
| User input handler | `onUserInputRequest` | Handle ask_user |
| Skills | `skillDirectories` config | Custom skills |
+| Disabled skills | `disabledSkills` config | Disable specific skills |
+| Config directory | `configDir` config | Override default config location |
+| Client name | `clientName` config | Identify app in User-Agent |
+| Working directory | `workingDirectory` config | Set session cwd |
+| **Experimental** | | |
+| Agent management | `session.rpc.agent.*` | List, select, deselect, get current agent |
+| Fleet mode | `session.rpc.fleet.start()` | Parallel sub-agent execution |
+| Manual compaction | `session.rpc.compaction.compact()` | Trigger compaction on demand |
### ❌ Not Available in SDK (CLI-Only)
@@ -66,20 +96,32 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| Export to file | `--share`, `/share` | Not in protocol |
| Export to gist | `--share-gist`, `/share gist` | Not in protocol |
| **Interactive UI** | | |
-| Slash commands | `/help`, `/clear`, etc. | TUI-only |
+| Slash commands | `/help`, `/clear`, `/exit`, etc. | TUI-only |
| Agent picker dialog | `/agent` | Interactive UI |
| Diff mode dialog | `/diff` | Interactive UI |
| Feedback dialog | `/feedback` | Interactive UI |
| Theme picker | `/theme` | Terminal UI |
+| Model picker | `/model` | Interactive UI (use SDK `setModel()` instead) |
+| Copy to clipboard | `/copy` | Terminal-specific |
+| Context management | `/context` | Interactive UI |
+| **Research & History** | | |
+| Deep research | `/research` | TUI workflow with web search |
+| Session history tools | `/chronicle` | Standup, tips, improve, reindex |
| **Terminal Features** | | |
| Color output | `--no-color` | Terminal-specific |
| Screen reader mode | `--screen-reader` | Accessibility |
| Rich diff rendering | `--plain-diff` | Terminal rendering |
| Startup banner | `--banner` | Visual element |
+| Streamer mode | `/streamer-mode` | TUI display mode |
+| Alternate screen buffer | `--alt-screen`, `--no-alt-screen` | Terminal rendering |
+| Mouse support | `--mouse`, `--no-mouse` | Terminal input |
| **Path/Permission Shortcuts** | | |
| Allow all paths | `--allow-all-paths` | Use permission handler |
| Allow all URLs | `--allow-all-urls` | Use permission handler |
-| YOLO mode | `--yolo` | Use permission handler |
+| Allow all permissions | `--yolo`, `--allow-all`, `/allow-all` | Use permission handler |
+| Granular tool permissions | `--allow-tool`, `--deny-tool` | Use `onPreToolUse` hook |
+| URL access control | `--allow-url`, `--deny-url` | Use permission handler |
+| Reset allowed tools | `/reset-allowed-tools` | TUI command |
| **Directory Management** | | |
| Add directory | `/add-dir`, `--add-dir` | Configure in session |
| List directories | `/list-dirs` | TUI command |
@@ -93,8 +135,13 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| User info | `/user` | TUI command |
| **Session Operations** | | |
| Clear conversation | `/clear` | TUI-only |
-| Compact context | `/compact` | Use `infiniteSessions` config |
-| Plan view | `/plan` | TUI-only |
+| Plan view | `/plan` | TUI-only (use SDK `session.rpc.plan.*` instead) |
+| Session management | `/session`, `/resume`, `/rename` | TUI workflow |
+| Fleet mode (interactive) | `/fleet` | TUI-only (use SDK `session.rpc.fleet.start()` instead) |
+| **Skills Management** | | |
+| Manage skills | `/skills` | Interactive UI |
+| **Task Management** | | |
+| View background tasks | `/tasks` | TUI command |
| **Usage & Stats** | | |
| Token usage | `/usage` | Subscribe to usage events |
| **Code Review** | | |
@@ -103,8 +150,20 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| Delegate to PR | `/delegate` | TUI workflow |
| **Terminal Setup** | | |
| Shell integration | `/terminal-setup` | Shell-specific |
-| **Experimental** | | |
-| Toggle experimental | `/experimental` | Runtime flag |
+| **Development** | | |
+| Toggle experimental | `/experimental`, `--experimental` | Runtime flag |
+| Custom instructions control | `--no-custom-instructions` | CLI flag |
+| Diagnose session | `/diagnose` | TUI command |
+| View/manage instructions | `/instructions` | TUI command |
+| Collect debug logs | `/collect-debug-logs` | Diagnostic tool |
+| Reindex workspace | `/reindex` | TUI command |
+| IDE integration | `/ide` | IDE-specific workflow |
+| **Non-interactive Mode** | | |
+| Prompt mode | `-p`, `--prompt` | Single-shot execution |
+| Interactive prompt | `-i`, `--interactive` | Auto-execute then interactive |
+| Silent output | `-s`, `--silent` | Script-friendly |
+| Continue session | `--continue` | Resume most recent |
+| Agent selection | `--agent ` | CLI flag |
## Workarounds
@@ -150,9 +209,10 @@ session.on("assistant.usage", (event) => {
### Context Compaction
-Instead of `/compact`, configure automatic compaction:
+Instead of `/compact`, configure automatic compaction or trigger it manually:
```typescript
+// Automatic compaction via config
const session = await client.createSession({
infiniteSessions: {
enabled: true,
@@ -160,10 +220,44 @@ const session = await client.createSession({
bufferExhaustionThreshold: 0.95, // Block and compact at 95% context utilization
},
});
+
+// Manual compaction (experimental)
+const result = await session.rpc.compaction.compact();
+console.log(`Removed ${result.tokensRemoved} tokens, ${result.messagesRemoved} messages`);
```
> **Note:** Thresholds are context utilization ratios (0.0-1.0), not absolute token counts.
+### Plan Management
+
+Read and write session plans programmatically:
+
+```typescript
+// Read the current plan
+const plan = await session.rpc.plan.read();
+if (plan.exists) {
+ console.log(plan.content);
+}
+
+// Update the plan
+await session.rpc.plan.update({ content: "# My Plan\n- Step 1\n- Step 2" });
+
+// Delete the plan
+await session.rpc.plan.delete();
+```
+
+### Message Steering
+
+Inject a message into the current LLM turn without aborting:
+
+```typescript
+// Steer the agent mid-turn
+await session.send({ prompt: "Focus on error handling first", mode: "immediate" });
+
+// Default: enqueue for next turn
+await session.send({ prompt: "Next, add tests" });
+```
+
## Protocol Limitations
The SDK can only access features exposed through the CLI's JSON-RPC protocol. If you need a CLI feature that's not available:
@@ -174,15 +268,23 @@ The SDK can only access features exposed through the CLI's JSON-RPC protocol. If
## Version Compatibility
-| SDK Version | CLI Version | Protocol Version |
-|-------------|-------------|------------------|
-| Check `package.json` | `copilot --version` | `getStatus().protocolVersion` |
+| SDK Protocol Range | CLI Protocol Version | Compatibility |
+|--------------------|---------------------|---------------|
+| v2–v3 | v3 | Full support |
+| v2–v3 | v2 | Supported with automatic v2 adapters |
+
+The SDK negotiates protocol versions with the CLI at startup. The SDK supports protocol versions 2 through 3. When connecting to a v2 CLI server, the SDK automatically adapts `tool.call` and `permission.request` messages to the v3 event model — no code changes required.
+
+Check versions at runtime:
-The SDK and CLI must have compatible protocol versions. The SDK will log warnings if versions are mismatched.
+```typescript
+const status = await client.getStatus();
+console.log("Protocol version:", status.protocolVersion);
+```
## See Also
-- [Getting Started Guide](./getting-started.md)
-- [Hooks Documentation](./hooks/overview.md)
-- [MCP Servers Guide](./mcp/overview.md)
+- [Getting Started Guide](../getting-started.md)
+- [Hooks Documentation](../hooks/index.md)
+- [MCP Servers Guide](../features/mcp.md)
- [Debugging Guide](./debugging.md)
diff --git a/docs/debugging.md b/docs/troubleshooting/debugging.md
similarity index 92%
rename from docs/debugging.md
rename to docs/troubleshooting/debugging.md
index bf953b2ffd..146d3fd5aa 100644
--- a/docs/debugging.md
+++ b/docs/troubleshooting/debugging.md
@@ -44,7 +44,21 @@ client = CopilotClient({"log_level": "debug"})
Go
-
+
+```go
+package main
+
+import copilot "github.com/github/copilot-sdk/go"
+
+func main() {
+ client := copilot.NewClient(&copilot.ClientOptions{
+ LogLevel: "debug",
+ })
+ _ = client
+}
+```
+
+
```go
import copilot "github.com/github/copilot-sdk/go"
@@ -59,6 +73,7 @@ client := copilot.NewClient(&copilot.ClientOptions{
.NET
+
```csharp
using GitHub.Copilot.SDK;
using Microsoft.Extensions.Logging;
@@ -110,7 +125,18 @@ const client = new CopilotClient({
Go
-
+
+```go
+package main
+
+func main() {
+ // The Go SDK does not currently support passing extra CLI arguments.
+ // For custom log directories, run the CLI manually with --log-dir
+ // and connect via CLIUrl option.
+}
+```
+
+
```go
// The Go SDK does not currently support passing extra CLI arguments.
// For custom log directories, run the CLI manually with --log-dir
@@ -271,14 +297,7 @@ var client = new CopilotClient(new CopilotClientOptions
copilot --server --stdio
```
-2. Enable auto-restart (enabled by default):
- ```typescript
- const client = new CopilotClient({
- autoRestart: true,
- });
- ```
-
-3. Check for port conflicts if using TCP mode:
+2. Check for port conflicts if using TCP mode:
```typescript
const client = new CopilotClient({
useStdio: false,
@@ -290,7 +309,7 @@ var client = new CopilotClient(new CopilotClientOptions
## MCP Server Debugging
-MCP (Model Context Protocol) servers can be tricky to debug. For comprehensive MCP debugging guidance, see the dedicated **[MCP Debugging Guide](./mcp/debugging.md)**.
+MCP (Model Context Protocol) servers can be tricky to debug. For comprehensive MCP debugging guidance, see the dedicated **[MCP Debugging Guide](./mcp-debugging.md)**.
### Quick MCP Checklist
@@ -308,7 +327,7 @@ Before integrating with the SDK, verify your MCP server works:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | /path/to/your/mcp-server
```
-See [MCP Debugging Guide](./mcp/debugging.md) for detailed troubleshooting.
+See [MCP Debugging Guide](./mcp-debugging.md) for detailed troubleshooting.
---
@@ -493,7 +512,7 @@ If you're still stuck:
## See Also
-- [Getting Started Guide](./getting-started.md)
-- [MCP Overview](./mcp/overview.md) - MCP configuration and setup
-- [MCP Debugging Guide](./mcp/debugging.md) - Detailed MCP troubleshooting
+- [Getting Started Guide](../getting-started.md)
+- [MCP Overview](../features/mcp.md) - MCP configuration and setup
+- [MCP Debugging Guide](./mcp-debugging.md) - Detailed MCP troubleshooting
- [API Reference](https://github.com/github/copilot-sdk)
diff --git a/docs/mcp/debugging.md b/docs/troubleshooting/mcp-debugging.md
similarity index 82%
rename from docs/mcp/debugging.md
rename to docs/troubleshooting/mcp-debugging.md
index 5ca51d1e3b..30e05fd3e9 100644
--- a/docs/mcp/debugging.md
+++ b/docs/troubleshooting/mcp-debugging.md
@@ -242,7 +242,37 @@ cd /expected/working/dir
#### .NET Console Apps / Tools
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class McpDotnetConfigExample
+{
+ public static void Main()
+ {
+ var servers = new Dictionary
+ {
+ ["my-dotnet-server"] = new McpLocalServerConfig
+ {
+ Type = "local",
+ Command = @"C:\Tools\MyServer\MyServer.exe",
+ Args = new List(),
+ Cwd = @"C:\Tools\MyServer",
+ Tools = new List { "*" },
+ },
+ ["my-dotnet-tool"] = new McpLocalServerConfig
+ {
+ Type = "local",
+ Command = "dotnet",
+ Args = new List { @"C:\Tools\MyTool\MyTool.dll" },
+ Cwd = @"C:\Tools\MyTool",
+ Tools = new List { "*" },
+ }
+ };
+ }
+}
+```
+
```csharp
// Correct configuration for .NET exe
["my-dotnet-server"] = new McpLocalServerConfig
@@ -267,7 +297,28 @@ cd /expected/working/dir
#### NPX Commands
-
+
+```csharp
+using GitHub.Copilot.SDK;
+
+public static class McpNpxConfigExample
+{
+ public static void Main()
+ {
+ var servers = new Dictionary
+ {
+ ["filesystem"] = new McpLocalServerConfig
+ {
+ Type = "local",
+ Command = "cmd",
+ Args = new List { "/c", "npx", "-y", "@modelcontextprotocol/server-filesystem", "C:\\allowed\\path" },
+ Tools = new List { "*" },
+ }
+ };
+ }
+}
+```
+
```csharp
// Windows needs cmd /c for npx
["filesystem"] = new McpLocalServerConfig
@@ -304,7 +355,19 @@ xattr -d com.apple.quarantine /path/to/mcp-server
#### Homebrew Paths
-
+
+```typescript
+import { MCPLocalServerConfig } from "@github/copilot-sdk";
+
+const mcpServers: Record = {
+ "my-server": {
+ command: "/opt/homebrew/bin/node",
+ args: ["/path/to/server.js"],
+ tools: ["*"],
+ },
+};
+```
+
```typescript
// GUI apps may not have /opt/homebrew in PATH
mcpServers: {
@@ -410,6 +473,6 @@ When opening an issue or asking for help, collect:
## See Also
-- [MCP Overview](./overview.md) - Configuration and setup
-- [General Debugging Guide](../debugging.md) - SDK-wide debugging
+- [MCP Overview](../features/mcp.md) - Configuration and setup
+- [General Debugging Guide](./debugging.md) - SDK-wide debugging
- [MCP Specification](https://modelcontextprotocol.io/) - Official protocol docs
diff --git a/dotnet/README.md b/dotnet/README.md
index bdb3e8dab4..cab1cf068c 100644
--- a/dotnet/README.md
+++ b/dotnet/README.md
@@ -28,10 +28,11 @@ using GitHub.Copilot.SDK;
await using var client = new CopilotClient();
await client.StartAsync();
-// Create a session
+// Create a session (OnPermissionRequest is required)
await using var session = await client.CreateSessionAsync(new SessionConfig
{
- Model = "gpt-5"
+ Model = "gpt-5",
+ OnPermissionRequest = PermissionHandler.ApproveAll,
});
// Wait for response using session.idle event
@@ -73,12 +74,12 @@ new CopilotClient(CopilotClientOptions? options = null)
- `UseStdio` - Use stdio transport instead of TCP (default: true)
- `LogLevel` - Log level (default: "info")
- `AutoStart` - Auto-start server (default: true)
-- `AutoRestart` - Auto-restart on crash (default: true)
- `Cwd` - Working directory for the CLI process
- `Environment` - Environment variables to pass to the CLI process
- `Logger` - `ILogger` instance for SDK logging
- `GitHubToken` - GitHub token for authentication. When provided, takes priority over other auth methods.
- `UseLoggedInUser` - Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `CliUrl`.
+- `Telemetry` - OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below.
#### Methods
@@ -110,6 +111,7 @@ Create a new conversation session.
- `Provider` - Custom API provider configuration (BYOK)
- `Streaming` - Enable streaming of response chunks (default: false)
- `InfiniteSessions` - Configure automatic context compaction (see below)
+- `OnPermissionRequest` - **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.ApproveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
- `OnUserInputRequest` - Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
@@ -117,6 +119,10 @@ Create a new conversation session.
Resume an existing session. Returns the session with `WorkspacePath` populated if infinite sessions were enabled.
+**ResumeSessionConfig:**
+
+- `OnPermissionRequest` - **Required.** Handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section.
+
##### `PingAsync(string? message = null): Task`
Ping the server to check connectivity.
@@ -265,18 +271,33 @@ session.On(evt =>
## Image Support
-The SDK supports image attachments via the `Attachments` parameter. You can attach images by providing their file path:
+The SDK supports image attachments via the `Attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment:
```csharp
+// File attachment — runtime reads from disk
await session.SendAsync(new MessageOptions
{
Prompt = "What's in this image?",
Attachments = new List
{
- new UserMessageDataAttachmentsItem
+ new UserMessageDataAttachmentsItemFile
{
- Type = UserMessageDataAttachmentsItemType.File,
- Path = "/path/to/image.jpg"
+ Path = "/path/to/image.jpg",
+ DisplayName = "image.jpg",
+ }
+ }
+});
+
+// Blob attachment — provide base64 data directly
+await session.SendAsync(new MessageOptions
+{
+ Prompt = "What's in this image?",
+ Attachments = new List
+ {
+ new UserMessageDataAttachmentsItemBlob
+ {
+ Data = base64ImageData,
+ MimeType = "image/png",
}
}
});
@@ -449,6 +470,24 @@ var session = await client.CreateSessionAsync(new SessionConfig
});
```
+#### Skipping Permission Prompts
+
+Set `skip_permission` in the tool's `AdditionalProperties` to allow it to execute without triggering a permission prompt:
+
+```csharp
+var safeLookup = AIFunctionFactory.Create(
+ async ([Description("Lookup ID")] string id) => {
+ // your logic
+ },
+ "safe_lookup",
+ "A read-only lookup that needs no confirmation",
+ new AIFunctionFactoryOptions
+ {
+ AdditionalProperties = new ReadOnlyDictionary(
+ new Dictionary { ["skip_permission"] = true })
+ });
+```
+
### System Message Customization
Control the system prompt using `SystemMessage` in session config:
@@ -470,6 +509,34 @@ var session = await client.CreateSessionAsync(new SessionConfig
});
```
+#### Customize Mode
+
+Use `Mode = SystemMessageMode.Customize` to selectively override individual sections of the prompt while preserving the rest:
+
+```csharp
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Model = "gpt-5",
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ [SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = "Respond in a warm, professional tone. Be thorough in explanations." },
+ [SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove },
+ [SystemPromptSections.Guidelines] = new() { Action = SectionOverrideAction.Append, Content = "\n* Always cite data sources" },
+ },
+ Content = "Focus on financial analysis and reporting."
+ }
+});
+```
+
+Available section IDs are defined as constants on `SystemPromptSections`: `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `LastInstructions`.
+
+Each section override supports four actions: `Replace`, `Remove`, `Append`, and `Prepend`. Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored.
+
+#### Replace Mode
+
For full control (removes all guardrails), use `Mode = SystemMessageMode.Replace`:
```csharp
@@ -529,6 +596,110 @@ var session = await client.CreateSessionAsync(new SessionConfig
});
```
+## Telemetry
+
+The SDK supports OpenTelemetry for distributed tracing. Provide a `Telemetry` config to enable trace export and automatic W3C Trace Context propagation.
+
+```csharp
+var client = new CopilotClient(new CopilotClientOptions
+{
+ Telemetry = new TelemetryConfig
+ {
+ OtlpEndpoint = "http://localhost:4318",
+ },
+});
+```
+
+**TelemetryConfig properties:**
+
+- `OtlpEndpoint` - OTLP HTTP endpoint URL
+- `FilePath` - File path for JSON-lines trace output
+- `ExporterType` - `"otlp-http"` or `"file"`
+- `SourceName` - Instrumentation scope name
+- `CaptureContent` - Whether to capture message content
+
+Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `CreateSessionAsync`, `ResumeSessionAsync`, and `SendAsync` calls, and inbound when the CLI invokes tool handlers.
+
+No extra dependencies — uses built-in `System.Diagnostics.Activity`.
+
+## Permission Handling
+
+An `OnPermissionRequest` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision.
+
+### Approve All (simplest)
+
+Use the built-in `PermissionHandler.ApproveAll` helper to allow every tool call without any checks:
+
+```csharp
+using GitHub.Copilot.SDK;
+
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Model = "gpt-5",
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+});
+```
+
+### Custom Permission Handler
+
+Provide your own `PermissionRequestHandler` delegate to inspect each request and apply custom logic:
+
+```csharp
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Model = "gpt-5",
+ OnPermissionRequest = async (request, invocation) =>
+ {
+ // request.Kind — string discriminator for the type of operation being requested:
+ // "shell" — executing a shell command
+ // "write" — writing or editing a file
+ // "read" — reading a file
+ // "mcp" — calling an MCP tool
+ // "custom_tool" — calling one of your registered tools
+ // "url" — fetching a URL
+ // "memory" — accessing or modifying assistant memory
+ // "hook" — invoking a registered hook
+ // request.ToolCallId — the tool call that triggered this request
+ // request.ToolName — name of the tool (for custom-tool / mcp)
+ // request.FileName — file being written (for write)
+ // request.FullCommandText — full shell command text (for shell)
+
+ if (request.Kind == "shell")
+ {
+ // Deny shell commands
+ return new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedInteractivelyByUser };
+ }
+
+ return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved };
+ }
+});
+```
+
+### Permission Result Kinds
+
+| Value | Meaning |
+|-------|---------|
+| `PermissionRequestResultKind.Approved` | Allow the tool to run |
+| `PermissionRequestResultKind.DeniedInteractivelyByUser` | User explicitly denied the request |
+| `PermissionRequestResultKind.DeniedCouldNotRequestFromUser` | No approval rule matched and user could not be asked |
+| `PermissionRequestResultKind.DeniedByRules` | Denied by a policy rule |
+| `PermissionRequestResultKind.NoResult` | Leave the permission request unanswered (the SDK returns without calling the RPC). Not allowed for protocol v2 permission requests (will be rejected). |
+
+### Resuming Sessions
+
+Pass `OnPermissionRequest` when resuming a session too — it is required:
+
+```csharp
+var session = await client.ResumeSessionAsync("session-id", new ResumeSessionConfig
+{
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+});
+```
+
+### Per-Tool Skip Permission
+
+To let a specific custom tool bypass the permission prompt entirely, set `skip_permission = true` in the tool's `AdditionalProperties`. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools.
+
## User Input Requests
Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler:
diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index 8cad6b0489..99c0eff003 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -14,6 +14,7 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using GitHub.Copilot.SDK.Rpc;
using System.Globalization;
@@ -54,6 +55,9 @@ namespace GitHub.Copilot.SDK;
///
public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
{
+ internal const string NoResultPermissionV2ErrorMessage =
+ "Permission handlers cannot return 'no-result' when connected to a protocol v2 server.";
+
///
/// Minimum protocol version this SDK can communicate with.
///
@@ -63,6 +67,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
private readonly CopilotClientOptions _options;
private readonly ILogger _logger;
private Task? _connectionTask;
+ private volatile bool _disconnected;
private bool _disposed;
private readonly int? _optionsPort;
private readonly string? _optionsHost;
@@ -70,6 +75,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
private int? _negotiatedProtocolVersion;
private List? _modelsCache;
private readonly SemaphoreSlim _modelsCacheLock = new(1, 1);
+ private readonly Func>>? _onListModels;
private readonly List> _lifecycleHandlers = [];
private readonly Dictionary>> _typedLifecycleHandlers = [];
private readonly object _lifecycleHandlersLock = new();
@@ -136,6 +142,7 @@ public CopilotClient(CopilotClientOptions? options = null)
}
_logger = _options.Logger ?? NullLogger.Instance;
+ _onListModels = _options.OnListModels;
// Parse CliUrl if provided
if (!string.IsNullOrEmpty(_options.CliUrl))
@@ -197,6 +204,7 @@ public Task StartAsync(CancellationToken cancellationToken = default)
async Task StartCoreAsync(CancellationToken ct)
{
_logger.LogDebug("Starting Copilot client");
+ _disconnected = false;
Task result;
@@ -357,6 +365,44 @@ private async Task CleanupConnectionAsync(List? errors)
}
}
+ private static (SystemMessageConfig? wireConfig, Dictionary>>? callbacks) ExtractTransformCallbacks(SystemMessageConfig? systemMessage)
+ {
+ if (systemMessage?.Mode != SystemMessageMode.Customize || systemMessage.Sections == null)
+ {
+ return (systemMessage, null);
+ }
+
+ var callbacks = new Dictionary>>();
+ var wireSections = new Dictionary();
+
+ foreach (var (sectionId, sectionOverride) in systemMessage.Sections)
+ {
+ if (sectionOverride.Transform != null)
+ {
+ callbacks[sectionId] = sectionOverride.Transform;
+ wireSections[sectionId] = new SectionOverride { Action = SectionOverrideAction.Transform };
+ }
+ else
+ {
+ wireSections[sectionId] = sectionOverride;
+ }
+ }
+
+ if (callbacks.Count == 0)
+ {
+ return (systemMessage, null);
+ }
+
+ var wireConfig = new SystemMessageConfig
+ {
+ Mode = systemMessage.Mode,
+ Content = systemMessage.Content,
+ Sections = wireSections
+ };
+
+ return (wireConfig, callbacks);
+ }
+
///
/// Creates a new Copilot session with the specified configuration.
///
@@ -401,33 +447,13 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
config.Hooks.OnSessionEnd != null ||
config.Hooks.OnErrorOccurred != null);
- var request = new CreateSessionRequest(
- config.Model,
- config.SessionId,
- config.ClientName,
- config.ReasoningEffort,
- config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
- config.SystemMessage,
- config.AvailableTools,
- config.ExcludedTools,
- config.Provider,
- (bool?)true,
- config.OnUserInputRequest != null ? true : null,
- hasHooks ? true : null,
- config.WorkingDirectory,
- config.Streaming is true ? true : null,
- config.McpServers,
- "direct",
- config.CustomAgents,
- config.ConfigDir,
- config.SkillDirectories,
- config.DisabledSkills,
- config.InfiniteSessions);
-
- var response = await InvokeRpcAsync(
- connection.Rpc, "session.create", [request], cancellationToken);
-
- var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath);
+ var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);
+
+ var sessionId = config.SessionId ?? Guid.NewGuid().ToString();
+
+ // Create and register the session before issuing the RPC so that
+ // events emitted by the CLI (e.g. session.start) are not dropped.
+ var session = new CopilotSession(sessionId, connection.Rpc, _logger);
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(config.OnPermissionRequest);
if (config.OnUserInputRequest != null)
@@ -438,10 +464,55 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
{
session.RegisterHooks(config.Hooks);
}
+ if (transformCallbacks != null)
+ {
+ session.RegisterTransformCallbacks(transformCallbacks);
+ }
+ if (config.OnEvent != null)
+ {
+ session.On(config.OnEvent);
+ }
+ _sessions[sessionId] = session;
- if (!_sessions.TryAdd(response.SessionId, session))
+ try
+ {
+ var (traceparent, tracestate) = TelemetryHelpers.GetTraceContext();
+
+ var request = new CreateSessionRequest(
+ config.Model,
+ sessionId,
+ config.ClientName,
+ config.ReasoningEffort,
+ config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
+ wireSystemMessage,
+ config.AvailableTools,
+ config.ExcludedTools,
+ config.Provider,
+ (bool?)true,
+ config.OnUserInputRequest != null ? true : null,
+ hasHooks ? true : null,
+ config.WorkingDirectory,
+ config.Streaming is true ? true : null,
+ config.McpServers,
+ "direct",
+ config.CustomAgents,
+ config.Agent,
+ config.ConfigDir,
+ config.SkillDirectories,
+ config.DisabledSkills,
+ config.InfiniteSessions,
+ traceparent,
+ tracestate);
+
+ var response = await InvokeRpcAsync(
+ connection.Rpc, "session.create", [request], cancellationToken);
+
+ session.WorkspacePath = response.WorkspacePath;
+ }
+ catch
{
- throw new InvalidOperationException($"Session {response.SessionId} already exists");
+ _sessions.TryRemove(sessionId, out _);
+ throw;
}
return session;
@@ -492,34 +563,11 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
config.Hooks.OnSessionEnd != null ||
config.Hooks.OnErrorOccurred != null);
- var request = new ResumeSessionRequest(
- sessionId,
- config.ClientName,
- config.Model,
- config.ReasoningEffort,
- config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
- config.SystemMessage,
- config.AvailableTools,
- config.ExcludedTools,
- config.Provider,
- (bool?)true,
- config.OnUserInputRequest != null ? true : null,
- hasHooks ? true : null,
- config.WorkingDirectory,
- config.ConfigDir,
- config.DisableResume is true ? true : null,
- config.Streaming is true ? true : null,
- config.McpServers,
- "direct",
- config.CustomAgents,
- config.SkillDirectories,
- config.DisabledSkills,
- config.InfiniteSessions);
-
- var response = await InvokeRpcAsync(
- connection.Rpc, "session.resume", [request], cancellationToken);
-
- var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath);
+ var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);
+
+ // Create and register the session before issuing the RPC so that
+ // events emitted by the CLI (e.g. session.start) are not dropped.
+ var session = new CopilotSession(sessionId, connection.Rpc, _logger);
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(config.OnPermissionRequest);
if (config.OnUserInputRequest != null)
@@ -530,9 +578,58 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
{
session.RegisterHooks(config.Hooks);
}
+ if (transformCallbacks != null)
+ {
+ session.RegisterTransformCallbacks(transformCallbacks);
+ }
+ if (config.OnEvent != null)
+ {
+ session.On(config.OnEvent);
+ }
+ _sessions[sessionId] = session;
+
+ try
+ {
+ var (traceparent, tracestate) = TelemetryHelpers.GetTraceContext();
+
+ var request = new ResumeSessionRequest(
+ sessionId,
+ config.ClientName,
+ config.Model,
+ config.ReasoningEffort,
+ config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
+ wireSystemMessage,
+ config.AvailableTools,
+ config.ExcludedTools,
+ config.Provider,
+ (bool?)true,
+ config.OnUserInputRequest != null ? true : null,
+ hasHooks ? true : null,
+ config.WorkingDirectory,
+ config.ConfigDir,
+ config.DisableResume is true ? true : null,
+ config.Streaming is true ? true : null,
+ config.McpServers,
+ "direct",
+ config.CustomAgents,
+ config.Agent,
+ config.SkillDirectories,
+ config.DisabledSkills,
+ config.InfiniteSessions,
+ traceparent,
+ tracestate);
+
+ var response = await InvokeRpcAsync(
+ connection.Rpc, "session.resume", [request], cancellationToken);
+
+ session.WorkspacePath = response.WorkspacePath;
+ }
+ catch
+ {
+ _sessions.TryRemove(sessionId, out _);
+ throw;
+ }
- // Replace any existing session entry to ensure new config (like permission handler) is used
- _sessions[response.SessionId] = session;
return session;
}
@@ -557,6 +654,7 @@ public ConnectionState State
if (_connectionTask == null) return ConnectionState.Disconnected;
if (_connectionTask.IsFaulted) return ConnectionState.Error;
if (!_connectionTask.IsCompleted) return ConnectionState.Connecting;
+ if (_disconnected) return ConnectionState.Disconnected;
return ConnectionState.Connected;
}
}
@@ -622,9 +720,6 @@ public async Task GetAuthStatusAsync(CancellationToken ca
/// Thrown when the client is not connected or not authenticated.
public async Task> ListModelsAsync(CancellationToken cancellationToken = default)
{
- var connection = await EnsureConnectedAsync(cancellationToken);
-
- // Use semaphore for async locking to prevent race condition with concurrent calls
await _modelsCacheLock.WaitAsync(cancellationToken);
try
{
@@ -634,14 +729,26 @@ public async Task> ListModelsAsync(CancellationToken cancellatio
return [.. _modelsCache]; // Return a copy to prevent cache mutation
}
- // Cache miss - fetch from backend while holding lock
- var response = await InvokeRpcAsync(
- connection.Rpc, "models.list", [], cancellationToken);
+ List models;
+ if (_onListModels is not null)
+ {
+ // Use custom handler instead of CLI RPC
+ models = await _onListModels(cancellationToken);
+ }
+ else
+ {
+ var connection = await EnsureConnectedAsync(cancellationToken);
+
+ // Cache miss - fetch from backend while holding lock
+ var response = await InvokeRpcAsync(
+ connection.Rpc, "models.list", [], cancellationToken);
+ models = response.Models;
+ }
- // Update cache before releasing lock
- _modelsCache = response.Models;
+ // Update cache before releasing lock (copy to prevent external mutation)
+ _modelsCache = [.. models];
- return [.. response.Models]; // Return a copy to prevent cache mutation
+ return [.. models]; // Return a copy to prevent cache mutation
}
finally
{
@@ -1022,6 +1129,17 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
startInfo.Environment["COPILOT_SDK_AUTH_TOKEN"] = options.GitHubToken;
}
+ // Set telemetry environment variables if configured
+ if (options.Telemetry is { } telemetry)
+ {
+ startInfo.Environment["COPILOT_OTEL_ENABLED"] = "true";
+ if (telemetry.OtlpEndpoint is not null) startInfo.Environment["OTEL_EXPORTER_OTLP_ENDPOINT"] = telemetry.OtlpEndpoint;
+ if (telemetry.FilePath is not null) startInfo.Environment["COPILOT_OTEL_FILE_EXPORTER_PATH"] = telemetry.FilePath;
+ if (telemetry.ExporterType is not null) startInfo.Environment["COPILOT_OTEL_EXPORTER_TYPE"] = telemetry.ExporterType;
+ if (telemetry.SourceName is not null) startInfo.Environment["COPILOT_OTEL_SOURCE_NAME"] = telemetry.SourceName;
+ if (telemetry.CaptureContent is { } capture) startInfo.Environment["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = capture ? "true" : "false";
+ }
+
var cliProcess = new Process { StartInfo = startInfo };
cliProcess.Start();
@@ -1154,8 +1272,12 @@ private async Task ConnectToServerAsync(Process? cliProcess, string?
rpc.AddLocalRpcMethod("permission.request", handler.OnPermissionRequestV2);
rpc.AddLocalRpcMethod("userInput.request", handler.OnUserInputRequest);
rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke);
+ rpc.AddLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform);
rpc.StartListening();
+ // Transition state to Disconnected if the JSON-RPC connection drops
+ _ = rpc.Completion.ContinueWith(_ => _disconnected = true, TaskScheduler.Default);
+
_rpc = new ServerRpc(rpc);
return new Connection(rpc, cliProcess, tcpClient, networkStream, stderrBuffer);
@@ -1184,6 +1306,12 @@ private static JsonSerializerOptions CreateSerializerOptions()
options.TypeInfoResolverChain.Add(SessionEventsJsonContext.Default);
options.TypeInfoResolverChain.Add(SDK.Rpc.RpcJsonContext.Default);
+ // StreamJsonRpc's RequestId needs serialization when CancellationToken fires during
+ // JSON-RPC operations. Its built-in converter (RequestIdSTJsonConverter) is internal,
+ // and [JsonSerializable] can't source-gen for it (SYSLIB1220), so we provide our own
+ // AOT-safe resolver + converter.
+ options.TypeInfoResolverChain.Add(new RequestIdTypeInfoResolver());
+
options.MakeReadOnly();
return options;
@@ -1273,13 +1401,23 @@ public async Task OnHooksInvoke(string sessionId, string ho
return new HooksInvokeResponse(output);
}
+ public async Task OnSystemMessageTransform(string sessionId, JsonElement sections)
+ {
+ var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
+ return await session.HandleSystemMessageTransformAsync(sections);
+ }
+
// Protocol v2 backward-compatibility adapters
public async Task OnToolCallV2(string sessionId,
string toolCallId,
string toolName,
- object? arguments)
+ object? arguments,
+ string? traceparent = null,
+ string? tracestate = null)
{
+ using var _ = TelemetryHelpers.RestoreTraceContext(traceparent, tracestate);
+
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
if (session.GetTool(toolName) is not { } tool)
{
@@ -1352,8 +1490,16 @@ public async Task OnPermissionRequestV2(string sess
try
{
var result = await session.HandlePermissionRequestAsync(permissionRequest);
+ if (result.Kind == new PermissionRequestResultKind("no-result"))
+ {
+ throw new InvalidOperationException(NoResultPermissionV2ErrorMessage);
+ }
return new PermissionRequestResponseV2(result);
}
+ catch (InvalidOperationException ex) when (ex.Message == NoResultPermissionV2ErrorMessage)
+ {
+ throw;
+ }
catch (Exception)
{
return new PermissionRequestResponseV2(new PermissionRequestResult
@@ -1407,22 +1553,28 @@ internal record CreateSessionRequest(
Dictionary? McpServers,
string? EnvValueMode,
List? CustomAgents,
+ string? Agent,
string? ConfigDir,
List? SkillDirectories,
List? DisabledSkills,
- InfiniteSessionConfig? InfiniteSessions);
+ InfiniteSessionConfig? InfiniteSessions,
+ string? Traceparent = null,
+ string? Tracestate = null);
internal record ToolDefinition(
string Name,
string? Description,
JsonElement Parameters, /* JSON schema */
- bool? OverridesBuiltInTool = null)
+ bool? OverridesBuiltInTool = null,
+ bool? SkipPermission = null)
{
public static ToolDefinition FromAIFunction(AIFunction function)
{
var overrides = function.AdditionalProperties.TryGetValue("is_override", out var val) && val is true;
+ var skipPerm = function.AdditionalProperties.TryGetValue("skip_permission", out var skipVal) && skipVal is true;
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
- overrides ? true : null);
+ overrides ? true : null,
+ skipPerm ? true : null);
}
}
@@ -1450,9 +1602,12 @@ internal record ResumeSessionRequest(
Dictionary? McpServers,
string? EnvValueMode,
List? CustomAgents,
+ string? Agent,
List? SkillDirectories,
List? DisabledSkills,
- InfiniteSessionConfig? InfiniteSessions);
+ InfiniteSessionConfig? InfiniteSessions,
+ string? Traceparent = null,
+ string? Tracestate = null);
internal record ResumeSessionResponse(
string SessionId,
@@ -1587,6 +1742,7 @@ private static LogLevel MapLevel(TraceEventType eventType)
[JsonSerializable(typeof(ResumeSessionResponse))]
[JsonSerializable(typeof(SessionMetadata))]
[JsonSerializable(typeof(SystemMessageConfig))]
+ [JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
[JsonSerializable(typeof(ToolCallResponseV2))]
[JsonSerializable(typeof(ToolDefinition))]
[JsonSerializable(typeof(ToolResultAIContent))]
@@ -1596,6 +1752,50 @@ private static LogLevel MapLevel(TraceEventType eventType)
[JsonSerializable(typeof(UserInputResponse))]
internal partial class ClientJsonContext : JsonSerializerContext;
+ ///
+ /// AOT-safe type info resolver for .
+ /// StreamJsonRpc's own RequestIdSTJsonConverter is internal (SYSLIB1220/CS0122),
+ /// so we provide our own converter and wire it through
+ /// to stay fully AOT/trimming-compatible.
+ ///
+ private sealed class RequestIdTypeInfoResolver : IJsonTypeInfoResolver
+ {
+ public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
+ {
+ if (type == typeof(RequestId))
+ return JsonMetadataServices.CreateValueInfo(options, new RequestIdJsonConverter());
+ return null;
+ }
+ }
+
+ private sealed class RequestIdJsonConverter : JsonConverter
+ {
+ public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return reader.TokenType switch
+ {
+ JsonTokenType.Number => reader.TryGetInt64(out long val)
+ ? new RequestId(val)
+ : new RequestId(reader.HasValueSequence
+ ? Encoding.UTF8.GetString(reader.ValueSequence)
+ : Encoding.UTF8.GetString(reader.ValueSpan)),
+ JsonTokenType.String => new RequestId(reader.GetString()!),
+ JsonTokenType.Null => RequestId.Null,
+ _ => throw new JsonException($"Unexpected token type for RequestId: {reader.TokenType}"),
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options)
+ {
+ if (value.Number.HasValue)
+ writer.WriteNumberValue(value.Number.Value);
+ else if (value.String is not null)
+ writer.WriteStringValue(value.String);
+ else
+ writer.WriteNullValue();
+ }
+ }
+
[GeneratedRegex(@"listening on port ([0-9]+)", RegexOptions.IgnoreCase)]
private static partial Regex ListeningOnPortRegex();
}
diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs
index 85e55e4b80..fabe4817ed 100644
--- a/dotnet/src/Generated/Rpc.cs
+++ b/dotnet/src/Generated/Rpc.cs
@@ -5,220 +5,293 @@
// AUTO-GENERATED FILE - DO NOT EDIT
// Generated from: api.schema.json
-// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings.
-#pragma warning disable CS1591
-
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using StreamJsonRpc;
namespace GitHub.Copilot.SDK.Rpc;
+/// Diagnostic IDs for the Copilot SDK.
+internal static class Diagnostics
+{
+ /// Indicates an experimental API that may change or be removed.
+ internal const string Experimental = "GHCP001";
+}
+
+/// RPC data type for Ping operations.
public class PingResult
{
- /// Echoed message (or default greeting)
+ /// Echoed message (or default greeting).
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
- /// Server timestamp in milliseconds
+ /// Server timestamp in milliseconds.
[JsonPropertyName("timestamp")]
public double Timestamp { get; set; }
- /// Server protocol version number
+ /// Server protocol version number.
[JsonPropertyName("protocolVersion")]
public double ProtocolVersion { get; set; }
}
+/// RPC data type for Ping operations.
internal class PingRequest
{
+ /// Optional message to echo back.
[JsonPropertyName("message")]
public string? Message { get; set; }
}
+/// Feature flags indicating what the model supports.
public class ModelCapabilitiesSupports
{
+ /// Whether this model supports vision/image input.
[JsonPropertyName("vision")]
public bool? Vision { get; set; }
- /// Whether this model supports reasoning effort configuration
+ /// Whether this model supports reasoning effort configuration.
[JsonPropertyName("reasoningEffort")]
public bool? ReasoningEffort { get; set; }
}
+/// Token limits for prompts, outputs, and context window.
public class ModelCapabilitiesLimits
{
+ /// Maximum number of prompt/input tokens.
[JsonPropertyName("max_prompt_tokens")]
public double? MaxPromptTokens { get; set; }
+ /// Maximum number of output/completion tokens.
[JsonPropertyName("max_output_tokens")]
public double? MaxOutputTokens { get; set; }
+ /// Maximum total context window size in tokens.
[JsonPropertyName("max_context_window_tokens")]
public double MaxContextWindowTokens { get; set; }
}
-/// Model capabilities and limits
+/// Model capabilities and limits.
public class ModelCapabilities
{
+ /// Feature flags indicating what the model supports.
[JsonPropertyName("supports")]
- public ModelCapabilitiesSupports Supports { get; set; } = new();
+ public ModelCapabilitiesSupports Supports { get => field ??= new(); set; }
+ /// Token limits for prompts, outputs, and context window.
[JsonPropertyName("limits")]
- public ModelCapabilitiesLimits Limits { get; set; } = new();
+ public ModelCapabilitiesLimits Limits { get => field ??= new(); set; }
}
-/// Policy state (if applicable)
+/// Policy state (if applicable).
public class ModelPolicy
{
+ /// Current policy state for this model.
[JsonPropertyName("state")]
public string State { get; set; } = string.Empty;
+ /// Usage terms or conditions for this model.
[JsonPropertyName("terms")]
public string Terms { get; set; } = string.Empty;
}
-/// Billing information
+/// Billing information.
public class ModelBilling
{
+ /// Billing cost multiplier relative to the base rate.
[JsonPropertyName("multiplier")]
public double Multiplier { get; set; }
}
+/// RPC data type for Model operations.
public class Model
{
- /// Model identifier (e.g., "claude-sonnet-4.5")
+ /// Model identifier (e.g., "claude-sonnet-4.5").
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
- /// Display name
+ /// Display name.
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
- /// Model capabilities and limits
+ /// Model capabilities and limits.
[JsonPropertyName("capabilities")]
- public ModelCapabilities Capabilities { get; set; } = new();
+ public ModelCapabilities Capabilities { get => field ??= new(); set; }
- /// Policy state (if applicable)
+ /// Policy state (if applicable).
[JsonPropertyName("policy")]
public ModelPolicy? Policy { get; set; }
- /// Billing information
+ /// Billing information.
[JsonPropertyName("billing")]
public ModelBilling? Billing { get; set; }
- /// Supported reasoning effort levels (only present if model supports reasoning effort)
+ /// Supported reasoning effort levels (only present if model supports reasoning effort).
[JsonPropertyName("supportedReasoningEfforts")]
public List? SupportedReasoningEfforts { get; set; }
- /// Default reasoning effort level (only present if model supports reasoning effort)
+ /// Default reasoning effort level (only present if model supports reasoning effort).
[JsonPropertyName("defaultReasoningEffort")]
public string? DefaultReasoningEffort { get; set; }
}
+/// RPC data type for ModelsList operations.
public class ModelsListResult
{
- /// List of available models with full metadata
+ /// List of available models with full metadata.
[JsonPropertyName("models")]
- public List Models { get; set; } = [];
+ public List Models { get => field ??= []; set; }
}
+/// RPC data type for Tool operations.
public class Tool
{
- /// Tool identifier (e.g., "bash", "grep", "str_replace_editor")
+ /// Tool identifier (e.g., "bash", "grep", "str_replace_editor").
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
- /// Optional namespaced name for declarative filtering (e.g., "playwright/navigate" for MCP tools)
+ /// Optional namespaced name for declarative filtering (e.g., "playwright/navigate" for MCP tools).
[JsonPropertyName("namespacedName")]
public string? NamespacedName { get; set; }
- /// Description of what the tool does
+ /// Description of what the tool does.
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
- /// JSON Schema for the tool's input parameters
+ /// JSON Schema for the tool's input parameters.
[JsonPropertyName("parameters")]
public Dictionary? Parameters { get; set; }
- /// Optional instructions for how to use this tool effectively
+ /// Optional instructions for how to use this tool effectively.
[JsonPropertyName("instructions")]
public string? Instructions { get; set; }
}
+/// RPC data type for ToolsList operations.
public class ToolsListResult
{
- /// List of available built-in tools with metadata
+ /// List of available built-in tools with metadata.
[JsonPropertyName("tools")]
- public List Tools { get; set; } = [];
+ public List Tools { get => field ??= []; set; }
}
+/// RPC data type for ToolsList operations.
internal class ToolsListRequest
{
+ /// Optional model ID — when provided, the returned tool list reflects model-specific overrides.
[JsonPropertyName("model")]
public string? Model { get; set; }
}
+/// RPC data type for AccountGetQuotaResultQuotaSnapshotsValue operations.
public class AccountGetQuotaResultQuotaSnapshotsValue
{
- /// Number of requests included in the entitlement
+ /// Number of requests included in the entitlement.
[JsonPropertyName("entitlementRequests")]
public double EntitlementRequests { get; set; }
- /// Number of requests used so far this period
+ /// Number of requests used so far this period.
[JsonPropertyName("usedRequests")]
public double UsedRequests { get; set; }
- /// Percentage of entitlement remaining
+ /// Percentage of entitlement remaining.
[JsonPropertyName("remainingPercentage")]
public double RemainingPercentage { get; set; }
- /// Number of overage requests made this period
+ /// Number of overage requests made this period.
[JsonPropertyName("overage")]
public double Overage { get; set; }
- /// Whether pay-per-request usage is allowed when quota is exhausted
+ /// Whether pay-per-request usage is allowed when quota is exhausted.
[JsonPropertyName("overageAllowedWithExhaustedQuota")]
public bool OverageAllowedWithExhaustedQuota { get; set; }
- /// Date when the quota resets (ISO 8601)
+ /// Date when the quota resets (ISO 8601).
[JsonPropertyName("resetDate")]
public string? ResetDate { get; set; }
}
+/// RPC data type for AccountGetQuota operations.
public class AccountGetQuotaResult
{
- /// Quota snapshots keyed by type (e.g., chat, completions, premium_interactions)
+ /// Quota snapshots keyed by type (e.g., chat, completions, premium_interactions).
[JsonPropertyName("quotaSnapshots")]
- public Dictionary QuotaSnapshots { get; set; } = [];
+ public Dictionary QuotaSnapshots { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionLog operations.
+public class SessionLogResult
+{
+ /// The unique identifier of the emitted session event.
+ [JsonPropertyName("eventId")]
+ public Guid EventId { get; set; }
+}
+
+/// RPC data type for SessionLog operations.
+internal class SessionLogRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Human-readable message.
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+
+ /// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info".
+ [JsonPropertyName("level")]
+ public SessionLogRequestLevel? Level { get; set; }
+
+ /// When true, the message is transient and not persisted to the session event log on disk.
+ [JsonPropertyName("ephemeral")]
+ public bool? Ephemeral { get; set; }
+
+ /// Optional URL the user can open in their browser for more details.
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
+/// RPC data type for SessionModelGetCurrent operations.
public class SessionModelGetCurrentResult
{
+ /// Currently active model identifier.
[JsonPropertyName("modelId")]
public string? ModelId { get; set; }
}
+/// RPC data type for SessionModelGetCurrent operations.
internal class SessionModelGetCurrentRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionModelSwitchTo operations.
public class SessionModelSwitchToResult
{
+ /// Currently active model identifier after the switch.
[JsonPropertyName("modelId")]
public string? ModelId { get; set; }
}
+/// RPC data type for SessionModelSwitchTo operations.
internal class SessionModelSwitchToRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// Model identifier to switch to.
[JsonPropertyName("modelId")]
public string ModelId { get; set; } = string.Empty;
+
+ /// Reasoning effort level to use for the model.
+ [JsonPropertyName("reasoningEffort")]
+ public string? ReasoningEffort { get; set; }
}
+/// RPC data type for SessionModeGet operations.
public class SessionModeGetResult
{
/// The current agent mode.
@@ -226,12 +299,15 @@ public class SessionModeGetResult
public SessionModeGetResultMode Mode { get; set; }
}
+/// RPC data type for SessionModeGet operations.
internal class SessionModeGetRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionModeSet operations.
public class SessionModeSetResult
{
/// The agent mode after switching.
@@ -239,291 +315,930 @@ public class SessionModeSetResult
public SessionModeGetResultMode Mode { get; set; }
}
+/// RPC data type for SessionModeSet operations.
internal class SessionModeSetRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// The mode to switch to. Valid values: "interactive", "plan", "autopilot".
[JsonPropertyName("mode")]
public SessionModeGetResultMode Mode { get; set; }
}
+/// RPC data type for SessionPlanRead operations.
public class SessionPlanReadResult
{
- /// Whether the plan file exists in the workspace
+ /// Whether the plan file exists in the workspace.
[JsonPropertyName("exists")]
public bool Exists { get; set; }
- /// The content of the plan file, or null if it does not exist
+ /// The content of the plan file, or null if it does not exist.
[JsonPropertyName("content")]
public string? Content { get; set; }
- /// Absolute file path of the plan file, or null if workspace is not enabled
+ /// Absolute file path of the plan file, or null if workspace is not enabled.
[JsonPropertyName("path")]
public string? Path { get; set; }
}
+/// RPC data type for SessionPlanRead operations.
internal class SessionPlanReadRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionPlanUpdate operations.
public class SessionPlanUpdateResult
{
}
+/// RPC data type for SessionPlanUpdate operations.
internal class SessionPlanUpdateRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// The new content for the plan file.
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}
+/// RPC data type for SessionPlanDelete operations.
public class SessionPlanDeleteResult
{
}
+/// RPC data type for SessionPlanDelete operations.
internal class SessionPlanDeleteRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionWorkspaceListFiles operations.
public class SessionWorkspaceListFilesResult
{
- /// Relative file paths in the workspace files directory
+ /// Relative file paths in the workspace files directory.
[JsonPropertyName("files")]
- public List Files { get; set; } = [];
+ public List Files { get => field ??= []; set; }
}
+/// RPC data type for SessionWorkspaceListFiles operations.
internal class SessionWorkspaceListFilesRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionWorkspaceReadFile operations.
public class SessionWorkspaceReadFileResult
{
- /// File content as a UTF-8 string
+ /// File content as a UTF-8 string.
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}
+/// RPC data type for SessionWorkspaceReadFile operations.
internal class SessionWorkspaceReadFileRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// Relative path within the workspace files directory.
[JsonPropertyName("path")]
public string Path { get; set; } = string.Empty;
}
+/// RPC data type for SessionWorkspaceCreateFile operations.
public class SessionWorkspaceCreateFileResult
{
}
+/// RPC data type for SessionWorkspaceCreateFile operations.
internal class SessionWorkspaceCreateFileRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// Relative path within the workspace files directory.
[JsonPropertyName("path")]
public string Path { get; set; } = string.Empty;
+ /// File content to write as a UTF-8 string.
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}
+/// RPC data type for SessionFleetStart operations.
+[Experimental(Diagnostics.Experimental)]
public class SessionFleetStartResult
{
- /// Whether fleet mode was successfully activated
+ /// Whether fleet mode was successfully activated.
[JsonPropertyName("started")]
public bool Started { get; set; }
}
+/// RPC data type for SessionFleetStart operations.
+[Experimental(Diagnostics.Experimental)]
internal class SessionFleetStartRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// Optional user prompt to combine with fleet instructions.
[JsonPropertyName("prompt")]
public string? Prompt { get; set; }
}
+/// RPC data type for Agent operations.
public class Agent
{
- /// Unique identifier of the custom agent
+ /// Unique identifier of the custom agent.
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
- /// Human-readable display name
+ /// Human-readable display name.
[JsonPropertyName("displayName")]
public string DisplayName { get; set; } = string.Empty;
- /// Description of the agent's purpose
+ /// Description of the agent's purpose.
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
}
+/// RPC data type for SessionAgentList operations.
+[Experimental(Diagnostics.Experimental)]
public class SessionAgentListResult
{
- /// Available custom agents
+ /// Available custom agents.
[JsonPropertyName("agents")]
- public List Agents { get; set; } = [];
+ public List Agents { get => field ??= []; set; }
}
+/// RPC data type for SessionAgentList operations.
+[Experimental(Diagnostics.Experimental)]
internal class SessionAgentListRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionAgentGetCurrentResultAgent operations.
public class SessionAgentGetCurrentResultAgent
{
- /// Unique identifier of the custom agent
+ /// Unique identifier of the custom agent.
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
- /// Human-readable display name
+ /// Human-readable display name.
[JsonPropertyName("displayName")]
public string DisplayName { get; set; } = string.Empty;
- /// Description of the agent's purpose
+ /// Description of the agent's purpose.
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
}
+/// RPC data type for SessionAgentGetCurrent operations.
+[Experimental(Diagnostics.Experimental)]
public class SessionAgentGetCurrentResult
{
- /// Currently selected custom agent, or null if using the default agent
+ /// Currently selected custom agent, or null if using the default agent.
[JsonPropertyName("agent")]
public SessionAgentGetCurrentResultAgent? Agent { get; set; }
}
+/// RPC data type for SessionAgentGetCurrent operations.
+[Experimental(Diagnostics.Experimental)]
internal class SessionAgentGetCurrentRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
-/// The newly selected custom agent
+/// The newly selected custom agent.
public class SessionAgentSelectResultAgent
{
- /// Unique identifier of the custom agent
+ /// Unique identifier of the custom agent.
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
- /// Human-readable display name
+ /// Human-readable display name.
[JsonPropertyName("displayName")]
public string DisplayName { get; set; } = string.Empty;
- /// Description of the agent's purpose
+ /// Description of the agent's purpose.
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
}
+/// RPC data type for SessionAgentSelect operations.
+[Experimental(Diagnostics.Experimental)]
public class SessionAgentSelectResult
{
- /// The newly selected custom agent
+ /// The newly selected custom agent.
[JsonPropertyName("agent")]
- public SessionAgentSelectResultAgent Agent { get; set; } = new();
+ public SessionAgentSelectResultAgent Agent { get => field ??= new(); set; }
}
+/// RPC data type for SessionAgentSelect operations.
+[Experimental(Diagnostics.Experimental)]
internal class SessionAgentSelectRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// Name of the custom agent to select.
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
}
+/// RPC data type for SessionAgentDeselect operations.
+[Experimental(Diagnostics.Experimental)]
public class SessionAgentDeselectResult
{
}
+/// RPC data type for SessionAgentDeselect operations.
+[Experimental(Diagnostics.Experimental)]
internal class SessionAgentDeselectRequest
{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionAgentReload operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionAgentReloadResult
+{
+ /// Reloaded custom agents.
+ [JsonPropertyName("agents")]
+ public List Agents { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionAgentReload operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionAgentReloadRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for Skill operations.
+public class Skill
+{
+ /// Unique identifier for the skill.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Description of what the skill does.
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = string.Empty;
+
+ /// Source location type (e.g., project, personal, plugin).
+ [JsonPropertyName("source")]
+ public string Source { get; set; } = string.Empty;
+
+ /// Whether the skill can be invoked by the user as a slash command.
+ [JsonPropertyName("userInvocable")]
+ public bool UserInvocable { get; set; }
+
+ /// Whether the skill is currently enabled.
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+
+ /// Absolute path to the skill file.
+ [JsonPropertyName("path")]
+ public string? Path { get; set; }
+}
+
+/// RPC data type for SessionSkillsList operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionSkillsListResult
+{
+ /// Available skills.
+ [JsonPropertyName("skills")]
+ public List Skills { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionSkillsList operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionSkillsListRequest
+{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionSkillsEnable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionSkillsEnableResult
+{
+}
+
+/// RPC data type for SessionSkillsEnable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionSkillsEnableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Name of the skill to enable.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionSkillsDisable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionSkillsDisableResult
+{
+}
+
+/// RPC data type for SessionSkillsDisable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionSkillsDisableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Name of the skill to disable.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionSkillsReload operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionSkillsReloadResult
+{
+}
+
+/// RPC data type for SessionSkillsReload operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionSkillsReloadRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for Server operations.
+public class Server
+{
+ /// Server name (config key).
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Connection status: connected, failed, pending, disabled, or not_configured.
+ [JsonPropertyName("status")]
+ public ServerStatus Status { get; set; }
+
+ /// Configuration source: user, workspace, plugin, or builtin.
+ [JsonPropertyName("source")]
+ public string? Source { get; set; }
+
+ /// Error message if the server failed to connect.
+ [JsonPropertyName("error")]
+ public string? Error { get; set; }
+}
+
+/// RPC data type for SessionMcpList operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionMcpListResult
+{
+ /// Configured MCP servers.
+ [JsonPropertyName("servers")]
+ public List Servers { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionMcpList operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionMcpListRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionMcpEnable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionMcpEnableResult
+{
+}
+
+/// RPC data type for SessionMcpEnable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionMcpEnableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Name of the MCP server to enable.
+ [JsonPropertyName("serverName")]
+ public string ServerName { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionMcpDisable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionMcpDisableResult
+{
+}
+
+/// RPC data type for SessionMcpDisable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionMcpDisableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Name of the MCP server to disable.
+ [JsonPropertyName("serverName")]
+ public string ServerName { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionMcpReload operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionMcpReloadResult
+{
+}
+
+/// RPC data type for SessionMcpReload operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionMcpReloadRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for Plugin operations.
+public class Plugin
+{
+ /// Plugin name.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Marketplace the plugin came from.
+ [JsonPropertyName("marketplace")]
+ public string Marketplace { get; set; } = string.Empty;
+
+ /// Installed version.
+ [JsonPropertyName("version")]
+ public string? Version { get; set; }
+
+ /// Whether the plugin is currently enabled.
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+}
+
+/// RPC data type for SessionPluginsList operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionPluginsListResult
+{
+ /// Installed plugins.
+ [JsonPropertyName("plugins")]
+ public List Plugins { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionPluginsList operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionPluginsListRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for Extension operations.
+public class Extension
+{
+ /// Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper').
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ /// Extension name (directory name).
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/).
+ [JsonPropertyName("source")]
+ public ExtensionSource Source { get; set; }
+
+ /// Current status: running, disabled, failed, or starting.
+ [JsonPropertyName("status")]
+ public ExtensionStatus Status { get; set; }
+
+ /// Process ID if the extension is running.
+ [JsonPropertyName("pid")]
+ public double? Pid { get; set; }
+}
+
+/// RPC data type for SessionExtensionsList operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionExtensionsListResult
+{
+ /// Discovered extensions and their current status.
+ [JsonPropertyName("extensions")]
+ public List Extensions { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionExtensionsList operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionExtensionsListRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionExtensionsEnable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionExtensionsEnableResult
+{
+}
+
+/// RPC data type for SessionExtensionsEnable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionExtensionsEnableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Source-qualified extension ID to enable.
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionExtensionsDisable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionExtensionsDisableResult
+{
+}
+
+/// RPC data type for SessionExtensionsDisable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionExtensionsDisableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Source-qualified extension ID to disable.
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionExtensionsReload operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionExtensionsReloadResult
+{
+}
+
+/// RPC data type for SessionExtensionsReload operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionExtensionsReloadRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionCompactionCompact operations.
+[Experimental(Diagnostics.Experimental)]
public class SessionCompactionCompactResult
{
- /// Whether compaction completed successfully
+ /// Whether compaction completed successfully.
[JsonPropertyName("success")]
public bool Success { get; set; }
- /// Number of tokens freed by compaction
+ /// Number of tokens freed by compaction.
[JsonPropertyName("tokensRemoved")]
public double TokensRemoved { get; set; }
- /// Number of messages removed during compaction
+ /// Number of messages removed during compaction.
[JsonPropertyName("messagesRemoved")]
public double MessagesRemoved { get; set; }
}
+/// RPC data type for SessionCompactionCompact operations.
+[Experimental(Diagnostics.Experimental)]
internal class SessionCompactionCompactRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionToolsHandlePendingToolCall operations.
public class SessionToolsHandlePendingToolCallResult
{
+ /// Whether the tool call result was handled successfully.
[JsonPropertyName("success")]
public bool Success { get; set; }
}
+/// RPC data type for SessionToolsHandlePendingToolCall operations.
internal class SessionToolsHandlePendingToolCallRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// Gets or sets the requestId value.
[JsonPropertyName("requestId")]
public string RequestId { get; set; } = string.Empty;
+ /// Gets or sets the result value.
[JsonPropertyName("result")]
public object? Result { get; set; }
+ /// Gets or sets the error value.
[JsonPropertyName("error")]
public string? Error { get; set; }
}
+/// RPC data type for SessionCommandsHandlePendingCommand operations.
+public class SessionCommandsHandlePendingCommandResult
+{
+ /// Gets or sets the success value.
+ [JsonPropertyName("success")]
+ public bool Success { get; set; }
+}
+
+/// RPC data type for SessionCommandsHandlePendingCommand operations.
+internal class SessionCommandsHandlePendingCommandRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Request ID from the command invocation event.
+ [JsonPropertyName("requestId")]
+ public string RequestId { get; set; } = string.Empty;
+
+ /// Error message if the command handler failed.
+ [JsonPropertyName("error")]
+ public string? Error { get; set; }
+}
+
+/// RPC data type for SessionUiElicitation operations.
+public class SessionUiElicitationResult
+{
+ /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed).
+ [JsonPropertyName("action")]
+ public SessionUiElicitationResultAction Action { get; set; }
+
+ /// The form values submitted by the user (present when action is 'accept').
+ [JsonPropertyName("content")]
+ public Dictionary? Content { get; set; }
+}
+
+/// JSON Schema describing the form fields to present to the user.
+public class SessionUiElicitationRequestRequestedSchema
+{
+ /// Schema type indicator (always 'object').
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+
+ /// Form field definitions, keyed by field name.
+ [JsonPropertyName("properties")]
+ public Dictionary Properties { get => field ??= []; set; }
+
+ /// List of required field names.
+ [JsonPropertyName("required")]
+ public List? Required { get; set; }
+}
+
+/// RPC data type for SessionUiElicitation operations.
+internal class SessionUiElicitationRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Message describing what information is needed from the user.
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+
+ /// JSON Schema describing the form fields to present to the user.
+ [JsonPropertyName("requestedSchema")]
+ public SessionUiElicitationRequestRequestedSchema RequestedSchema { get => field ??= new(); set; }
+}
+
+/// RPC data type for SessionPermissionsHandlePendingPermissionRequest operations.
public class SessionPermissionsHandlePendingPermissionRequestResult
{
+ /// Whether the permission request was handled successfully.
[JsonPropertyName("success")]
public bool Success { get; set; }
}
+/// RPC data type for SessionPermissionsHandlePendingPermissionRequest operations.
internal class SessionPermissionsHandlePendingPermissionRequestRequest
{
+ /// Target session identifier.
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
+ /// Gets or sets the requestId value.
[JsonPropertyName("requestId")]
public string RequestId { get; set; } = string.Empty;
+ /// Gets or sets the result value.
[JsonPropertyName("result")]
public object Result { get; set; } = null!;
}
+/// RPC data type for SessionShellExec operations.
+public class SessionShellExecResult
+{
+ /// Unique identifier for tracking streamed output.
+ [JsonPropertyName("processId")]
+ public string ProcessId { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionShellExec operations.
+internal class SessionShellExecRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Shell command to execute.
+ [JsonPropertyName("command")]
+ public string Command { get; set; } = string.Empty;
+
+ /// Working directory (defaults to session working directory).
+ [JsonPropertyName("cwd")]
+ public string? Cwd { get; set; }
+
+ /// Timeout in milliseconds (default: 30000).
+ [JsonPropertyName("timeout")]
+ public double? Timeout { get; set; }
+}
+
+/// RPC data type for SessionShellKill operations.
+public class SessionShellKillResult
+{
+ /// Whether the signal was sent successfully.
+ [JsonPropertyName("killed")]
+ public bool Killed { get; set; }
+}
+
+/// RPC data type for SessionShellKill operations.
+internal class SessionShellKillRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Process identifier returned by shell.exec.
+ [JsonPropertyName("processId")]
+ public string ProcessId { get; set; } = string.Empty;
+
+ /// Signal to send (default: SIGTERM).
+ [JsonPropertyName("signal")]
+ public SessionShellKillRequestSignal? Signal { get; set; }
+}
+
+/// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info".
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionLogRequestLevel
+{
+ /// The info variant.
+ [JsonStringEnumMemberName("info")]
+ Info,
+ /// The warning variant.
+ [JsonStringEnumMemberName("warning")]
+ Warning,
+ /// The error variant.
+ [JsonStringEnumMemberName("error")]
+ Error,
+}
+
+
+/// The current agent mode.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SessionModeGetResultMode
{
+ /// The interactive variant.
[JsonStringEnumMemberName("interactive")]
Interactive,
+ /// The plan variant.
[JsonStringEnumMemberName("plan")]
Plan,
+ /// The autopilot variant.
[JsonStringEnumMemberName("autopilot")]
Autopilot,
}
-/// Typed server-scoped RPC methods (no session required).
+/// Connection status: connected, failed, pending, disabled, or not_configured.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ServerStatus
+{
+ /// The connected variant.
+ [JsonStringEnumMemberName("connected")]
+ Connected,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+ /// The pending variant.
+ [JsonStringEnumMemberName("pending")]
+ Pending,
+ /// The disabled variant.
+ [JsonStringEnumMemberName("disabled")]
+ Disabled,
+ /// The not_configured variant.
+ [JsonStringEnumMemberName("not_configured")]
+ NotConfigured,
+}
+
+
+/// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/).
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ExtensionSource
+{
+ /// The project variant.
+ [JsonStringEnumMemberName("project")]
+ Project,
+ /// The user variant.
+ [JsonStringEnumMemberName("user")]
+ User,
+}
+
+
+/// Current status: running, disabled, failed, or starting.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ExtensionStatus
+{
+ /// The running variant.
+ [JsonStringEnumMemberName("running")]
+ Running,
+ /// The disabled variant.
+ [JsonStringEnumMemberName("disabled")]
+ Disabled,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+ /// The starting variant.
+ [JsonStringEnumMemberName("starting")]
+ Starting,
+}
+
+
+/// The user's response: accept (submitted), decline (rejected), or cancel (dismissed).
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionUiElicitationResultAction
+{
+ /// The accept variant.
+ [JsonStringEnumMemberName("accept")]
+ Accept,
+ /// The decline variant.
+ [JsonStringEnumMemberName("decline")]
+ Decline,
+ /// The cancel variant.
+ [JsonStringEnumMemberName("cancel")]
+ Cancel,
+}
+
+
+/// Signal to send (default: SIGTERM).
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionShellKillRequestSignal
+{
+ /// The SIGTERM variant.
+ [JsonStringEnumMemberName("SIGTERM")]
+ SIGTERM,
+ /// The SIGKILL variant.
+ [JsonStringEnumMemberName("SIGKILL")]
+ SIGKILL,
+ /// The SIGINT variant.
+ [JsonStringEnumMemberName("SIGINT")]
+ SIGINT,
+}
+
+
+/// Provides server-scoped RPC methods (no session required).
public class ServerRpc
{
private readonly JsonRpc _rpc;
@@ -553,7 +1268,7 @@ public async Task PingAsync(string? message = null, CancellationToke
public ServerAccountApi Account { get; }
}
-/// Server-scoped Models APIs.
+/// Provides server-scoped Models APIs.
public class ServerModelsApi
{
private readonly JsonRpc _rpc;
@@ -570,7 +1285,7 @@ public async Task ListAsync(CancellationToken cancellationToke
}
}
-/// Server-scoped Tools APIs.
+/// Provides server-scoped Tools APIs.
public class ServerToolsApi
{
private readonly JsonRpc _rpc;
@@ -588,7 +1303,7 @@ public async Task ListAsync(string? model = null, CancellationT
}
}
-/// Server-scoped Account APIs.
+/// Provides server-scoped Account APIs.
public class ServerAccountApi
{
private readonly JsonRpc _rpc;
@@ -605,7 +1320,7 @@ public async Task GetQuotaAsync(CancellationToken cancell
}
}
-/// Typed session-scoped RPC methods.
+/// Provides typed session-scoped RPC methods.
public class SessionRpc
{
private readonly JsonRpc _rpc;
@@ -621,30 +1336,75 @@ internal SessionRpc(JsonRpc rpc, string sessionId)
Workspace = new WorkspaceApi(rpc, sessionId);
Fleet = new FleetApi(rpc, sessionId);
Agent = new AgentApi(rpc, sessionId);
+ Skills = new SkillsApi(rpc, sessionId);
+ Mcp = new McpApi(rpc, sessionId);
+ Plugins = new PluginsApi(rpc, sessionId);
+ Extensions = new ExtensionsApi(rpc, sessionId);
Compaction = new CompactionApi(rpc, sessionId);
Tools = new ToolsApi(rpc, sessionId);
+ Commands = new CommandsApi(rpc, sessionId);
+ Ui = new UiApi(rpc, sessionId);
Permissions = new PermissionsApi(rpc, sessionId);
+ Shell = new ShellApi(rpc, sessionId);
}
+ /// Model APIs.
public ModelApi Model { get; }
+ /// Mode APIs.
public ModeApi Mode { get; }
+ /// Plan APIs.
public PlanApi Plan { get; }
+ /// Workspace APIs.
public WorkspaceApi Workspace { get; }
+ /// Fleet APIs.
public FleetApi Fleet { get; }
+ /// Agent APIs.
public AgentApi Agent { get; }
+ /// Skills APIs.
+ public SkillsApi Skills { get; }
+
+ /// Mcp APIs.
+ public McpApi Mcp { get; }
+
+ /// Plugins APIs.
+ public PluginsApi Plugins { get; }
+
+ /// Extensions APIs.
+ public ExtensionsApi Extensions { get; }
+
+ /// Compaction APIs.
public CompactionApi Compaction { get; }
+ /// Tools APIs.
public ToolsApi Tools { get; }
+ /// Commands APIs.
+ public CommandsApi Commands { get; }
+
+ /// Ui APIs.
+ public UiApi Ui { get; }
+
+ /// Permissions APIs.
public PermissionsApi Permissions { get; }
+
+ /// Shell APIs.
+ public ShellApi Shell { get; }
+
+ /// Calls "session.log".
+ public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, string? url = null, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionLogRequest { SessionId = _sessionId, Message = message, Level = level, Ephemeral = ephemeral, Url = url };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.log", [request], cancellationToken);
+ }
}
+/// Provides session-scoped Model APIs.
public class ModelApi
{
private readonly JsonRpc _rpc;
@@ -664,13 +1424,14 @@ public async Task GetCurrentAsync(CancellationToke
}
/// Calls "session.model.switchTo".
- public async Task SwitchToAsync(string modelId, CancellationToken cancellationToken = default)
+ public async Task SwitchToAsync(string modelId, string? reasoningEffort = null, CancellationToken cancellationToken = default)
{
- var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId };
+ var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId, ReasoningEffort = reasoningEffort };
return await CopilotClient.InvokeRpcAsync(_rpc, "session.model.switchTo", [request], cancellationToken);
}
}
+/// Provides session-scoped Mode APIs.
public class ModeApi
{
private readonly JsonRpc _rpc;
@@ -697,6 +1458,7 @@ public async Task SetAsync(SessionModeGetResultMode mode,
}
}
+/// Provides session-scoped Plan APIs.
public class PlanApi
{
private readonly JsonRpc _rpc;
@@ -730,6 +1492,7 @@ public async Task DeleteAsync(CancellationToken cancell
}
}
+/// Provides session-scoped Workspace APIs.
public class WorkspaceApi
{
private readonly JsonRpc _rpc;
@@ -763,6 +1526,8 @@ public async Task CreateFileAsync(string path,
}
}
+/// Provides session-scoped Fleet APIs.
+[Experimental(Diagnostics.Experimental)]
public class FleetApi
{
private readonly JsonRpc _rpc;
@@ -775,13 +1540,15 @@ internal FleetApi(JsonRpc rpc, string sessionId)
}
/// Calls "session.fleet.start".
- public async Task StartAsync(string? prompt, CancellationToken cancellationToken = default)
+ public async Task StartAsync(string? prompt = null, CancellationToken cancellationToken = default)
{
var request = new SessionFleetStartRequest { SessionId = _sessionId, Prompt = prompt };
return await CopilotClient.InvokeRpcAsync(_rpc, "session.fleet.start", [request], cancellationToken);
}
}
+/// Provides session-scoped Agent APIs.
+[Experimental(Diagnostics.Experimental)]
public class AgentApi
{
private readonly JsonRpc _rpc;
@@ -820,8 +1587,164 @@ public async Task DeselectAsync(CancellationToken ca
var request = new SessionAgentDeselectRequest { SessionId = _sessionId };
return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.deselect", [request], cancellationToken);
}
+
+ /// Calls "session.agent.reload".
+ public async Task ReloadAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionAgentReloadRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.reload", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Skills APIs.
+[Experimental(Diagnostics.Experimental)]
+public class SkillsApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal SkillsApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.skills.list".
+ public async Task ListAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionSkillsListRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.list", [request], cancellationToken);
+ }
+
+ /// Calls "session.skills.enable".
+ public async Task EnableAsync(string name, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionSkillsEnableRequest { SessionId = _sessionId, Name = name };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.enable", [request], cancellationToken);
+ }
+
+ /// Calls "session.skills.disable".
+ public async Task DisableAsync(string name, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionSkillsDisableRequest { SessionId = _sessionId, Name = name };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.disable", [request], cancellationToken);
+ }
+
+ /// Calls "session.skills.reload".
+ public async Task ReloadAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionSkillsReloadRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.reload", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Mcp APIs.
+[Experimental(Diagnostics.Experimental)]
+public class McpApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal McpApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.mcp.list".
+ public async Task ListAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionMcpListRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.list", [request], cancellationToken);
+ }
+
+ /// Calls "session.mcp.enable".
+ public async Task EnableAsync(string serverName, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionMcpEnableRequest { SessionId = _sessionId, ServerName = serverName };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.enable", [request], cancellationToken);
+ }
+
+ /// Calls "session.mcp.disable".
+ public async Task DisableAsync(string serverName, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionMcpDisableRequest { SessionId = _sessionId, ServerName = serverName };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.disable", [request], cancellationToken);
+ }
+
+ /// Calls "session.mcp.reload".
+ public async Task ReloadAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionMcpReloadRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.reload", [request], cancellationToken);
+ }
}
+/// Provides session-scoped Plugins APIs.
+[Experimental(Diagnostics.Experimental)]
+public class PluginsApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal PluginsApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.plugins.list".
+ public async Task ListAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionPluginsListRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.plugins.list", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Extensions APIs.
+[Experimental(Diagnostics.Experimental)]
+public class ExtensionsApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal ExtensionsApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.extensions.list".
+ public async Task ListAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionExtensionsListRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.list", [request], cancellationToken);
+ }
+
+ /// Calls "session.extensions.enable".
+ public async Task EnableAsync(string id, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionExtensionsEnableRequest { SessionId = _sessionId, Id = id };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.enable", [request], cancellationToken);
+ }
+
+ /// Calls "session.extensions.disable".
+ public async Task DisableAsync(string id, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionExtensionsDisableRequest { SessionId = _sessionId, Id = id };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.disable", [request], cancellationToken);
+ }
+
+ /// Calls "session.extensions.reload".
+ public async Task ReloadAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionExtensionsReloadRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.reload", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Compaction APIs.
+[Experimental(Diagnostics.Experimental)]
public class CompactionApi
{
private readonly JsonRpc _rpc;
@@ -841,6 +1764,7 @@ public async Task CompactAsync(CancellationToken
}
}
+/// Provides session-scoped Tools APIs.
public class ToolsApi
{
private readonly JsonRpc _rpc;
@@ -853,13 +1777,54 @@ internal ToolsApi(JsonRpc rpc, string sessionId)
}
/// Calls "session.tools.handlePendingToolCall".
- public async Task HandlePendingToolCallAsync(string requestId, object? result, string? error, CancellationToken cancellationToken = default)
+ public async Task HandlePendingToolCallAsync(string requestId, object? result = null, string? error = null, CancellationToken cancellationToken = default)
{
var request = new SessionToolsHandlePendingToolCallRequest { SessionId = _sessionId, RequestId = requestId, Result = result, Error = error };
return await CopilotClient.InvokeRpcAsync(_rpc, "session.tools.handlePendingToolCall", [request], cancellationToken);
}
}
+/// Provides session-scoped Commands APIs.
+public class CommandsApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal CommandsApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.commands.handlePendingCommand".
+ public async Task HandlePendingCommandAsync(string requestId, string? error = null, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionCommandsHandlePendingCommandRequest { SessionId = _sessionId, RequestId = requestId, Error = error };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.commands.handlePendingCommand", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Ui APIs.
+public class UiApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal UiApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.ui.elicitation".
+ public async Task ElicitationAsync(string message, SessionUiElicitationRequestRequestedSchema requestedSchema, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionUiElicitationRequest { SessionId = _sessionId, Message = message, RequestedSchema = requestedSchema };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.ui.elicitation", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Permissions APIs.
public class PermissionsApi
{
private readonly JsonRpc _rpc;
@@ -879,6 +1844,33 @@ public async Task Handle
}
}
+/// Provides session-scoped Shell APIs.
+public class ShellApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal ShellApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.shell.exec".
+ public async Task ExecAsync(string command, string? cwd = null, double? timeout = null, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionShellExecRequest { SessionId = _sessionId, Command = command, Cwd = cwd, Timeout = timeout };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.shell.exec", [request], cancellationToken);
+ }
+
+ /// Calls "session.shell.kill".
+ public async Task KillAsync(string processId, SessionShellKillRequestSignal? signal = null, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionShellKillRequest { SessionId = _sessionId, ProcessId = processId, Signal = signal };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.shell.kill", [request], cancellationToken);
+ }
+}
+
[JsonSourceGenerationOptions(
JsonSerializerDefaults.Web,
AllowOutOfOrderMetadataProperties = true,
@@ -886,6 +1878,7 @@ public async Task Handle
[JsonSerializable(typeof(AccountGetQuotaResult))]
[JsonSerializable(typeof(AccountGetQuotaResultQuotaSnapshotsValue))]
[JsonSerializable(typeof(Agent))]
+[JsonSerializable(typeof(Extension))]
[JsonSerializable(typeof(Model))]
[JsonSerializable(typeof(ModelBilling))]
[JsonSerializable(typeof(ModelCapabilities))]
@@ -895,6 +1888,8 @@ public async Task Handle
[JsonSerializable(typeof(ModelsListResult))]
[JsonSerializable(typeof(PingRequest))]
[JsonSerializable(typeof(PingResult))]
+[JsonSerializable(typeof(Plugin))]
+[JsonSerializable(typeof(Server))]
[JsonSerializable(typeof(SessionAgentDeselectRequest))]
[JsonSerializable(typeof(SessionAgentDeselectResult))]
[JsonSerializable(typeof(SessionAgentGetCurrentRequest))]
@@ -902,13 +1897,35 @@ public async Task Handle
[JsonSerializable(typeof(SessionAgentGetCurrentResultAgent))]
[JsonSerializable(typeof(SessionAgentListRequest))]
[JsonSerializable(typeof(SessionAgentListResult))]
+[JsonSerializable(typeof(SessionAgentReloadRequest))]
+[JsonSerializable(typeof(SessionAgentReloadResult))]
[JsonSerializable(typeof(SessionAgentSelectRequest))]
[JsonSerializable(typeof(SessionAgentSelectResult))]
[JsonSerializable(typeof(SessionAgentSelectResultAgent))]
+[JsonSerializable(typeof(SessionCommandsHandlePendingCommandRequest))]
+[JsonSerializable(typeof(SessionCommandsHandlePendingCommandResult))]
[JsonSerializable(typeof(SessionCompactionCompactRequest))]
[JsonSerializable(typeof(SessionCompactionCompactResult))]
+[JsonSerializable(typeof(SessionExtensionsDisableRequest))]
+[JsonSerializable(typeof(SessionExtensionsDisableResult))]
+[JsonSerializable(typeof(SessionExtensionsEnableRequest))]
+[JsonSerializable(typeof(SessionExtensionsEnableResult))]
+[JsonSerializable(typeof(SessionExtensionsListRequest))]
+[JsonSerializable(typeof(SessionExtensionsListResult))]
+[JsonSerializable(typeof(SessionExtensionsReloadRequest))]
+[JsonSerializable(typeof(SessionExtensionsReloadResult))]
[JsonSerializable(typeof(SessionFleetStartRequest))]
[JsonSerializable(typeof(SessionFleetStartResult))]
+[JsonSerializable(typeof(SessionLogRequest))]
+[JsonSerializable(typeof(SessionLogResult))]
+[JsonSerializable(typeof(SessionMcpDisableRequest))]
+[JsonSerializable(typeof(SessionMcpDisableResult))]
+[JsonSerializable(typeof(SessionMcpEnableRequest))]
+[JsonSerializable(typeof(SessionMcpEnableResult))]
+[JsonSerializable(typeof(SessionMcpListRequest))]
+[JsonSerializable(typeof(SessionMcpListResult))]
+[JsonSerializable(typeof(SessionMcpReloadRequest))]
+[JsonSerializable(typeof(SessionMcpReloadResult))]
[JsonSerializable(typeof(SessionModeGetRequest))]
[JsonSerializable(typeof(SessionModeGetResult))]
[JsonSerializable(typeof(SessionModeSetRequest))]
@@ -925,14 +1942,32 @@ public async Task Handle
[JsonSerializable(typeof(SessionPlanReadResult))]
[JsonSerializable(typeof(SessionPlanUpdateRequest))]
[JsonSerializable(typeof(SessionPlanUpdateResult))]
+[JsonSerializable(typeof(SessionPluginsListRequest))]
+[JsonSerializable(typeof(SessionPluginsListResult))]
+[JsonSerializable(typeof(SessionShellExecRequest))]
+[JsonSerializable(typeof(SessionShellExecResult))]
+[JsonSerializable(typeof(SessionShellKillRequest))]
+[JsonSerializable(typeof(SessionShellKillResult))]
+[JsonSerializable(typeof(SessionSkillsDisableRequest))]
+[JsonSerializable(typeof(SessionSkillsDisableResult))]
+[JsonSerializable(typeof(SessionSkillsEnableRequest))]
+[JsonSerializable(typeof(SessionSkillsEnableResult))]
+[JsonSerializable(typeof(SessionSkillsListRequest))]
+[JsonSerializable(typeof(SessionSkillsListResult))]
+[JsonSerializable(typeof(SessionSkillsReloadRequest))]
+[JsonSerializable(typeof(SessionSkillsReloadResult))]
[JsonSerializable(typeof(SessionToolsHandlePendingToolCallRequest))]
[JsonSerializable(typeof(SessionToolsHandlePendingToolCallResult))]
+[JsonSerializable(typeof(SessionUiElicitationRequest))]
+[JsonSerializable(typeof(SessionUiElicitationRequestRequestedSchema))]
+[JsonSerializable(typeof(SessionUiElicitationResult))]
[JsonSerializable(typeof(SessionWorkspaceCreateFileRequest))]
[JsonSerializable(typeof(SessionWorkspaceCreateFileResult))]
[JsonSerializable(typeof(SessionWorkspaceListFilesRequest))]
[JsonSerializable(typeof(SessionWorkspaceListFilesResult))]
[JsonSerializable(typeof(SessionWorkspaceReadFileRequest))]
[JsonSerializable(typeof(SessionWorkspaceReadFileResult))]
+[JsonSerializable(typeof(Skill))]
[JsonSerializable(typeof(Tool))]
[JsonSerializable(typeof(ToolsListRequest))]
[JsonSerializable(typeof(ToolsListResult))]
diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs
index c497038c6a..d5ef13d530 100644
--- a/dotnet/src/Generated/SessionEvents.cs
+++ b/dotnet/src/Generated/SessionEvents.cs
@@ -5,20 +5,19 @@
// AUTO-GENERATED FILE - DO NOT EDIT
// Generated from: session-events.schema.json
-// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings.
-#pragma warning disable CS1591
-
+using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace GitHub.Copilot.SDK;
///
-/// Base class for all session events with polymorphic JSON serialization.
+/// Provides the base class from which all session events derive.
///
+[DebuggerDisplay("{DebuggerDisplay,nq}")]
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "type",
- UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
+ IgnoreUnrecognizedTypeDiscriminators = true)]
[JsonDerivedType(typeof(AbortEvent), "abort")]
[JsonDerivedType(typeof(AssistantIntentEvent), "assistant.intent")]
[JsonDerivedType(typeof(AssistantMessageEvent), "assistant.message")]
@@ -30,7 +29,9 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(AssistantTurnStartEvent), "assistant.turn_start")]
[JsonDerivedType(typeof(AssistantUsageEvent), "assistant.usage")]
[JsonDerivedType(typeof(CommandCompletedEvent), "command.completed")]
+[JsonDerivedType(typeof(CommandExecuteEvent), "command.execute")]
[JsonDerivedType(typeof(CommandQueuedEvent), "command.queued")]
+[JsonDerivedType(typeof(CommandsChangedEvent), "commands.changed")]
[JsonDerivedType(typeof(ElicitationCompletedEvent), "elicitation.completed")]
[JsonDerivedType(typeof(ElicitationRequestedEvent), "elicitation.requested")]
[JsonDerivedType(typeof(ExitPlanModeCompletedEvent), "exit_plan_mode.completed")]
@@ -39,25 +40,33 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(ExternalToolRequestedEvent), "external_tool.requested")]
[JsonDerivedType(typeof(HookEndEvent), "hook.end")]
[JsonDerivedType(typeof(HookStartEvent), "hook.start")]
+[JsonDerivedType(typeof(McpOauthCompletedEvent), "mcp.oauth_completed")]
+[JsonDerivedType(typeof(McpOauthRequiredEvent), "mcp.oauth_required")]
[JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")]
[JsonDerivedType(typeof(PermissionCompletedEvent), "permission.completed")]
[JsonDerivedType(typeof(PermissionRequestedEvent), "permission.requested")]
+[JsonDerivedType(typeof(SessionBackgroundTasksChangedEvent), "session.background_tasks_changed")]
[JsonDerivedType(typeof(SessionCompactionCompleteEvent), "session.compaction_complete")]
[JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")]
[JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")]
[JsonDerivedType(typeof(SessionErrorEvent), "session.error")]
+[JsonDerivedType(typeof(SessionExtensionsLoadedEvent), "session.extensions_loaded")]
[JsonDerivedType(typeof(SessionHandoffEvent), "session.handoff")]
[JsonDerivedType(typeof(SessionIdleEvent), "session.idle")]
[JsonDerivedType(typeof(SessionInfoEvent), "session.info")]
+[JsonDerivedType(typeof(SessionMcpServerStatusChangedEvent), "session.mcp_server_status_changed")]
+[JsonDerivedType(typeof(SessionMcpServersLoadedEvent), "session.mcp_servers_loaded")]
[JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")]
[JsonDerivedType(typeof(SessionModelChangeEvent), "session.model_change")]
[JsonDerivedType(typeof(SessionPlanChangedEvent), "session.plan_changed")]
[JsonDerivedType(typeof(SessionResumeEvent), "session.resume")]
[JsonDerivedType(typeof(SessionShutdownEvent), "session.shutdown")]
+[JsonDerivedType(typeof(SessionSkillsLoadedEvent), "session.skills_loaded")]
[JsonDerivedType(typeof(SessionSnapshotRewindEvent), "session.snapshot_rewind")]
[JsonDerivedType(typeof(SessionStartEvent), "session.start")]
[JsonDerivedType(typeof(SessionTaskCompleteEvent), "session.task_complete")]
[JsonDerivedType(typeof(SessionTitleChangedEvent), "session.title_changed")]
+[JsonDerivedType(typeof(SessionToolsUpdatedEvent), "session.tools_updated")]
[JsonDerivedType(typeof(SessionTruncationEvent), "session.truncation")]
[JsonDerivedType(typeof(SessionUsageInfoEvent), "session.usage_info")]
[JsonDerivedType(typeof(SessionWarningEvent), "session.warning")]
@@ -69,6 +78,7 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(SubagentSelectedEvent), "subagent.selected")]
[JsonDerivedType(typeof(SubagentStartedEvent), "subagent.started")]
[JsonDerivedType(typeof(SystemMessageEvent), "system.message")]
+[JsonDerivedType(typeof(SystemNotificationEvent), "system.notification")]
[JsonDerivedType(typeof(ToolExecutionCompleteEvent), "tool.execution_complete")]
[JsonDerivedType(typeof(ToolExecutionPartialResultEvent), "tool.execution_partial_result")]
[JsonDerivedType(typeof(ToolExecutionProgressEvent), "tool.execution_progress")]
@@ -77,17 +87,21 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(UserInputCompletedEvent), "user_input.completed")]
[JsonDerivedType(typeof(UserInputRequestedEvent), "user_input.requested")]
[JsonDerivedType(typeof(UserMessageEvent), "user.message")]
-public abstract partial class SessionEvent
+public partial class SessionEvent
{
+ /// Unique event identifier (UUID v4), generated when the event is emitted.
[JsonPropertyName("id")]
public Guid Id { get; set; }
+ /// ISO 8601 timestamp when the event was created.
[JsonPropertyName("timestamp")]
public DateTimeOffset Timestamp { get; set; }
+ /// ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
[JsonPropertyName("parentId")]
public Guid? ParentId { get; set; }
+ /// When true, the event is transient and not persisted to the session event log on disk.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("ephemeral")]
public bool? Ephemeral { get; set; }
@@ -96,1780 +110,2655 @@ public abstract partial class SessionEvent
/// The event type discriminator.
///
[JsonIgnore]
- public abstract string Type { get; }
+ public virtual string Type => "unknown";
+ /// Deserializes a JSON string into a .
public static SessionEvent FromJson(string json) =>
JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!;
+ /// Serializes this event to a JSON string.
public string ToJson() =>
JsonSerializer.Serialize(this, SessionEventsJsonContext.Default.SessionEvent);
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private string DebuggerDisplay => ToJson();
}
-///
-/// Event: session.start
-///
+/// Session initialization metadata including context and configuration.
+/// Represents the session.start event.
public partial class SessionStartEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.start";
+ /// The session.start event payload.
[JsonPropertyName("data")]
public required SessionStartData Data { get; set; }
}
-///
-/// Event: session.resume
-///
+/// Session resume metadata including current context and event count.
+/// Represents the session.resume event.
public partial class SessionResumeEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.resume";
+ /// The session.resume event payload.
[JsonPropertyName("data")]
public required SessionResumeData Data { get; set; }
}
-///
-/// Event: session.error
-///
+/// Error details for timeline display including message and optional diagnostic information.
+/// Represents the session.error event.
public partial class SessionErrorEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.error";
+ /// The session.error event payload.
[JsonPropertyName("data")]
public required SessionErrorData Data { get; set; }
}
-///
-/// Event: session.idle
-///
+/// Payload indicating the agent is idle; includes any background tasks still in flight.
+/// Represents the session.idle event.
public partial class SessionIdleEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.idle";
+ /// The session.idle event payload.
[JsonPropertyName("data")]
public required SessionIdleData Data { get; set; }
}
-///
-/// Event: session.title_changed
-///
+/// Session title change payload containing the new display title.
+/// Represents the session.title_changed event.
public partial class SessionTitleChangedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.title_changed";
+ /// The session.title_changed event payload.
[JsonPropertyName("data")]
public required SessionTitleChangedData Data { get; set; }
}
-///
-/// Event: session.info
-///
+/// Informational message for timeline display with categorization.
+/// Represents the session.info event.
public partial class SessionInfoEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.info";
+ /// The session.info event payload.
[JsonPropertyName("data")]
public required SessionInfoData Data { get; set; }
}
-///
-/// Event: session.warning
-///
+/// Warning message for timeline display with categorization.
+/// Represents the session.warning event.
public partial class SessionWarningEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.warning";
+ /// The session.warning event payload.
[JsonPropertyName("data")]
public required SessionWarningData Data { get; set; }
}
-///
-/// Event: session.model_change
-///
+/// Model change details including previous and new model identifiers.
+/// Represents the session.model_change event.
public partial class SessionModelChangeEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.model_change";
+ /// The session.model_change event payload.
[JsonPropertyName("data")]
public required SessionModelChangeData Data { get; set; }
}
-///
-/// Event: session.mode_changed
-///
+/// Agent mode change details including previous and new modes.
+/// Represents the session.mode_changed event.
public partial class SessionModeChangedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.mode_changed";
+ /// The session.mode_changed event payload.
[JsonPropertyName("data")]
public required SessionModeChangedData Data { get; set; }
}
-///
-/// Event: session.plan_changed
-///
+/// Plan file operation details indicating what changed.
+/// Represents the session.plan_changed event.
public partial class SessionPlanChangedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.plan_changed";
+ /// The session.plan_changed event payload.
[JsonPropertyName("data")]
public required SessionPlanChangedData Data { get; set; }
}
-///
-/// Event: session.workspace_file_changed
-///
+/// Workspace file change details including path and operation type.
+/// Represents the session.workspace_file_changed event.
public partial class SessionWorkspaceFileChangedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.workspace_file_changed";
+ /// The session.workspace_file_changed event payload.
[JsonPropertyName("data")]
public required SessionWorkspaceFileChangedData Data { get; set; }
}
-///
-/// Event: session.handoff
-///
+/// Session handoff metadata including source, context, and repository information.
+/// Represents the session.handoff event.
public partial class SessionHandoffEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.handoff";
+ /// The session.handoff event payload.
[JsonPropertyName("data")]
public required SessionHandoffData Data { get; set; }
}
-///
-/// Event: session.truncation
-///
+/// Conversation truncation statistics including token counts and removed content metrics.
+/// Represents the session.truncation event.
public partial class SessionTruncationEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.truncation";
+ /// The session.truncation event payload.
[JsonPropertyName("data")]
public required SessionTruncationData Data { get; set; }
}
-///
-/// Event: session.snapshot_rewind
-///
+/// Session rewind details including target event and count of removed events.
+/// Represents the session.snapshot_rewind event.
public partial class SessionSnapshotRewindEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.snapshot_rewind";
+ /// The session.snapshot_rewind event payload.
[JsonPropertyName("data")]
public required SessionSnapshotRewindData Data { get; set; }
}
-///
-/// Event: session.shutdown
-///
+/// Session termination metrics including usage statistics, code changes, and shutdown reason.
+/// Represents the session.shutdown event.
public partial class SessionShutdownEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.shutdown";
+ /// The session.shutdown event payload.
[JsonPropertyName("data")]
public required SessionShutdownData Data { get; set; }
}
-///
-/// Event: session.context_changed
-///
+/// Updated working directory and git context after the change.
+/// Represents the session.context_changed event.
public partial class SessionContextChangedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.context_changed";
+ /// The session.context_changed event payload.
[JsonPropertyName("data")]
public required SessionContextChangedData Data { get; set; }
}
-///
-/// Event: session.usage_info
-///
+/// Current context window usage statistics including token and message counts.
+/// Represents the session.usage_info event.
public partial class SessionUsageInfoEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.usage_info";
+ /// The session.usage_info event payload.
[JsonPropertyName("data")]
public required SessionUsageInfoData Data { get; set; }
}
-///
-/// Event: session.compaction_start
-///
+/// Context window breakdown at the start of LLM-powered conversation compaction.
+/// Represents the session.compaction_start event.
public partial class SessionCompactionStartEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.compaction_start";
+ /// The session.compaction_start event payload.
[JsonPropertyName("data")]
public required SessionCompactionStartData Data { get; set; }
}
-///
-/// Event: session.compaction_complete
-///
+/// Conversation compaction results including success status, metrics, and optional error details.
+/// Represents the session.compaction_complete event.
public partial class SessionCompactionCompleteEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.compaction_complete";
+ /// The session.compaction_complete event payload.
[JsonPropertyName("data")]
public required SessionCompactionCompleteData Data { get; set; }
}
-///
-/// Event: session.task_complete
-///
+/// Task completion notification with summary from the agent.
+/// Represents the session.task_complete event.
public partial class SessionTaskCompleteEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "session.task_complete";
+ /// The session.task_complete event payload.
[JsonPropertyName("data")]
public required SessionTaskCompleteData Data { get; set; }
}
-///
-/// Event: user.message
-///
+/// Represents the user.message event.
public partial class UserMessageEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "user.message";
+ /// The user.message event payload.
[JsonPropertyName("data")]
public required UserMessageData Data { get; set; }
}
-///
-/// Event: pending_messages.modified
-///
+/// Empty payload; the event signals that the pending message queue has changed.
+/// Represents the pending_messages.modified event.
public partial class PendingMessagesModifiedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "pending_messages.modified";
+ /// The pending_messages.modified event payload.
[JsonPropertyName("data")]
public required PendingMessagesModifiedData Data { get; set; }
}
-///
-/// Event: assistant.turn_start
-///
+/// Turn initialization metadata including identifier and interaction tracking.
+/// Represents the assistant.turn_start event.
public partial class AssistantTurnStartEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.turn_start";
+ /// The assistant.turn_start event payload.
[JsonPropertyName("data")]
public required AssistantTurnStartData Data { get; set; }
}
-///
-/// Event: assistant.intent
-///
+/// Agent intent description for current activity or plan.
+/// Represents the assistant.intent event.
public partial class AssistantIntentEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.intent";
+ /// The assistant.intent event payload.
[JsonPropertyName("data")]
public required AssistantIntentData Data { get; set; }
}
-///
-/// Event: assistant.reasoning
-///
+/// Assistant reasoning content for timeline display with complete thinking text.
+/// Represents the assistant.reasoning event.
public partial class AssistantReasoningEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.reasoning";
+ /// The assistant.reasoning event payload.
[JsonPropertyName("data")]
public required AssistantReasoningData Data { get; set; }
}
-///
-/// Event: assistant.reasoning_delta
-///
+/// Streaming reasoning delta for incremental extended thinking updates.
+/// Represents the assistant.reasoning_delta event.
public partial class AssistantReasoningDeltaEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.reasoning_delta";
+ /// The assistant.reasoning_delta event payload.
[JsonPropertyName("data")]
public required AssistantReasoningDeltaData Data { get; set; }
}
-///
-/// Event: assistant.streaming_delta
-///
+/// Streaming response progress with cumulative byte count.
+/// Represents the assistant.streaming_delta event.
public partial class AssistantStreamingDeltaEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.streaming_delta";
+ /// The assistant.streaming_delta event payload.
[JsonPropertyName("data")]
public required AssistantStreamingDeltaData Data { get; set; }
}
-///
-/// Event: assistant.message
-///
+/// Assistant response containing text content, optional tool requests, and interaction metadata.
+/// Represents the assistant.message event.
public partial class AssistantMessageEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.message";
+ /// The assistant.message event payload.
[JsonPropertyName("data")]
public required AssistantMessageData Data { get; set; }
}
-///
-/// Event: assistant.message_delta
-///
+/// Streaming assistant message delta for incremental response updates.
+/// Represents the assistant.message_delta event.
public partial class AssistantMessageDeltaEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.message_delta";
+ /// The assistant.message_delta event payload.
[JsonPropertyName("data")]
public required AssistantMessageDeltaData Data { get; set; }
}
-///
-/// Event: assistant.turn_end
-///
+/// Turn completion metadata including the turn identifier.
+/// Represents the assistant.turn_end event.
public partial class AssistantTurnEndEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.turn_end";
+ /// The assistant.turn_end event payload.
[JsonPropertyName("data")]
public required AssistantTurnEndData Data { get; set; }
}
-///
-/// Event: assistant.usage
-///
+/// LLM API call usage metrics including tokens, costs, quotas, and billing information.
+/// Represents the assistant.usage event.
public partial class AssistantUsageEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "assistant.usage";
+ /// The assistant.usage event payload.
[JsonPropertyName("data")]
public required AssistantUsageData Data { get; set; }
}
-///
-/// Event: abort
-///
+/// Turn abort information including the reason for termination.
+/// Represents the abort event.
public partial class AbortEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "abort";
+ /// The abort event payload.
[JsonPropertyName("data")]
public required AbortData Data { get; set; }
}
-///
-/// Event: tool.user_requested
-///
+/// User-initiated tool invocation request with tool name and arguments.
+/// Represents the tool.user_requested event.
public partial class ToolUserRequestedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "tool.user_requested";
+ /// The tool.user_requested event payload.
[JsonPropertyName("data")]
public required ToolUserRequestedData Data { get; set; }
}
-///
-/// Event: tool.execution_start
-///
+/// Tool execution startup details including MCP server information when applicable.
+/// Represents the tool.execution_start event.
public partial class ToolExecutionStartEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "tool.execution_start";
+ /// The tool.execution_start event payload.
[JsonPropertyName("data")]
public required ToolExecutionStartData Data { get; set; }
}
-///
-/// Event: tool.execution_partial_result
-///
+/// Streaming tool execution output for incremental result display.
+/// Represents the tool.execution_partial_result event.
public partial class ToolExecutionPartialResultEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "tool.execution_partial_result";
+ /// The tool.execution_partial_result event payload.
[JsonPropertyName("data")]
public required ToolExecutionPartialResultData Data { get; set; }
}
-///
-/// Event: tool.execution_progress
-///
+/// Tool execution progress notification with status message.
+/// Represents the tool.execution_progress event.
public partial class ToolExecutionProgressEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "tool.execution_progress";
+ /// The tool.execution_progress event payload.
[JsonPropertyName("data")]
public required ToolExecutionProgressData Data { get; set; }
}
-///
-/// Event: tool.execution_complete
-///
+/// Tool execution completion results including success status, detailed output, and error information.
+/// Represents the tool.execution_complete event.
public partial class ToolExecutionCompleteEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "tool.execution_complete";
+ /// The tool.execution_complete event payload.
[JsonPropertyName("data")]
public required ToolExecutionCompleteData Data { get; set; }
}
-///
-/// Event: skill.invoked
-///
+/// Skill invocation details including content, allowed tools, and plugin metadata.
+/// Represents the skill.invoked event.
public partial class SkillInvokedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "skill.invoked";
+ /// The skill.invoked event payload.
[JsonPropertyName("data")]
public required SkillInvokedData Data { get; set; }
}
-///
-/// Event: subagent.started
-///
+/// Sub-agent startup details including parent tool call and agent information.
+/// Represents the subagent.started event.
public partial class SubagentStartedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "subagent.started";
+ /// The subagent.started event payload.
[JsonPropertyName("data")]
public required SubagentStartedData Data { get; set; }
}
-///
-/// Event: subagent.completed
-///
+/// Sub-agent completion details for successful execution.
+/// Represents the subagent.completed event.
public partial class SubagentCompletedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "subagent.completed";
+ /// The subagent.completed event payload.
[JsonPropertyName("data")]
public required SubagentCompletedData Data { get; set; }
}
-///
-/// Event: subagent.failed
-///
+/// Sub-agent failure details including error message and agent information.
+/// Represents the subagent.failed event.
public partial class SubagentFailedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "subagent.failed";
+ /// The subagent.failed event payload.
[JsonPropertyName("data")]
public required SubagentFailedData Data { get; set; }
}
-///
-/// Event: subagent.selected
-///
+/// Custom agent selection details including name and available tools.
+/// Represents the subagent.selected event.
public partial class SubagentSelectedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "subagent.selected";
+ /// The subagent.selected event payload.
[JsonPropertyName("data")]
public required SubagentSelectedData Data { get; set; }
}
-///
-/// Event: subagent.deselected
-///
+/// Empty payload; the event signals that the custom agent was deselected, returning to the default agent.
+/// Represents the subagent.deselected event.
public partial class SubagentDeselectedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "subagent.deselected";
+ /// The subagent.deselected event payload.
[JsonPropertyName("data")]
public required SubagentDeselectedData Data { get; set; }
}
-///
-/// Event: hook.start
-///
+/// Hook invocation start details including type and input data.
+/// Represents the hook.start event.
public partial class HookStartEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "hook.start";
+ /// The hook.start event payload.
[JsonPropertyName("data")]
public required HookStartData Data { get; set; }
}
-///
-/// Event: hook.end
-///
+/// Hook invocation completion details including output, success status, and error information.
+/// Represents the hook.end event.
public partial class HookEndEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "hook.end";
+ /// The hook.end event payload.
[JsonPropertyName("data")]
public required HookEndData Data { get; set; }
}
-///
-/// Event: system.message
-///
+/// System or developer message content with role and optional template metadata.
+/// Represents the system.message event.
public partial class SystemMessageEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "system.message";
+ /// The system.message event payload.
[JsonPropertyName("data")]
public required SystemMessageData Data { get; set; }
}
-///
-/// Event: permission.requested
-///
+/// System-generated notification for runtime events like background task completion.
+/// Represents the system.notification event.
+public partial class SystemNotificationEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "system.notification";
+
+ /// The system.notification event payload.
+ [JsonPropertyName("data")]
+ public required SystemNotificationData Data { get; set; }
+}
+
+/// Permission request notification requiring client approval with request details.
+/// Represents the permission.requested event.
public partial class PermissionRequestedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "permission.requested";
+ /// The permission.requested event payload.
[JsonPropertyName("data")]
public required PermissionRequestedData Data { get; set; }
}
-///
-/// Event: permission.completed
-///
+/// Permission request completion notification signaling UI dismissal.
+/// Represents the permission.completed event.
public partial class PermissionCompletedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "permission.completed";
+ /// The permission.completed event payload.
[JsonPropertyName("data")]
public required PermissionCompletedData Data { get; set; }
}
-///
-/// Event: user_input.requested
-///
+/// User input request notification with question and optional predefined choices.
+/// Represents the user_input.requested event.
public partial class UserInputRequestedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "user_input.requested";
+ /// The user_input.requested event payload.
[JsonPropertyName("data")]
public required UserInputRequestedData Data { get; set; }
}
-///
-/// Event: user_input.completed
-///
+/// User input request completion notification signaling UI dismissal.
+/// Represents the user_input.completed event.
public partial class UserInputCompletedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "user_input.completed";
+ /// The user_input.completed event payload.
[JsonPropertyName("data")]
public required UserInputCompletedData Data { get; set; }
}
-///
-/// Event: elicitation.requested
-///
+/// Elicitation request; may be form-based (structured input) or URL-based (browser redirect).
+/// Represents the elicitation.requested event.
public partial class ElicitationRequestedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "elicitation.requested";
+ /// The elicitation.requested event payload.
[JsonPropertyName("data")]
public required ElicitationRequestedData Data { get; set; }
}
-///
-/// Event: elicitation.completed
-///
+/// Elicitation request completion notification signaling UI dismissal.
+/// Represents the elicitation.completed event.
public partial class ElicitationCompletedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "elicitation.completed";
+ /// The elicitation.completed event payload.
[JsonPropertyName("data")]
public required ElicitationCompletedData Data { get; set; }
}
-///
-/// Event: external_tool.requested
-///
+/// OAuth authentication request for an MCP server.
+/// Represents the mcp.oauth_required event.
+public partial class McpOauthRequiredEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "mcp.oauth_required";
+
+ /// The mcp.oauth_required event payload.
+ [JsonPropertyName("data")]
+ public required McpOauthRequiredData Data { get; set; }
+}
+
+/// MCP OAuth request completion notification.
+/// Represents the mcp.oauth_completed event.
+public partial class McpOauthCompletedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "mcp.oauth_completed";
+
+ /// The mcp.oauth_completed event payload.
+ [JsonPropertyName("data")]
+ public required McpOauthCompletedData Data { get; set; }
+}
+
+/// External tool invocation request for client-side tool execution.
+/// Represents the external_tool.requested event.
public partial class ExternalToolRequestedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "external_tool.requested";
+ /// The external_tool.requested event payload.
[JsonPropertyName("data")]
public required ExternalToolRequestedData Data { get; set; }
}
-///
-/// Event: external_tool.completed
-///
+/// External tool completion notification signaling UI dismissal.
+/// Represents the external_tool.completed event.
public partial class ExternalToolCompletedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "external_tool.completed";
+ /// The external_tool.completed event payload.
[JsonPropertyName("data")]
public required ExternalToolCompletedData Data { get; set; }
}
-///
-/// Event: command.queued
-///
+/// Queued slash command dispatch request for client execution.
+/// Represents the command.queued event.
public partial class CommandQueuedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "command.queued";
+ /// The command.queued event payload.
[JsonPropertyName("data")]
public required CommandQueuedData Data { get; set; }
}
-///
-/// Event: command.completed
-///
+/// Registered command dispatch request routed to the owning client.
+/// Represents the command.execute event.
+public partial class CommandExecuteEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "command.execute";
+
+ /// The command.execute event payload.
+ [JsonPropertyName("data")]
+ public required CommandExecuteData Data { get; set; }
+}
+
+/// Queued command completion notification signaling UI dismissal.
+/// Represents the command.completed event.
public partial class CommandCompletedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "command.completed";
+ /// The command.completed event payload.
[JsonPropertyName("data")]
public required CommandCompletedData Data { get; set; }
}
-///
-/// Event: exit_plan_mode.requested
-///
+/// SDK command registration change notification.
+/// Represents the commands.changed event.
+public partial class CommandsChangedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "commands.changed";
+
+ /// The commands.changed event payload.
+ [JsonPropertyName("data")]
+ public required CommandsChangedData Data { get; set; }
+}
+
+/// Plan approval request with plan content and available user actions.
+/// Represents the exit_plan_mode.requested event.
public partial class ExitPlanModeRequestedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "exit_plan_mode.requested";
+ /// The exit_plan_mode.requested event payload.
[JsonPropertyName("data")]
public required ExitPlanModeRequestedData Data { get; set; }
}
-///
-/// Event: exit_plan_mode.completed
-///
+/// Plan mode exit completion notification signaling UI dismissal.
+/// Represents the exit_plan_mode.completed event.
public partial class ExitPlanModeCompletedEvent : SessionEvent
{
+ ///
[JsonIgnore]
public override string Type => "exit_plan_mode.completed";
+ /// The exit_plan_mode.completed event payload.
[JsonPropertyName("data")]
public required ExitPlanModeCompletedData Data { get; set; }
}
+/// Represents the session.tools_updated event.
+public partial class SessionToolsUpdatedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.tools_updated";
+
+ /// The session.tools_updated event payload.
+ [JsonPropertyName("data")]
+ public required SessionToolsUpdatedData Data { get; set; }
+}
+
+/// Represents the session.background_tasks_changed event.
+public partial class SessionBackgroundTasksChangedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.background_tasks_changed";
+
+ /// The session.background_tasks_changed event payload.
+ [JsonPropertyName("data")]
+ public required SessionBackgroundTasksChangedData Data { get; set; }
+}
+
+/// Represents the session.skills_loaded event.
+public partial class SessionSkillsLoadedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.skills_loaded";
+
+ /// The session.skills_loaded event payload.
+ [JsonPropertyName("data")]
+ public required SessionSkillsLoadedData Data { get; set; }
+}
+
+/// Represents the session.mcp_servers_loaded event.
+public partial class SessionMcpServersLoadedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.mcp_servers_loaded";
+
+ /// The session.mcp_servers_loaded event payload.
+ [JsonPropertyName("data")]
+ public required SessionMcpServersLoadedData Data { get; set; }
+}
+
+/// Represents the session.mcp_server_status_changed event.
+public partial class SessionMcpServerStatusChangedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.mcp_server_status_changed";
+
+ /// The session.mcp_server_status_changed event payload.
+ [JsonPropertyName("data")]
+ public required SessionMcpServerStatusChangedData Data { get; set; }
+}
+
+/// Represents the session.extensions_loaded event.
+public partial class SessionExtensionsLoadedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.extensions_loaded";
+
+ /// The session.extensions_loaded event payload.
+ [JsonPropertyName("data")]
+ public required SessionExtensionsLoadedData Data { get; set; }
+}
+
+/// Session initialization metadata including context and configuration.
public partial class SessionStartData
{
+ /// Unique identifier for the session.
[JsonPropertyName("sessionId")]
public required string SessionId { get; set; }
+ /// Schema version number for the session event format.
[JsonPropertyName("version")]
public required double Version { get; set; }
+ /// Identifier of the software producing the events (e.g., "copilot-agent").
[JsonPropertyName("producer")]
public required string Producer { get; set; }
+ /// Version string of the Copilot application.
[JsonPropertyName("copilotVersion")]
public required string CopilotVersion { get; set; }
+ /// ISO 8601 timestamp when the session was created.
[JsonPropertyName("startTime")]
public required DateTimeOffset StartTime { get; set; }
+ /// Model selected at session creation time, if any.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("selectedModel")]
public string? SelectedModel { get; set; }
+ /// Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh").
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("reasoningEffort")]
+ public string? ReasoningEffort { get; set; }
+
+ /// Working directory and git context at session start.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("context")]
public SessionStartDataContext? Context { get; set; }
+
+ /// Whether the session was already in use by another client at start time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("alreadyInUse")]
+ public bool? AlreadyInUse { get; set; }
}
+/// Session resume metadata including current context and event count.
public partial class SessionResumeData
{
+ /// ISO 8601 timestamp when the session was resumed.
[JsonPropertyName("resumeTime")]
public required DateTimeOffset ResumeTime { get; set; }
+ /// Total number of persisted events in the session at the time of resume.
[JsonPropertyName("eventCount")]
public required double EventCount { get; set; }
+ /// Model currently selected at resume time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("selectedModel")]
+ public string? SelectedModel { get; set; }
+
+ /// Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh").
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("reasoningEffort")]
+ public string? ReasoningEffort { get; set; }
+
+ /// Updated working directory and git context at resume time.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("context")]
public SessionResumeDataContext? Context { get; set; }
+
+ /// Whether the session was already in use by another client at resume time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("alreadyInUse")]
+ public bool? AlreadyInUse { get; set; }
}
+/// Error details for timeline display including message and optional diagnostic information.
public partial class SessionErrorData
{
+ /// Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "query").
[JsonPropertyName("errorType")]
public required string ErrorType { get; set; }
+ /// Human-readable error message.
[JsonPropertyName("message")]
public required string Message { get; set; }
+ /// Error stack trace, when available.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("stack")]
public string? Stack { get; set; }
+ /// HTTP status code from the upstream request, if applicable.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("statusCode")]
public double? StatusCode { get; set; }
+ /// GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("providerCallId")]
public string? ProviderCallId { get; set; }
+
+ /// Optional URL associated with this error that the user can open in a browser.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
+/// Payload indicating the agent is idle; includes any background tasks still in flight.
public partial class SessionIdleData
{
+ /// Background tasks still running when the agent became idle.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("backgroundTasks")]
public SessionIdleDataBackgroundTasks? BackgroundTasks { get; set; }
}
+/// Session title change payload containing the new display title.
public partial class SessionTitleChangedData
{
+ /// The new display title for the session.
[JsonPropertyName("title")]
public required string Title { get; set; }
}
+/// Informational message for timeline display with categorization.
public partial class SessionInfoData
{
+ /// Category of informational message (e.g., "notification", "timing", "context_window", "mcp", "snapshot", "configuration", "authentication", "model").
[JsonPropertyName("infoType")]
public required string InfoType { get; set; }
+ /// Human-readable informational message for display in the timeline.
[JsonPropertyName("message")]
public required string Message { get; set; }
+
+ /// Optional URL associated with this message that the user can open in a browser.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
+/// Warning message for timeline display with categorization.
public partial class SessionWarningData
{
+ /// Category of warning (e.g., "subscription", "policy", "mcp").
[JsonPropertyName("warningType")]
public required string WarningType { get; set; }
+ /// Human-readable warning message for display in the timeline.
[JsonPropertyName("message")]
public required string Message { get; set; }
+
+ /// Optional URL associated with this warning that the user can open in a browser.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
+/// Model change details including previous and new model identifiers.
public partial class SessionModelChangeData
{
+ /// Model that was previously selected, if any.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("previousModel")]
public string? PreviousModel { get; set; }
+ /// Newly selected model identifier.
[JsonPropertyName("newModel")]
public required string NewModel { get; set; }
+
+ /// Reasoning effort level before the model change, if applicable.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("previousReasoningEffort")]
+ public string? PreviousReasoningEffort { get; set; }
+
+ /// Reasoning effort level after the model change, if applicable.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("reasoningEffort")]
+ public string? ReasoningEffort { get; set; }
}
+/// Agent mode change details including previous and new modes.
public partial class SessionModeChangedData
{
+ /// Agent mode before the change (e.g., "interactive", "plan", "autopilot").
[JsonPropertyName("previousMode")]
public required string PreviousMode { get; set; }
+ /// Agent mode after the change (e.g., "interactive", "plan", "autopilot").
[JsonPropertyName("newMode")]
public required string NewMode { get; set; }
}
+/// Plan file operation details indicating what changed.
public partial class SessionPlanChangedData
{
+ /// The type of operation performed on the plan file.
[JsonPropertyName("operation")]
public required SessionPlanChangedDataOperation Operation { get; set; }
}
+/// Workspace file change details including path and operation type.
public partial class SessionWorkspaceFileChangedData
{
+ /// Relative path within the session workspace files directory.
[JsonPropertyName("path")]
public required string Path { get; set; }
+ /// Whether the file was newly created or updated.
[JsonPropertyName("operation")]
public required SessionWorkspaceFileChangedDataOperation Operation { get; set; }
}
+/// Session handoff metadata including source, context, and repository information.
public partial class SessionHandoffData
{
+ /// ISO 8601 timestamp when the handoff occurred.
[JsonPropertyName("handoffTime")]
public required DateTimeOffset HandoffTime { get; set; }
+ /// Origin type of the session being handed off.
[JsonPropertyName("sourceType")]
public required SessionHandoffDataSourceType SourceType { get; set; }
+ /// Repository context for the handed-off session.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("repository")]
public SessionHandoffDataRepository? Repository { get; set; }
+ /// Additional context information for the handoff.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("context")]
public string? Context { get; set; }
+ /// Summary of the work done in the source session.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("summary")]
public string? Summary { get; set; }
+ /// Session ID of the remote session being handed off.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("remoteSessionId")]
public string? RemoteSessionId { get; set; }
+
+ /// GitHub host URL for the source session (e.g., https://github.com or https://tenant.ghe.com).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("host")]
+ public string? Host { get; set; }
}
+/// Conversation truncation statistics including token counts and removed content metrics.
public partial class SessionTruncationData
{
+ /// Maximum token count for the model's context window.
[JsonPropertyName("tokenLimit")]
public required double TokenLimit { get; set; }
+ /// Total tokens in conversation messages before truncation.
[JsonPropertyName("preTruncationTokensInMessages")]
public required double PreTruncationTokensInMessages { get; set; }
+ /// Number of conversation messages before truncation.
[JsonPropertyName("preTruncationMessagesLength")]
public required double PreTruncationMessagesLength { get; set; }
+ /// Total tokens in conversation messages after truncation.
[JsonPropertyName("postTruncationTokensInMessages")]
public required double PostTruncationTokensInMessages { get; set; }
+ /// Number of conversation messages after truncation.
[JsonPropertyName("postTruncationMessagesLength")]
public required double PostTruncationMessagesLength { get; set; }
+ /// Number of tokens removed by truncation.
[JsonPropertyName("tokensRemovedDuringTruncation")]
public required double TokensRemovedDuringTruncation { get; set; }
+ /// Number of messages removed by truncation.
[JsonPropertyName("messagesRemovedDuringTruncation")]
public required double MessagesRemovedDuringTruncation { get; set; }
+ /// Identifier of the component that performed truncation (e.g., "BasicTruncator").
[JsonPropertyName("performedBy")]
public required string PerformedBy { get; set; }
}
+/// Session rewind details including target event and count of removed events.
public partial class SessionSnapshotRewindData
{
+ /// Event ID that was rewound to; all events after this one were removed.
[JsonPropertyName("upToEventId")]
public required string UpToEventId { get; set; }
+ /// Number of events that were removed by the rewind.
[JsonPropertyName("eventsRemoved")]
public required double EventsRemoved { get; set; }
}
+/// Session termination metrics including usage statistics, code changes, and shutdown reason.
public partial class SessionShutdownData
{
+ /// Whether the session ended normally ("routine") or due to a crash/fatal error ("error").
[JsonPropertyName("shutdownType")]
public required SessionShutdownDataShutdownType ShutdownType { get; set; }
+ /// Error description when shutdownType is "error".
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("errorReason")]
public string? ErrorReason { get; set; }
+ /// Total number of premium API requests used during the session.
[JsonPropertyName("totalPremiumRequests")]
public required double TotalPremiumRequests { get; set; }
+ /// Cumulative time spent in API calls during the session, in milliseconds.
[JsonPropertyName("totalApiDurationMs")]
public required double TotalApiDurationMs { get; set; }
+ /// Unix timestamp (milliseconds) when the session started.
[JsonPropertyName("sessionStartTime")]
public required double SessionStartTime { get; set; }
+ /// Aggregate code change metrics for the session.
[JsonPropertyName("codeChanges")]
public required SessionShutdownDataCodeChanges CodeChanges { get; set; }
+ /// Per-model usage breakdown, keyed by model identifier.
[JsonPropertyName("modelMetrics")]
public required Dictionary ModelMetrics { get; set; }
+ /// Model that was selected at the time of shutdown.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("currentModel")]
public string? CurrentModel { get; set; }
+
+ /// Total tokens in context window at shutdown.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("currentTokens")]
+ public double? CurrentTokens { get; set; }
+
+ /// System message token count at shutdown.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("systemTokens")]
+ public double? SystemTokens { get; set; }
+
+ /// Non-system message token count at shutdown.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("conversationTokens")]
+ public double? ConversationTokens { get; set; }
+
+ /// Tool definitions token count at shutdown.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolDefinitionsTokens")]
+ public double? ToolDefinitionsTokens { get; set; }
}
+/// Updated working directory and git context after the change.
public partial class SessionContextChangedData
{
+ /// Current working directory path.
[JsonPropertyName("cwd")]
public required string Cwd { get; set; }
+ /// Root directory of the git repository, resolved via git rev-parse.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("gitRoot")]
public string? GitRoot { get; set; }
+ /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("repository")]
public string? Repository { get; set; }
+ /// Hosting platform type of the repository (github or ado).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("hostType")]
+ public SessionStartDataContextHostType? HostType { get; set; }
+
+ /// Current git branch name.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("branch")]
public string? Branch { get; set; }
+
+ /// Head commit of current git branch at session start time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("headCommit")]
+ public string? HeadCommit { get; set; }
+
+ /// Base commit of current git branch at session start time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("baseCommit")]
+ public string? BaseCommit { get; set; }
}
+/// Current context window usage statistics including token and message counts.
public partial class SessionUsageInfoData
{
+ /// Maximum token count for the model's context window.
[JsonPropertyName("tokenLimit")]
public required double TokenLimit { get; set; }
+ /// Current number of tokens in the context window.
[JsonPropertyName("currentTokens")]
public required double CurrentTokens { get; set; }
+ /// Current number of messages in the conversation.
[JsonPropertyName("messagesLength")]
public required double MessagesLength { get; set; }
+
+ /// Token count from system message(s).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("systemTokens")]
+ public double? SystemTokens { get; set; }
+
+ /// Token count from non-system messages (user, assistant, tool).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("conversationTokens")]
+ public double? ConversationTokens { get; set; }
+
+ /// Token count from tool definitions.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolDefinitionsTokens")]
+ public double? ToolDefinitionsTokens { get; set; }
+
+ /// Whether this is the first usage_info event emitted in this session.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("isInitial")]
+ public bool? IsInitial { get; set; }
}
+/// Context window breakdown at the start of LLM-powered conversation compaction.
public partial class SessionCompactionStartData
{
+ /// Token count from system message(s) at compaction start.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("systemTokens")]
+ public double? SystemTokens { get; set; }
+
+ /// Token count from non-system messages (user, assistant, tool) at compaction start.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("conversationTokens")]
+ public double? ConversationTokens { get; set; }
+
+ /// Token count from tool definitions at compaction start.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolDefinitionsTokens")]
+ public double? ToolDefinitionsTokens { get; set; }
}
+/// Conversation compaction results including success status, metrics, and optional error details.
public partial class SessionCompactionCompleteData
{
+ /// Whether compaction completed successfully.
[JsonPropertyName("success")]
public required bool Success { get; set; }
+ /// Error message if compaction failed.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("error")]
public string? Error { get; set; }
+ /// Total tokens in conversation before compaction.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("preCompactionTokens")]
public double? PreCompactionTokens { get; set; }
+ /// Total tokens in conversation after compaction.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("postCompactionTokens")]
public double? PostCompactionTokens { get; set; }
+ /// Number of messages before compaction.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("preCompactionMessagesLength")]
public double? PreCompactionMessagesLength { get; set; }
+ /// Number of messages removed during compaction.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("messagesRemoved")]
public double? MessagesRemoved { get; set; }
+ /// Number of tokens removed during compaction.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("tokensRemoved")]
public double? TokensRemoved { get; set; }
+ /// LLM-generated summary of the compacted conversation history.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("summaryContent")]
public string? SummaryContent { get; set; }
+ /// Checkpoint snapshot number created for recovery.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("checkpointNumber")]
public double? CheckpointNumber { get; set; }
+ /// File path where the checkpoint was stored.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("checkpointPath")]
public string? CheckpointPath { get; set; }
+ /// Token usage breakdown for the compaction LLM call.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("compactionTokensUsed")]
public SessionCompactionCompleteDataCompactionTokensUsed? CompactionTokensUsed { get; set; }
+ /// GitHub request tracing ID (x-github-request-id header) for the compaction LLM call.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("requestId")]
public string? RequestId { get; set; }
+
+ /// Token count from system message(s) after compaction.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("systemTokens")]
+ public double? SystemTokens { get; set; }
+
+ /// Token count from non-system messages (user, assistant, tool) after compaction.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("conversationTokens")]
+ public double? ConversationTokens { get; set; }
+
+ /// Token count from tool definitions after compaction.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolDefinitionsTokens")]
+ public double? ToolDefinitionsTokens { get; set; }
}
+/// Task completion notification with summary from the agent.
public partial class SessionTaskCompleteData
{
+ /// Summary of the completed task, provided by the agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("summary")]
public string? Summary { get; set; }
+
+ /// Whether the tool call succeeded. False when validation failed (e.g., invalid arguments).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("success")]
+ public bool? Success { get; set; }
}
+/// Event payload for .
public partial class UserMessageData
{
+ /// The user's message text as displayed in the timeline.
[JsonPropertyName("content")]
public required string Content { get; set; }
+ /// Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("transformedContent")]
public string? TransformedContent { get; set; }
+ /// Files, selections, or GitHub references attached to the message.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("attachments")]
public UserMessageDataAttachmentsItem[]? Attachments { get; set; }
+ /// Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("source")]
public string? Source { get; set; }
+ /// The agent mode that was active when this message was sent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("agentMode")]
public UserMessageDataAgentMode? AgentMode { get; set; }
+ /// CAPI interaction ID for correlating this user message with its turn.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("interactionId")]
public string? InteractionId { get; set; }
}
+/// Empty payload; the event signals that the pending message queue has changed.
public partial class PendingMessagesModifiedData
{
}
+/// Turn initialization metadata including identifier and interaction tracking.
public partial class AssistantTurnStartData
{
+ /// Identifier for this turn within the agentic loop, typically a stringified turn number.
[JsonPropertyName("turnId")]
public required string TurnId { get; set; }
+ /// CAPI interaction ID for correlating this turn with upstream telemetry.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("interactionId")]
public string? InteractionId { get; set; }
}
+/// Agent intent description for current activity or plan.
public partial class AssistantIntentData
{
+ /// Short description of what the agent is currently doing or planning to do.
[JsonPropertyName("intent")]
public required string Intent { get; set; }
}
+/// Assistant reasoning content for timeline display with complete thinking text.
public partial class AssistantReasoningData
{
+ /// Unique identifier for this reasoning block.
[JsonPropertyName("reasoningId")]
public required string ReasoningId { get; set; }
+ /// The complete extended thinking text from the model.
[JsonPropertyName("content")]
public required string Content { get; set; }
}
+/// Streaming reasoning delta for incremental extended thinking updates.
public partial class AssistantReasoningDeltaData
{
+ /// Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning event.
[JsonPropertyName("reasoningId")]
public required string ReasoningId { get; set; }
+ /// Incremental text chunk to append to the reasoning content.
[JsonPropertyName("deltaContent")]
public required string DeltaContent { get; set; }
}
+/// Streaming response progress with cumulative byte count.
public partial class AssistantStreamingDeltaData
{
+ /// Cumulative total bytes received from the streaming response so far.
[JsonPropertyName("totalResponseSizeBytes")]
public required double TotalResponseSizeBytes { get; set; }
}
+/// Assistant response containing text content, optional tool requests, and interaction metadata.
public partial class AssistantMessageData
{
+ /// Unique identifier for this assistant message.
[JsonPropertyName("messageId")]
public required string MessageId { get; set; }
+ /// The assistant's text response content.
[JsonPropertyName("content")]
public required string Content { get; set; }
+ /// Tool invocations requested by the assistant in this message.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("toolRequests")]
public AssistantMessageDataToolRequestsItem[]? ToolRequests { get; set; }
+ /// Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("reasoningOpaque")]
public string? ReasoningOpaque { get; set; }
+ /// Readable reasoning text from the model's extended thinking.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("reasoningText")]
public string? ReasoningText { get; set; }
+ /// Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("encryptedContent")]
public string? EncryptedContent { get; set; }
+ /// Generation phase for phased-output models (e.g., thinking vs. response phases).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("phase")]
public string? Phase { get; set; }
+ /// Actual output token count from the API response (completion_tokens), used for accurate token accounting.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("outputTokens")]
public double? OutputTokens { get; set; }
+ /// CAPI interaction ID for correlating this message with upstream telemetry.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("interactionId")]
public string? InteractionId { get; set; }
+ /// Tool call ID of the parent tool invocation when this event originates from a sub-agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("parentToolCallId")]
public string? ParentToolCallId { get; set; }
}
+/// Streaming assistant message delta for incremental response updates.
public partial class AssistantMessageDeltaData
{
+ /// Message ID this delta belongs to, matching the corresponding assistant.message event.
[JsonPropertyName("messageId")]
public required string MessageId { get; set; }
+ /// Incremental text chunk to append to the message content.
[JsonPropertyName("deltaContent")]
public required string DeltaContent { get; set; }
+ /// Tool call ID of the parent tool invocation when this event originates from a sub-agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("parentToolCallId")]
public string? ParentToolCallId { get; set; }
}
+/// Turn completion metadata including the turn identifier.
public partial class AssistantTurnEndData
{
+ /// Identifier of the turn that has ended, matching the corresponding assistant.turn_start event.
[JsonPropertyName("turnId")]
public required string TurnId { get; set; }
}
+/// LLM API call usage metrics including tokens, costs, quotas, and billing information.
public partial class AssistantUsageData
{
+ /// Model identifier used for this API call.
[JsonPropertyName("model")]
public required string Model { get; set; }
+ /// Number of input tokens consumed.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("inputTokens")]
public double? InputTokens { get; set; }
+ /// Number of output tokens produced.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("outputTokens")]
public double? OutputTokens { get; set; }
+ /// Number of tokens read from prompt cache.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("cacheReadTokens")]
public double? CacheReadTokens { get; set; }
+ /// Number of tokens written to prompt cache.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("cacheWriteTokens")]
public double? CacheWriteTokens { get; set; }
+ /// Model multiplier cost for billing purposes.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("cost")]
public double? Cost { get; set; }
+ /// Duration of the API call in milliseconds.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("duration")]
public double? Duration { get; set; }
+ /// What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("initiator")]
public string? Initiator { get; set; }
+ /// Completion ID from the model provider (e.g., chatcmpl-abc123).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("apiCallId")]
public string? ApiCallId { get; set; }
+ /// GitHub request tracing ID (x-github-request-id header) for server-side log correlation.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("providerCallId")]
public string? ProviderCallId { get; set; }
+ /// Parent tool call ID when this usage originates from a sub-agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("parentToolCallId")]
public string? ParentToolCallId { get; set; }
+ /// Per-quota resource usage snapshots, keyed by quota identifier.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("quotaSnapshots")]
public Dictionary? QuotaSnapshots { get; set; }
+ /// Per-request cost and usage data from the CAPI copilot_usage response field.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("copilotUsage")]
public AssistantUsageDataCopilotUsage? CopilotUsage { get; set; }
+
+ /// Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh").
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("reasoningEffort")]
+ public string? ReasoningEffort { get; set; }
}
+/// Turn abort information including the reason for termination.
public partial class AbortData
{
+ /// Reason the current turn was aborted (e.g., "user initiated").
[JsonPropertyName("reason")]
public required string Reason { get; set; }
}
+/// User-initiated tool invocation request with tool name and arguments.
public partial class ToolUserRequestedData
{
+ /// Unique identifier for this tool call.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Name of the tool the user wants to invoke.
[JsonPropertyName("toolName")]
public required string ToolName { get; set; }
+ /// Arguments for the tool invocation.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("arguments")]
public object? Arguments { get; set; }
}
+/// Tool execution startup details including MCP server information when applicable.
public partial class ToolExecutionStartData
{
+ /// Unique identifier for this tool call.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Name of the tool being executed.
[JsonPropertyName("toolName")]
public required string ToolName { get; set; }
+ /// Arguments passed to the tool.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("arguments")]
public object? Arguments { get; set; }
+ /// Name of the MCP server hosting this tool, when the tool is an MCP tool.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("mcpServerName")]
public string? McpServerName { get; set; }
+ /// Original tool name on the MCP server, when the tool is an MCP tool.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("mcpToolName")]
public string? McpToolName { get; set; }
+ /// Tool call ID of the parent tool invocation when this event originates from a sub-agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("parentToolCallId")]
public string? ParentToolCallId { get; set; }
}
+/// Streaming tool execution output for incremental result display.
public partial class ToolExecutionPartialResultData
{
+ /// Tool call ID this partial result belongs to.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Incremental output chunk from the running tool.
[JsonPropertyName("partialOutput")]
public required string PartialOutput { get; set; }
}
+/// Tool execution progress notification with status message.
public partial class ToolExecutionProgressData
{
+ /// Tool call ID this progress notification belongs to.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Human-readable progress status message (e.g., from an MCP server).
[JsonPropertyName("progressMessage")]
public required string ProgressMessage { get; set; }
}
+/// Tool execution completion results including success status, detailed output, and error information.
public partial class ToolExecutionCompleteData
{
+ /// Unique identifier for the completed tool call.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Whether the tool execution completed successfully.
[JsonPropertyName("success")]
public required bool Success { get; set; }
+ /// Model identifier that generated this tool call.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("model")]
public string? Model { get; set; }
+ /// CAPI interaction ID for correlating this tool execution with upstream telemetry.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("interactionId")]
public string? InteractionId { get; set; }
+ /// Whether this tool call was explicitly requested by the user rather than the assistant.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("isUserRequested")]
public bool? IsUserRequested { get; set; }
+ /// Tool execution result on success.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("result")]
public ToolExecutionCompleteDataResult? Result { get; set; }
+ /// Error details when the tool execution failed.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("error")]
public ToolExecutionCompleteDataError? Error { get; set; }
+ /// Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("toolTelemetry")]
public Dictionary? ToolTelemetry { get; set; }
+ /// Tool call ID of the parent tool invocation when this event originates from a sub-agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("parentToolCallId")]
public string? ParentToolCallId { get; set; }
}
+/// Skill invocation details including content, allowed tools, and plugin metadata.
public partial class SkillInvokedData
{
+ /// Name of the invoked skill.
[JsonPropertyName("name")]
public required string Name { get; set; }
+ /// File path to the SKILL.md definition.
[JsonPropertyName("path")]
public required string Path { get; set; }
+ /// Full content of the skill file, injected into the conversation for the model.
[JsonPropertyName("content")]
public required string Content { get; set; }
+ /// Tool names that should be auto-approved when this skill is active.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("allowedTools")]
public string[]? AllowedTools { get; set; }
+ /// Name of the plugin this skill originated from, when applicable.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("pluginName")]
public string? PluginName { get; set; }
+ /// Version of the plugin this skill originated from, when applicable.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("pluginVersion")]
public string? PluginVersion { get; set; }
}
+/// Sub-agent startup details including parent tool call and agent information.
public partial class SubagentStartedData
{
+ /// Tool call ID of the parent tool invocation that spawned this sub-agent.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Internal name of the sub-agent.
[JsonPropertyName("agentName")]
public required string AgentName { get; set; }
+ /// Human-readable display name of the sub-agent.
[JsonPropertyName("agentDisplayName")]
public required string AgentDisplayName { get; set; }
+ /// Description of what the sub-agent does.
[JsonPropertyName("agentDescription")]
public required string AgentDescription { get; set; }
}
+/// Sub-agent completion details for successful execution.
public partial class SubagentCompletedData
{
+ /// Tool call ID of the parent tool invocation that spawned this sub-agent.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Internal name of the sub-agent.
[JsonPropertyName("agentName")]
public required string AgentName { get; set; }
+ /// Human-readable display name of the sub-agent.
[JsonPropertyName("agentDisplayName")]
public required string AgentDisplayName { get; set; }
}
+/// Sub-agent failure details including error message and agent information.
public partial class SubagentFailedData
{
+ /// Tool call ID of the parent tool invocation that spawned this sub-agent.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Internal name of the sub-agent.
[JsonPropertyName("agentName")]
public required string AgentName { get; set; }
+ /// Human-readable display name of the sub-agent.
[JsonPropertyName("agentDisplayName")]
public required string AgentDisplayName { get; set; }
+ /// Error message describing why the sub-agent failed.
[JsonPropertyName("error")]
public required string Error { get; set; }
}
+/// Custom agent selection details including name and available tools.
public partial class SubagentSelectedData
{
+ /// Internal name of the selected custom agent.
[JsonPropertyName("agentName")]
public required string AgentName { get; set; }
+ /// Human-readable display name of the selected custom agent.
[JsonPropertyName("agentDisplayName")]
public required string AgentDisplayName { get; set; }
+ /// List of tool names available to this agent, or null for all tools.
[JsonPropertyName("tools")]
public string[]? Tools { get; set; }
}
+/// Empty payload; the event signals that the custom agent was deselected, returning to the default agent.
public partial class SubagentDeselectedData
{
}
+/// Hook invocation start details including type and input data.
public partial class HookStartData
{
+ /// Unique identifier for this hook invocation.
[JsonPropertyName("hookInvocationId")]
public required string HookInvocationId { get; set; }
+ /// Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart").
[JsonPropertyName("hookType")]
public required string HookType { get; set; }
+ /// Input data passed to the hook.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("input")]
public object? Input { get; set; }
}
+/// Hook invocation completion details including output, success status, and error information.
public partial class HookEndData
{
+ /// Identifier matching the corresponding hook.start event.
[JsonPropertyName("hookInvocationId")]
public required string HookInvocationId { get; set; }
+ /// Type of hook that was invoked (e.g., "preToolUse", "postToolUse", "sessionStart").
[JsonPropertyName("hookType")]
public required string HookType { get; set; }
+ /// Output data produced by the hook.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("output")]
public object? Output { get; set; }
+ /// Whether the hook completed successfully.
[JsonPropertyName("success")]
public required bool Success { get; set; }
+ /// Error details when the hook failed.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("error")]
public HookEndDataError? Error { get; set; }
}
+/// System or developer message content with role and optional template metadata.
public partial class SystemMessageData
{
+ /// The system or developer prompt text.
[JsonPropertyName("content")]
public required string Content { get; set; }
+ /// Message role: "system" for system prompts, "developer" for developer-injected instructions.
[JsonPropertyName("role")]
public required SystemMessageDataRole Role { get; set; }
+ /// Optional name identifier for the message source.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("name")]
public string? Name { get; set; }
+ /// Metadata about the prompt template and its construction.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("metadata")]
public SystemMessageDataMetadata? Metadata { get; set; }
}
-public partial class PermissionRequestedData
+/// System-generated notification for runtime events like background task completion.
+public partial class SystemNotificationData
{
- [JsonPropertyName("requestId")]
- public required string RequestId { get; set; }
+ /// The notification text, typically wrapped in <system_notification> XML tags.
+ [JsonPropertyName("content")]
+ public required string Content { get; set; }
- [JsonPropertyName("permissionRequest")]
- public required object PermissionRequest { get; set; }
+ /// Structured metadata identifying what triggered this notification.
+ [JsonPropertyName("kind")]
+ public required SystemNotificationDataKind Kind { get; set; }
+}
+
+/// Permission request notification requiring client approval with request details.
+public partial class PermissionRequestedData
+{
+ /// Unique identifier for this permission request; used to respond via session.respondToPermission().
+ [JsonPropertyName("requestId")]
+ public required string RequestId { get; set; }
+
+ /// Details of the permission being requested.
+ [JsonPropertyName("permissionRequest")]
+ public required PermissionRequest PermissionRequest { get; set; }
}
+/// Permission request completion notification signaling UI dismissal.
public partial class PermissionCompletedData
{
+ /// Request ID of the resolved permission request; clients should dismiss any UI for this request.
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
+ /// The result of the permission request.
[JsonPropertyName("result")]
public required PermissionCompletedDataResult Result { get; set; }
}
+/// User input request notification with question and optional predefined choices.
public partial class UserInputRequestedData
{
+ /// Unique identifier for this input request; used to respond via session.respondToUserInput().
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
+ /// The question or prompt to present to the user.
[JsonPropertyName("question")]
public required string Question { get; set; }
+ /// Predefined choices for the user to select from, if applicable.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("choices")]
public string[]? Choices { get; set; }
+ /// Whether the user can provide a free-form text response in addition to predefined choices.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("allowFreeform")]
public bool? AllowFreeform { get; set; }
+
+ /// The LLM-assigned tool call ID that triggered this request; used by remote UIs to correlate responses.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
}
+/// User input request completion notification signaling UI dismissal.
public partial class UserInputCompletedData
{
+ /// Request ID of the resolved user input request; clients should dismiss any UI for this request.
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
}
+/// Elicitation request; may be form-based (structured input) or URL-based (browser redirect).
public partial class ElicitationRequestedData
{
+ /// Unique identifier for this elicitation request; used to respond via session.respondToElicitation().
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
+ /// Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id for remote UIs.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// The source that initiated the request (MCP server name, or absent for agent-initiated).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("elicitationSource")]
+ public string? ElicitationSource { get; set; }
+
+ /// Message describing what information is needed from the user.
[JsonPropertyName("message")]
public required string Message { get; set; }
+ /// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("mode")]
- public string? Mode { get; set; }
+ public ElicitationRequestedDataMode? Mode { get; set; }
+ /// JSON Schema describing the form fields to present to the user (form mode only).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("requestedSchema")]
- public required ElicitationRequestedDataRequestedSchema RequestedSchema { get; set; }
+ public ElicitationRequestedDataRequestedSchema? RequestedSchema { get; set; }
+
+ /// URL to open in the user's browser (url mode only).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
+/// Elicitation request completion notification signaling UI dismissal.
public partial class ElicitationCompletedData
{
+ /// Request ID of the resolved elicitation request; clients should dismiss any UI for this request.
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
}
+/// OAuth authentication request for an MCP server.
+public partial class McpOauthRequiredData
+{
+ /// Unique identifier for this OAuth request; used to respond via session.respondToMcpOAuth().
+ [JsonPropertyName("requestId")]
+ public required string RequestId { get; set; }
+
+ /// Display name of the MCP server that requires OAuth.
+ [JsonPropertyName("serverName")]
+ public required string ServerName { get; set; }
+
+ /// URL of the MCP server that requires OAuth.
+ [JsonPropertyName("serverUrl")]
+ public required string ServerUrl { get; set; }
+
+ /// Static OAuth client configuration, if the server specifies one.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("staticClientConfig")]
+ public McpOauthRequiredDataStaticClientConfig? StaticClientConfig { get; set; }
+}
+
+/// MCP OAuth request completion notification.
+public partial class McpOauthCompletedData
+{
+ /// Request ID of the resolved OAuth request.
+ [JsonPropertyName("requestId")]
+ public required string RequestId { get; set; }
+}
+
+/// External tool invocation request for client-side tool execution.
public partial class ExternalToolRequestedData
{
+ /// Unique identifier for this request; used to respond via session.respondToExternalTool().
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
+ /// Session ID that this external tool request belongs to.
[JsonPropertyName("sessionId")]
public required string SessionId { get; set; }
+ /// Tool call ID assigned to this external tool invocation.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Name of the external tool to invoke.
[JsonPropertyName("toolName")]
public required string ToolName { get; set; }
+ /// Arguments to pass to the external tool.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("arguments")]
public object? Arguments { get; set; }
+
+ /// W3C Trace Context traceparent header for the execute_tool span.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("traceparent")]
+ public string? Traceparent { get; set; }
+
+ /// W3C Trace Context tracestate header for the execute_tool span.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("tracestate")]
+ public string? Tracestate { get; set; }
}
+/// External tool completion notification signaling UI dismissal.
public partial class ExternalToolCompletedData
{
+ /// Request ID of the resolved external tool request; clients should dismiss any UI for this request.
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
}
+/// Queued slash command dispatch request for client execution.
public partial class CommandQueuedData
{
+ /// Unique identifier for this request; used to respond via session.respondToQueuedCommand().
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
+ /// The slash command text to be executed (e.g., /help, /clear).
[JsonPropertyName("command")]
public required string Command { get; set; }
}
+/// Registered command dispatch request routed to the owning client.
+public partial class CommandExecuteData
+{
+ /// Unique identifier; used to respond via session.commands.handlePendingCommand().
+ [JsonPropertyName("requestId")]
+ public required string RequestId { get; set; }
+
+ /// The full command text (e.g., /deploy production).
+ [JsonPropertyName("command")]
+ public required string Command { get; set; }
+
+ /// Command name without leading /.
+ [JsonPropertyName("commandName")]
+ public required string CommandName { get; set; }
+
+ /// Raw argument string after the command name.
+ [JsonPropertyName("args")]
+ public required string Args { get; set; }
+}
+
+/// Queued command completion notification signaling UI dismissal.
public partial class CommandCompletedData
{
+ /// Request ID of the resolved command request; clients should dismiss any UI for this request.
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
}
+/// SDK command registration change notification.
+public partial class CommandsChangedData
+{
+ /// Current list of registered SDK commands.
+ [JsonPropertyName("commands")]
+ public required CommandsChangedDataCommandsItem[] Commands { get; set; }
+}
+
+/// Plan approval request with plan content and available user actions.
public partial class ExitPlanModeRequestedData
{
+ /// Unique identifier for this request; used to respond via session.respondToExitPlanMode().
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
+ /// Summary of the plan that was created.
[JsonPropertyName("summary")]
public required string Summary { get; set; }
+ /// Full content of the plan file.
[JsonPropertyName("planContent")]
public required string PlanContent { get; set; }
+ /// Available actions the user can take (e.g., approve, edit, reject).
[JsonPropertyName("actions")]
public required string[] Actions { get; set; }
+ /// The recommended action for the user to take.
[JsonPropertyName("recommendedAction")]
public required string RecommendedAction { get; set; }
}
+/// Plan mode exit completion notification signaling UI dismissal.
public partial class ExitPlanModeCompletedData
{
+ /// Request ID of the resolved exit plan mode request; clients should dismiss any UI for this request.
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
}
+/// Event payload for .
+public partial class SessionToolsUpdatedData
+{
+ /// Gets or sets the model value.
+ [JsonPropertyName("model")]
+ public required string Model { get; set; }
+}
+
+/// Event payload for .
+public partial class SessionBackgroundTasksChangedData
+{
+}
+
+/// Event payload for .
+public partial class SessionSkillsLoadedData
+{
+ /// Array of resolved skill metadata.
+ [JsonPropertyName("skills")]
+ public required SessionSkillsLoadedDataSkillsItem[] Skills { get; set; }
+}
+
+/// Event payload for .
+public partial class SessionMcpServersLoadedData
+{
+ /// Array of MCP server status summaries.
+ [JsonPropertyName("servers")]
+ public required SessionMcpServersLoadedDataServersItem[] Servers { get; set; }
+}
+
+/// Event payload for .
+public partial class SessionMcpServerStatusChangedData
+{
+ /// Name of the MCP server whose status changed.
+ [JsonPropertyName("serverName")]
+ public required string ServerName { get; set; }
+
+ /// New connection status: connected, failed, pending, disabled, or not_configured.
+ [JsonPropertyName("status")]
+ public required SessionMcpServersLoadedDataServersItemStatus Status { get; set; }
+}
+
+/// Event payload for .
+public partial class SessionExtensionsLoadedData
+{
+ /// Array of discovered extensions and their status.
+ [JsonPropertyName("extensions")]
+ public required SessionExtensionsLoadedDataExtensionsItem[] Extensions { get; set; }
+}
+
+/// Working directory and git context at session start.
+/// Nested data type for SessionStartDataContext.
public partial class SessionStartDataContext
{
+ /// Current working directory path.
[JsonPropertyName("cwd")]
public required string Cwd { get; set; }
+ /// Root directory of the git repository, resolved via git rev-parse.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("gitRoot")]
public string? GitRoot { get; set; }
+ /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("repository")]
public string? Repository { get; set; }
+ /// Hosting platform type of the repository (github or ado).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("hostType")]
+ public SessionStartDataContextHostType? HostType { get; set; }
+
+ /// Current git branch name.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("branch")]
public string? Branch { get; set; }
+
+ /// Head commit of current git branch at session start time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("headCommit")]
+ public string? HeadCommit { get; set; }
+
+ /// Base commit of current git branch at session start time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("baseCommit")]
+ public string? BaseCommit { get; set; }
}
+/// Updated working directory and git context at resume time.
+/// Nested data type for SessionResumeDataContext.
public partial class SessionResumeDataContext
{
+ /// Current working directory path.
[JsonPropertyName("cwd")]
public required string Cwd { get; set; }
+ /// Root directory of the git repository, resolved via git rev-parse.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("gitRoot")]
public string? GitRoot { get; set; }
+ /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("repository")]
public string? Repository { get; set; }
+ /// Hosting platform type of the repository (github or ado).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("hostType")]
+ public SessionStartDataContextHostType? HostType { get; set; }
+
+ /// Current git branch name.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("branch")]
public string? Branch { get; set; }
+
+ /// Head commit of current git branch at session start time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("headCommit")]
+ public string? HeadCommit { get; set; }
+
+ /// Base commit of current git branch at session start time.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("baseCommit")]
+ public string? BaseCommit { get; set; }
}
+/// A background agent task.
+/// Nested data type for SessionIdleDataBackgroundTasksAgentsItem.
public partial class SessionIdleDataBackgroundTasksAgentsItem
{
+ /// Unique identifier of the background agent.
[JsonPropertyName("agentId")]
public required string AgentId { get; set; }
+ /// Type of the background agent.
[JsonPropertyName("agentType")]
public required string AgentType { get; set; }
+ /// Human-readable description of the agent task.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("description")]
public string? Description { get; set; }
}
+/// A background shell command.
+/// Nested data type for SessionIdleDataBackgroundTasksShellsItem.
public partial class SessionIdleDataBackgroundTasksShellsItem
{
+ /// Unique identifier of the background shell.
[JsonPropertyName("shellId")]
public required string ShellId { get; set; }
+ /// Human-readable description of the shell command.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("description")]
public string? Description { get; set; }
}
+/// Background tasks still running when the agent became idle.
+/// Nested data type for SessionIdleDataBackgroundTasks.
public partial class SessionIdleDataBackgroundTasks
{
+ /// Currently running background agents.
[JsonPropertyName("agents")]
public required SessionIdleDataBackgroundTasksAgentsItem[] Agents { get; set; }
+ /// Currently running background shell commands.
[JsonPropertyName("shells")]
public required SessionIdleDataBackgroundTasksShellsItem[] Shells { get; set; }
}
+/// Repository context for the handed-off session.
+/// Nested data type for SessionHandoffDataRepository.
public partial class SessionHandoffDataRepository
{
+ /// Repository owner (user or organization).
[JsonPropertyName("owner")]
public required string Owner { get; set; }
+ /// Repository name.
[JsonPropertyName("name")]
public required string Name { get; set; }
+ /// Git branch name, if applicable.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("branch")]
public string? Branch { get; set; }
}
+/// Aggregate code change metrics for the session.
+/// Nested data type for SessionShutdownDataCodeChanges.
public partial class SessionShutdownDataCodeChanges
{
+ /// Total number of lines added during the session.
[JsonPropertyName("linesAdded")]
public required double LinesAdded { get; set; }
+ /// Total number of lines removed during the session.
[JsonPropertyName("linesRemoved")]
public required double LinesRemoved { get; set; }
+ /// List of file paths that were modified during the session.
[JsonPropertyName("filesModified")]
public required string[] FilesModified { get; set; }
}
+/// Token usage breakdown for the compaction LLM call.
+/// Nested data type for SessionCompactionCompleteDataCompactionTokensUsed.
public partial class SessionCompactionCompleteDataCompactionTokensUsed
{
+ /// Input tokens consumed by the compaction LLM call.
[JsonPropertyName("input")]
public required double Input { get; set; }
+ /// Output tokens produced by the compaction LLM call.
[JsonPropertyName("output")]
public required double Output { get; set; }
+ /// Cached input tokens reused in the compaction LLM call.
[JsonPropertyName("cachedInput")]
public required double CachedInput { get; set; }
}
+/// Optional line range to scope the attachment to a specific section of the file.
+/// Nested data type for UserMessageDataAttachmentsItemFileLineRange.
public partial class UserMessageDataAttachmentsItemFileLineRange
{
+ /// Start line number (1-based).
[JsonPropertyName("start")]
public required double Start { get; set; }
+ /// End line number (1-based, inclusive).
[JsonPropertyName("end")]
public required double End { get; set; }
}
+/// File attachment.
+/// The file variant of .
public partial class UserMessageDataAttachmentsItemFile : UserMessageDataAttachmentsItem
{
+ ///
[JsonIgnore]
public override string Type => "file";
+ /// Absolute file path.
[JsonPropertyName("path")]
public required string Path { get; set; }
+ /// User-facing display name for the attachment.
[JsonPropertyName("displayName")]
public required string DisplayName { get; set; }
+ /// Optional line range to scope the attachment to a specific section of the file.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("lineRange")]
public UserMessageDataAttachmentsItemFileLineRange? LineRange { get; set; }
}
-public partial class UserMessageDataAttachmentsItemDirectoryLineRange
-{
- [JsonPropertyName("start")]
- public required double Start { get; set; }
-
- [JsonPropertyName("end")]
- public required double End { get; set; }
-}
-
+/// Directory attachment.
+/// The directory variant of .
public partial class UserMessageDataAttachmentsItemDirectory : UserMessageDataAttachmentsItem
{
+ ///
[JsonIgnore]
public override string Type => "directory";
+ /// Absolute directory path.
[JsonPropertyName("path")]
public required string Path { get; set; }
+ /// User-facing display name for the attachment.
[JsonPropertyName("displayName")]
public required string DisplayName { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("lineRange")]
- public UserMessageDataAttachmentsItemDirectoryLineRange? LineRange { get; set; }
}
+/// Start position of the selection.
+/// Nested data type for UserMessageDataAttachmentsItemSelectionSelectionStart.
public partial class UserMessageDataAttachmentsItemSelectionSelectionStart
{
+ /// Start line number (0-based).
[JsonPropertyName("line")]
public required double Line { get; set; }
+ /// Start character offset within the line (0-based).
[JsonPropertyName("character")]
public required double Character { get; set; }
}
+/// End position of the selection.
+/// Nested data type for UserMessageDataAttachmentsItemSelectionSelectionEnd.
public partial class UserMessageDataAttachmentsItemSelectionSelectionEnd
{
+ /// End line number (0-based).
[JsonPropertyName("line")]
public required double Line { get; set; }
+ /// End character offset within the line (0-based).
[JsonPropertyName("character")]
public required double Character { get; set; }
}
+/// Position range of the selection within the file.
+/// Nested data type for UserMessageDataAttachmentsItemSelectionSelection.
public partial class UserMessageDataAttachmentsItemSelectionSelection
{
+ /// Start position of the selection.
[JsonPropertyName("start")]
public required UserMessageDataAttachmentsItemSelectionSelectionStart Start { get; set; }
+ /// End position of the selection.
[JsonPropertyName("end")]
public required UserMessageDataAttachmentsItemSelectionSelectionEnd End { get; set; }
}
+/// Code selection attachment from an editor.
+/// The selection variant of .
public partial class UserMessageDataAttachmentsItemSelection : UserMessageDataAttachmentsItem
{
+ ///
[JsonIgnore]
public override string Type => "selection";
+ /// Absolute path to the file containing the selection.
[JsonPropertyName("filePath")]
public required string FilePath { get; set; }
+ /// User-facing display name for the selection.
[JsonPropertyName("displayName")]
public required string DisplayName { get; set; }
+ /// The selected text content.
[JsonPropertyName("text")]
public required string Text { get; set; }
+ /// Position range of the selection within the file.
[JsonPropertyName("selection")]
public required UserMessageDataAttachmentsItemSelectionSelection Selection { get; set; }
}
+/// GitHub issue, pull request, or discussion reference.
+/// The github_reference variant of .
public partial class UserMessageDataAttachmentsItemGithubReference : UserMessageDataAttachmentsItem
{
+ ///
[JsonIgnore]
public override string Type => "github_reference";
+ /// Issue, pull request, or discussion number.
[JsonPropertyName("number")]
public required double Number { get; set; }
+ /// Title of the referenced item.
[JsonPropertyName("title")]
public required string Title { get; set; }
+ /// Type of GitHub reference.
[JsonPropertyName("referenceType")]
public required UserMessageDataAttachmentsItemGithubReferenceReferenceType ReferenceType { get; set; }
+ /// Current state of the referenced item (e.g., open, closed, merged).
[JsonPropertyName("state")]
public required string State { get; set; }
+ /// URL to the referenced item on GitHub.
[JsonPropertyName("url")]
public required string Url { get; set; }
}
+/// Blob attachment with inline base64-encoded data.
+/// The blob variant of .
+public partial class UserMessageDataAttachmentsItemBlob : UserMessageDataAttachmentsItem
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "blob";
+
+ /// Base64-encoded content.
+ [JsonPropertyName("data")]
+ public required string Data { get; set; }
+
+ /// MIME type of the inline data.
+ [JsonPropertyName("mimeType")]
+ public required string MimeType { get; set; }
+
+ /// User-facing display name for the attachment.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("displayName")]
+ public string? DisplayName { get; set; }
+}
+
+/// A user message attachment — a file, directory, code selection, blob, or GitHub reference.
+/// Polymorphic base type discriminated by type.
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "type",
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
@@ -1877,163 +2766,233 @@ public partial class UserMessageDataAttachmentsItemGithubReference : UserMessage
[JsonDerivedType(typeof(UserMessageDataAttachmentsItemDirectory), "directory")]
[JsonDerivedType(typeof(UserMessageDataAttachmentsItemSelection), "selection")]
[JsonDerivedType(typeof(UserMessageDataAttachmentsItemGithubReference), "github_reference")]
+[JsonDerivedType(typeof(UserMessageDataAttachmentsItemBlob), "blob")]
public partial class UserMessageDataAttachmentsItem
{
+ /// The type discriminator.
[JsonPropertyName("type")]
public virtual string Type { get; set; } = string.Empty;
}
+/// A tool invocation request from the assistant.
+/// Nested data type for AssistantMessageDataToolRequestsItem.
public partial class AssistantMessageDataToolRequestsItem
{
+ /// Unique identifier for this tool call.
[JsonPropertyName("toolCallId")]
public required string ToolCallId { get; set; }
+ /// Name of the tool being invoked.
[JsonPropertyName("name")]
public required string Name { get; set; }
+ /// Arguments to pass to the tool, format depends on the tool.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("arguments")]
public object? Arguments { get; set; }
+ /// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("type")]
public AssistantMessageDataToolRequestsItemType? Type { get; set; }
+
+ /// Human-readable display title for the tool.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolTitle")]
+ public string? ToolTitle { get; set; }
+
+ /// Resolved intention summary describing what this specific call does.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("intentionSummary")]
+ public string? IntentionSummary { get; set; }
}
+/// Token usage detail for a single billing category.
+/// Nested data type for AssistantUsageDataCopilotUsageTokenDetailsItem.
public partial class AssistantUsageDataCopilotUsageTokenDetailsItem
{
+ /// Number of tokens in this billing batch.
[JsonPropertyName("batchSize")]
public required double BatchSize { get; set; }
+ /// Cost per batch of tokens.
[JsonPropertyName("costPerBatch")]
public required double CostPerBatch { get; set; }
+ /// Total token count for this entry.
[JsonPropertyName("tokenCount")]
public required double TokenCount { get; set; }
+ /// Token category (e.g., "input", "output").
[JsonPropertyName("tokenType")]
public required string TokenType { get; set; }
}
+/// Per-request cost and usage data from the CAPI copilot_usage response field.
+/// Nested data type for AssistantUsageDataCopilotUsage.
public partial class AssistantUsageDataCopilotUsage
{
+ /// Itemized token usage breakdown.
[JsonPropertyName("tokenDetails")]
public required AssistantUsageDataCopilotUsageTokenDetailsItem[] TokenDetails { get; set; }
+ /// Total cost in nano-AIU (AI Units) for this request.
[JsonPropertyName("totalNanoAiu")]
public required double TotalNanoAiu { get; set; }
}
+/// Plain text content block.
+/// The text variant of .
public partial class ToolExecutionCompleteDataResultContentsItemText : ToolExecutionCompleteDataResultContentsItem
{
+ ///
[JsonIgnore]
public override string Type => "text";
+ /// The text content.
[JsonPropertyName("text")]
public required string Text { get; set; }
}
+/// Terminal/shell output content block with optional exit code and working directory.
+/// The terminal variant of .
public partial class ToolExecutionCompleteDataResultContentsItemTerminal : ToolExecutionCompleteDataResultContentsItem
{
+ ///
[JsonIgnore]
public override string Type => "terminal";
+ /// Terminal/shell output text.
[JsonPropertyName("text")]
public required string Text { get; set; }
+ /// Process exit code, if the command has completed.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("exitCode")]
public double? ExitCode { get; set; }
+ /// Working directory where the command was executed.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("cwd")]
public string? Cwd { get; set; }
}
+/// Image content block with base64-encoded data.
+/// The image variant of .
public partial class ToolExecutionCompleteDataResultContentsItemImage : ToolExecutionCompleteDataResultContentsItem
{
+ ///
[JsonIgnore]
public override string Type => "image";
+ /// Base64-encoded image data.
[JsonPropertyName("data")]
public required string Data { get; set; }
+ /// MIME type of the image (e.g., image/png, image/jpeg).
[JsonPropertyName("mimeType")]
public required string MimeType { get; set; }
}
+/// Audio content block with base64-encoded data.
+/// The audio variant of .
public partial class ToolExecutionCompleteDataResultContentsItemAudio : ToolExecutionCompleteDataResultContentsItem
{
+ ///
[JsonIgnore]
public override string Type => "audio";
+ /// Base64-encoded audio data.
[JsonPropertyName("data")]
public required string Data { get; set; }
+ /// MIME type of the audio (e.g., audio/wav, audio/mpeg).
[JsonPropertyName("mimeType")]
public required string MimeType { get; set; }
}
+/// Icon image for a resource.
+/// Nested data type for ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem.
public partial class ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem
{
+ /// URL or path to the icon image.
[JsonPropertyName("src")]
public required string Src { get; set; }
+ /// MIME type of the icon image.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("mimeType")]
public string? MimeType { get; set; }
+ /// Available icon sizes (e.g., ['16x16', '32x32']).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("sizes")]
public string[]? Sizes { get; set; }
+ /// Theme variant this icon is intended for.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("theme")]
public ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItemTheme? Theme { get; set; }
}
+/// Resource link content block referencing an external resource.
+/// The resource_link variant of .
public partial class ToolExecutionCompleteDataResultContentsItemResourceLink : ToolExecutionCompleteDataResultContentsItem
{
+ ///
[JsonIgnore]
public override string Type => "resource_link";
+ /// Icons associated with this resource.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("icons")]
public ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem[]? Icons { get; set; }
+ /// Resource name identifier.
[JsonPropertyName("name")]
public required string Name { get; set; }
+ /// Human-readable display title for the resource.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("title")]
public string? Title { get; set; }
+ /// URI identifying the resource.
[JsonPropertyName("uri")]
public required string Uri { get; set; }
+ /// Human-readable description of the resource.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("description")]
public string? Description { get; set; }
+ /// MIME type of the resource content.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("mimeType")]
public string? MimeType { get; set; }
+ /// Size of the resource in bytes.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("size")]
public double? Size { get; set; }
}
+/// Embedded resource content block with inline text or binary data.
+/// The resource variant of .
public partial class ToolExecutionCompleteDataResultContentsItemResource : ToolExecutionCompleteDataResultContentsItem
{
+ ///
[JsonIgnore]
public override string Type => "resource";
+ /// The embedded resource contents, either text or base64-encoded binary.
[JsonPropertyName("resource")]
public required object Resource { get; set; }
}
+/// A content block within a tool result, which may be text, terminal output, image, audio, or a resource.
+/// Polymorphic base type discriminated by type.
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "type",
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
@@ -2045,179 +3004,813 @@ public partial class ToolExecutionCompleteDataResultContentsItemResource : ToolE
[JsonDerivedType(typeof(ToolExecutionCompleteDataResultContentsItemResource), "resource")]
public partial class ToolExecutionCompleteDataResultContentsItem
{
+ /// The type discriminator.
[JsonPropertyName("type")]
public virtual string Type { get; set; } = string.Empty;
}
+/// Tool execution result on success.
+/// Nested data type for ToolExecutionCompleteDataResult.
public partial class ToolExecutionCompleteDataResult
{
+ /// Concise tool result text sent to the LLM for chat completion, potentially truncated for token efficiency.
[JsonPropertyName("content")]
public required string Content { get; set; }
+ /// Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("detailedContent")]
public string? DetailedContent { get; set; }
+ /// Structured content blocks (text, images, audio, resources) returned by the tool in their native format.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("contents")]
public ToolExecutionCompleteDataResultContentsItem[]? Contents { get; set; }
}
+/// Error details when the tool execution failed.
+/// Nested data type for ToolExecutionCompleteDataError.
public partial class ToolExecutionCompleteDataError
{
+ /// Human-readable error message.
[JsonPropertyName("message")]
public required string Message { get; set; }
+ /// Machine-readable error code.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("code")]
public string? Code { get; set; }
}
+/// Error details when the hook failed.
+/// Nested data type for HookEndDataError.
public partial class HookEndDataError
{
+ /// Human-readable error message.
[JsonPropertyName("message")]
public required string Message { get; set; }
+ /// Error stack trace, when available.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("stack")]
public string? Stack { get; set; }
}
+/// Metadata about the prompt template and its construction.
+/// Nested data type for SystemMessageDataMetadata.
public partial class SystemMessageDataMetadata
{
+ /// Version identifier of the prompt template used.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("promptVersion")]
public string? PromptVersion { get; set; }
+ /// Template variables used when constructing the prompt.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("variables")]
public Dictionary? Variables { get; set; }
}
+/// The agent_completed variant of .
+public partial class SystemNotificationDataKindAgentCompleted : SystemNotificationDataKind
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "agent_completed";
+
+ /// Unique identifier of the background agent.
+ [JsonPropertyName("agentId")]
+ public required string AgentId { get; set; }
+
+ /// Type of the agent (e.g., explore, task, general-purpose).
+ [JsonPropertyName("agentType")]
+ public required string AgentType { get; set; }
+
+ /// Whether the agent completed successfully or failed.
+ [JsonPropertyName("status")]
+ public required SystemNotificationDataKindAgentCompletedStatus Status { get; set; }
+
+ /// Human-readable description of the agent task.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ /// The full prompt given to the background agent.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("prompt")]
+ public string? Prompt { get; set; }
+}
+
+/// The agent_idle variant of .
+public partial class SystemNotificationDataKindAgentIdle : SystemNotificationDataKind
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "agent_idle";
+
+ /// Unique identifier of the background agent.
+ [JsonPropertyName("agentId")]
+ public required string AgentId { get; set; }
+
+ /// Type of the agent (e.g., explore, task, general-purpose).
+ [JsonPropertyName("agentType")]
+ public required string AgentType { get; set; }
+
+ /// Human-readable description of the agent task.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
+
+/// The shell_completed variant of .
+public partial class SystemNotificationDataKindShellCompleted : SystemNotificationDataKind
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "shell_completed";
+
+ /// Unique identifier of the shell session.
+ [JsonPropertyName("shellId")]
+ public required string ShellId { get; set; }
+
+ /// Exit code of the shell command, if available.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("exitCode")]
+ public double? ExitCode { get; set; }
+
+ /// Human-readable description of the command.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
+
+/// The shell_detached_completed variant of .
+public partial class SystemNotificationDataKindShellDetachedCompleted : SystemNotificationDataKind
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "shell_detached_completed";
+
+ /// Unique identifier of the detached shell session.
+ [JsonPropertyName("shellId")]
+ public required string ShellId { get; set; }
+
+ /// Human-readable description of the command.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
+
+/// Structured metadata identifying what triggered this notification.
+/// Polymorphic base type discriminated by type.
+[JsonPolymorphic(
+ TypeDiscriminatorPropertyName = "type",
+ UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
+[JsonDerivedType(typeof(SystemNotificationDataKindAgentCompleted), "agent_completed")]
+[JsonDerivedType(typeof(SystemNotificationDataKindAgentIdle), "agent_idle")]
+[JsonDerivedType(typeof(SystemNotificationDataKindShellCompleted), "shell_completed")]
+[JsonDerivedType(typeof(SystemNotificationDataKindShellDetachedCompleted), "shell_detached_completed")]
+public partial class SystemNotificationDataKind
+{
+ /// The type discriminator.
+ [JsonPropertyName("type")]
+ public virtual string Type { get; set; } = string.Empty;
+}
+
+
+/// Nested data type for PermissionRequestShellCommandsItem.
+public partial class PermissionRequestShellCommandsItem
+{
+ /// Command identifier (e.g., executable name).
+ [JsonPropertyName("identifier")]
+ public required string Identifier { get; set; }
+
+ /// Whether this command is read-only (no side effects).
+ [JsonPropertyName("readOnly")]
+ public required bool ReadOnly { get; set; }
+}
+
+/// Nested data type for PermissionRequestShellPossibleUrlsItem.
+public partial class PermissionRequestShellPossibleUrlsItem
+{
+ /// URL that may be accessed by the command.
+ [JsonPropertyName("url")]
+ public required string Url { get; set; }
+}
+
+/// Shell command permission request.
+/// The shell variant of .
+public partial class PermissionRequestShell : PermissionRequest
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "shell";
+
+ /// Tool call ID that triggered this permission request.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// The complete shell command text to be executed.
+ [JsonPropertyName("fullCommandText")]
+ public required string FullCommandText { get; set; }
+
+ /// Human-readable description of what the command intends to do.
+ [JsonPropertyName("intention")]
+ public required string Intention { get; set; }
+
+ /// Parsed command identifiers found in the command text.
+ [JsonPropertyName("commands")]
+ public required PermissionRequestShellCommandsItem[] Commands { get; set; }
+
+ /// File paths that may be read or written by the command.
+ [JsonPropertyName("possiblePaths")]
+ public required string[] PossiblePaths { get; set; }
+
+ /// URLs that may be accessed by the command.
+ [JsonPropertyName("possibleUrls")]
+ public required PermissionRequestShellPossibleUrlsItem[] PossibleUrls { get; set; }
+
+ /// Whether the command includes a file write redirection (e.g., > or >>).
+ [JsonPropertyName("hasWriteFileRedirection")]
+ public required bool HasWriteFileRedirection { get; set; }
+
+ /// Whether the UI can offer session-wide approval for this command pattern.
+ [JsonPropertyName("canOfferSessionApproval")]
+ public required bool CanOfferSessionApproval { get; set; }
+
+ /// Optional warning message about risks of running this command.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("warning")]
+ public string? Warning { get; set; }
+}
+
+/// File write permission request.
+/// The write variant of .
+public partial class PermissionRequestWrite : PermissionRequest
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "write";
+
+ /// Tool call ID that triggered this permission request.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// Human-readable description of the intended file change.
+ [JsonPropertyName("intention")]
+ public required string Intention { get; set; }
+
+ /// Path of the file being written to.
+ [JsonPropertyName("fileName")]
+ public required string FileName { get; set; }
+
+ /// Unified diff showing the proposed changes.
+ [JsonPropertyName("diff")]
+ public required string Diff { get; set; }
+
+ /// Complete new file contents for newly created files.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("newFileContents")]
+ public string? NewFileContents { get; set; }
+}
+
+/// File or directory read permission request.
+/// The read variant of .
+public partial class PermissionRequestRead : PermissionRequest
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "read";
+
+ /// Tool call ID that triggered this permission request.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// Human-readable description of why the file is being read.
+ [JsonPropertyName("intention")]
+ public required string Intention { get; set; }
+
+ /// Path of the file or directory being read.
+ [JsonPropertyName("path")]
+ public required string Path { get; set; }
+}
+
+/// MCP tool invocation permission request.
+/// The mcp variant of .
+public partial class PermissionRequestMcp : PermissionRequest
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "mcp";
+
+ /// Tool call ID that triggered this permission request.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// Name of the MCP server providing the tool.
+ [JsonPropertyName("serverName")]
+ public required string ServerName { get; set; }
+
+ /// Internal name of the MCP tool.
+ [JsonPropertyName("toolName")]
+ public required string ToolName { get; set; }
+
+ /// Human-readable title of the MCP tool.
+ [JsonPropertyName("toolTitle")]
+ public required string ToolTitle { get; set; }
+
+ /// Arguments to pass to the MCP tool.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("args")]
+ public object? Args { get; set; }
+
+ /// Whether this MCP tool is read-only (no side effects).
+ [JsonPropertyName("readOnly")]
+ public required bool ReadOnly { get; set; }
+}
+
+/// URL access permission request.
+/// The url variant of .
+public partial class PermissionRequestUrl : PermissionRequest
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "url";
+
+ /// Tool call ID that triggered this permission request.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// Human-readable description of why the URL is being accessed.
+ [JsonPropertyName("intention")]
+ public required string Intention { get; set; }
+
+ /// URL to be fetched.
+ [JsonPropertyName("url")]
+ public required string Url { get; set; }
+}
+
+/// Memory storage permission request.
+/// The memory variant of .
+public partial class PermissionRequestMemory : PermissionRequest
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "memory";
+
+ /// Tool call ID that triggered this permission request.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// Topic or subject of the memory being stored.
+ [JsonPropertyName("subject")]
+ public required string Subject { get; set; }
+
+ /// The fact or convention being stored.
+ [JsonPropertyName("fact")]
+ public required string Fact { get; set; }
+
+ /// Source references for the stored fact.
+ [JsonPropertyName("citations")]
+ public required string Citations { get; set; }
+}
+
+/// Custom tool invocation permission request.
+/// The custom-tool variant of .
+public partial class PermissionRequestCustomTool : PermissionRequest
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "custom-tool";
+
+ /// Tool call ID that triggered this permission request.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// Name of the custom tool.
+ [JsonPropertyName("toolName")]
+ public required string ToolName { get; set; }
+
+ /// Description of what the custom tool does.
+ [JsonPropertyName("toolDescription")]
+ public required string ToolDescription { get; set; }
+
+ /// Arguments to pass to the custom tool.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("args")]
+ public object? Args { get; set; }
+}
+
+/// Hook confirmation permission request.
+/// The hook variant of .
+public partial class PermissionRequestHook : PermissionRequest
+{
+ ///
+ [JsonIgnore]
+ public override string Kind => "hook";
+
+ /// Tool call ID that triggered this permission request.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// Name of the tool the hook is gating.
+ [JsonPropertyName("toolName")]
+ public required string ToolName { get; set; }
+
+ /// Arguments of the tool call being gated.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolArgs")]
+ public object? ToolArgs { get; set; }
+
+ /// Optional message from the hook explaining why confirmation is needed.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("hookMessage")]
+ public string? HookMessage { get; set; }
+}
+
+/// Details of the permission being requested.
+/// Polymorphic base type discriminated by kind.
+[JsonPolymorphic(
+ TypeDiscriminatorPropertyName = "kind",
+ UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
+[JsonDerivedType(typeof(PermissionRequestShell), "shell")]
+[JsonDerivedType(typeof(PermissionRequestWrite), "write")]
+[JsonDerivedType(typeof(PermissionRequestRead), "read")]
+[JsonDerivedType(typeof(PermissionRequestMcp), "mcp")]
+[JsonDerivedType(typeof(PermissionRequestUrl), "url")]
+[JsonDerivedType(typeof(PermissionRequestMemory), "memory")]
+[JsonDerivedType(typeof(PermissionRequestCustomTool), "custom-tool")]
+[JsonDerivedType(typeof(PermissionRequestHook), "hook")]
+public partial class PermissionRequest
+{
+ /// The type discriminator.
+ [JsonPropertyName("kind")]
+ public virtual string Kind { get; set; } = string.Empty;
+}
+
+
+/// The result of the permission request.
+/// Nested data type for PermissionCompletedDataResult.
public partial class PermissionCompletedDataResult
{
+ /// The outcome of the permission request.
[JsonPropertyName("kind")]
public required PermissionCompletedDataResultKind Kind { get; set; }
}
+/// JSON Schema describing the form fields to present to the user (form mode only).
+/// Nested data type for ElicitationRequestedDataRequestedSchema.
public partial class ElicitationRequestedDataRequestedSchema
{
+ /// Schema type indicator (always 'object').
[JsonPropertyName("type")]
public required string Type { get; set; }
+ /// Form field definitions, keyed by field name.
[JsonPropertyName("properties")]
public required Dictionary Properties { get; set; }
+ /// List of required field names.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("required")]
public string[]? Required { get; set; }
}
+/// Static OAuth client configuration, if the server specifies one.
+/// Nested data type for McpOauthRequiredDataStaticClientConfig.
+public partial class McpOauthRequiredDataStaticClientConfig
+{
+ /// OAuth client ID for the server.
+ [JsonPropertyName("clientId")]
+ public required string ClientId { get; set; }
+
+ /// Whether this is a public OAuth client.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("publicClient")]
+ public bool? PublicClient { get; set; }
+}
+
+/// Nested data type for CommandsChangedDataCommandsItem.
+public partial class CommandsChangedDataCommandsItem
+{
+ /// Gets or sets the name value.
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Gets or sets the description value.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
+
+/// Nested data type for SessionSkillsLoadedDataSkillsItem.
+public partial class SessionSkillsLoadedDataSkillsItem
+{
+ /// Unique identifier for the skill.
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Description of what the skill does.
+ [JsonPropertyName("description")]
+ public required string Description { get; set; }
+
+ /// Source location type of the skill (e.g., project, personal, plugin).
+ [JsonPropertyName("source")]
+ public required string Source { get; set; }
+
+ /// Whether the skill can be invoked by the user as a slash command.
+ [JsonPropertyName("userInvocable")]
+ public required bool UserInvocable { get; set; }
+
+ /// Whether the skill is currently enabled.
+ [JsonPropertyName("enabled")]
+ public required bool Enabled { get; set; }
+
+ /// Absolute path to the skill file, if available.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("path")]
+ public string? Path { get; set; }
+}
+
+/// Nested data type for SessionMcpServersLoadedDataServersItem.
+public partial class SessionMcpServersLoadedDataServersItem
+{
+ /// Server name (config key).
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Connection status: connected, failed, pending, disabled, or not_configured.
+ [JsonPropertyName("status")]
+ public required SessionMcpServersLoadedDataServersItemStatus Status { get; set; }
+
+ /// Configuration source: user, workspace, plugin, or builtin.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("source")]
+ public string? Source { get; set; }
+
+ /// Error message if the server failed to connect.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("error")]
+ public string? Error { get; set; }
+}
+
+/// Nested data type for SessionExtensionsLoadedDataExtensionsItem.
+public partial class SessionExtensionsLoadedDataExtensionsItem
+{
+ /// Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper').
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ /// Extension name (directory name).
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Discovery source.
+ [JsonPropertyName("source")]
+ public required SessionExtensionsLoadedDataExtensionsItemSource Source { get; set; }
+
+ /// Current status: running, disabled, failed, or starting.
+ [JsonPropertyName("status")]
+ public required SessionExtensionsLoadedDataExtensionsItemStatus Status { get; set; }
+}
+
+/// Hosting platform type of the repository (github or ado).
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionStartDataContextHostType
+{
+ /// The github variant.
+ [JsonStringEnumMemberName("github")]
+ Github,
+ /// The ado variant.
+ [JsonStringEnumMemberName("ado")]
+ Ado,
+}
+
+/// The type of operation performed on the plan file.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SessionPlanChangedDataOperation
{
+ /// The create variant.
[JsonStringEnumMemberName("create")]
Create,
+ /// The update variant.
[JsonStringEnumMemberName("update")]
Update,
+ /// The delete variant.
[JsonStringEnumMemberName("delete")]
Delete,
}
+/// Whether the file was newly created or updated.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SessionWorkspaceFileChangedDataOperation
{
+ /// The create variant.
[JsonStringEnumMemberName("create")]
Create,
+ /// The update variant.
[JsonStringEnumMemberName("update")]
Update,
}
+/// Origin type of the session being handed off.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SessionHandoffDataSourceType
{
+ /// The remote variant.
[JsonStringEnumMemberName("remote")]
Remote,
+ /// The local variant.
[JsonStringEnumMemberName("local")]
Local,
}
+/// Whether the session ended normally ("routine") or due to a crash/fatal error ("error").
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SessionShutdownDataShutdownType
{
+ /// The routine variant.
[JsonStringEnumMemberName("routine")]
Routine,
+ /// The error variant.
[JsonStringEnumMemberName("error")]
Error,
}
+/// Type of GitHub reference.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum UserMessageDataAttachmentsItemGithubReferenceReferenceType
{
+ /// The issue variant.
[JsonStringEnumMemberName("issue")]
Issue,
+ /// The pr variant.
[JsonStringEnumMemberName("pr")]
Pr,
+ /// The discussion variant.
[JsonStringEnumMemberName("discussion")]
Discussion,
}
+/// The agent mode that was active when this message was sent.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum UserMessageDataAgentMode
{
+ /// The interactive variant.
[JsonStringEnumMemberName("interactive")]
Interactive,
+ /// The plan variant.
[JsonStringEnumMemberName("plan")]
Plan,
+ /// The autopilot variant.
[JsonStringEnumMemberName("autopilot")]
Autopilot,
+ /// The shell variant.
[JsonStringEnumMemberName("shell")]
Shell,
}
+/// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AssistantMessageDataToolRequestsItemType
{
+ /// The function variant.
[JsonStringEnumMemberName("function")]
Function,
+ /// The custom variant.
[JsonStringEnumMemberName("custom")]
Custom,
}
+/// Theme variant this icon is intended for.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItemTheme
{
+ /// The light variant.
[JsonStringEnumMemberName("light")]
Light,
+ /// The dark variant.
[JsonStringEnumMemberName("dark")]
Dark,
}
+/// Message role: "system" for system prompts, "developer" for developer-injected instructions.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SystemMessageDataRole
{
+ /// The system variant.
[JsonStringEnumMemberName("system")]
System,
+ /// The developer variant.
[JsonStringEnumMemberName("developer")]
Developer,
}
+/// Whether the agent completed successfully or failed.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SystemNotificationDataKindAgentCompletedStatus
+{
+ /// The completed variant.
+ [JsonStringEnumMemberName("completed")]
+ Completed,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+}
+
+/// The outcome of the permission request.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PermissionCompletedDataResultKind
{
+ /// The approved variant.
[JsonStringEnumMemberName("approved")]
Approved,
+ /// The denied-by-rules variant.
[JsonStringEnumMemberName("denied-by-rules")]
DeniedByRules,
+ /// The denied-no-approval-rule-and-could-not-request-from-user variant.
[JsonStringEnumMemberName("denied-no-approval-rule-and-could-not-request-from-user")]
DeniedNoApprovalRuleAndCouldNotRequestFromUser,
+ /// The denied-interactively-by-user variant.
[JsonStringEnumMemberName("denied-interactively-by-user")]
DeniedInteractivelyByUser,
+ /// The denied-by-content-exclusion-policy variant.
[JsonStringEnumMemberName("denied-by-content-exclusion-policy")]
DeniedByContentExclusionPolicy,
}
+/// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ElicitationRequestedDataMode
+{
+ /// The form variant.
+ [JsonStringEnumMemberName("form")]
+ Form,
+ /// The url variant.
+ [JsonStringEnumMemberName("url")]
+ Url,
+}
+
+/// Connection status: connected, failed, pending, disabled, or not_configured.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionMcpServersLoadedDataServersItemStatus
+{
+ /// The connected variant.
+ [JsonStringEnumMemberName("connected")]
+ Connected,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+ /// The pending variant.
+ [JsonStringEnumMemberName("pending")]
+ Pending,
+ /// The disabled variant.
+ [JsonStringEnumMemberName("disabled")]
+ Disabled,
+ /// The not_configured variant.
+ [JsonStringEnumMemberName("not_configured")]
+ NotConfigured,
+}
+
+/// Discovery source.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionExtensionsLoadedDataExtensionsItemSource
+{
+ /// The project variant.
+ [JsonStringEnumMemberName("project")]
+ Project,
+ /// The user variant.
+ [JsonStringEnumMemberName("user")]
+ User,
+}
+
+/// Current status: running, disabled, failed, or starting.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionExtensionsLoadedDataExtensionsItemStatus
+{
+ /// The running variant.
+ [JsonStringEnumMemberName("running")]
+ Running,
+ /// The disabled variant.
+ [JsonStringEnumMemberName("disabled")]
+ Disabled,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+ /// The starting variant.
+ [JsonStringEnumMemberName("starting")]
+ Starting,
+}
+
[JsonSourceGenerationOptions(
JsonSerializerDefaults.Web,
AllowOutOfOrderMetadataProperties = true,
@@ -2248,8 +3841,13 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(AssistantUsageEvent))]
[JsonSerializable(typeof(CommandCompletedData))]
[JsonSerializable(typeof(CommandCompletedEvent))]
+[JsonSerializable(typeof(CommandExecuteData))]
+[JsonSerializable(typeof(CommandExecuteEvent))]
[JsonSerializable(typeof(CommandQueuedData))]
[JsonSerializable(typeof(CommandQueuedEvent))]
+[JsonSerializable(typeof(CommandsChangedData))]
+[JsonSerializable(typeof(CommandsChangedDataCommandsItem))]
+[JsonSerializable(typeof(CommandsChangedEvent))]
[JsonSerializable(typeof(ElicitationCompletedData))]
[JsonSerializable(typeof(ElicitationCompletedEvent))]
[JsonSerializable(typeof(ElicitationRequestedData))]
@@ -2268,13 +3866,31 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(HookEndEvent))]
[JsonSerializable(typeof(HookStartData))]
[JsonSerializable(typeof(HookStartEvent))]
+[JsonSerializable(typeof(McpOauthCompletedData))]
+[JsonSerializable(typeof(McpOauthCompletedEvent))]
+[JsonSerializable(typeof(McpOauthRequiredData))]
+[JsonSerializable(typeof(McpOauthRequiredDataStaticClientConfig))]
+[JsonSerializable(typeof(McpOauthRequiredEvent))]
[JsonSerializable(typeof(PendingMessagesModifiedData))]
[JsonSerializable(typeof(PendingMessagesModifiedEvent))]
[JsonSerializable(typeof(PermissionCompletedData))]
[JsonSerializable(typeof(PermissionCompletedDataResult))]
[JsonSerializable(typeof(PermissionCompletedEvent))]
+[JsonSerializable(typeof(PermissionRequest))]
+[JsonSerializable(typeof(PermissionRequestCustomTool))]
+[JsonSerializable(typeof(PermissionRequestHook))]
+[JsonSerializable(typeof(PermissionRequestMcp))]
+[JsonSerializable(typeof(PermissionRequestMemory))]
+[JsonSerializable(typeof(PermissionRequestRead))]
+[JsonSerializable(typeof(PermissionRequestShell))]
+[JsonSerializable(typeof(PermissionRequestShellCommandsItem))]
+[JsonSerializable(typeof(PermissionRequestShellPossibleUrlsItem))]
+[JsonSerializable(typeof(PermissionRequestUrl))]
+[JsonSerializable(typeof(PermissionRequestWrite))]
[JsonSerializable(typeof(PermissionRequestedData))]
[JsonSerializable(typeof(PermissionRequestedEvent))]
+[JsonSerializable(typeof(SessionBackgroundTasksChangedData))]
+[JsonSerializable(typeof(SessionBackgroundTasksChangedEvent))]
[JsonSerializable(typeof(SessionCompactionCompleteData))]
[JsonSerializable(typeof(SessionCompactionCompleteDataCompactionTokensUsed))]
[JsonSerializable(typeof(SessionCompactionCompleteEvent))]
@@ -2285,6 +3901,9 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SessionErrorData))]
[JsonSerializable(typeof(SessionErrorEvent))]
[JsonSerializable(typeof(SessionEvent))]
+[JsonSerializable(typeof(SessionExtensionsLoadedData))]
+[JsonSerializable(typeof(SessionExtensionsLoadedDataExtensionsItem))]
+[JsonSerializable(typeof(SessionExtensionsLoadedEvent))]
[JsonSerializable(typeof(SessionHandoffData))]
[JsonSerializable(typeof(SessionHandoffDataRepository))]
[JsonSerializable(typeof(SessionHandoffEvent))]
@@ -2295,6 +3914,11 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SessionIdleEvent))]
[JsonSerializable(typeof(SessionInfoData))]
[JsonSerializable(typeof(SessionInfoEvent))]
+[JsonSerializable(typeof(SessionMcpServerStatusChangedData))]
+[JsonSerializable(typeof(SessionMcpServerStatusChangedEvent))]
+[JsonSerializable(typeof(SessionMcpServersLoadedData))]
+[JsonSerializable(typeof(SessionMcpServersLoadedDataServersItem))]
+[JsonSerializable(typeof(SessionMcpServersLoadedEvent))]
[JsonSerializable(typeof(SessionModeChangedData))]
[JsonSerializable(typeof(SessionModeChangedEvent))]
[JsonSerializable(typeof(SessionModelChangeData))]
@@ -2307,6 +3931,9 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SessionShutdownData))]
[JsonSerializable(typeof(SessionShutdownDataCodeChanges))]
[JsonSerializable(typeof(SessionShutdownEvent))]
+[JsonSerializable(typeof(SessionSkillsLoadedData))]
+[JsonSerializable(typeof(SessionSkillsLoadedDataSkillsItem))]
+[JsonSerializable(typeof(SessionSkillsLoadedEvent))]
[JsonSerializable(typeof(SessionSnapshotRewindData))]
[JsonSerializable(typeof(SessionSnapshotRewindEvent))]
[JsonSerializable(typeof(SessionStartData))]
@@ -2316,6 +3943,8 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SessionTaskCompleteEvent))]
[JsonSerializable(typeof(SessionTitleChangedData))]
[JsonSerializable(typeof(SessionTitleChangedEvent))]
+[JsonSerializable(typeof(SessionToolsUpdatedData))]
+[JsonSerializable(typeof(SessionToolsUpdatedEvent))]
[JsonSerializable(typeof(SessionTruncationData))]
[JsonSerializable(typeof(SessionTruncationEvent))]
[JsonSerializable(typeof(SessionUsageInfoData))]
@@ -2339,6 +3968,13 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SystemMessageData))]
[JsonSerializable(typeof(SystemMessageDataMetadata))]
[JsonSerializable(typeof(SystemMessageEvent))]
+[JsonSerializable(typeof(SystemNotificationData))]
+[JsonSerializable(typeof(SystemNotificationDataKind))]
+[JsonSerializable(typeof(SystemNotificationDataKindAgentCompleted))]
+[JsonSerializable(typeof(SystemNotificationDataKindAgentIdle))]
+[JsonSerializable(typeof(SystemNotificationDataKindShellCompleted))]
+[JsonSerializable(typeof(SystemNotificationDataKindShellDetachedCompleted))]
+[JsonSerializable(typeof(SystemNotificationEvent))]
[JsonSerializable(typeof(ToolExecutionCompleteData))]
[JsonSerializable(typeof(ToolExecutionCompleteDataError))]
[JsonSerializable(typeof(ToolExecutionCompleteDataResult))]
@@ -2365,8 +4001,8 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(UserInputRequestedEvent))]
[JsonSerializable(typeof(UserMessageData))]
[JsonSerializable(typeof(UserMessageDataAttachmentsItem))]
+[JsonSerializable(typeof(UserMessageDataAttachmentsItemBlob))]
[JsonSerializable(typeof(UserMessageDataAttachmentsItemDirectory))]
-[JsonSerializable(typeof(UserMessageDataAttachmentsItemDirectoryLineRange))]
[JsonSerializable(typeof(UserMessageDataAttachmentsItemFile))]
[JsonSerializable(typeof(UserMessageDataAttachmentsItemFileLineRange))]
[JsonSerializable(typeof(UserMessageDataAttachmentsItemGithubReference))]
@@ -2375,4 +4011,5 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(UserMessageDataAttachmentsItemSelectionSelectionEnd))]
[JsonSerializable(typeof(UserMessageDataAttachmentsItemSelectionSelectionStart))]
[JsonSerializable(typeof(UserMessageEvent))]
+[JsonSerializable(typeof(JsonElement))]
internal partial class SessionEventsJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj
index 8ae53ca74c..38eb0cf3aa 100644
--- a/dotnet/src/GitHub.Copilot.SDK.csproj
+++ b/dotnet/src/GitHub.Copilot.SDK.csproj
@@ -11,6 +11,7 @@
https://github.com/github/copilot-sdk
README.md
https://github.com/github/copilot-sdk
+ copilot.png
github;copilot;sdk;jsonrpc;agent
true
true
@@ -19,12 +20,17 @@
true
+
+ $(NoWarn);GHCP001
+
+
true
+
diff --git a/dotnet/src/SdkProtocolVersion.cs b/dotnet/src/SdkProtocolVersion.cs
index f3d8f04c5a..889af460bb 100644
--- a/dotnet/src/SdkProtocolVersion.cs
+++ b/dotnet/src/SdkProtocolVersion.cs
@@ -16,8 +16,5 @@ internal static class SdkProtocolVersion
///
/// Gets the SDK protocol version.
///
- public static int GetVersion()
- {
- return Version;
- }
+ public static int GetVersion() => Version;
}
diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs
index 397eae0fa2..675a3e0c0d 100644
--- a/dotnet/src/Session.cs
+++ b/dotnet/src/Session.cs
@@ -2,12 +2,15 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
+using GitHub.Copilot.SDK.Rpc;
using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
using StreamJsonRpc;
+using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
-using GitHub.Copilot.SDK.Rpc;
+using System.Threading.Channels;
namespace GitHub.Copilot.SDK;
@@ -52,22 +55,29 @@ namespace GitHub.Copilot.SDK;
///
public sealed partial class CopilotSession : IAsyncDisposable
{
- ///
- /// Multicast delegate used as a thread-safe, insertion-ordered handler list.
- /// The compiler-generated add/remove accessors use a lock-free CAS loop over the backing field.
- /// Dispatch reads the field once (inherent snapshot, no allocation).
- /// Expected handler count is small (typically 1–3), so Delegate.Combine/Remove cost is negligible.
- ///
- private event SessionEventHandler? EventHandlers;
private readonly Dictionary _toolHandlers = [];
private readonly JsonRpc _rpc;
+ private readonly ILogger _logger;
+
private volatile PermissionRequestHandler? _permissionHandler;
private volatile UserInputHandler? _userInputHandler;
+ private ImmutableArray _eventHandlers = ImmutableArray.Empty;
+
private SessionHooks? _hooks;
private readonly SemaphoreSlim _hooksLock = new(1, 1);
+ private Dictionary>>? _transformCallbacks;
+ private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1);
private SessionRpc? _sessionRpc;
private int _isDisposed;
+ ///
+ /// Channel that serializes event dispatch. enqueues;
+ /// a single background consumer () dequeues and
+ /// invokes handlers one at a time, preserving arrival order.
+ ///
+ private readonly Channel _eventChannel = Channel.CreateUnbounded(
+ new() { SingleReader = true });
+
///
/// Gets the unique identifier for this session.
///
@@ -86,22 +96,27 @@ public sealed partial class CopilotSession : IAsyncDisposable
/// The path to the workspace containing checkpoints/, plan.md, and files/ subdirectories,
/// or null if infinite sessions are disabled.
///
- public string? WorkspacePath { get; }
+ public string? WorkspacePath { get; internal set; }
///
/// Initializes a new instance of the class.
///
/// The unique identifier for this session.
/// The JSON-RPC connection to the Copilot CLI.
+ /// Logger for diagnostics.
/// The workspace path if infinite sessions are enabled.
///
/// This constructor is internal. Use to create sessions.
///
- internal CopilotSession(string sessionId, JsonRpc rpc, string? workspacePath = null)
+ internal CopilotSession(string sessionId, JsonRpc rpc, ILogger logger, string? workspacePath = null)
{
SessionId = sessionId;
_rpc = rpc;
+ _logger = logger;
WorkspacePath = workspacePath;
+
+ // Start the asynchronous processing loop.
+ _ = ProcessEventsAsync();
}
private Task InvokeRpcAsync(string method, object?[]? args, CancellationToken cancellationToken)
@@ -139,12 +154,16 @@ private Task InvokeRpcAsync(string method, object?[]? args, CancellationTo
///
public async Task SendAsync(MessageOptions options, CancellationToken cancellationToken = default)
{
+ var (traceparent, tracestate) = TelemetryHelpers.GetTraceContext();
+
var request = new SendMessageRequest
{
SessionId = SessionId,
Prompt = options.Prompt,
Attachments = options.Attachments,
- Mode = options.Mode
+ Mode = options.Mode,
+ Traceparent = traceparent,
+ Tracestate = tracestate
};
var response = await InvokeRpcAsync(
@@ -186,7 +205,7 @@ public async Task SendAsync(MessageOptions options, CancellationToken ca
CancellationToken cancellationToken = default)
{
var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(60);
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
AssistantMessageEvent? lastAssistantMessage = null;
void Handler(SessionEvent evt)
@@ -236,7 +255,9 @@ void Handler(SessionEvent evt)
/// Multiple handlers can be registered and will all receive events.
///
///
- /// Handler exceptions are allowed to propagate so they are not lost.
+ /// Handlers are invoked serially in event-arrival order on a background thread.
+ /// A handler will never be called concurrently with itself or with other handlers
+ /// on the same session.
///
///
///
@@ -259,27 +280,53 @@ void Handler(SessionEvent evt)
///
public IDisposable On(SessionEventHandler handler)
{
- EventHandlers += handler;
- return new ActionDisposable(() => EventHandlers -= handler);
+ ImmutableInterlocked.Update(ref _eventHandlers, array => array.Add(handler));
+ return new ActionDisposable(() => ImmutableInterlocked.Update(ref _eventHandlers, array => array.Remove(handler)));
}
///
- /// Dispatches an event to all registered handlers.
+ /// Enqueues an event for serial dispatch to all registered handlers.
///
/// The session event to dispatch.
///
- /// This method is internal. Handler exceptions are allowed to propagate so they are not lost.
- /// Broadcast request events (external_tool.requested, permission.requested) are handled
- /// internally before being forwarded to user handlers.
+ /// This method is non-blocking. Broadcast request events (external_tool.requested,
+ /// permission.requested) are fired concurrently so that a stalled handler does not
+ /// block event delivery. The event is then placed into an in-memory channel and
+ /// processed by a single background consumer (),
+ /// which guarantees user handlers see events one at a time, in order.
///
internal void DispatchEvent(SessionEvent sessionEvent)
{
- // Handle broadcast request events (protocol v3) before dispatching to user handlers.
- // Fire-and-forget: the response is sent asynchronously via RPC.
- HandleBroadcastEventAsync(sessionEvent);
+ // Fire broadcast work concurrently (fire-and-forget with error logging).
+ // This is done outside the channel so broadcast handlers don't block the
+ // consumer loop — important when a secondary client's handler intentionally
+ // never completes (multi-client permission scenario).
+ _ = HandleBroadcastEventAsync(sessionEvent);
+
+ // Queue the event for serial processing by user handlers.
+ _eventChannel.Writer.TryWrite(sessionEvent);
+ }
- // Reading the field once gives us a snapshot; delegates are immutable.
- EventHandlers?.Invoke(sessionEvent);
+ ///
+ /// Single-reader consumer loop that processes events from the channel.
+ /// Ensures user event handlers are invoked serially and in FIFO order.
+ ///
+ private async Task ProcessEventsAsync()
+ {
+ await foreach (var sessionEvent in _eventChannel.Reader.ReadAllAsync())
+ {
+ foreach (var handler in _eventHandlers)
+ {
+ try
+ {
+ handler(sessionEvent);
+ }
+ catch (Exception ex)
+ {
+ LogEventHandlerError(ex);
+ }
+ }
+ }
}
///
@@ -339,7 +386,7 @@ internal async Task HandlePermissionRequestAsync(JsonEl
};
}
- var request = JsonSerializer.Deserialize(permissionRequestData.GetRawText(), SessionJsonContext.Default.PermissionRequest)
+ var request = JsonSerializer.Deserialize(permissionRequestData.GetRawText(), SessionEventsJsonContext.Default.PermissionRequest)
?? throw new InvalidOperationException("Failed to deserialize permission request");
var invocation = new PermissionInvocation
@@ -355,37 +402,45 @@ internal async Task HandlePermissionRequestAsync(JsonEl
/// Implements the protocol v3 broadcast model where tool calls and permission requests
/// are broadcast as session events to all clients.
///
- private async void HandleBroadcastEventAsync(SessionEvent sessionEvent)
+ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent)
{
- switch (sessionEvent)
+ try
{
- case ExternalToolRequestedEvent toolEvent:
- {
- var data = toolEvent.Data;
- if (string.IsNullOrEmpty(data.RequestId) || string.IsNullOrEmpty(data.ToolName))
- return;
-
- var tool = GetTool(data.ToolName);
- if (tool is null)
- return; // This client doesn't handle this tool; another client will.
-
- await ExecuteToolAndRespondAsync(data.RequestId, data.ToolName, data.ToolCallId, data.Arguments, tool);
- break;
- }
-
- case PermissionRequestedEvent permEvent:
- {
- var data = permEvent.Data;
- if (string.IsNullOrEmpty(data.RequestId) || data.PermissionRequest is null)
- return;
-
- var handler = _permissionHandler;
- if (handler is null)
- return; // This client doesn't handle permissions; another client will.
-
- await ExecutePermissionAndRespondAsync(data.RequestId, data.PermissionRequest, handler);
- break;
- }
+ switch (sessionEvent)
+ {
+ case ExternalToolRequestedEvent toolEvent:
+ {
+ var data = toolEvent.Data;
+ if (string.IsNullOrEmpty(data.RequestId) || string.IsNullOrEmpty(data.ToolName))
+ return;
+
+ var tool = GetTool(data.ToolName);
+ if (tool is null)
+ return; // This client doesn't handle this tool; another client will.
+
+ using (TelemetryHelpers.RestoreTraceContext(data.Traceparent, data.Tracestate))
+ await ExecuteToolAndRespondAsync(data.RequestId, data.ToolName, data.ToolCallId, data.Arguments, tool);
+ break;
+ }
+
+ case PermissionRequestedEvent permEvent:
+ {
+ var data = permEvent.Data;
+ if (string.IsNullOrEmpty(data.RequestId) || data.PermissionRequest is null)
+ return;
+
+ var handler = _permissionHandler;
+ if (handler is null)
+ return; // This client doesn't handle permissions; another client will.
+
+ await ExecutePermissionAndRespondAsync(data.RequestId, data.PermissionRequest, handler);
+ break;
+ }
+ }
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ LogBroadcastHandlerError(ex);
}
}
@@ -457,27 +512,20 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName,
///
/// Executes a permission handler and sends the result back via the HandlePendingPermissionRequest RPC.
///
- private async Task ExecutePermissionAndRespondAsync(string requestId, object permissionRequestData, PermissionRequestHandler handler)
+ private async Task ExecutePermissionAndRespondAsync(string requestId, PermissionRequest permissionRequest, PermissionRequestHandler handler)
{
try
{
- // PermissionRequestedData.PermissionRequest is typed as `object` in generated code,
- // but StreamJsonRpc deserializes it as a JsonElement.
- if (permissionRequestData is not JsonElement permJsonElement)
- {
- throw new InvalidOperationException(
- $"Permission request data must be a {nameof(JsonElement)}; received {permissionRequestData.GetType().Name}");
- }
-
- var request = JsonSerializer.Deserialize(permJsonElement.GetRawText(), SessionJsonContext.Default.PermissionRequest)
- ?? throw new InvalidOperationException("Failed to deserialize permission request");
-
var invocation = new PermissionInvocation
{
SessionId = SessionId
};
- var result = await handler(request, invocation);
+ var result = await handler(permissionRequest, invocation);
+ if (result.Kind == new PermissionRequestResultKind("no-result"))
+ {
+ return;
+ }
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, result);
}
catch (Exception)
@@ -607,6 +655,72 @@ internal void RegisterHooks(SessionHooks hooks)
};
}
+ ///
+ /// Registers transform callbacks for system message sections.
+ ///
+ /// The transform callbacks keyed by section identifier.
+ internal void RegisterTransformCallbacks(Dictionary>>? callbacks)
+ {
+ _transformCallbacksLock.Wait();
+ try
+ {
+ _transformCallbacks = callbacks;
+ }
+ finally
+ {
+ _transformCallbacksLock.Release();
+ }
+ }
+
+ ///
+ /// Handles a systemMessage.transform RPC call from the Copilot CLI.
+ ///
+ /// The raw JSON element containing sections to transform.
+ /// A task that resolves with the transformed sections.
+ internal async Task HandleSystemMessageTransformAsync(JsonElement sections)
+ {
+ Dictionary>>? callbacks;
+ await _transformCallbacksLock.WaitAsync();
+ try
+ {
+ callbacks = _transformCallbacks;
+ }
+ finally
+ {
+ _transformCallbacksLock.Release();
+ }
+
+ var parsed = JsonSerializer.Deserialize(
+ sections.GetRawText(),
+ SessionJsonContext.Default.DictionaryStringSystemMessageTransformSection) ?? new();
+
+ var result = new Dictionary();
+ foreach (var (sectionId, data) in parsed)
+ {
+ Func>? callback = null;
+ callbacks?.TryGetValue(sectionId, out callback);
+
+ if (callback != null)
+ {
+ try
+ {
+ var transformed = await callback(data.Content ?? "");
+ result[sectionId] = new SystemMessageTransformSection { Content = transformed };
+ }
+ catch
+ {
+ result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" };
+ }
+ }
+ else
+ {
+ result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" };
+ }
+ }
+
+ return new SystemMessageTransformRpcResponse { Sections = result };
+ }
+
///
/// Gets the complete list of messages and events in the session.
///
@@ -674,15 +788,48 @@ await InvokeRpcAsync
///
[JsonPropertyName("kind")]
@@ -931,7 +968,86 @@ public enum SystemMessageMode
Append,
/// Replace the default system message entirely.
[JsonStringEnumMemberName("replace")]
- Replace
+ Replace,
+ /// Override individual sections of the system prompt.
+ [JsonStringEnumMemberName("customize")]
+ Customize
+}
+
+///
+/// Specifies the operation to perform on a system prompt section.
+///
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SectionOverrideAction
+{
+ /// Replace the section content entirely.
+ [JsonStringEnumMemberName("replace")]
+ Replace,
+ /// Remove the section from the prompt.
+ [JsonStringEnumMemberName("remove")]
+ Remove,
+ /// Append content after the existing section.
+ [JsonStringEnumMemberName("append")]
+ Append,
+ /// Prepend content before the existing section.
+ [JsonStringEnumMemberName("prepend")]
+ Prepend,
+ /// Transform the section content via a callback.
+ [JsonStringEnumMemberName("transform")]
+ Transform
+}
+
+///
+/// Override operation for a single system prompt section.
+///
+public class SectionOverride
+{
+ ///
+ /// The operation to perform on this section. Ignored when Transform is set.
+ ///
+ [JsonPropertyName("action")]
+ public SectionOverrideAction? Action { get; set; }
+
+ ///
+ /// Content for the override. Optional for all actions. Ignored for remove.
+ ///
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ ///
+ /// Transform callback. When set, takes precedence over Action.
+ /// Receives current section content, returns transformed content.
+ /// Not serialized — the SDK handles this locally.
+ ///
+ [JsonIgnore]
+ public Func>? Transform { get; set; }
+}
+
+///
+/// Known system prompt section identifiers for the "customize" mode.
+///
+public static class SystemPromptSections
+{
+ /// Agent identity preamble and mode statement.
+ public const string Identity = "identity";
+ /// Response style, conciseness rules, output formatting preferences.
+ public const string Tone = "tone";
+ /// Tool usage patterns, parallel calling, batching guidelines.
+ public const string ToolEfficiency = "tool_efficiency";
+ /// CWD, OS, git root, directory listing, available tools.
+ public const string EnvironmentContext = "environment_context";
+ /// Coding rules, linting/testing, ecosystem tools, style.
+ public const string CodeChangeRules = "code_change_rules";
+ /// Tips, behavioral best practices, behavioral guidelines.
+ public const string Guidelines = "guidelines";
+ /// Environment limitations, prohibited actions, security policies.
+ public const string Safety = "safety";
+ /// Per-tool usage instructions.
+ public const string ToolInstructions = "tool_instructions";
+ /// Repository and organization custom instructions.
+ public const string CustomInstructions = "custom_instructions";
+ /// End-of-prompt instructions: parallel tool calling, persistence, task completion.
+ public const string LastInstructions = "last_instructions";
}
///
@@ -940,13 +1056,21 @@ public enum SystemMessageMode
public class SystemMessageConfig
{
///
- /// How the system message is applied (append or replace).
+ /// How the system message is applied (append, replace, or customize).
///
public SystemMessageMode? Mode { get; set; }
+
///
- /// Content of the system message.
+ /// Content of the system message. Used by append and replace modes.
+ /// In customize mode, additional content appended after all sections.
///
public string? Content { get; set; }
+
+ ///
+ /// Section-level overrides for customize mode.
+ /// Keys are section identifiers (see ).
+ ///
+ public Dictionary? Sections { get; set; }
}
///
@@ -1197,6 +1321,7 @@ protected SessionConfig(SessionConfig? other)
ClientName = other.ClientName;
ConfigDir = other.ConfigDir;
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
+ Agent = other.Agent;
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
Hooks = other.Hooks;
@@ -1205,6 +1330,7 @@ protected SessionConfig(SessionConfig? other)
? new Dictionary(other.McpServers, other.McpServers.Comparer)
: null;
Model = other.Model;
+ OnEvent = other.OnEvent;
OnPermissionRequest = other.OnPermissionRequest;
OnUserInputRequest = other.OnUserInputRequest;
Provider = other.Provider;
@@ -1307,6 +1433,12 @@ protected SessionConfig(SessionConfig? other)
///
public List? CustomAgents { get; set; }
+ ///
+ /// Name of the custom agent to activate when the session starts.
+ /// Must match the of one of the agents in .
+ ///
+ public string? Agent { get; set; }
+
///
/// Directories to load skills from.
///
@@ -1323,6 +1455,18 @@ protected SessionConfig(SessionConfig? other)
///
public InfiniteSessionConfig? InfiniteSessions { get; set; }
+ ///
+ /// Optional event handler that is registered on the session before the
+ /// session.create RPC is issued.
+ ///
+ ///
+ /// Equivalent to calling immediately
+ /// after creation, but executes earlier in the lifecycle so no events are missed.
+ /// Using this property rather than guarantees that early events emitted
+ /// by the CLI during session creation (e.g. session.start) are delivered to the handler.
+ ///
+ public SessionEventHandler? OnEvent { get; set; }
+
///
/// Creates a shallow clone of this instance.
///
@@ -1361,6 +1505,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
ClientName = other.ClientName;
ConfigDir = other.ConfigDir;
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
+ Agent = other.Agent;
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
DisableResume = other.DisableResume;
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
@@ -1370,6 +1515,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
? new Dictionary(other.McpServers, other.McpServers.Comparer)
: null;
Model = other.Model;
+ OnEvent = other.OnEvent;
OnPermissionRequest = other.OnPermissionRequest;
OnUserInputRequest = other.OnUserInputRequest;
Provider = other.Provider;
@@ -1476,6 +1622,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
///
public List? CustomAgents { get; set; }
+ ///
+ /// Name of the custom agent to activate when the session starts.
+ /// Must match the of one of the agents in .
+ ///
+ public string? Agent { get; set; }
+
///
/// Directories to load skills from.
///
@@ -1491,6 +1643,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
///
public InfiniteSessionConfig? InfiniteSessions { get; set; }
+ ///
+ /// Optional event handler registered before the session.resume RPC is issued,
+ /// ensuring early events are delivered. See .
+ ///
+ public SessionEventHandler? OnEvent { get; set; }
+
///
/// Creates a shallow clone of this instance.
///
@@ -1961,6 +2119,30 @@ public class SetForegroundSessionResponse
public string? Error { get; set; }
}
+///
+/// Content data for a single system prompt section in a transform RPC call.
+///
+public class SystemMessageTransformSection
+{
+ ///
+ /// The content of the section.
+ ///
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+}
+
+///
+/// Response to a systemMessage.transform RPC call.
+///
+public class SystemMessageTransformRpcResponse
+{
+ ///
+ /// The transformed sections keyed by section identifier.
+ ///
+ [JsonPropertyName("sections")]
+ public Dictionary? Sections { get; set; }
+}
+
[JsonSourceGenerationOptions(
JsonSerializerDefaults.Web,
AllowOutOfOrderMetadataProperties = true,
@@ -1982,7 +2164,6 @@ public class SetForegroundSessionResponse
[JsonSerializable(typeof(ModelPolicy))]
[JsonSerializable(typeof(ModelSupports))]
[JsonSerializable(typeof(ModelVisionLimits))]
-[JsonSerializable(typeof(PermissionRequest))]
[JsonSerializable(typeof(PermissionRequestResult))]
[JsonSerializable(typeof(PingRequest))]
[JsonSerializable(typeof(PingResponse))]
@@ -1991,6 +2172,7 @@ public class SetForegroundSessionResponse
[JsonSerializable(typeof(SessionLifecycleEvent))]
[JsonSerializable(typeof(SessionLifecycleEventMetadata))]
[JsonSerializable(typeof(SessionListFilter))]
+[JsonSerializable(typeof(SectionOverride))]
[JsonSerializable(typeof(SessionMetadata))]
[JsonSerializable(typeof(SetForegroundSessionResponse))]
[JsonSerializable(typeof(SystemMessageConfig))]
diff --git a/dotnet/test/ClientTests.cs b/dotnet/test/ClientTests.cs
index 3c3f3bdaae..6c70ffaa3a 100644
--- a/dotnet/test/ClientTests.cs
+++ b/dotnet/test/ClientTests.cs
@@ -274,4 +274,104 @@ public async Task Should_Throw_When_ResumeSession_Called_Without_PermissionHandl
Assert.Contains("OnPermissionRequest", ex.Message);
Assert.Contains("is required", ex.Message);
}
+
+ [Fact]
+ public async Task ListModels_WithCustomHandler_CallsHandler()
+ {
+ var customModels = new List
+ {
+ new()
+ {
+ Id = "my-custom-model",
+ Name = "My Custom Model",
+ Capabilities = new ModelCapabilities
+ {
+ Supports = new ModelSupports { Vision = false, ReasoningEffort = false },
+ Limits = new ModelLimits { MaxContextWindowTokens = 128000 }
+ }
+ }
+ };
+
+ var callCount = 0;
+ await using var client = new CopilotClient(new CopilotClientOptions
+ {
+ OnListModels = (ct) =>
+ {
+ callCount++;
+ return Task.FromResult(customModels);
+ }
+ });
+ await client.StartAsync();
+
+ var models = await client.ListModelsAsync();
+ Assert.Equal(1, callCount);
+ Assert.Single(models);
+ Assert.Equal("my-custom-model", models[0].Id);
+ }
+
+ [Fact]
+ public async Task ListModels_WithCustomHandler_CachesResults()
+ {
+ var customModels = new List
+ {
+ new()
+ {
+ Id = "cached-model",
+ Name = "Cached Model",
+ Capabilities = new ModelCapabilities
+ {
+ Supports = new ModelSupports { Vision = false, ReasoningEffort = false },
+ Limits = new ModelLimits { MaxContextWindowTokens = 128000 }
+ }
+ }
+ };
+
+ var callCount = 0;
+ await using var client = new CopilotClient(new CopilotClientOptions
+ {
+ OnListModels = (ct) =>
+ {
+ callCount++;
+ return Task.FromResult(customModels);
+ }
+ });
+ await client.StartAsync();
+
+ await client.ListModelsAsync();
+ await client.ListModelsAsync();
+ Assert.Equal(1, callCount); // Only called once due to caching
+ }
+
+ [Fact]
+ public async Task ListModels_WithCustomHandler_WorksWithoutStart()
+ {
+ var customModels = new List
+ {
+ new()
+ {
+ Id = "no-start-model",
+ Name = "No Start Model",
+ Capabilities = new ModelCapabilities
+ {
+ Supports = new ModelSupports { Vision = false, ReasoningEffort = false },
+ Limits = new ModelLimits { MaxContextWindowTokens = 128000 }
+ }
+ }
+ };
+
+ var callCount = 0;
+ await using var client = new CopilotClient(new CopilotClientOptions
+ {
+ OnListModels = (ct) =>
+ {
+ callCount++;
+ return Task.FromResult(customModels);
+ }
+ });
+
+ var models = await client.ListModelsAsync();
+ Assert.Equal(1, callCount);
+ Assert.Single(models);
+ Assert.Equal("no-start-model", models[0].Id);
+ }
}
diff --git a/dotnet/test/CloneTests.cs b/dotnet/test/CloneTests.cs
index 8982c5d64f..a0051ffbc5 100644
--- a/dotnet/test/CloneTests.cs
+++ b/dotnet/test/CloneTests.cs
@@ -22,7 +22,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
CliUrl = "http://localhost:8080",
LogLevel = "debug",
AutoStart = false,
- AutoRestart = false,
+
Environment = new Dictionary { ["KEY"] = "value" },
GitHubToken = "ghp_test",
UseLoggedInUser = false,
@@ -38,7 +38,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
Assert.Equal(original.CliUrl, clone.CliUrl);
Assert.Equal(original.LogLevel, clone.LogLevel);
Assert.Equal(original.AutoStart, clone.AutoStart);
- Assert.Equal(original.AutoRestart, clone.AutoRestart);
+
Assert.Equal(original.Environment, clone.Environment);
Assert.Equal(original.GitHubToken, clone.GitHubToken);
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
@@ -88,6 +88,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Streaming = true,
McpServers = new Dictionary { ["server1"] = new object() },
CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
+ Agent = "agent1",
SkillDirectories = ["/skills"],
DisabledSkills = ["skill1"],
};
@@ -105,6 +106,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Assert.Equal(original.Streaming, clone.Streaming);
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
+ Assert.Equal(original.Agent, clone.Agent);
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
}
@@ -242,4 +244,32 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
Assert.Null(clone.DisabledSkills);
Assert.Null(clone.Tools);
}
+
+ [Fact]
+ public void SessionConfig_Clone_CopiesAgentProperty()
+ {
+ var original = new SessionConfig
+ {
+ Agent = "test-agent",
+ CustomAgents = [new CustomAgentConfig { Name = "test-agent", Prompt = "You are a test agent." }],
+ };
+
+ var clone = original.Clone();
+
+ Assert.Equal("test-agent", clone.Agent);
+ }
+
+ [Fact]
+ public void ResumeSessionConfig_Clone_CopiesAgentProperty()
+ {
+ var original = new ResumeSessionConfig
+ {
+ Agent = "test-agent",
+ CustomAgents = [new CustomAgentConfig { Name = "test-agent", Prompt = "You are a test agent." }],
+ };
+
+ var clone = original.Clone();
+
+ Assert.Equal("test-agent", clone.Agent);
+ }
}
diff --git a/dotnet/test/ForwardCompatibilityTests.cs b/dotnet/test/ForwardCompatibilityTests.cs
new file mode 100644
index 0000000000..d3f5b77851
--- /dev/null
+++ b/dotnet/test/ForwardCompatibilityTests.cs
@@ -0,0 +1,100 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using Xunit;
+
+namespace GitHub.Copilot.SDK.Test;
+
+///
+/// Tests for forward-compatible handling of unknown session event types.
+/// Verifies that the SDK gracefully handles event types introduced by newer CLI versions.
+///
+public class ForwardCompatibilityTests
+{
+ [Fact]
+ public void FromJson_KnownEventType_DeserializesNormally()
+ {
+ var json = """
+ {
+ "id": "00000000-0000-0000-0000-000000000001",
+ "timestamp": "2026-01-01T00:00:00Z",
+ "parentId": null,
+ "type": "user.message",
+ "data": {
+ "content": "Hello"
+ }
+ }
+ """;
+
+ var result = SessionEvent.FromJson(json);
+
+ Assert.IsType(result);
+ Assert.Equal("user.message", result.Type);
+ }
+
+ [Fact]
+ public void FromJson_UnknownEventType_ReturnsBaseSessionEvent()
+ {
+ var json = """
+ {
+ "id": "12345678-1234-1234-1234-123456789abc",
+ "timestamp": "2026-06-15T10:30:00Z",
+ "parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
+ "type": "future.feature_from_server",
+ "data": { "key": "value" }
+ }
+ """;
+
+ var result = SessionEvent.FromJson(json);
+
+ Assert.IsType(result);
+ Assert.Equal("unknown", result.Type);
+ }
+
+ [Fact]
+ public void FromJson_UnknownEventType_PreservesBaseMetadata()
+ {
+ var json = """
+ {
+ "id": "12345678-1234-1234-1234-123456789abc",
+ "timestamp": "2026-06-15T10:30:00Z",
+ "parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
+ "type": "future.feature_from_server",
+ "data": {}
+ }
+ """;
+
+ var result = SessionEvent.FromJson(json);
+
+ Assert.Equal(Guid.Parse("12345678-1234-1234-1234-123456789abc"), result.Id);
+ Assert.Equal(DateTimeOffset.Parse("2026-06-15T10:30:00Z"), result.Timestamp);
+ Assert.Equal(Guid.Parse("abcdefab-abcd-abcd-abcd-abcdefabcdef"), result.ParentId);
+ }
+
+ [Fact]
+ public void FromJson_MultipleEvents_MixedKnownAndUnknown()
+ {
+ var events = new[]
+ {
+ """{"id":"00000000-0000-0000-0000-000000000001","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Hi"}}""",
+ """{"id":"00000000-0000-0000-0000-000000000002","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"future.unknown_type","data":{}}""",
+ """{"id":"00000000-0000-0000-0000-000000000003","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Bye"}}""",
+ };
+
+ var results = events.Select(SessionEvent.FromJson).ToList();
+
+ Assert.Equal(3, results.Count);
+ Assert.IsType(results[0]);
+ Assert.IsType(results[1]);
+ Assert.IsType(results[2]);
+ }
+
+ [Fact]
+ public void SessionEvent_Type_DefaultsToUnknown()
+ {
+ var evt = new SessionEvent();
+
+ Assert.Equal("unknown", evt.Type);
+ }
+}
diff --git a/dotnet/test/GitHub.Copilot.SDK.Test.csproj b/dotnet/test/GitHub.Copilot.SDK.Test.csproj
index fbc9f17c37..8e0dbf6b79 100644
--- a/dotnet/test/GitHub.Copilot.SDK.Test.csproj
+++ b/dotnet/test/GitHub.Copilot.SDK.Test.csproj
@@ -2,6 +2,7 @@
false
+ $(NoWarn);GHCP001
diff --git a/dotnet/test/Harness/TestHelper.cs b/dotnet/test/Harness/TestHelper.cs
index 6dd919bc74..a04e436569 100644
--- a/dotnet/test/Harness/TestHelper.cs
+++ b/dotnet/test/Harness/TestHelper.cs
@@ -10,7 +10,7 @@ public static class TestHelper
CopilotSession session,
TimeSpan? timeout = null)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var cts = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(60));
AssistantMessageEvent? finalAssistantMessage = null;
@@ -78,7 +78,7 @@ public static async Task GetNextEventOfTypeAsync(
CopilotSession session,
TimeSpan? timeout = null) where T : SessionEvent
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var cts = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(60));
using var subscription = session.On(evt =>
diff --git a/dotnet/test/MultiClientTests.cs b/dotnet/test/MultiClientTests.cs
index 131fd31d05..bdd264a4a5 100644
--- a/dotnet/test/MultiClientTests.cs
+++ b/dotnet/test/MultiClientTests.cs
@@ -109,10 +109,10 @@ public async Task Both_Clients_See_Tool_Request_And_Completion_Events()
});
// Set up event waiters BEFORE sending the prompt to avoid race conditions
- var client1Requested = new TaskCompletionSource();
- var client2Requested = new TaskCompletionSource();
- var client1Completed = new TaskCompletionSource();
- var client2Completed = new TaskCompletionSource();
+ var client1Requested = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var client2Requested = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var client1Completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var client2Completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var sub1 = session1.On(evt =>
{
@@ -134,11 +134,9 @@ public async Task Both_Clients_See_Tool_Request_And_Completion_Events()
Assert.Contains("MAGIC_hello_42", response!.Data.Content ?? string.Empty);
// Wait for all broadcast events to arrive on both clients
- var timeout = Task.Delay(TimeSpan.FromSeconds(10));
- var allEvents = Task.WhenAll(
+ await Task.WhenAll(
client1Requested.Task, client2Requested.Task,
- client1Completed.Task, client2Completed.Task);
- Assert.Equal(allEvents, await Task.WhenAny(allEvents, timeout));
+ client1Completed.Task, client2Completed.Task).WaitAsync(TimeSpan.FromSeconds(10));
await session2.DisposeAsync();
diff --git a/dotnet/test/PermissionRequestResultKindTests.cs b/dotnet/test/PermissionRequestResultKindTests.cs
index d0cfed6f01..ea77295e25 100644
--- a/dotnet/test/PermissionRequestResultKindTests.cs
+++ b/dotnet/test/PermissionRequestResultKindTests.cs
@@ -21,6 +21,7 @@ public void WellKnownKinds_HaveExpectedValues()
Assert.Equal("denied-by-rules", PermissionRequestResultKind.DeniedByRules.Value);
Assert.Equal("denied-no-approval-rule-and-could-not-request-from-user", PermissionRequestResultKind.DeniedCouldNotRequestFromUser.Value);
Assert.Equal("denied-interactively-by-user", PermissionRequestResultKind.DeniedInteractivelyByUser.Value);
+ Assert.Equal("no-result", new PermissionRequestResultKind("no-result").Value);
}
[Fact]
@@ -115,6 +116,7 @@ public void JsonRoundTrip_PreservesAllKinds()
PermissionRequestResultKind.DeniedByRules,
PermissionRequestResultKind.DeniedCouldNotRequestFromUser,
PermissionRequestResultKind.DeniedInteractivelyByUser,
+ new PermissionRequestResultKind("no-result"),
};
foreach (var kind in kinds)
diff --git a/dotnet/test/PermissionTests.cs b/dotnet/test/PermissionTests.cs
index 59a3cb4dd5..3ab36dad19 100644
--- a/dotnet/test/PermissionTests.cs
+++ b/dotnet/test/PermissionTests.cs
@@ -231,7 +231,7 @@ public async Task Should_Receive_ToolCallId_In_Permission_Requests()
{
OnPermissionRequest = (request, invocation) =>
{
- if (!string.IsNullOrEmpty(request.ToolCallId))
+ if (request is PermissionRequestShell shell && !string.IsNullOrEmpty(shell.ToolCallId))
{
receivedToolCallId = true;
}
diff --git a/dotnet/test/RpcTests.cs b/dotnet/test/RpcTests.cs
index a13695589d..e041033bd5 100644
--- a/dotnet/test/RpcTests.cs
+++ b/dotnet/test/RpcTests.cs
@@ -72,8 +72,8 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo()
var before = await session.Rpc.Model.GetCurrentAsync();
Assert.NotNull(before.ModelId);
- // Switch to a different model
- var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-4.1");
+ // Switch to a different model with reasoning effort
+ var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-4.1", reasoningEffort: "high");
Assert.Equal("gpt-4.1", result.ModelId);
// Verify the switch persisted
diff --git a/dotnet/test/SerializationTests.cs b/dotnet/test/SerializationTests.cs
new file mode 100644
index 0000000000..6fb266be15
--- /dev/null
+++ b/dotnet/test/SerializationTests.cs
@@ -0,0 +1,80 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using Xunit;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using StreamJsonRpc;
+
+namespace GitHub.Copilot.SDK.Test;
+
+///
+/// Tests for JSON serialization compatibility, particularly for StreamJsonRpc types
+/// that are needed when CancellationTokens fire during JSON-RPC operations.
+/// This test suite verifies the fix for https://github.com/PureWeen/PolyPilot/issues/319
+///
+public class SerializationTests
+{
+ ///
+ /// Verifies that StreamJsonRpc.RequestId can be round-tripped using the SDK's configured
+ /// JsonSerializerOptions. This is critical for preventing NotSupportedException when
+ /// StandardCancellationStrategy fires during JSON-RPC operations.
+ ///
+ [Fact]
+ public void RequestId_CanBeSerializedAndDeserialized_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+
+ // Long id
+ var jsonLong = JsonSerializer.Serialize(new RequestId(42L), options);
+ Assert.Equal("42", jsonLong);
+ Assert.Equal(new RequestId(42L), JsonSerializer.Deserialize(jsonLong, options));
+
+ // String id
+ var jsonStr = JsonSerializer.Serialize(new RequestId("req-1"), options);
+ Assert.Equal("\"req-1\"", jsonStr);
+ Assert.Equal(new RequestId("req-1"), JsonSerializer.Deserialize(jsonStr, options));
+
+ // Null id
+ var jsonNull = JsonSerializer.Serialize(RequestId.Null, options);
+ Assert.Equal("null", jsonNull);
+ Assert.Equal(RequestId.Null, JsonSerializer.Deserialize(jsonNull, options));
+ }
+
+ [Theory]
+ [InlineData(0L)]
+ [InlineData(-1L)]
+ [InlineData(long.MaxValue)]
+ public void RequestId_NumericEdgeCases_RoundTrip(long id)
+ {
+ var options = GetSerializerOptions();
+ var requestId = new RequestId(id);
+ var json = JsonSerializer.Serialize(requestId, options);
+ Assert.Equal(requestId, JsonSerializer.Deserialize(json, options));
+ }
+
+ ///
+ /// Verifies the SDK's options can resolve type info for RequestId,
+ /// ensuring AOT-safe serialization without falling back to reflection.
+ ///
+ [Fact]
+ public void SerializerOptions_CanResolveRequestIdTypeInfo()
+ {
+ var options = GetSerializerOptions();
+ var typeInfo = options.GetTypeInfo(typeof(RequestId));
+ Assert.NotNull(typeInfo);
+ Assert.Equal(typeof(RequestId), typeInfo.Type);
+ }
+
+ private static JsonSerializerOptions GetSerializerOptions()
+ {
+ var prop = typeof(CopilotClient)
+ .GetProperty("SerializerOptionsForMessageFormatter",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
+
+ var options = (JsonSerializerOptions?)prop?.GetValue(null);
+ Assert.NotNull(options);
+ return options;
+ }
+}
diff --git a/dotnet/test/SessionEventSerializationTests.cs b/dotnet/test/SessionEventSerializationTests.cs
new file mode 100644
index 0000000000..e7be64422c
--- /dev/null
+++ b/dotnet/test/SessionEventSerializationTests.cs
@@ -0,0 +1,180 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Xunit;
+
+namespace GitHub.Copilot.SDK.Test;
+
+public class SessionEventSerializationTests
+{
+ public static TheoryData JsonElementBackedEvents => new()
+ {
+ {
+ new AssistantMessageEvent
+ {
+ Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
+ Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"),
+ ParentId = Guid.Parse("22222222-2222-2222-2222-222222222222"),
+ Data = new AssistantMessageData
+ {
+ MessageId = "msg-1",
+ Content = "",
+ ToolRequests =
+ [
+ new AssistantMessageDataToolRequestsItem
+ {
+ ToolCallId = "call-1",
+ Name = "view",
+ Arguments = ParseJsonElement("""{"path":"README.md"}"""),
+ Type = AssistantMessageDataToolRequestsItemType.Function,
+ },
+ ],
+ },
+ },
+ "assistant.message"
+ },
+ {
+ new ToolExecutionStartEvent
+ {
+ Id = Guid.Parse("33333333-3333-3333-3333-333333333333"),
+ Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"),
+ ParentId = Guid.Parse("44444444-4444-4444-4444-444444444444"),
+ Data = new ToolExecutionStartData
+ {
+ ToolCallId = "call-1",
+ ToolName = "view",
+ Arguments = ParseJsonElement("""{"path":"README.md"}"""),
+ },
+ },
+ "tool.execution_start"
+ },
+ {
+ new ToolExecutionCompleteEvent
+ {
+ Id = Guid.Parse("55555555-5555-5555-5555-555555555555"),
+ Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"),
+ ParentId = Guid.Parse("66666666-6666-6666-6666-666666666666"),
+ Data = new ToolExecutionCompleteData
+ {
+ ToolCallId = "call-1",
+ Success = true,
+ Result = new ToolExecutionCompleteDataResult
+ {
+ Content = "ok",
+ DetailedContent = "ok",
+ },
+ ToolTelemetry = new Dictionary
+ {
+ ["properties"] = ParseJsonElement("""{"command":"view"}"""),
+ ["metrics"] = ParseJsonElement("""{"resultLength":2}"""),
+ },
+ },
+ },
+ "tool.execution_complete"
+ },
+ {
+ new SessionShutdownEvent
+ {
+ Id = Guid.Parse("77777777-7777-7777-7777-777777777777"),
+ Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:52.987Z"),
+ ParentId = Guid.Parse("88888888-8888-8888-8888-888888888888"),
+ Data = new SessionShutdownData
+ {
+ ShutdownType = SessionShutdownDataShutdownType.Routine,
+ TotalPremiumRequests = 1,
+ TotalApiDurationMs = 100,
+ SessionStartTime = 1773609948932,
+ CodeChanges = new SessionShutdownDataCodeChanges
+ {
+ LinesAdded = 1,
+ LinesRemoved = 0,
+ FilesModified = ["README.md"],
+ },
+ ModelMetrics = new Dictionary
+ {
+ ["gpt-5.4"] = ParseJsonElement("""
+ {
+ "requests": {
+ "count": 1,
+ "cost": 1
+ },
+ "usage": {
+ "inputTokens": 10,
+ "outputTokens": 5,
+ "cacheReadTokens": 0,
+ "cacheWriteTokens": 0
+ }
+ }
+ """),
+ },
+ CurrentModel = "gpt-5.4",
+ },
+ },
+ "session.shutdown"
+ }
+ };
+
+ private static JsonElement ParseJsonElement(string json)
+ {
+ using var document = JsonDocument.Parse(json);
+ return document.RootElement.Clone();
+ }
+
+ [Theory]
+ [MemberData(nameof(JsonElementBackedEvents))]
+ public void SessionEvent_ToJson_RoundTrips_JsonElementBackedPayloads(SessionEvent sessionEvent, string expectedType)
+ {
+ var serialized = sessionEvent.ToJson();
+
+ using var document = JsonDocument.Parse(serialized);
+ var root = document.RootElement;
+
+ Assert.Equal(expectedType, root.GetProperty("type").GetString());
+
+ switch (expectedType)
+ {
+ case "assistant.message":
+ Assert.Equal(
+ "README.md",
+ root.GetProperty("data")
+ .GetProperty("toolRequests")[0]
+ .GetProperty("arguments")
+ .GetProperty("path")
+ .GetString());
+ break;
+
+ case "tool.execution_start":
+ Assert.Equal(
+ "README.md",
+ root.GetProperty("data")
+ .GetProperty("arguments")
+ .GetProperty("path")
+ .GetString());
+ break;
+
+ case "tool.execution_complete":
+ Assert.Equal(
+ "view",
+ root.GetProperty("data")
+ .GetProperty("toolTelemetry")
+ .GetProperty("properties")
+ .GetProperty("command")
+ .GetString());
+ break;
+
+ case "session.shutdown":
+ Assert.Equal(
+ 1,
+ root.GetProperty("data")
+ .GetProperty("modelMetrics")
+ .GetProperty("gpt-5.4")
+ .GetProperty("requests")
+ .GetProperty("count")
+ .GetInt32());
+ break;
+ }
+ }
+}
diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs
index e710835dc6..5aecaccbaf 100644
--- a/dotnet/test/SessionTests.cs
+++ b/dotnet/test/SessionTests.cs
@@ -3,6 +3,7 @@
*--------------------------------------------------------------------------------------------*/
using GitHub.Copilot.SDK.Test.Harness;
+using GitHub.Copilot.SDK.Rpc;
using Microsoft.Extensions.AI;
using System.ComponentModel;
using Xunit;
@@ -90,6 +91,37 @@ public async Task Should_Create_A_Session_With_Replaced_SystemMessage_Config()
Assert.Equal(testSystemMessage, GetSystemMessage(traffic[0]));
}
+ [Fact]
+ public async Task Should_Create_A_Session_With_Customized_SystemMessage_Config()
+ {
+ var customTone = "Respond in a warm, professional tone. Be thorough in explanations.";
+ var appendedContent = "Always mention quarterly earnings.";
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ [SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = customTone },
+ [SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove },
+ },
+ Content = appendedContent
+ }
+ });
+
+ await session.SendAsync(new MessageOptions { Prompt = "Who are you?" });
+ var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
+ Assert.NotNull(assistantMessage);
+
+ var traffic = await Ctx.GetExchangesAsync();
+ Assert.NotEmpty(traffic);
+ var systemMessage = GetSystemMessage(traffic[0]);
+ Assert.Contains(customTone, systemMessage);
+ Assert.Contains(appendedContent, systemMessage);
+ Assert.DoesNotContain("", systemMessage);
+ }
+
[Fact]
public async Task Should_Create_A_Session_With_AvailableTools()
{
@@ -244,12 +276,44 @@ await session.SendAsync(new MessageOptions
[Fact]
public async Task Should_Receive_Session_Events()
{
- var session = await CreateSessionAsync();
+ // Use OnEvent to capture events dispatched during session creation.
+ // session.start is emitted during the session.create RPC; if the session
+ // weren't registered in the sessions map before the RPC, it would be dropped.
+ var earlyEvents = new List();
+ var sessionStartReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ OnEvent = evt =>
+ {
+ earlyEvents.Add(evt);
+ if (evt is SessionStartEvent)
+ sessionStartReceived.TrySetResult(true);
+ },
+ });
+
+ // session.start is dispatched asynchronously via the event channel;
+ // wait briefly for the consumer to deliver it.
+ var started = await Task.WhenAny(sessionStartReceived.Task, Task.Delay(TimeSpan.FromSeconds(5)));
+ Assert.Equal(sessionStartReceived.Task, started);
+ Assert.Contains(earlyEvents, evt => evt is SessionStartEvent);
+
var receivedEvents = new List();
- var idleReceived = new TaskCompletionSource();
+ var idleReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var concurrentCount = 0;
+ var maxConcurrent = 0;
session.On(evt =>
{
+ // Track concurrent handler invocations to verify serial dispatch.
+ var current = Interlocked.Increment(ref concurrentCount);
+ var seenMax = Volatile.Read(ref maxConcurrent);
+ if (current > seenMax)
+ Interlocked.CompareExchange(ref maxConcurrent, current, seenMax);
+
+ Thread.Sleep(10);
+
+ Interlocked.Decrement(ref concurrentCount);
+
receivedEvents.Add(evt);
if (evt is SessionIdleEvent)
{
@@ -261,8 +325,7 @@ public async Task Should_Receive_Session_Events()
await session.SendAsync(new MessageOptions { Prompt = "What is 100+200?" });
// Wait for session to become idle (indicating message processing is complete)
- var completed = await Task.WhenAny(idleReceived.Task, Task.Delay(TimeSpan.FromSeconds(60)));
- Assert.Equal(idleReceived.Task, completed);
+ await idleReceived.Task.WaitAsync(TimeSpan.FromSeconds(60));
// Should have received multiple events (user message, assistant message, idle, etc.)
Assert.NotEmpty(receivedEvents);
@@ -270,6 +333,9 @@ public async Task Should_Receive_Session_Events()
Assert.Contains(receivedEvents, evt => evt is AssistantMessageEvent);
Assert.Contains(receivedEvents, evt => evt is SessionIdleEvent);
+ // Events must be dispatched serially — never more than one handler invocation at a time.
+ Assert.Equal(1, maxConcurrent);
+
// Verify the assistant response contains the expected answer
var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
Assert.NotNull(assistantMessage);
@@ -404,4 +470,136 @@ public async Task Should_Set_Model_On_Existing_Session()
var modelChanged = await modelChangedTask;
Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
}
+
+ [Fact]
+ public async Task Should_Set_Model_With_ReasoningEffort()
+ {
+ var session = await CreateSessionAsync();
+
+ var modelChangedTask = TestHelper.GetNextEventOfTypeAsync(session);
+
+ await session.SetModelAsync("gpt-4.1", "high");
+
+ var modelChanged = await modelChangedTask;
+ Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
+ Assert.Equal("high", modelChanged.Data.ReasoningEffort);
+ }
+
+ [Fact]
+ public async Task Should_Log_Messages_At_Various_Levels()
+ {
+ var session = await CreateSessionAsync();
+ var events = new List();
+ session.On(evt => events.Add(evt));
+
+ await session.LogAsync("Info message");
+ await session.LogAsync("Warning message", level: SessionLogRequestLevel.Warning);
+ await session.LogAsync("Error message", level: SessionLogRequestLevel.Error);
+ await session.LogAsync("Ephemeral message", ephemeral: true);
+
+ // Poll until all 4 notification events arrive
+ await WaitForAsync(() =>
+ {
+ var notifications = events.Where(e =>
+ e is SessionInfoEvent info && info.Data.InfoType == "notification" ||
+ e is SessionWarningEvent warn && warn.Data.WarningType == "notification" ||
+ e is SessionErrorEvent err && err.Data.ErrorType == "notification"
+ ).ToList();
+ return notifications.Count >= 4;
+ }, timeout: TimeSpan.FromSeconds(10));
+
+ var infoEvent = events.OfType().First(e => e.Data.Message == "Info message");
+ Assert.Equal("notification", infoEvent.Data.InfoType);
+
+ var warningEvent = events.OfType().First(e => e.Data.Message == "Warning message");
+ Assert.Equal("notification", warningEvent.Data.WarningType);
+
+ var errorEvent = events.OfType().First(e => e.Data.Message == "Error message");
+ Assert.Equal("notification", errorEvent.Data.ErrorType);
+
+ var ephemeralEvent = events.OfType().First(e => e.Data.Message == "Ephemeral message");
+ Assert.Equal("notification", ephemeralEvent.Data.InfoType);
+ }
+
+ [Fact]
+ public async Task Handler_Exception_Does_Not_Halt_Event_Delivery()
+ {
+ var session = await CreateSessionAsync();
+ var eventCount = 0;
+ var gotIdle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ session.On(evt =>
+ {
+ eventCount++;
+
+ // Throw on the first event to verify the loop keeps going.
+ if (eventCount == 1)
+ throw new InvalidOperationException("boom");
+
+ if (evt is SessionIdleEvent)
+ gotIdle.TrySetResult();
+ });
+
+ await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" });
+
+ await gotIdle.Task.WaitAsync(TimeSpan.FromSeconds(30));
+
+ // Handler saw more than just the first (throwing) event.
+ Assert.True(eventCount > 1);
+ }
+
+ [Fact]
+ public async Task DisposeAsync_From_Handler_Does_Not_Deadlock()
+ {
+ var session = await CreateSessionAsync();
+ var disposed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ session.On(evt =>
+ {
+ if (evt is UserMessageEvent)
+ {
+ // Call DisposeAsync from within a handler — must not deadlock.
+ session.DisposeAsync().AsTask().ContinueWith(_ => disposed.TrySetResult());
+ }
+ });
+
+ await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" });
+
+ // If this times out, we deadlocked.
+ await disposed.Task.WaitAsync(TimeSpan.FromSeconds(10));
+ }
+
+ [Fact]
+ public async Task Should_Accept_Blob_Attachments()
+ {
+ var session = await CreateSessionAsync();
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Describe this image",
+ Attachments =
+ [
+ new UserMessageDataAttachmentsItemBlob
+ {
+ Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
+ MimeType = "image/png",
+ DisplayName = "test-pixel.png",
+ },
+ ],
+ });
+
+ // Just verify send doesn't throw — blob attachment support varies by runtime
+ await session.DisposeAsync();
+ }
+
+ private static async Task WaitForAsync(Func condition, TimeSpan timeout)
+ {
+ var deadline = DateTime.UtcNow + timeout;
+ while (!condition())
+ {
+ if (DateTime.UtcNow > deadline)
+ throw new TimeoutException($"Condition not met within {timeout}");
+ await Task.Delay(100);
+ }
+ }
}
diff --git a/dotnet/test/SystemMessageTransformTests.cs b/dotnet/test/SystemMessageTransformTests.cs
new file mode 100644
index 0000000000..cdddc5a79f
--- /dev/null
+++ b/dotnet/test/SystemMessageTransformTests.cs
@@ -0,0 +1,140 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using GitHub.Copilot.SDK.Test.Harness;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace GitHub.Copilot.SDK.Test;
+
+public class SystemMessageTransformTests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "system_message_transform", output)
+{
+ [Fact]
+ public async Task Should_Invoke_Transform_Callbacks_With_Section_Content()
+ {
+ var identityCallbackInvoked = false;
+ var toneCallbackInvoked = false;
+
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ ["identity"] = new SectionOverride
+ {
+ Transform = async (content) =>
+ {
+ Assert.False(string.IsNullOrEmpty(content));
+ identityCallbackInvoked = true;
+ return content;
+ }
+ },
+ ["tone"] = new SectionOverride
+ {
+ Transform = async (content) =>
+ {
+ Assert.False(string.IsNullOrEmpty(content));
+ toneCallbackInvoked = true;
+ return content;
+ }
+ }
+ }
+ }
+ });
+
+ await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "test.txt"), "Hello transform!");
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Read the contents of test.txt and tell me what it says"
+ });
+
+ await TestHelper.GetFinalAssistantMessageAsync(session);
+
+ Assert.True(identityCallbackInvoked, "Expected identity transform callback to be invoked");
+ Assert.True(toneCallbackInvoked, "Expected tone transform callback to be invoked");
+ }
+
+ [Fact]
+ public async Task Should_Apply_Transform_Modifications_To_Section_Content()
+ {
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ ["identity"] = new SectionOverride
+ {
+ Transform = async (content) =>
+ {
+ return content + "\nAlways end your reply with TRANSFORM_MARKER";
+ }
+ }
+ }
+ }
+ });
+
+ await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "hello.txt"), "Hello!");
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Read the contents of hello.txt"
+ });
+
+ await TestHelper.GetFinalAssistantMessageAsync(session);
+
+ // Verify the transform result was actually applied to the system message
+ var traffic = await Ctx.GetExchangesAsync();
+ Assert.NotEmpty(traffic);
+ var systemMessage = GetSystemMessage(traffic[0]);
+ Assert.Contains("TRANSFORM_MARKER", systemMessage);
+ }
+
+ [Fact]
+ public async Task Should_Work_With_Static_Overrides_And_Transforms_Together()
+ {
+ var transformCallbackInvoked = false;
+
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ ["safety"] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Remove
+ },
+ ["identity"] = new SectionOverride
+ {
+ Transform = async (content) =>
+ {
+ transformCallbackInvoked = true;
+ return content;
+ }
+ }
+ }
+ }
+ });
+
+ await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "combo.txt"), "Combo test!");
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Read the contents of combo.txt and tell me what it says"
+ });
+
+ await TestHelper.GetFinalAssistantMessageAsync(session);
+
+ Assert.True(transformCallbackInvoked, "Expected identity transform callback to be invoked");
+ }
+}
diff --git a/dotnet/test/TelemetryTests.cs b/dotnet/test/TelemetryTests.cs
new file mode 100644
index 0000000000..2d23d584f0
--- /dev/null
+++ b/dotnet/test/TelemetryTests.cs
@@ -0,0 +1,65 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using System.Diagnostics;
+using Xunit;
+
+namespace GitHub.Copilot.SDK.Test;
+
+public class TelemetryTests
+{
+ [Fact]
+ public void TelemetryConfig_DefaultValues_AreNull()
+ {
+ var config = new TelemetryConfig();
+
+ Assert.Null(config.OtlpEndpoint);
+ Assert.Null(config.FilePath);
+ Assert.Null(config.ExporterType);
+ Assert.Null(config.SourceName);
+ Assert.Null(config.CaptureContent);
+ }
+
+ [Fact]
+ public void TelemetryConfig_CanSetAllProperties()
+ {
+ var config = new TelemetryConfig
+ {
+ OtlpEndpoint = "http://localhost:4318",
+ FilePath = "/tmp/traces.json",
+ ExporterType = "otlp-http",
+ SourceName = "my-app",
+ CaptureContent = true
+ };
+
+ Assert.Equal("http://localhost:4318", config.OtlpEndpoint);
+ Assert.Equal("/tmp/traces.json", config.FilePath);
+ Assert.Equal("otlp-http", config.ExporterType);
+ Assert.Equal("my-app", config.SourceName);
+ Assert.True(config.CaptureContent);
+ }
+
+ [Fact]
+ public void CopilotClientOptions_Telemetry_DefaultsToNull()
+ {
+ var options = new CopilotClientOptions();
+
+ Assert.Null(options.Telemetry);
+ }
+
+ [Fact]
+ public void CopilotClientOptions_Clone_CopiesTelemetry()
+ {
+ var telemetry = new TelemetryConfig
+ {
+ OtlpEndpoint = "http://localhost:4318",
+ ExporterType = "otlp-http"
+ };
+
+ var options = new CopilotClientOptions { Telemetry = telemetry };
+ var clone = options.Clone();
+
+ Assert.Same(telemetry, clone.Telemetry);
+ }
+}
diff --git a/dotnet/test/ToolsTests.cs b/dotnet/test/ToolsTests.cs
index b31ef1f93d..c2350cbffe 100644
--- a/dotnet/test/ToolsTests.cs
+++ b/dotnet/test/ToolsTests.cs
@@ -181,6 +181,42 @@ static string CustomGrep([Description("Search query")] string query)
=> $"CUSTOM_GREP_RESULT: {query}";
}
+ [Fact]
+ public async Task SkipPermission_Sent_In_Tool_Definition()
+ {
+ [Description("A tool that skips permission")]
+ static string SafeLookup([Description("Lookup ID")] string id)
+ => $"RESULT: {id}";
+
+ var tool = AIFunctionFactory.Create((Delegate)SafeLookup, new AIFunctionFactoryOptions
+ {
+ Name = "safe_lookup",
+ AdditionalProperties = new ReadOnlyDictionary(
+ new Dictionary { ["skip_permission"] = true })
+ });
+
+ var didRunPermissionRequest = false;
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ Tools = [tool],
+ OnPermissionRequest = (_, _) =>
+ {
+ didRunPermissionRequest = true;
+ return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.NoResult });
+ }
+ });
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Use safe_lookup to look up 'test123'"
+ });
+
+ var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
+ Assert.NotNull(assistantMessage);
+ Assert.Contains("RESULT", assistantMessage!.Data.Content ?? string.Empty);
+ Assert.False(didRunPermissionRequest);
+ }
+
[Fact(Skip = "Behaves as if no content was in the result. Likely that binary results aren't fully implemented yet.")]
public async Task Can_Return_Binary_Result()
{
@@ -237,11 +273,9 @@ await session.SendAsync(new MessageOptions
Assert.Contains("HELLO", assistantMessage!.Data.Content ?? string.Empty);
// Should have received a custom-tool permission request with the correct tool name
- var customToolRequest = permissionRequests.FirstOrDefault(r => r.Kind == "custom-tool");
+ var customToolRequest = permissionRequests.OfType().FirstOrDefault();
Assert.NotNull(customToolRequest);
- Assert.True(customToolRequest!.ExtensionData?.ContainsKey("toolName") ?? false);
- var toolName = ((JsonElement)customToolRequest.ExtensionData!["toolName"]).GetString();
- Assert.Equal("encrypt_string", toolName);
+ Assert.Equal("encrypt_string", customToolRequest!.ToolName);
[Description("Encrypts a string")]
static string EncryptStringForPermission([Description("String to encrypt")] string input)
diff --git a/go/README.md b/go/README.md
index 4cc73398c7..f29ef9fb73 100644
--- a/go/README.md
+++ b/go/README.md
@@ -44,9 +44,10 @@ func main() {
}
defer client.Stop()
- // Create a session
+ // Create a session (OnPermissionRequest is required)
session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
- Model: "gpt-5",
+ Model: "gpt-5",
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
})
if err != nil {
log.Fatal(err)
@@ -138,10 +139,10 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `UseStdio` (bool): Use stdio transport instead of TCP (default: true)
- `LogLevel` (string): Log level (default: "info")
- `AutoStart` (\*bool): Auto-start server on first use (default: true). Use `Bool(false)` to disable.
-- `AutoRestart` (\*bool): Auto-restart on crash (default: true). Use `Bool(false)` to disable.
- `Env` ([]string): Environment variables for CLI process (default: inherits from current process)
- `GitHubToken` (string): GitHub token for authentication. When provided, takes priority over other auth methods.
- `UseLoggedInUser` (\*bool): Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `CLIUrl`.
+- `Telemetry` (\*TelemetryConfig): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below.
**SessionConfig:**
@@ -149,15 +150,20 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `ReasoningEffort` (string): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `ListModels()` to check which models support this option.
- `SessionID` (string): Custom session ID
- `Tools` ([]Tool): Custom tools exposed to the CLI
-- `SystemMessage` (\*SystemMessageConfig): System message configuration
+- `SystemMessage` (\*SystemMessageConfig): System message configuration. Supports three modes:
+ - **append** (default): Appends `Content` after the SDK-managed prompt
+ - **replace**: Replaces the entire prompt with `Content`
+ - **customize**: Selectively override individual sections via `Sections` map (keys: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionLastInstructions`; values: `SectionOverride` with `Action` and optional `Content`)
- `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section.
- `Streaming` (bool): Enable streaming delta events
- `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration
+- `OnPermissionRequest` (PermissionHandlerFunc): **Required.** Handler called before each tool execution to approve or deny it. Use `copilot.PermissionHandler.ApproveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
- `OnUserInputRequest` (UserInputHandler): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `Hooks` (\*SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
**ResumeSessionConfig:**
+- `OnPermissionRequest` (PermissionHandlerFunc): **Required.** Handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section.
- `Tools` ([]Tool): Tools to expose when resuming
- `ReasoningEffort` (string): Reasoning effort level for models that support it
- `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section.
@@ -174,13 +180,60 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
### Helper Functions
-- `Bool(v bool) *bool` - Helper to create bool pointers for `AutoStart`/`AutoRestart` options
+- `Bool(v bool) *bool` - Helper to create bool pointers for `AutoStart` option
+
+### System Message Customization
+
+Control the system prompt using `SystemMessage` in session config:
+
+```go
+session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ SystemMessage: &copilot.SystemMessageConfig{
+ Content: "Always check for security vulnerabilities before suggesting changes.",
+ },
+})
+```
+
+The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `Content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `Mode: "replace"` or `Mode: "customize"`.
+
+#### Customize Mode
+
+Use `Mode: "customize"` to selectively override individual sections of the prompt while preserving the rest:
+
+```go
+session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ // Replace the tone/style section
+ copilot.SectionTone: {Action: "replace", Content: "Respond in a warm, professional tone. Be thorough in explanations."},
+ // Remove coding-specific rules
+ copilot.SectionCodeChangeRules: {Action: "remove"},
+ // Append to existing guidelines
+ copilot.SectionGuidelines: {Action: "append", Content: "\n* Always cite data sources"},
+ },
+ // Additional instructions appended after all sections
+ Content: "Focus on financial analysis and reporting.",
+ },
+})
+```
+
+Available section constants: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionLastInstructions`.
+
+Each section override supports four actions:
+- **`replace`** — Replace the section content entirely
+- **`remove`** — Remove the section from the prompt
+- **`append`** — Add content after the existing section
+- **`prepend`** — Add content before the existing section
+
+Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
## Image Support
-The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path:
+The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment:
```go
+// File attachment — runtime reads from disk
_, err = session.Send(context.Background(), copilot.MessageOptions{
Prompt: "What's in this image?",
Attachments: []copilot.Attachment{
@@ -190,6 +243,19 @@ _, err = session.Send(context.Background(), copilot.MessageOptions{
},
},
})
+
+// Blob attachment — provide base64 data directly
+mimeType := "image/png"
+_, err = session.Send(context.Background(), copilot.MessageOptions{
+ Prompt: "What's in this image?",
+ Attachments: []copilot.Attachment{
+ {
+ Type: copilot.AttachmentTypeBlob,
+ Data: &base64ImageData,
+ MIMEType: &mimeType,
+ },
+ },
+})
```
Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like:
@@ -281,6 +347,18 @@ editFile := copilot.DefineTool("edit_file", "Custom file editor with project-spe
editFile.OverridesBuiltInTool = true
```
+#### Skipping Permission Prompts
+
+Set `SkipPermission = true` on a tool to allow it to execute without triggering a permission prompt:
+
+```go
+safeLookup := copilot.DefineTool("safe_lookup", "A read-only lookup that needs no confirmation",
+ func(params LookupParams, inv copilot.ToolInvocation) (any, error) {
+ // your logic
+ })
+safeLookup.SkipPermission = true
+```
+
## Streaming
Enable streaming to receive assistant response chunks as they're generated:
@@ -461,6 +539,103 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi
> - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `Type: "azure"`, not `Type: "openai"`.
> - The `BaseURL` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically.
+## Telemetry
+
+The SDK supports OpenTelemetry for distributed tracing. Provide a `Telemetry` config to enable trace export and automatic W3C Trace Context propagation.
+
+```go
+client, err := copilot.NewClient(copilot.ClientOptions{
+ Telemetry: &copilot.TelemetryConfig{
+ OTLPEndpoint: "http://localhost:4318",
+ },
+})
+```
+
+**TelemetryConfig fields:**
+
+- `OTLPEndpoint` (string): OTLP HTTP endpoint URL
+- `FilePath` (string): File path for JSON-lines trace output
+- `ExporterType` (string): `"otlp-http"` or `"file"`
+- `SourceName` (string): Instrumentation scope name
+- `CaptureContent` (bool): Whether to capture message content
+
+Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `CreateSession`, `ResumeSession`, and `Send` calls, and inbound when the CLI invokes tool handlers.
+
+> **Note:** The current `ToolHandler` signature does not accept a `context.Context`, so the inbound trace context cannot be passed to handler code. Spans created inside a tool handler will not be automatically parented to the CLI's `execute_tool` span. A future version may add a context parameter.
+
+Dependency: `go.opentelemetry.io/otel`
+
+## Permission Handling
+
+An `OnPermissionRequest` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision.
+
+### Approve All (simplest)
+
+Use the built-in `PermissionHandler.ApproveAll` helper to allow every tool call without any checks:
+
+```go
+session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
+ Model: "gpt-5",
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+})
+```
+
+### Custom Permission Handler
+
+Provide your own `PermissionHandlerFunc` to inspect each request and apply custom logic:
+
+```go
+session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
+ Model: "gpt-5",
+ OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ // request.Kind — what type of operation is being requested:
+ // copilot.KindShell — executing a shell command
+ // copilot.Write — writing or editing a file
+ // copilot.Read — reading a file
+ // copilot.MCP — calling an MCP tool
+ // copilot.CustomTool — calling one of your registered tools
+ // copilot.URL — fetching a URL
+ // copilot.Memory — accessing or updating Copilot-managed memory
+ // copilot.Hook — invoking a registered hook
+ // request.ToolCallID — pointer to the tool call that triggered this request
+ // request.ToolName — pointer to the name of the tool (for custom-tool / mcp)
+ // request.FileName — pointer to the file being written (for write)
+ // request.FullCommandText — pointer to the full shell command (for shell)
+
+ if request.Kind == copilot.KindShell {
+ // Deny shell commands
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindDeniedInteractivelyByUser}, nil
+ }
+
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
+ },
+})
+```
+
+### Permission Result Kinds
+
+| Constant | Meaning |
+|----------|---------|
+| `PermissionRequestResultKindApproved` | Allow the tool to run |
+| `PermissionRequestResultKindDeniedInteractivelyByUser` | User explicitly denied the request |
+| `PermissionRequestResultKindDeniedCouldNotRequestFromUser` | No approval rule matched and user could not be asked |
+| `PermissionRequestResultKindDeniedByRules` | Denied by a policy rule |
+| `PermissionRequestResultKindNoResult` | Leave the permission request unanswered (protocol v1 only; not allowed for protocol v2) |
+
+### Resuming Sessions
+
+Pass `OnPermissionRequest` when resuming a session too — it is required:
+
+```go
+session, err := client.ResumeSession(context.Background(), sessionID, &copilot.ResumeSessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+})
+```
+
+### Per-Tool Skip Permission
+
+To let a specific custom tool bypass the permission prompt entirely, set `SkipPermission = true` on the tool. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools.
+
## User Input Requests
Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler:
diff --git a/go/client.go b/go/client.go
index a43530adb5..22be47ec69 100644
--- a/go/client.go
+++ b/go/client.go
@@ -44,11 +44,15 @@ import (
"sync/atomic"
"time"
+ "github.com/google/uuid"
+
"github.com/github/copilot-sdk/go/internal/embeddedcli"
"github.com/github/copilot-sdk/go/internal/jsonrpc2"
"github.com/github/copilot-sdk/go/rpc"
)
+const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server"
+
// Client manages the connection to the Copilot CLI server and provides session management.
//
// The Client can either spawn a CLI server process or connect to an existing server.
@@ -69,19 +73,19 @@ import (
// }
// defer client.Stop()
type Client struct {
- options ClientOptions
- process *exec.Cmd
- client *jsonrpc2.Client
- actualPort int
- actualHost string
- state ConnectionState
- sessions map[string]*Session
- sessionsMux sync.Mutex
- isExternalServer bool
- conn net.Conn // stores net.Conn for external TCP connections
- useStdio bool // resolved value from options
- autoStart bool // resolved value from options
- autoRestart bool // resolved value from options
+ options ClientOptions
+ process *exec.Cmd
+ client *jsonrpc2.Client
+ actualPort int
+ actualHost string
+ state ConnectionState
+ sessions map[string]*Session
+ sessionsMux sync.Mutex
+ isExternalServer bool
+ conn net.Conn // stores net.Conn for external TCP connections
+ useStdio bool // resolved value from options
+ autoStart bool // resolved value from options
+
modelsCache []ModelInfo
modelsCacheMux sync.Mutex
lifecycleHandlers []SessionLifecycleHandler
@@ -92,6 +96,7 @@ type Client struct {
processErrorPtr *error
osProcess atomic.Pointer[os.Process]
negotiatedProtocolVersion int
+ onListModels func(ctx context.Context) ([]ModelInfo, error)
// RPC provides typed server-scoped RPC methods.
// This field is nil until the client is connected via Start().
@@ -129,7 +134,6 @@ func NewClient(options *ClientOptions) *Client {
isExternalServer: false,
useStdio: true,
autoStart: true, // default
- autoRestart: true, // default
}
if options != nil {
@@ -179,15 +183,15 @@ func NewClient(options *ClientOptions) *Client {
if options.AutoStart != nil {
client.autoStart = *options.AutoStart
}
- if options.AutoRestart != nil {
- client.autoRestart = *options.AutoRestart
- }
if options.GitHubToken != "" {
opts.GitHubToken = options.GitHubToken
}
if options.UseLoggedInUser != nil {
opts.UseLoggedInUser = options.UseLoggedInUser
}
+ if options.OnListModels != nil {
+ client.onListModels = options.OnListModels
+ }
}
// Default Env to current environment if not set
@@ -439,12 +443,12 @@ func (c *Client) ForceStop() {
c.RPC = nil
}
-func (c *Client) ensureConnected() error {
+func (c *Client) ensureConnected(ctx context.Context) error {
if c.client != nil {
return nil
}
if c.autoStart {
- return c.Start(context.Background())
+ return c.Start(ctx)
}
return fmt.Errorf("client not connected. Call Start() first")
}
@@ -478,23 +482,54 @@ func (c *Client) ensureConnected() error {
// },
// },
// })
+//
+// extractTransformCallbacks separates transform callbacks from a SystemMessageConfig,
+// returning a wire-safe config and a map of callbacks (nil if none).
+func extractTransformCallbacks(config *SystemMessageConfig) (*SystemMessageConfig, map[string]SectionTransformFn) {
+ if config == nil || config.Mode != "customize" || len(config.Sections) == 0 {
+ return config, nil
+ }
+
+ callbacks := make(map[string]SectionTransformFn)
+ wireSections := make(map[string]SectionOverride)
+ for id, override := range config.Sections {
+ if override.Transform != nil {
+ callbacks[id] = override.Transform
+ wireSections[id] = SectionOverride{Action: "transform"}
+ } else {
+ wireSections[id] = override
+ }
+ }
+
+ if len(callbacks) == 0 {
+ return config, nil
+ }
+
+ wireConfig := &SystemMessageConfig{
+ Mode: config.Mode,
+ Content: config.Content,
+ Sections: wireSections,
+ }
+ return wireConfig, callbacks
+}
+
func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) {
if config == nil || config.OnPermissionRequest == nil {
return nil, fmt.Errorf("an OnPermissionRequest handler is required when creating a session. For example, to allow all permissions, use &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}")
}
- if err := c.ensureConnected(); err != nil {
+ if err := c.ensureConnected(ctx); err != nil {
return nil, err
}
req := createSessionRequest{}
req.Model = config.Model
- req.SessionID = config.SessionID
req.ClientName = config.ClientName
req.ReasoningEffort = config.ReasoningEffort
req.ConfigDir = config.ConfigDir
req.Tools = config.Tools
- req.SystemMessage = config.SystemMessage
+ wireSystemMessage, transformCallbacks := extractTransformCallbacks(config.SystemMessage)
+ req.SystemMessage = wireSystemMessage
req.AvailableTools = config.AvailableTools
req.ExcludedTools = config.ExcludedTools
req.Provider = config.Provider
@@ -502,6 +537,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.MCPServers = config.MCPServers
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
+ req.Agent = config.Agent
req.SkillDirectories = config.SkillDirectories
req.DisabledSkills = config.DisabledSkills
req.InfiniteSessions = config.InfiniteSessions
@@ -522,17 +558,19 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
}
req.RequestPermission = Bool(true)
- result, err := c.client.Request("session.create", req)
- if err != nil {
- return nil, fmt.Errorf("failed to create session: %w", err)
- }
+ traceparent, tracestate := getTraceContext(ctx)
+ req.Traceparent = traceparent
+ req.Tracestate = tracestate
- var response createSessionResponse
- if err := json.Unmarshal(result, &response); err != nil {
- return nil, fmt.Errorf("failed to unmarshal response: %w", err)
+ sessionID := config.SessionID
+ if sessionID == "" {
+ sessionID = uuid.New().String()
}
+ req.SessionID = sessionID
- session := newSession(response.SessionID, c.client, response.WorkspacePath)
+ // Create and register the session before issuing the RPC so that
+ // events emitted by the CLI (e.g. session.start) are not dropped.
+ session := newSession(sessionID, c.client, "")
session.registerTools(config.Tools)
session.registerPermissionHandler(config.OnPermissionRequest)
@@ -542,11 +580,35 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
if config.Hooks != nil {
session.registerHooks(config.Hooks)
}
+ if transformCallbacks != nil {
+ session.registerTransformCallbacks(transformCallbacks)
+ }
+ if config.OnEvent != nil {
+ session.On(config.OnEvent)
+ }
c.sessionsMux.Lock()
- c.sessions[response.SessionID] = session
+ c.sessions[sessionID] = session
c.sessionsMux.Unlock()
+ result, err := c.client.Request("session.create", req)
+ if err != nil {
+ c.sessionsMux.Lock()
+ delete(c.sessions, sessionID)
+ c.sessionsMux.Unlock()
+ return nil, fmt.Errorf("failed to create session: %w", err)
+ }
+
+ var response createSessionResponse
+ if err := json.Unmarshal(result, &response); err != nil {
+ c.sessionsMux.Lock()
+ delete(c.sessions, sessionID)
+ c.sessionsMux.Unlock()
+ return nil, fmt.Errorf("failed to unmarshal response: %w", err)
+ }
+
+ session.workspacePath = response.WorkspacePath
+
return session, nil
}
@@ -580,7 +642,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
return nil, fmt.Errorf("an OnPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use &copilot.ResumeSessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}")
}
- if err := c.ensureConnected(); err != nil {
+ if err := c.ensureConnected(ctx); err != nil {
return nil, err
}
@@ -589,7 +651,8 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.ClientName = config.ClientName
req.Model = config.Model
req.ReasoningEffort = config.ReasoningEffort
- req.SystemMessage = config.SystemMessage
+ wireSystemMessage, transformCallbacks := extractTransformCallbacks(config.SystemMessage)
+ req.SystemMessage = wireSystemMessage
req.Tools = config.Tools
req.Provider = config.Provider
req.AvailableTools = config.AvailableTools
@@ -616,22 +679,20 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.MCPServers = config.MCPServers
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
+ req.Agent = config.Agent
req.SkillDirectories = config.SkillDirectories
req.DisabledSkills = config.DisabledSkills
req.InfiniteSessions = config.InfiniteSessions
req.RequestPermission = Bool(true)
- result, err := c.client.Request("session.resume", req)
- if err != nil {
- return nil, fmt.Errorf("failed to resume session: %w", err)
- }
+ traceparent, tracestate := getTraceContext(ctx)
+ req.Traceparent = traceparent
+ req.Tracestate = tracestate
- var response resumeSessionResponse
- if err := json.Unmarshal(result, &response); err != nil {
- return nil, fmt.Errorf("failed to unmarshal response: %w", err)
- }
+ // Create and register the session before issuing the RPC so that
+ // events emitted by the CLI (e.g. session.start) are not dropped.
+ session := newSession(sessionID, c.client, "")
- session := newSession(response.SessionID, c.client, response.WorkspacePath)
session.registerTools(config.Tools)
session.registerPermissionHandler(config.OnPermissionRequest)
if config.OnUserInputRequest != nil {
@@ -640,11 +701,35 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
if config.Hooks != nil {
session.registerHooks(config.Hooks)
}
+ if transformCallbacks != nil {
+ session.registerTransformCallbacks(transformCallbacks)
+ }
+ if config.OnEvent != nil {
+ session.On(config.OnEvent)
+ }
c.sessionsMux.Lock()
- c.sessions[response.SessionID] = session
+ c.sessions[sessionID] = session
c.sessionsMux.Unlock()
+ result, err := c.client.Request("session.resume", req)
+ if err != nil {
+ c.sessionsMux.Lock()
+ delete(c.sessions, sessionID)
+ c.sessionsMux.Unlock()
+ return nil, fmt.Errorf("failed to resume session: %w", err)
+ }
+
+ var response resumeSessionResponse
+ if err := json.Unmarshal(result, &response); err != nil {
+ c.sessionsMux.Lock()
+ delete(c.sessions, sessionID)
+ c.sessionsMux.Unlock()
+ return nil, fmt.Errorf("failed to unmarshal response: %w", err)
+ }
+
+ session.workspacePath = response.WorkspacePath
+
return session, nil
}
@@ -669,7 +754,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
//
// sessions, err := client.ListSessions(context.Background(), &SessionListFilter{Repository: "owner/repo"})
func (c *Client) ListSessions(ctx context.Context, filter *SessionListFilter) ([]SessionMetadata, error) {
- if err := c.ensureConnected(); err != nil {
+ if err := c.ensureConnected(ctx); err != nil {
return nil, err
}
@@ -704,7 +789,7 @@ func (c *Client) ListSessions(ctx context.Context, filter *SessionListFilter) ([
// log.Fatal(err)
// }
func (c *Client) DeleteSession(ctx context.Context, sessionID string) error {
- if err := c.ensureConnected(); err != nil {
+ if err := c.ensureConnected(ctx); err != nil {
return err
}
@@ -751,7 +836,7 @@ func (c *Client) DeleteSession(ctx context.Context, sessionID string) error {
// })
// }
func (c *Client) GetLastSessionID(ctx context.Context) (*string, error) {
- if err := c.ensureConnected(); err != nil {
+ if err := c.ensureConnected(ctx); err != nil {
return nil, err
}
@@ -783,14 +868,8 @@ func (c *Client) GetLastSessionID(ctx context.Context) (*string, error) {
// fmt.Printf("TUI is displaying session: %s\n", *sessionID)
// }
func (c *Client) GetForegroundSessionID(ctx context.Context) (*string, error) {
- if c.client == nil {
- if c.autoStart {
- if err := c.Start(ctx); err != nil {
- return nil, err
- }
- } else {
- return nil, fmt.Errorf("client not connected. Call Start() first")
- }
+ if err := c.ensureConnected(ctx); err != nil {
+ return nil, err
}
result, err := c.client.Request("session.getForeground", getForegroundSessionRequest{})
@@ -817,14 +896,8 @@ func (c *Client) GetForegroundSessionID(ctx context.Context) (*string, error) {
// log.Fatal(err)
// }
func (c *Client) SetForegroundSessionID(ctx context.Context, sessionID string) error {
- if c.client == nil {
- if c.autoStart {
- if err := c.Start(ctx); err != nil {
- return err
- }
- } else {
- return fmt.Errorf("client not connected. Call Start() first")
- }
+ if err := c.ensureConnected(ctx); err != nil {
+ return err
}
result, err := c.client.Request("session.setForeground", setForegroundSessionRequest{SessionID: sessionID})
@@ -1033,40 +1106,51 @@ func (c *Client) GetAuthStatus(ctx context.Context) (*GetAuthStatusResponse, err
// Results are cached after the first successful call to avoid rate limiting.
// The cache is cleared when the client disconnects.
func (c *Client) ListModels(ctx context.Context) ([]ModelInfo, error) {
- if c.client == nil {
- return nil, fmt.Errorf("client not connected")
- }
-
// Use mutex for locking to prevent race condition with concurrent calls
c.modelsCacheMux.Lock()
defer c.modelsCacheMux.Unlock()
// Check cache (already inside lock)
if c.modelsCache != nil {
- // Return a copy to prevent cache mutation
result := make([]ModelInfo, len(c.modelsCache))
copy(result, c.modelsCache)
return result, nil
}
- // Cache miss - fetch from backend while holding lock
- result, err := c.client.Request("models.list", listModelsRequest{})
- if err != nil {
- return nil, err
- }
+ var models []ModelInfo
+ if c.onListModels != nil {
+ // Use custom handler instead of CLI RPC
+ var err error
+ models, err = c.onListModels(ctx)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ if c.client == nil {
+ return nil, fmt.Errorf("client not connected")
+ }
+ // Cache miss - fetch from backend while holding lock
+ result, err := c.client.Request("models.list", listModelsRequest{})
+ if err != nil {
+ return nil, err
+ }
- var response listModelsResponse
- if err := json.Unmarshal(result, &response); err != nil {
- return nil, fmt.Errorf("failed to unmarshal models response: %w", err)
+ var response listModelsResponse
+ if err := json.Unmarshal(result, &response); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal models response: %w", err)
+ }
+ models = response.Models
}
- // Update cache before releasing lock
- c.modelsCache = response.Models
+ // Update cache before releasing lock (copy to prevent external mutation)
+ cache := make([]ModelInfo, len(models))
+ copy(cache, models)
+ c.modelsCache = cache
// Return a copy to prevent cache mutation
- models := make([]ModelInfo, len(response.Models))
- copy(models, response.Models)
- return models, nil
+ result := make([]ModelInfo, len(models))
+ copy(result, models)
+ return result, nil
}
// minProtocolVersion is the minimum protocol version this SDK can communicate with.
@@ -1143,7 +1227,7 @@ func (c *Client) startCLIServer(ctx context.Context) error {
args = append([]string{cliPath}, args...)
}
- c.process = exec.CommandContext(ctx, command, args...)
+ c.process = exec.Command(command, args...)
// Configure platform-specific process attributes (e.g., hide window on Windows)
configureProcAttr(c.process)
@@ -1159,6 +1243,30 @@ func (c *Client) startCLIServer(ctx context.Context) error {
c.process.Env = append(c.process.Env, "COPILOT_SDK_AUTH_TOKEN="+c.options.GitHubToken)
}
+ if c.options.Telemetry != nil {
+ t := c.options.Telemetry
+ c.process.Env = append(c.process.Env, "COPILOT_OTEL_ENABLED=true")
+ if t.OTLPEndpoint != "" {
+ c.process.Env = append(c.process.Env, "OTEL_EXPORTER_OTLP_ENDPOINT="+t.OTLPEndpoint)
+ }
+ if t.FilePath != "" {
+ c.process.Env = append(c.process.Env, "COPILOT_OTEL_FILE_EXPORTER_PATH="+t.FilePath)
+ }
+ if t.ExporterType != "" {
+ c.process.Env = append(c.process.Env, "COPILOT_OTEL_EXPORTER_TYPE="+t.ExporterType)
+ }
+ if t.SourceName != "" {
+ c.process.Env = append(c.process.Env, "COPILOT_OTEL_SOURCE_NAME="+t.SourceName)
+ }
+ if t.CaptureContent != nil {
+ val := "false"
+ if *t.CaptureContent {
+ val = "true"
+ }
+ c.process.Env = append(c.process.Env, "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT="+val)
+ }
+ }
+
if c.useStdio {
// For stdio mode, we need stdin/stdout pipes
stdin, err := c.process.StdinPipe()
@@ -1180,6 +1288,15 @@ func (c *Client) startCLIServer(ctx context.Context) error {
// Create JSON-RPC client immediately
c.client = jsonrpc2.NewClient(stdin, stdout)
c.client.SetProcessDone(c.processDone, c.processErrorPtr)
+ c.client.SetOnClose(func() {
+ // Run in a goroutine to avoid deadlocking with Stop/ForceStop,
+ // which hold startStopMux while waiting for readLoop to finish.
+ go func() {
+ c.startStopMux.Lock()
+ defer c.startStopMux.Unlock()
+ c.state = StateDisconnected
+ }()
+ })
c.RPC = rpc.NewServerRpc(c.client)
c.setupNotificationHandler()
c.client.Start()
@@ -1199,14 +1316,16 @@ func (c *Client) startCLIServer(ctx context.Context) error {
c.monitorProcess()
scanner := bufio.NewScanner(stdout)
- timeout := time.After(10 * time.Second)
portRegex := regexp.MustCompile(`listening on port (\d+)`)
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
for {
select {
- case <-timeout:
+ case <-ctx.Done():
killErr := c.killProcess()
- return errors.Join(errors.New("timeout waiting for CLI server to start"), killErr)
+ return errors.Join(fmt.Errorf("failed waiting for CLI server to start: %w", ctx.Err()), killErr)
case <-c.processDone:
killErr := c.killProcess()
return errors.Join(errors.New("CLI server process exited before reporting port"), killErr)
@@ -1278,12 +1397,13 @@ func (c *Client) connectViaTcp(ctx context.Context) error {
return fmt.Errorf("server port not available")
}
- // Create TCP connection that cancels on context done or after 10 seconds
+ // Merge a 10-second timeout with the caller's context so whichever
+ // deadline comes first wins.
address := net.JoinHostPort(c.actualHost, fmt.Sprintf("%d", c.actualPort))
- dialer := net.Dialer{
- Timeout: 10 * time.Second,
- }
- conn, err := dialer.DialContext(ctx, "tcp", address)
+ dialCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+ var dialer net.Dialer
+ conn, err := dialer.DialContext(dialCtx, "tcp", address)
if err != nil {
return fmt.Errorf("failed to connect to CLI server at %s: %w", address, err)
}
@@ -1295,6 +1415,13 @@ func (c *Client) connectViaTcp(ctx context.Context) error {
if c.processDone != nil {
c.client.SetProcessDone(c.processDone, c.processErrorPtr)
}
+ c.client.SetOnClose(func() {
+ go func() {
+ c.startStopMux.Lock()
+ defer c.startStopMux.Unlock()
+ c.state = StateDisconnected
+ }()
+ })
c.RPC = rpc.NewServerRpc(c.client)
c.setupNotificationHandler()
c.client.Start()
@@ -1314,6 +1441,7 @@ func (c *Client) setupNotificationHandler() {
c.client.SetRequestHandler("permission.request", jsonrpc2.RequestHandlerFor(c.handlePermissionRequestV2))
c.client.SetRequestHandler("userInput.request", jsonrpc2.RequestHandlerFor(c.handleUserInputRequest))
c.client.SetRequestHandler("hooks.invoke", jsonrpc2.RequestHandlerFor(c.handleHooksInvoke))
+ c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform))
}
func (c *Client) handleSessionEvent(req sessionEventRequest) {
@@ -1380,16 +1508,38 @@ func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jso
return result, nil
}
+// handleSystemMessageTransform handles a system message transform request from the CLI server.
+func (c *Client) handleSystemMessageTransform(req systemMessageTransformRequest) (systemMessageTransformResponse, *jsonrpc2.Error) {
+ if req.SessionID == "" {
+ return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32602, Message: "invalid system message transform payload"}
+ }
+
+ c.sessionsMux.Lock()
+ session, ok := c.sessions[req.SessionID]
+ c.sessionsMux.Unlock()
+ if !ok {
+ return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)}
+ }
+
+ resp, err := session.handleSystemMessageTransform(req.Sections)
+ if err != nil {
+ return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32603, Message: err.Error()}
+ }
+ return resp, nil
+}
+
// ========================================================================
// Protocol v2 backward-compatibility adapters
// ========================================================================
// toolCallRequestV2 is the v2 RPC request payload for tool.call.
type toolCallRequestV2 struct {
- SessionID string `json:"sessionId"`
- ToolCallID string `json:"toolCallId"`
- ToolName string `json:"toolName"`
- Arguments any `json:"arguments"`
+ SessionID string `json:"sessionId"`
+ ToolCallID string `json:"toolCallId"`
+ ToolName string `json:"toolName"`
+ Arguments any `json:"arguments"`
+ Traceparent string `json:"traceparent,omitempty"`
+ Tracestate string `json:"tracestate,omitempty"`
}
// toolCallResponseV2 is the v2 RPC response payload for tool.call.
@@ -1431,7 +1581,15 @@ func (c *Client) handleToolCallRequestV2(req toolCallRequestV2) (*toolCallRespon
}}, nil
}
- invocation := ToolInvocation(req)
+ ctx := contextWithTraceParent(context.Background(), req.Traceparent, req.Tracestate)
+
+ invocation := ToolInvocation{
+ SessionID: req.SessionID,
+ ToolCallID: req.ToolCallID,
+ ToolName: req.ToolName,
+ Arguments: req.Arguments,
+ TraceContext: ctx,
+ }
result, err := handler(invocation)
if err != nil {
@@ -1480,6 +1638,9 @@ func (c *Client) handlePermissionRequestV2(req permissionRequestV2) (*permission
},
}, nil
}
+ if result.Kind == "no-result" {
+ return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionV2Error}
+ }
return &permissionResponseV2{Result: result}, nil
}
diff --git a/go/client_test.go b/go/client_test.go
index d740fd79b4..d7a526cab6 100644
--- a/go/client_test.go
+++ b/go/client_test.go
@@ -1,6 +1,7 @@
package copilot
import (
+ "context"
"encoding/json"
"os"
"path/filepath"
@@ -413,6 +414,60 @@ func TestResumeSessionRequest_ClientName(t *testing.T) {
})
}
+func TestCreateSessionRequest_Agent(t *testing.T) {
+ t.Run("includes agent in JSON when set", func(t *testing.T) {
+ req := createSessionRequest{Agent: "test-agent"}
+ data, err := json.Marshal(req)
+ if err != nil {
+ t.Fatalf("Failed to marshal: %v", err)
+ }
+ var m map[string]any
+ if err := json.Unmarshal(data, &m); err != nil {
+ t.Fatalf("Failed to unmarshal: %v", err)
+ }
+ if m["agent"] != "test-agent" {
+ t.Errorf("Expected agent to be 'test-agent', got %v", m["agent"])
+ }
+ })
+
+ t.Run("omits agent from JSON when empty", func(t *testing.T) {
+ req := createSessionRequest{}
+ data, _ := json.Marshal(req)
+ var m map[string]any
+ json.Unmarshal(data, &m)
+ if _, ok := m["agent"]; ok {
+ t.Error("Expected agent to be omitted when empty")
+ }
+ })
+}
+
+func TestResumeSessionRequest_Agent(t *testing.T) {
+ t.Run("includes agent in JSON when set", func(t *testing.T) {
+ req := resumeSessionRequest{SessionID: "s1", Agent: "test-agent"}
+ data, err := json.Marshal(req)
+ if err != nil {
+ t.Fatalf("Failed to marshal: %v", err)
+ }
+ var m map[string]any
+ if err := json.Unmarshal(data, &m); err != nil {
+ t.Fatalf("Failed to unmarshal: %v", err)
+ }
+ if m["agent"] != "test-agent" {
+ t.Errorf("Expected agent to be 'test-agent', got %v", m["agent"])
+ }
+ })
+
+ t.Run("omits agent from JSON when empty", func(t *testing.T) {
+ req := resumeSessionRequest{SessionID: "s1"}
+ data, _ := json.Marshal(req)
+ var m map[string]any
+ json.Unmarshal(data, &m)
+ if _, ok := m["agent"]; ok {
+ t.Error("Expected agent to be omitted when empty")
+ }
+ })
+}
+
func TestOverridesBuiltInTool(t *testing.T) {
t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) {
tool := Tool{
@@ -494,6 +549,91 @@ func TestClient_ResumeSession_RequiresPermissionHandler(t *testing.T) {
})
}
+func TestListModelsWithCustomHandler(t *testing.T) {
+ customModels := []ModelInfo{
+ {
+ ID: "my-custom-model",
+ Name: "My Custom Model",
+ Capabilities: ModelCapabilities{
+ Supports: ModelSupports{Vision: false, ReasoningEffort: false},
+ Limits: ModelLimits{MaxContextWindowTokens: 128000},
+ },
+ },
+ }
+
+ callCount := 0
+ handler := func(ctx context.Context) ([]ModelInfo, error) {
+ callCount++
+ return customModels, nil
+ }
+
+ client := NewClient(&ClientOptions{OnListModels: handler})
+
+ models, err := client.ListModels(t.Context())
+ if err != nil {
+ t.Fatalf("ListModels failed: %v", err)
+ }
+ if callCount != 1 {
+ t.Errorf("expected handler called once, got %d", callCount)
+ }
+ if len(models) != 1 || models[0].ID != "my-custom-model" {
+ t.Errorf("unexpected models: %+v", models)
+ }
+}
+
+func TestListModelsHandlerCachesResults(t *testing.T) {
+ customModels := []ModelInfo{
+ {
+ ID: "cached-model",
+ Name: "Cached Model",
+ Capabilities: ModelCapabilities{
+ Supports: ModelSupports{Vision: false, ReasoningEffort: false},
+ Limits: ModelLimits{MaxContextWindowTokens: 128000},
+ },
+ },
+ }
+
+ callCount := 0
+ handler := func(ctx context.Context) ([]ModelInfo, error) {
+ callCount++
+ return customModels, nil
+ }
+
+ client := NewClient(&ClientOptions{OnListModels: handler})
+
+ _, _ = client.ListModels(t.Context())
+ _, _ = client.ListModels(t.Context())
+ if callCount != 1 {
+ t.Errorf("expected handler called once due to caching, got %d", callCount)
+ }
+}
+
+func TestClient_StartContextCancellationDoesNotKillProcess(t *testing.T) {
+ cliPath := findCLIPathForTest()
+ if cliPath == "" {
+ t.Skip("CLI not found")
+ }
+
+ client := NewClient(&ClientOptions{CLIPath: cliPath})
+ t.Cleanup(func() { client.ForceStop() })
+
+ // Start with a context, then cancel it after the client is connected.
+ ctx, cancel := context.WithCancel(t.Context())
+ if err := client.Start(ctx); err != nil {
+ t.Fatalf("Start failed: %v", err)
+ }
+ cancel() // cancel the context that was used for Start
+
+ // The CLI process should still be alive and responsive.
+ resp, err := client.Ping(t.Context(), "still alive")
+ if err != nil {
+ t.Fatalf("Ping after context cancellation failed: %v", err)
+ }
+ if resp == nil {
+ t.Fatal("expected non-nil ping response")
+ }
+}
+
func TestClient_StartStopRace(t *testing.T) {
cliPath := findCLIPathForTest()
if cliPath == "" {
diff --git a/go/generated_session_events.go b/go/generated_session_events.go
index 86f5066f75..591ff53af5 100644
--- a/go/generated_session_events.go
+++ b/go/generated_session_events.go
@@ -26,14 +26,136 @@ func (r *SessionEvent) Marshal() ([]byte, error) {
}
type SessionEvent struct {
+ // Session initialization metadata including context and configuration
+ //
+ // Session resume metadata including current context and event count
+ //
+ // Error details for timeline display including message and optional diagnostic information
+ //
// Payload indicating the agent is idle; includes any background tasks still in flight
//
- // Empty payload; the event signals that LLM-powered conversation compaction has begun
+ // Session title change payload containing the new display title
+ //
+ // Informational message for timeline display with categorization
+ //
+ // Warning message for timeline display with categorization
+ //
+ // Model change details including previous and new model identifiers
+ //
+ // Agent mode change details including previous and new modes
+ //
+ // Plan file operation details indicating what changed
+ //
+ // Workspace file change details including path and operation type
+ //
+ // Session handoff metadata including source, context, and repository information
+ //
+ // Conversation truncation statistics including token counts and removed content metrics
+ //
+ // Session rewind details including target event and count of removed events
+ //
+ // Session termination metrics including usage statistics, code changes, and shutdown
+ // reason
+ //
+ // Updated working directory and git context after the change
+ //
+ // Current context window usage statistics including token and message counts
+ //
+ // Context window breakdown at the start of LLM-powered conversation compaction
+ //
+ // Conversation compaction results including success status, metrics, and optional error
+ // details
+ //
+ // Task completion notification with summary from the agent
//
// Empty payload; the event signals that the pending message queue has changed
//
+ // Turn initialization metadata including identifier and interaction tracking
+ //
+ // Agent intent description for current activity or plan
+ //
+ // Assistant reasoning content for timeline display with complete thinking text
+ //
+ // Streaming reasoning delta for incremental extended thinking updates
+ //
+ // Streaming response progress with cumulative byte count
+ //
+ // Assistant response containing text content, optional tool requests, and interaction
+ // metadata
+ //
+ // Streaming assistant message delta for incremental response updates
+ //
+ // Turn completion metadata including the turn identifier
+ //
+ // LLM API call usage metrics including tokens, costs, quotas, and billing information
+ //
+ // Turn abort information including the reason for termination
+ //
+ // User-initiated tool invocation request with tool name and arguments
+ //
+ // Tool execution startup details including MCP server information when applicable
+ //
+ // Streaming tool execution output for incremental result display
+ //
+ // Tool execution progress notification with status message
+ //
+ // Tool execution completion results including success status, detailed output, and error
+ // information
+ //
+ // Skill invocation details including content, allowed tools, and plugin metadata
+ //
+ // Sub-agent startup details including parent tool call and agent information
+ //
+ // Sub-agent completion details for successful execution
+ //
+ // Sub-agent failure details including error message and agent information
+ //
+ // Custom agent selection details including name and available tools
+ //
// Empty payload; the event signals that the custom agent was deselected, returning to the
// default agent
+ //
+ // Hook invocation start details including type and input data
+ //
+ // Hook invocation completion details including output, success status, and error
+ // information
+ //
+ // System or developer message content with role and optional template metadata
+ //
+ // System-generated notification for runtime events like background task completion
+ //
+ // Permission request notification requiring client approval with request details
+ //
+ // Permission request completion notification signaling UI dismissal
+ //
+ // User input request notification with question and optional predefined choices
+ //
+ // User input request completion notification signaling UI dismissal
+ //
+ // Elicitation request; may be form-based (structured input) or URL-based (browser
+ // redirect)
+ //
+ // Elicitation request completion notification signaling UI dismissal
+ //
+ // OAuth authentication request for an MCP server
+ //
+ // MCP OAuth request completion notification
+ //
+ // External tool invocation request for client-side tool execution
+ //
+ // External tool completion notification signaling UI dismissal
+ //
+ // Queued slash command dispatch request for client execution
+ //
+ // Registered command dispatch request routed to the owning client
+ //
+ // Queued command completion notification signaling UI dismissal
+ //
+ // SDK command registration change notification
+ //
+ // Plan approval request with plan content and available user actions
+ //
+ // Plan mode exit completion notification signaling UI dismissal
Data Data `json:"data"`
// When true, the event is transient and not persisted to the session event log on disk
Ephemeral *bool `json:"ephemeral,omitempty"`
@@ -47,15 +169,141 @@ type SessionEvent struct {
Type SessionEventType `json:"type"`
}
+// Session initialization metadata including context and configuration
+//
+// # Session resume metadata including current context and event count
+//
+// # Error details for timeline display including message and optional diagnostic information
+//
// Payload indicating the agent is idle; includes any background tasks still in flight
//
-// Empty payload; the event signals that LLM-powered conversation compaction has begun
+// # Session title change payload containing the new display title
+//
+// # Informational message for timeline display with categorization
+//
+// # Warning message for timeline display with categorization
+//
+// # Model change details including previous and new model identifiers
+//
+// # Agent mode change details including previous and new modes
+//
+// # Plan file operation details indicating what changed
+//
+// # Workspace file change details including path and operation type
+//
+// # Session handoff metadata including source, context, and repository information
+//
+// # Conversation truncation statistics including token counts and removed content metrics
+//
+// # Session rewind details including target event and count of removed events
+//
+// Session termination metrics including usage statistics, code changes, and shutdown
+// reason
+//
+// # Updated working directory and git context after the change
+//
+// # Current context window usage statistics including token and message counts
+//
+// # Context window breakdown at the start of LLM-powered conversation compaction
+//
+// Conversation compaction results including success status, metrics, and optional error
+// details
+//
+// # Task completion notification with summary from the agent
//
// Empty payload; the event signals that the pending message queue has changed
//
+// # Turn initialization metadata including identifier and interaction tracking
+//
+// # Agent intent description for current activity or plan
+//
+// # Assistant reasoning content for timeline display with complete thinking text
+//
+// # Streaming reasoning delta for incremental extended thinking updates
+//
+// # Streaming response progress with cumulative byte count
+//
+// Assistant response containing text content, optional tool requests, and interaction
+// metadata
+//
+// # Streaming assistant message delta for incremental response updates
+//
+// # Turn completion metadata including the turn identifier
+//
+// # LLM API call usage metrics including tokens, costs, quotas, and billing information
+//
+// # Turn abort information including the reason for termination
+//
+// # User-initiated tool invocation request with tool name and arguments
+//
+// # Tool execution startup details including MCP server information when applicable
+//
+// # Streaming tool execution output for incremental result display
+//
+// # Tool execution progress notification with status message
+//
+// Tool execution completion results including success status, detailed output, and error
+// information
+//
+// # Skill invocation details including content, allowed tools, and plugin metadata
+//
+// # Sub-agent startup details including parent tool call and agent information
+//
+// # Sub-agent completion details for successful execution
+//
+// # Sub-agent failure details including error message and agent information
+//
+// # Custom agent selection details including name and available tools
+//
// Empty payload; the event signals that the custom agent was deselected, returning to the
// default agent
+//
+// # Hook invocation start details including type and input data
+//
+// Hook invocation completion details including output, success status, and error
+// information
+//
+// # System or developer message content with role and optional template metadata
+//
+// # System-generated notification for runtime events like background task completion
+//
+// # Permission request notification requiring client approval with request details
+//
+// # Permission request completion notification signaling UI dismissal
+//
+// # User input request notification with question and optional predefined choices
+//
+// # User input request completion notification signaling UI dismissal
+//
+// Elicitation request; may be form-based (structured input) or URL-based (browser
+// redirect)
+//
+// # Elicitation request completion notification signaling UI dismissal
+//
+// # OAuth authentication request for an MCP server
+//
+// # MCP OAuth request completion notification
+//
+// # External tool invocation request for client-side tool execution
+//
+// # External tool completion notification signaling UI dismissal
+//
+// # Queued slash command dispatch request for client execution
+//
+// # Registered command dispatch request routed to the owning client
+//
+// # Queued command completion notification signaling UI dismissal
+//
+// # SDK command registration change notification
+//
+// # Plan approval request with plan content and available user actions
+//
+// Plan mode exit completion notification signaling UI dismissal
type Data struct {
+ // Whether the session was already in use by another client at start time
+ //
+ // Whether the session was already in use by another client at resume time
+ AlreadyInUse *bool `json:"alreadyInUse,omitempty"`
// Working directory and git context at session start
//
// Updated working directory and git context at resume time
@@ -66,7 +314,14 @@ type Data struct {
CopilotVersion *string `json:"copilotVersion,omitempty"`
// Identifier of the software producing the events (e.g., "copilot-agent")
Producer *string `json:"producer,omitempty"`
+ // Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high",
+ // "xhigh")
+ //
+ // Reasoning effort level after the model change, if applicable
+ ReasoningEffort *string `json:"reasoningEffort,omitempty"`
// Model selected at session creation time, if any
+ //
+ // Model currently selected at resume time
SelectedModel *string `json:"selectedModel,omitempty"`
// Unique identifier for the session
//
@@ -100,6 +355,14 @@ type Data struct {
Stack *string `json:"stack,omitempty"`
// HTTP status code from the upstream request, if applicable
StatusCode *int64 `json:"statusCode,omitempty"`
+ // Optional URL associated with this error that the user can open in a browser
+ //
+ // Optional URL associated with this message that the user can open in a browser
+ //
+ // Optional URL associated with this warning that the user can open in a browser
+ //
+ // URL to open in the user's browser (url mode only)
+ URL *string `json:"url,omitempty"`
// Background tasks still running when the agent became idle
BackgroundTasks *BackgroundTasks `json:"backgroundTasks,omitempty"`
// The new display title for the session
@@ -113,6 +376,8 @@ type Data struct {
NewModel *string `json:"newModel,omitempty"`
// Model that was previously selected, if any
PreviousModel *string `json:"previousModel,omitempty"`
+ // Reasoning effort level before the model change, if applicable
+ PreviousReasoningEffort *string `json:"previousReasoningEffort,omitempty"`
// Agent mode after the change (e.g., "interactive", "plan", "autopilot")
NewMode *string `json:"newMode,omitempty"`
// Agent mode before the change (e.g., "interactive", "plan", "autopilot")
@@ -127,17 +392,21 @@ type Data struct {
Path *string `json:"path,omitempty"`
// ISO 8601 timestamp when the handoff occurred
HandoffTime *time.Time `json:"handoffTime,omitempty"`
+ // GitHub host URL for the source session (e.g., https://github.com or
+ // https://tenant.ghe.com)
+ Host *string `json:"host,omitempty"`
// Session ID of the remote session being handed off
RemoteSessionID *string `json:"remoteSessionId,omitempty"`
// Repository context for the handed-off session
//
- // Repository identifier in "owner/name" format, derived from the git remote URL
+ // Repository identifier derived from the git remote URL ("owner/name" for GitHub,
+ // "org/project/repo" for Azure DevOps)
Repository *RepositoryUnion `json:"repository"`
// Origin type of the session being handed off
SourceType *SourceType `json:"sourceType,omitempty"`
// Summary of the work done in the source session
//
- // Optional summary of the completed task, provided by the agent
+ // Summary of the completed task, provided by the agent
//
// Summary of the plan that was created
Summary *string `json:"summary,omitempty"`
@@ -163,8 +432,20 @@ type Data struct {
UpToEventID *string `json:"upToEventId,omitempty"`
// Aggregate code change metrics for the session
CodeChanges *CodeChanges `json:"codeChanges,omitempty"`
+ // Non-system message token count at shutdown
+ //
+ // Token count from non-system messages (user, assistant, tool)
+ //
+ // Token count from non-system messages (user, assistant, tool) at compaction start
+ //
+ // Token count from non-system messages (user, assistant, tool) after compaction
+ ConversationTokens *float64 `json:"conversationTokens,omitempty"`
// Model that was selected at the time of shutdown
CurrentModel *string `json:"currentModel,omitempty"`
+ // Total tokens in context window at shutdown
+ //
+ // Current number of tokens in the context window
+ CurrentTokens *float64 `json:"currentTokens,omitempty"`
// Error description when shutdownType is "error"
ErrorReason *string `json:"errorReason,omitempty"`
// Per-model usage breakdown, keyed by model identifier
@@ -173,18 +454,40 @@ type Data struct {
SessionStartTime *float64 `json:"sessionStartTime,omitempty"`
// Whether the session ended normally ("routine") or due to a crash/fatal error ("error")
ShutdownType *ShutdownType `json:"shutdownType,omitempty"`
+ // System message token count at shutdown
+ //
+ // Token count from system message(s)
+ //
+ // Token count from system message(s) at compaction start
+ //
+ // Token count from system message(s) after compaction
+ SystemTokens *float64 `json:"systemTokens,omitempty"`
+ // Tool definitions token count at shutdown
+ //
+ // Token count from tool definitions
+ //
+ // Token count from tool definitions at compaction start
+ //
+ // Token count from tool definitions after compaction
+ ToolDefinitionsTokens *float64 `json:"toolDefinitionsTokens,omitempty"`
// Cumulative time spent in API calls during the session, in milliseconds
TotalAPIDurationMS *float64 `json:"totalApiDurationMs,omitempty"`
// Total number of premium API requests used during the session
TotalPremiumRequests *float64 `json:"totalPremiumRequests,omitempty"`
+ // Base commit of current git branch at session start time
+ BaseCommit *string `json:"baseCommit,omitempty"`
// Current git branch name
Branch *string `json:"branch,omitempty"`
// Current working directory path
Cwd *string `json:"cwd,omitempty"`
// Root directory of the git repository, resolved via git rev-parse
GitRoot *string `json:"gitRoot,omitempty"`
- // Current number of tokens in the context window
- CurrentTokens *float64 `json:"currentTokens,omitempty"`
+ // Head commit of current git branch at session start time
+ HeadCommit *string `json:"headCommit,omitempty"`
+ // Hosting platform type of the repository (github or ado)
+ HostType *HostType `json:"hostType,omitempty"`
+ // Whether this is the first usage_info event emitted in this session
+ IsInitial *bool `json:"isInitial,omitempty"`
// Current number of messages in the conversation
MessagesLength *float64 `json:"messagesLength,omitempty"`
// Checkpoint snapshot number created for recovery
@@ -229,6 +532,11 @@ type Data struct {
// Request ID of the resolved elicitation request; clients should dismiss any UI for this
// request
//
+ // Unique identifier for this OAuth request; used to respond via
+ // session.respondToMcpOAuth()
+ //
+ // Request ID of the resolved OAuth request
+ //
// Unique identifier for this request; used to respond via session.respondToExternalTool()
//
// Request ID of the resolved external tool request; clients should dismiss any UI for this
@@ -236,6 +544,8 @@ type Data struct {
//
// Unique identifier for this request; used to respond via session.respondToQueuedCommand()
//
+ // Unique identifier; used to respond via session.commands.handlePendingCommand()
+ //
// Request ID of the resolved command request; clients should dismiss any UI for this
// request
//
@@ -246,6 +556,8 @@ type Data struct {
RequestID *string `json:"requestId,omitempty"`
// Whether compaction completed successfully
//
+ // Whether the tool call succeeded. False when validation failed (e.g., invalid arguments)
+ //
// Whether the tool execution completed successfully
//
// Whether the hook completed successfully
@@ -267,6 +579,8 @@ type Data struct {
// Full content of the skill file, injected into the conversation for the model
//
// The system or developer prompt text
+ //
+ // The notification text, typically wrapped in XML tags
Content *string `json:"content,omitempty"`
// CAPI interaction ID for correlating this user message with its turn
//
@@ -364,6 +678,12 @@ type Data struct {
//
// Tool call ID of the parent tool invocation that spawned this sub-agent
//
+ // The LLM-assigned tool call ID that triggered this request; used by remote UIs to
+ // correlate responses
+ //
+ // Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id
+ // for remote UIs
+ //
// Tool call ID assigned to this external tool invocation
ToolCallID *string `json:"toolCallId,omitempty"`
// Name of the tool the user wants to invoke
@@ -426,6 +746,8 @@ type Data struct {
Metadata *Metadata `json:"metadata,omitempty"`
// Message role: "system" for system prompts, "developer" for developer-injected instructions
Role *Role `json:"role,omitempty"`
+ // Structured metadata identifying what triggered this notification
+ Kind *KindClass `json:"kind,omitempty"`
// Details of the permission being requested
PermissionRequest *PermissionRequest `json:"permissionRequest,omitempty"`
// Whether the user can provide a free-form text response in addition to predefined choices
@@ -434,20 +756,62 @@ type Data struct {
Choices []string `json:"choices,omitempty"`
// The question or prompt to present to the user
Question *string `json:"question,omitempty"`
- // Elicitation mode; currently only "form" is supported. Defaults to "form" when absent.
+ // The source that initiated the request (MCP server name, or absent for agent-initiated)
+ ElicitationSource *string `json:"elicitationSource,omitempty"`
+ // Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to
+ // "form" when absent.
Mode *Mode `json:"mode,omitempty"`
- // JSON Schema describing the form fields to present to the user
+ // JSON Schema describing the form fields to present to the user (form mode only)
RequestedSchema *RequestedSchema `json:"requestedSchema,omitempty"`
+ // Display name of the MCP server that requires OAuth
+ //
+ // Name of the MCP server whose status changed
+ ServerName *string `json:"serverName,omitempty"`
+ // URL of the MCP server that requires OAuth
+ ServerURL *string `json:"serverUrl,omitempty"`
+ // Static OAuth client configuration, if the server specifies one
+ StaticClientConfig *StaticClientConfig `json:"staticClientConfig,omitempty"`
+ // W3C Trace Context traceparent header for the execute_tool span
+ Traceparent *string `json:"traceparent,omitempty"`
+ // W3C Trace Context tracestate header for the execute_tool span
+ Tracestate *string `json:"tracestate,omitempty"`
// The slash command text to be executed (e.g., /help, /clear)
+ //
+ // The full command text (e.g., /deploy production)
Command *string `json:"command,omitempty"`
+ // Raw argument string after the command name
+ Args *string `json:"args,omitempty"`
+ // Command name without leading /
+ CommandName *string `json:"commandName,omitempty"`
+ // Current list of registered SDK commands
+ Commands []DataCommand `json:"commands,omitempty"`
// Available actions the user can take (e.g., approve, edit, reject)
Actions []string `json:"actions,omitempty"`
// Full content of the plan file
PlanContent *string `json:"planContent,omitempty"`
// The recommended action for the user to take
RecommendedAction *string `json:"recommendedAction,omitempty"`
+ // Array of resolved skill metadata
+ Skills []Skill `json:"skills,omitempty"`
+ // Array of MCP server status summaries
+ Servers []Server `json:"servers,omitempty"`
+ // New connection status: connected, failed, pending, disabled, or not_configured
+ Status *ServerStatus `json:"status,omitempty"`
+ // Array of discovered extensions and their status
+ Extensions []Extension `json:"extensions,omitempty"`
}
+// A user message attachment — a file, directory, code selection, blob, or GitHub reference
+//
+// # File attachment
+//
+// # Directory attachment
+//
+// # Code selection attachment from an editor
+//
+// # GitHub issue, pull request, or discussion reference
+//
+// Blob attachment with inline base64-encoded data
type Attachment struct {
// User-facing display name for the attachment
//
@@ -455,7 +819,9 @@ type Attachment struct {
DisplayName *string `json:"displayName,omitempty"`
// Optional line range to scope the attachment to a specific section of the file
LineRange *LineRange `json:"lineRange,omitempty"`
- // Absolute file or directory path
+ // Absolute file path
+ //
+ // Absolute directory path
Path *string `json:"path,omitempty"`
// Attachment type discriminator
Type AttachmentType `json:"type"`
@@ -475,6 +841,10 @@ type Attachment struct {
Title *string `json:"title,omitempty"`
// URL to the referenced item on GitHub
URL *string `json:"url,omitempty"`
+ // Base64-encoded content
+ Data *string `json:"data,omitempty"`
+ // MIME type of the inline data
+ MIMEType *string `json:"mimeType,omitempty"`
}
// Optional line range to scope the attachment to a specific section of the file
@@ -487,10 +857,13 @@ type LineRange struct {
// Position range of the selection within the file
type SelectionClass struct {
- End End `json:"end"`
+ // End position of the selection
+ End End `json:"end"`
+ // Start position of the selection
Start Start `json:"start"`
}
+// End position of the selection
type End struct {
// End character offset within the line (0-based)
Character float64 `json:"character"`
@@ -498,6 +871,7 @@ type End struct {
Line float64 `json:"line"`
}
+// Start position of the selection
type Start struct {
// Start character offset within the line (0-based)
Character float64 `json:"character"`
@@ -513,6 +887,7 @@ type BackgroundTasks struct {
Shells []Shell `json:"shells"`
}
+// A background agent task
type Agent struct {
// Unique identifier of the background agent
AgentID string `json:"agentId"`
@@ -522,6 +897,7 @@ type Agent struct {
Description *string `json:"description,omitempty"`
}
+// A background shell command
type Shell struct {
// Human-readable description of the shell command
Description *string `json:"description,omitempty"`
@@ -539,6 +915,11 @@ type CodeChanges struct {
LinesRemoved float64 `json:"linesRemoved"`
}
+type DataCommand struct {
+ Description *string `json:"description,omitempty"`
+ Name string `json:"name"`
+}
+
// Token usage breakdown for the compaction LLM call
type CompactionTokensUsed struct {
// Cached input tokens reused in the compaction LLM call
@@ -553,13 +934,20 @@ type CompactionTokensUsed struct {
//
// Updated working directory and git context at resume time
type ContextClass struct {
+ // Base commit of current git branch at session start time
+ BaseCommit *string `json:"baseCommit,omitempty"`
// Current git branch name
Branch *string `json:"branch,omitempty"`
// Current working directory path
Cwd string `json:"cwd"`
// Root directory of the git repository, resolved via git rev-parse
GitRoot *string `json:"gitRoot,omitempty"`
- // Repository identifier in "owner/name" format, derived from the git remote URL
+ // Head commit of current git branch at session start time
+ HeadCommit *string `json:"headCommit,omitempty"`
+ // Hosting platform type of the repository (github or ado)
+ HostType *HostType `json:"hostType,omitempty"`
+ // Repository identifier derived from the git remote URL ("owner/name" for GitHub,
+ // "org/project/repo" for Azure DevOps)
Repository *string `json:"repository,omitempty"`
}
@@ -571,6 +959,7 @@ type CopilotUsage struct {
TotalNanoAiu float64 `json:"totalNanoAiu"`
}
+// Token usage detail for a single billing category
type TokenDetail struct {
// Number of tokens in this billing batch
BatchSize float64 `json:"batchSize"`
@@ -594,6 +983,40 @@ type ErrorClass struct {
Stack *string `json:"stack,omitempty"`
}
+type Extension struct {
+ // Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')
+ ID string `json:"id"`
+ // Extension name (directory name)
+ Name string `json:"name"`
+ // Discovery source
+ Source Source `json:"source"`
+ // Current status: running, disabled, failed, or starting
+ Status ExtensionStatus `json:"status"`
+}
+
+// Structured metadata identifying what triggered this notification
+type KindClass struct {
+ // Unique identifier of the background agent
+ AgentID *string `json:"agentId,omitempty"`
+ // Type of the agent (e.g., explore, task, general-purpose)
+ AgentType *string `json:"agentType,omitempty"`
+ // Human-readable description of the agent task
+ //
+ // Human-readable description of the command
+ Description *string `json:"description,omitempty"`
+ // The full prompt given to the background agent
+ Prompt *string `json:"prompt,omitempty"`
+ // Whether the agent completed successfully or failed
+ Status *KindStatus `json:"status,omitempty"`
+ Type KindType `json:"type"`
+ // Exit code of the shell command, if available
+ ExitCode *float64 `json:"exitCode,omitempty"`
+ // Unique identifier of the shell session
+ //
+ // Unique identifier of the detached shell session
+ ShellID *string `json:"shellId,omitempty"`
+}
+
// Metadata about the prompt template and its construction
type Metadata struct {
// Version identifier of the prompt template used
@@ -630,11 +1053,27 @@ type Usage struct {
}
// Details of the permission being requested
+//
+// # Shell command permission request
+//
+// # File write permission request
+//
+// # File or directory read permission request
+//
+// # MCP tool invocation permission request
+//
+// # URL access permission request
+//
+// # Memory storage permission request
+//
+// # Custom tool invocation permission request
+//
+// Hook confirmation permission request
type PermissionRequest struct {
// Whether the UI can offer session-wide approval for this command pattern
CanOfferSessionApproval *bool `json:"canOfferSessionApproval,omitempty"`
// Parsed command identifiers found in the command text
- Commands []Command `json:"commands,omitempty"`
+ Commands []PermissionRequestCommand `json:"commands,omitempty"`
// The complete shell command text to be executed
FullCommandText *string `json:"fullCommandText,omitempty"`
// Whether the command includes a file write redirection (e.g., > or >>)
@@ -676,6 +1115,8 @@ type PermissionRequest struct {
// Internal name of the MCP tool
//
// Name of the custom tool
+ //
+ // Name of the tool the hook is gating
ToolName *string `json:"toolName,omitempty"`
// Human-readable title of the MCP tool
ToolTitle *string `json:"toolTitle,omitempty"`
@@ -689,9 +1130,13 @@ type PermissionRequest struct {
Subject *string `json:"subject,omitempty"`
// Description of what the custom tool does
ToolDescription *string `json:"toolDescription,omitempty"`
+ // Optional message from the hook explaining why confirmation is needed
+ HookMessage *string `json:"hookMessage,omitempty"`
+ // Arguments of the tool call being gated
+ ToolArgs interface{} `json:"toolArgs"`
}
-type Command struct {
+type PermissionRequestCommand struct {
// Command identifier (e.g., executable name)
Identifier string `json:"identifier"`
// Whether this command is read-only (no side effects)
@@ -732,13 +1177,14 @@ type RepositoryClass struct {
Owner string `json:"owner"`
}
-// JSON Schema describing the form fields to present to the user
+// JSON Schema describing the form fields to present to the user (form mode only)
type RequestedSchema struct {
// Form field definitions, keyed by field name
Properties map[string]interface{} `json:"properties"`
// List of required field names
- Required []string `json:"required,omitempty"`
- Type RequestedSchemaType `json:"type"`
+ Required []string `json:"required,omitempty"`
+ // Schema type indicator (always 'object')
+ Type RequestedSchemaType `json:"type"`
}
// Tool execution result on success
@@ -758,6 +1204,20 @@ type Result struct {
Kind *ResultKind `json:"kind,omitempty"`
}
+// A content block within a tool result, which may be text, terminal output, image, audio,
+// or a resource
+//
+// # Plain text content block
+//
+// Terminal/shell output content block with optional exit code and working directory
+//
+// # Image content block with base64-encoded data
+//
+// # Audio content block with base64-encoded data
+//
+// # Resource link content block referencing an external resource
+//
+// Embedded resource content block with inline text or binary data
type Content struct {
// The text content
//
@@ -795,6 +1255,7 @@ type Content struct {
Resource *ResourceClass `json:"resource,omitempty"`
}
+// Icon image for a resource
type Icon struct {
// MIME type of the icon image
MIMEType *string `json:"mimeType,omitempty"`
@@ -820,13 +1281,52 @@ type ResourceClass struct {
Blob *string `json:"blob,omitempty"`
}
+type Server struct {
+ // Error message if the server failed to connect
+ Error *string `json:"error,omitempty"`
+ // Server name (config key)
+ Name string `json:"name"`
+ // Configuration source: user, workspace, plugin, or builtin
+ Source *string `json:"source,omitempty"`
+ // Connection status: connected, failed, pending, disabled, or not_configured
+ Status ServerStatus `json:"status"`
+}
+
+type Skill struct {
+ // Description of what the skill does
+ Description string `json:"description"`
+ // Whether the skill is currently enabled
+ Enabled bool `json:"enabled"`
+ // Unique identifier for the skill
+ Name string `json:"name"`
+ // Absolute path to the skill file, if available
+ Path *string `json:"path,omitempty"`
+ // Source location type of the skill (e.g., project, personal, plugin)
+ Source string `json:"source"`
+ // Whether the skill can be invoked by the user as a slash command
+ UserInvocable bool `json:"userInvocable"`
+}
+
+// Static OAuth client configuration, if the server specifies one
+type StaticClientConfig struct {
+ // OAuth client ID for the server
+ ClientID string `json:"clientId"`
+ // Whether this is a public OAuth client
+ PublicClient *bool `json:"publicClient,omitempty"`
+}
+
+// A tool invocation request from the assistant
type ToolRequest struct {
// Arguments to pass to the tool, format depends on the tool
Arguments interface{} `json:"arguments"`
+ // Resolved intention summary describing what this specific call does
+ IntentionSummary *string `json:"intentionSummary"`
// Name of the tool being invoked
Name string `json:"name"`
// Unique identifier for this tool call
ToolCallID string `json:"toolCallId"`
+ // Human-readable display title for the tool
+ ToolTitle *string `json:"toolTitle,omitempty"`
// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool
// calls. Defaults to "function" when absent.
Type *ToolRequestType `json:"type,omitempty"`
@@ -836,34 +1336,81 @@ type ToolRequest struct {
type AgentMode string
const (
- AgentModeShell AgentMode = "shell"
- Autopilot AgentMode = "autopilot"
- Interactive AgentMode = "interactive"
- Plan AgentMode = "plan"
+ AgentModeShell AgentMode = "shell"
+ AgentModeAutopilot AgentMode = "autopilot"
+ AgentModeInteractive AgentMode = "interactive"
+ AgentModePlan AgentMode = "plan"
)
// Type of GitHub reference
type ReferenceType string
const (
- Discussion ReferenceType = "discussion"
- Issue ReferenceType = "issue"
- PR ReferenceType = "pr"
+ ReferenceTypeDiscussion ReferenceType = "discussion"
+ ReferenceTypeIssue ReferenceType = "issue"
+ ReferenceTypePr ReferenceType = "pr"
)
type AttachmentType string
const (
- Directory AttachmentType = "directory"
- File AttachmentType = "file"
- GithubReference AttachmentType = "github_reference"
- Selection AttachmentType = "selection"
+ AttachmentTypeBlob AttachmentType = "blob"
+ AttachmentTypeDirectory AttachmentType = "directory"
+ AttachmentTypeFile AttachmentType = "file"
+ AttachmentTypeGithubReference AttachmentType = "github_reference"
+ AttachmentTypeSelection AttachmentType = "selection"
+)
+
+// Hosting platform type of the repository (github or ado)
+type HostType string
+
+const (
+ HostTypeAdo HostType = "ado"
+ HostTypeGithub HostType = "github"
+)
+
+// Discovery source
+type Source string
+
+const (
+ SourceProject Source = "project"
+ SourceUser Source = "user"
+)
+
+// Current status: running, disabled, failed, or starting
+type ExtensionStatus string
+
+const (
+ ExtensionStatusDisabled ExtensionStatus = "disabled"
+ ExtensionStatusFailed ExtensionStatus = "failed"
+ ExtensionStatusRunning ExtensionStatus = "running"
+ ExtensionStatusStarting ExtensionStatus = "starting"
)
+// Whether the agent completed successfully or failed
+type KindStatus string
+
+const (
+ KindStatusCompleted KindStatus = "completed"
+ KindStatusFailed KindStatus = "failed"
+)
+
+type KindType string
+
+const (
+ KindTypeAgentCompleted KindType = "agent_completed"
+ KindTypeAgentIdle KindType = "agent_idle"
+ KindTypeShellCompleted KindType = "shell_completed"
+ KindTypeShellDetachedCompleted KindType = "shell_detached_completed"
+)
+
+// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to
+// "form" when absent.
type Mode string
const (
- Form Mode = "form"
+ ModeForm Mode = "form"
+ ModeURL Mode = "url"
)
// The type of operation performed on the plan file
@@ -872,81 +1419,95 @@ const (
type Operation string
const (
- Create Operation = "create"
- Delete Operation = "delete"
- Update Operation = "update"
+ OperationCreate Operation = "create"
+ OperationDelete Operation = "delete"
+ OperationUpdate Operation = "update"
)
type PermissionRequestKind string
const (
- CustomTool PermissionRequestKind = "custom-tool"
- KindShell PermissionRequestKind = "shell"
- MCP PermissionRequestKind = "mcp"
- Memory PermissionRequestKind = "memory"
- Read PermissionRequestKind = "read"
- URL PermissionRequestKind = "url"
- Write PermissionRequestKind = "write"
+ PermissionRequestKindCustomTool PermissionRequestKind = "custom-tool"
+ PermissionRequestKindHook PermissionRequestKind = "hook"
+ PermissionRequestKindShell PermissionRequestKind = "shell"
+ PermissionRequestKindURL PermissionRequestKind = "url"
+ PermissionRequestKindMcp PermissionRequestKind = "mcp"
+ PermissionRequestKindMemory PermissionRequestKind = "memory"
+ PermissionRequestKindRead PermissionRequestKind = "read"
+ PermissionRequestKindWrite PermissionRequestKind = "write"
)
type RequestedSchemaType string
const (
- Object RequestedSchemaType = "object"
+ RequestedSchemaTypeObject RequestedSchemaType = "object"
)
// Theme variant this icon is intended for
type Theme string
const (
- Dark Theme = "dark"
- Light Theme = "light"
+ ThemeDark Theme = "dark"
+ ThemeLight Theme = "light"
)
type ContentType string
const (
- Audio ContentType = "audio"
- Image ContentType = "image"
- Resource ContentType = "resource"
- ResourceLink ContentType = "resource_link"
- Terminal ContentType = "terminal"
- Text ContentType = "text"
+ ContentTypeAudio ContentType = "audio"
+ ContentTypeImage ContentType = "image"
+ ContentTypeResource ContentType = "resource"
+ ContentTypeResourceLink ContentType = "resource_link"
+ ContentTypeTerminal ContentType = "terminal"
+ ContentTypeText ContentType = "text"
)
// The outcome of the permission request
type ResultKind string
const (
- Approved ResultKind = "approved"
- DeniedByContentExclusionPolicy ResultKind = "denied-by-content-exclusion-policy"
- DeniedByRules ResultKind = "denied-by-rules"
- DeniedInteractivelyByUser ResultKind = "denied-interactively-by-user"
- DeniedNoApprovalRuleAndCouldNotRequestFromUser ResultKind = "denied-no-approval-rule-and-could-not-request-from-user"
+ ResultKindApproved ResultKind = "approved"
+ ResultKindDeniedByContentExclusionPolicy ResultKind = "denied-by-content-exclusion-policy"
+ ResultKindDeniedByRules ResultKind = "denied-by-rules"
+ ResultKindDeniedInteractivelyByUser ResultKind = "denied-interactively-by-user"
+ ResultKindDeniedNoApprovalRuleAndCouldNotRequestFromUser ResultKind = "denied-no-approval-rule-and-could-not-request-from-user"
)
// Message role: "system" for system prompts, "developer" for developer-injected instructions
type Role string
const (
- Developer Role = "developer"
- System Role = "system"
+ RoleDeveloper Role = "developer"
+ RoleSystem Role = "system"
+)
+
+// Connection status: connected, failed, pending, disabled, or not_configured
+//
+// New connection status: connected, failed, pending, disabled, or not_configured
+type ServerStatus string
+
+const (
+ ServerStatusConnected ServerStatus = "connected"
+ ServerStatusDisabled ServerStatus = "disabled"
+ ServerStatusNotConfigured ServerStatus = "not_configured"
+ ServerStatusPending ServerStatus = "pending"
+ ServerStatusFailed ServerStatus = "failed"
)
// Whether the session ended normally ("routine") or due to a crash/fatal error ("error")
type ShutdownType string
const (
- Error ShutdownType = "error"
- Routine ShutdownType = "routine"
+ ShutdownTypeError ShutdownType = "error"
+ ShutdownTypeRoutine ShutdownType = "routine"
)
// Origin type of the session being handed off
type SourceType string
const (
- Local SourceType = "local"
- Remote SourceType = "remote"
+ SourceTypeLocal SourceType = "local"
+ SourceTypeRemote SourceType = "remote"
)
// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool
@@ -954,71 +1515,82 @@ const (
type ToolRequestType string
const (
- Custom ToolRequestType = "custom"
- Function ToolRequestType = "function"
+ ToolRequestTypeCustom ToolRequestType = "custom"
+ ToolRequestTypeFunction ToolRequestType = "function"
)
type SessionEventType string
const (
- Abort SessionEventType = "abort"
- AssistantIntent SessionEventType = "assistant.intent"
- AssistantMessage SessionEventType = "assistant.message"
- AssistantMessageDelta SessionEventType = "assistant.message_delta"
- AssistantReasoning SessionEventType = "assistant.reasoning"
- AssistantReasoningDelta SessionEventType = "assistant.reasoning_delta"
- AssistantStreamingDelta SessionEventType = "assistant.streaming_delta"
- AssistantTurnEnd SessionEventType = "assistant.turn_end"
- AssistantTurnStart SessionEventType = "assistant.turn_start"
- AssistantUsage SessionEventType = "assistant.usage"
- CommandCompleted SessionEventType = "command.completed"
- CommandQueued SessionEventType = "command.queued"
- ElicitationCompleted SessionEventType = "elicitation.completed"
- ElicitationRequested SessionEventType = "elicitation.requested"
- ExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed"
- ExitPlanModeRequested SessionEventType = "exit_plan_mode.requested"
- ExternalToolCompleted SessionEventType = "external_tool.completed"
- ExternalToolRequested SessionEventType = "external_tool.requested"
- HookEnd SessionEventType = "hook.end"
- HookStart SessionEventType = "hook.start"
- PendingMessagesModified SessionEventType = "pending_messages.modified"
- PermissionCompleted SessionEventType = "permission.completed"
- PermissionRequested SessionEventType = "permission.requested"
- SessionCompactionComplete SessionEventType = "session.compaction_complete"
- SessionCompactionStart SessionEventType = "session.compaction_start"
- SessionContextChanged SessionEventType = "session.context_changed"
- SessionError SessionEventType = "session.error"
- SessionHandoff SessionEventType = "session.handoff"
- SessionIdle SessionEventType = "session.idle"
- SessionInfo SessionEventType = "session.info"
- SessionModeChanged SessionEventType = "session.mode_changed"
- SessionModelChange SessionEventType = "session.model_change"
- SessionPlanChanged SessionEventType = "session.plan_changed"
- SessionResume SessionEventType = "session.resume"
- SessionShutdown SessionEventType = "session.shutdown"
- SessionSnapshotRewind SessionEventType = "session.snapshot_rewind"
- SessionStart SessionEventType = "session.start"
- SessionTaskComplete SessionEventType = "session.task_complete"
- SessionTitleChanged SessionEventType = "session.title_changed"
- SessionTruncation SessionEventType = "session.truncation"
- SessionUsageInfo SessionEventType = "session.usage_info"
- SessionWarning SessionEventType = "session.warning"
- SessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed"
- SkillInvoked SessionEventType = "skill.invoked"
- SubagentCompleted SessionEventType = "subagent.completed"
- SubagentDeselected SessionEventType = "subagent.deselected"
- SubagentFailed SessionEventType = "subagent.failed"
- SubagentSelected SessionEventType = "subagent.selected"
- SubagentStarted SessionEventType = "subagent.started"
- SystemMessage SessionEventType = "system.message"
- ToolExecutionComplete SessionEventType = "tool.execution_complete"
- ToolExecutionPartialResult SessionEventType = "tool.execution_partial_result"
- ToolExecutionProgress SessionEventType = "tool.execution_progress"
- ToolExecutionStart SessionEventType = "tool.execution_start"
- ToolUserRequested SessionEventType = "tool.user_requested"
- UserInputCompleted SessionEventType = "user_input.completed"
- UserInputRequested SessionEventType = "user_input.requested"
- UserMessage SessionEventType = "user.message"
+ SessionEventTypeAbort SessionEventType = "abort"
+ SessionEventTypeAssistantIntent SessionEventType = "assistant.intent"
+ SessionEventTypeAssistantMessage SessionEventType = "assistant.message"
+ SessionEventTypeAssistantMessageDelta SessionEventType = "assistant.message_delta"
+ SessionEventTypeAssistantReasoning SessionEventType = "assistant.reasoning"
+ SessionEventTypeAssistantReasoningDelta SessionEventType = "assistant.reasoning_delta"
+ SessionEventTypeAssistantStreamingDelta SessionEventType = "assistant.streaming_delta"
+ SessionEventTypeAssistantTurnEnd SessionEventType = "assistant.turn_end"
+ SessionEventTypeAssistantTurnStart SessionEventType = "assistant.turn_start"
+ SessionEventTypeAssistantUsage SessionEventType = "assistant.usage"
+ SessionEventTypeCommandCompleted SessionEventType = "command.completed"
+ SessionEventTypeCommandExecute SessionEventType = "command.execute"
+ SessionEventTypeCommandQueued SessionEventType = "command.queued"
+ SessionEventTypeCommandsChanged SessionEventType = "commands.changed"
+ SessionEventTypeElicitationCompleted SessionEventType = "elicitation.completed"
+ SessionEventTypeElicitationRequested SessionEventType = "elicitation.requested"
+ SessionEventTypeExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed"
+ SessionEventTypeExitPlanModeRequested SessionEventType = "exit_plan_mode.requested"
+ SessionEventTypeExternalToolCompleted SessionEventType = "external_tool.completed"
+ SessionEventTypeExternalToolRequested SessionEventType = "external_tool.requested"
+ SessionEventTypeHookEnd SessionEventType = "hook.end"
+ SessionEventTypeHookStart SessionEventType = "hook.start"
+ SessionEventTypeMcpOauthCompleted SessionEventType = "mcp.oauth_completed"
+ SessionEventTypeMcpOauthRequired SessionEventType = "mcp.oauth_required"
+ SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified"
+ SessionEventTypePermissionCompleted SessionEventType = "permission.completed"
+ SessionEventTypePermissionRequested SessionEventType = "permission.requested"
+ SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed"
+ SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete"
+ SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start"
+ SessionEventTypeSessionContextChanged SessionEventType = "session.context_changed"
+ SessionEventTypeSessionError SessionEventType = "session.error"
+ SessionEventTypeSessionExtensionsLoaded SessionEventType = "session.extensions_loaded"
+ SessionEventTypeSessionHandoff SessionEventType = "session.handoff"
+ SessionEventTypeSessionIdle SessionEventType = "session.idle"
+ SessionEventTypeSessionInfo SessionEventType = "session.info"
+ SessionEventTypeSessionMcpServerStatusChanged SessionEventType = "session.mcp_server_status_changed"
+ SessionEventTypeSessionMcpServersLoaded SessionEventType = "session.mcp_servers_loaded"
+ SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed"
+ SessionEventTypeSessionModelChange SessionEventType = "session.model_change"
+ SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed"
+ SessionEventTypeSessionResume SessionEventType = "session.resume"
+ SessionEventTypeSessionShutdown SessionEventType = "session.shutdown"
+ SessionEventTypeSessionSkillsLoaded SessionEventType = "session.skills_loaded"
+ SessionEventTypeSessionSnapshotRewind SessionEventType = "session.snapshot_rewind"
+ SessionEventTypeSessionStart SessionEventType = "session.start"
+ SessionEventTypeSessionTaskComplete SessionEventType = "session.task_complete"
+ SessionEventTypeSessionTitleChanged SessionEventType = "session.title_changed"
+ SessionEventTypeSessionToolsUpdated SessionEventType = "session.tools_updated"
+ SessionEventTypeSessionTruncation SessionEventType = "session.truncation"
+ SessionEventTypeSessionUsageInfo SessionEventType = "session.usage_info"
+ SessionEventTypeSessionWarning SessionEventType = "session.warning"
+ SessionEventTypeSessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed"
+ SessionEventTypeSkillInvoked SessionEventType = "skill.invoked"
+ SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed"
+ SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected"
+ SessionEventTypeSubagentFailed SessionEventType = "subagent.failed"
+ SessionEventTypeSubagentSelected SessionEventType = "subagent.selected"
+ SessionEventTypeSubagentStarted SessionEventType = "subagent.started"
+ SessionEventTypeSystemMessage SessionEventType = "system.message"
+ SessionEventTypeSystemNotification SessionEventType = "system.notification"
+ SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete"
+ SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result"
+ SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress"
+ SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start"
+ SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested"
+ SessionEventTypeUserInputCompleted SessionEventType = "user_input.completed"
+ SessionEventTypeUserInputRequested SessionEventType = "user_input.requested"
+ SessionEventTypeUserMessage SessionEventType = "user.message"
)
type ContextUnion struct {
diff --git a/go/go.mod b/go/go.mod
index c835cc889e..ed06061a0d 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -6,3 +6,16 @@ require (
github.com/google/jsonschema-go v0.4.2
github.com/klauspost/compress v1.18.3
)
+
+require (
+ github.com/google/uuid v1.6.0
+ go.opentelemetry.io/otel v1.35.0
+)
+
+require (
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/trace v1.35.0 // indirect
+)
diff --git a/go/go.sum b/go/go.sum
index 0cc670e8f1..ec2bbcc1e8 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -1,6 +1,29 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go/internal/e2e/agent_and_compact_rpc_test.go b/go/internal/e2e/agent_and_compact_rpc_test.go
index 338f4da674..cbd52a326e 100644
--- a/go/internal/e2e/agent_and_compact_rpc_test.go
+++ b/go/internal/e2e/agent_and_compact_rpc_test.go
@@ -215,7 +215,7 @@ func TestAgentSelectionRpc(t *testing.T) {
}
})
- t.Run("should return empty list when no custom agents configured", func(t *testing.T) {
+ t.Run("should return no custom agents when none configured", func(t *testing.T) {
client := copilot.NewClient(&copilot.ClientOptions{
CLIPath: cliPath,
UseStdio: copilot.Bool(true),
@@ -238,8 +238,13 @@ func TestAgentSelectionRpc(t *testing.T) {
t.Fatalf("Failed to list agents: %v", err)
}
- if len(result.Agents) != 0 {
- t.Errorf("Expected empty agent list, got %d agents", len(result.Agents))
+ // The CLI may return built-in/default agents even when no custom agents
+ // are configured, so just verify none of the known custom agent names appear.
+ customNames := map[string]bool{"test-agent": true, "another-agent": true}
+ for _, agent := range result.Agents {
+ if customNames[agent.Name] {
+ t.Errorf("Expected no custom agents, but found %q", agent.Name)
+ }
}
if err := client.Stop(); err != nil {
diff --git a/go/internal/e2e/compaction_test.go b/go/internal/e2e/compaction_test.go
index aee80704dc..888ab2aa95 100644
--- a/go/internal/e2e/compaction_test.go
+++ b/go/internal/e2e/compaction_test.go
@@ -36,10 +36,10 @@ func TestCompaction(t *testing.T) {
var compactionCompleteEvents []copilot.SessionEvent
session.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.SessionCompactionStart {
+ if event.Type == copilot.SessionEventTypeSessionCompactionStart {
compactionStartEvents = append(compactionStartEvents, event)
}
- if event.Type == copilot.SessionCompactionComplete {
+ if event.Type == copilot.SessionEventTypeSessionCompactionComplete {
compactionCompleteEvents = append(compactionCompleteEvents, event)
}
})
@@ -105,7 +105,7 @@ func TestCompaction(t *testing.T) {
var compactionEvents []copilot.SessionEvent
session.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.SessionCompactionStart || event.Type == copilot.SessionCompactionComplete {
+ if event.Type == copilot.SessionEventTypeSessionCompactionStart || event.Type == copilot.SessionEventTypeSessionCompactionComplete {
compactionEvents = append(compactionEvents, event)
}
})
diff --git a/go/internal/e2e/multi_client_test.go b/go/internal/e2e/multi_client_test.go
index 9571ab58ed..3c7dc34c3d 100644
--- a/go/internal/e2e/multi_client_test.go
+++ b/go/internal/e2e/multi_client_test.go
@@ -79,13 +79,13 @@ func TestMultiClient(t *testing.T) {
client2Completed := make(chan struct{}, 1)
session1.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.ExternalToolRequested {
+ if event.Type == copilot.SessionEventTypeExternalToolRequested {
select {
case client1Requested <- struct{}{}:
default:
}
}
- if event.Type == copilot.ExternalToolCompleted {
+ if event.Type == copilot.SessionEventTypeExternalToolCompleted {
select {
case client1Completed <- struct{}{}:
default:
@@ -93,13 +93,13 @@ func TestMultiClient(t *testing.T) {
}
})
session2.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.ExternalToolRequested {
+ if event.Type == copilot.SessionEventTypeExternalToolRequested {
select {
case client2Requested <- struct{}{}:
default:
}
}
- if event.Type == copilot.ExternalToolCompleted {
+ if event.Type == copilot.SessionEventTypeExternalToolCompleted {
select {
case client2Completed <- struct{}{}:
default:
@@ -120,7 +120,7 @@ func TestMultiClient(t *testing.T) {
}
// Wait for all broadcast events to arrive on both clients
- timeout := time.After(10 * time.Second)
+ timeout := time.After(30 * time.Second)
for _, ch := range []chan struct{}{client1Requested, client2Requested, client1Completed, client2Completed} {
select {
case <-ch:
@@ -197,10 +197,10 @@ func TestMultiClient(t *testing.T) {
// Both clients should have seen permission.requested events
mu1.Lock()
- c1PermRequested := filterEventsByType(client1Events, copilot.PermissionRequested)
+ c1PermRequested := filterEventsByType(client1Events, copilot.SessionEventTypePermissionRequested)
mu1.Unlock()
mu2.Lock()
- c2PermRequested := filterEventsByType(client2Events, copilot.PermissionRequested)
+ c2PermRequested := filterEventsByType(client2Events, copilot.SessionEventTypePermissionRequested)
mu2.Unlock()
if len(c1PermRequested) == 0 {
@@ -212,10 +212,10 @@ func TestMultiClient(t *testing.T) {
// Both clients should have seen permission.completed events with approved result
mu1.Lock()
- c1PermCompleted := filterEventsByType(client1Events, copilot.PermissionCompleted)
+ c1PermCompleted := filterEventsByType(client1Events, copilot.SessionEventTypePermissionCompleted)
mu1.Unlock()
mu2.Lock()
- c2PermCompleted := filterEventsByType(client2Events, copilot.PermissionCompleted)
+ c2PermCompleted := filterEventsByType(client2Events, copilot.SessionEventTypePermissionCompleted)
mu2.Unlock()
if len(c1PermCompleted) == 0 {
@@ -293,10 +293,10 @@ func TestMultiClient(t *testing.T) {
// Both clients should have seen permission.requested events
mu1.Lock()
- c1PermRequested := filterEventsByType(client1Events, copilot.PermissionRequested)
+ c1PermRequested := filterEventsByType(client1Events, copilot.SessionEventTypePermissionRequested)
mu1.Unlock()
mu2.Lock()
- c2PermRequested := filterEventsByType(client2Events, copilot.PermissionRequested)
+ c2PermRequested := filterEventsByType(client2Events, copilot.SessionEventTypePermissionRequested)
mu2.Unlock()
if len(c1PermRequested) == 0 {
@@ -308,10 +308,10 @@ func TestMultiClient(t *testing.T) {
// Both clients should see the denial in the completed event
mu1.Lock()
- c1PermCompleted := filterEventsByType(client1Events, copilot.PermissionCompleted)
+ c1PermCompleted := filterEventsByType(client1Events, copilot.SessionEventTypePermissionCompleted)
mu1.Unlock()
mu2.Lock()
- c2PermCompleted := filterEventsByType(client2Events, copilot.PermissionCompleted)
+ c2PermCompleted := filterEventsByType(client2Events, copilot.SessionEventTypePermissionCompleted)
mu2.Unlock()
if len(c1PermCompleted) == 0 {
diff --git a/go/internal/e2e/permissions_test.go b/go/internal/e2e/permissions_test.go
index 328e7e7884..98f6200435 100644
--- a/go/internal/e2e/permissions_test.go
+++ b/go/internal/e2e/permissions_test.go
@@ -173,7 +173,7 @@ func TestPermissions(t *testing.T) {
permissionDenied := false
session.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.ToolExecutionComplete &&
+ if event.Type == copilot.SessionEventTypeToolExecutionComplete &&
event.Data.Success != nil && !*event.Data.Success &&
event.Data.Error != nil && event.Data.Error.ErrorClass != nil &&
strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") {
@@ -223,7 +223,7 @@ func TestPermissions(t *testing.T) {
permissionDenied := false
session2.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.ToolExecutionComplete &&
+ if event.Type == copilot.SessionEventTypeToolExecutionComplete &&
event.Data.Success != nil && !*event.Data.Success &&
event.Data.Error != nil && event.Data.Error.ErrorClass != nil &&
strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") {
diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go
index 61a5e338d8..3d69b97ad7 100644
--- a/go/internal/e2e/rpc_test.go
+++ b/go/internal/e2e/rpc_test.go
@@ -168,9 +168,11 @@ func TestSessionRpc(t *testing.T) {
t.Error("Expected initial modelId to be defined")
}
- // Switch to a different model
+ // Switch to a different model with reasoning effort
+ re := "high"
result, err := session.RPC.Model.SwitchTo(t.Context(), &rpc.SessionModelSwitchToParams{
- ModelID: "gpt-4.1",
+ ModelID: "gpt-4.1",
+ ReasoningEffort: &re,
})
if err != nil {
t.Fatalf("Failed to switch model: %v", err)
@@ -201,7 +203,7 @@ func TestSessionRpc(t *testing.T) {
t.Fatalf("Failed to create session: %v", err)
}
- if err := session.SetModel(t.Context(), "gpt-4.1"); err != nil {
+ if err := session.SetModel(t.Context(), "gpt-4.1", copilot.SetModelOptions{ReasoningEffort: "high"}); err != nil {
t.Fatalf("SetModel returned error: %v", err)
}
})
@@ -217,16 +219,16 @@ func TestSessionRpc(t *testing.T) {
if err != nil {
t.Fatalf("Failed to get mode: %v", err)
}
- if initial.Mode != rpc.Interactive {
+ if initial.Mode != rpc.ModeInteractive {
t.Errorf("Expected initial mode 'interactive', got %q", initial.Mode)
}
// Switch to plan mode
- planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Plan})
+ planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.ModePlan})
if err != nil {
t.Fatalf("Failed to set mode to plan: %v", err)
}
- if planResult.Mode != rpc.Plan {
+ if planResult.Mode != rpc.ModePlan {
t.Errorf("Expected mode 'plan', got %q", planResult.Mode)
}
@@ -235,16 +237,16 @@ func TestSessionRpc(t *testing.T) {
if err != nil {
t.Fatalf("Failed to get mode after plan: %v", err)
}
- if afterPlan.Mode != rpc.Plan {
+ if afterPlan.Mode != rpc.ModePlan {
t.Errorf("Expected mode 'plan' after set, got %q", afterPlan.Mode)
}
// Switch back to interactive
- interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Interactive})
+ interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.ModeInteractive})
if err != nil {
t.Fatalf("Failed to set mode to interactive: %v", err)
}
- if interactiveResult.Mode != rpc.Interactive {
+ if interactiveResult.Mode != rpc.ModeInteractive {
t.Errorf("Expected mode 'interactive', got %q", interactiveResult.Mode)
}
})
diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go
index d1902311f3..7f1817da92 100644
--- a/go/internal/e2e/session_test.go
+++ b/go/internal/e2e/session_test.go
@@ -3,11 +3,13 @@ package e2e
import (
"regexp"
"strings"
+ "sync"
"testing"
"time"
copilot "github.com/github/copilot-sdk/go"
"github.com/github/copilot-sdk/go/internal/e2e/testharness"
+ "github.com/github/copilot-sdk/go/rpc"
)
func TestSession(t *testing.T) {
@@ -182,6 +184,51 @@ func TestSession(t *testing.T) {
}
})
+ t.Run("should create a session with customized systemMessage config", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ customTone := "Respond in a warm, professional tone. Be thorough in explanations."
+ appendedContent := "Always mention quarterly earnings."
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ copilot.SectionTone: {Action: "replace", Content: customTone},
+ copilot.SectionCodeChangeRules: {Action: "remove"},
+ },
+ Content: appendedContent,
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Who are you?"})
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ // Validate the system message sent to the model
+ traffic, err := ctx.GetExchanges()
+ if err != nil {
+ t.Fatalf("Failed to get exchanges: %v", err)
+ }
+ if len(traffic) == 0 {
+ t.Fatal("Expected at least one exchange")
+ }
+ systemMessage := getSystemMessage(traffic[0])
+ if !strings.Contains(systemMessage, customTone) {
+ t.Errorf("Expected system message to contain custom tone, got %q", systemMessage)
+ }
+ if !strings.Contains(systemMessage, appendedContent) {
+ t.Errorf("Expected system message to contain appended content, got %q", systemMessage)
+ }
+ if strings.Contains(systemMessage, "") {
+ t.Error("Expected system message to NOT contain code_change_instructions (it was removed)")
+ }
+ })
+
t.Run("should create a session with availableTools", func(t *testing.T) {
ctx.ConfigureForTest(t)
@@ -504,7 +551,7 @@ func TestSession(t *testing.T) {
toolStartCh := make(chan *copilot.SessionEvent, 1)
toolStartErrCh := make(chan error, 1)
go func() {
- evt, err := testharness.GetNextEventOfType(session, copilot.ToolExecutionStart, 60*time.Second)
+ evt, err := testharness.GetNextEventOfType(session, copilot.SessionEventTypeToolExecutionStart, 60*time.Second)
if err != nil {
toolStartErrCh <- err
} else {
@@ -515,7 +562,7 @@ func TestSession(t *testing.T) {
sessionIdleCh := make(chan *copilot.SessionEvent, 1)
sessionIdleErrCh := make(chan error, 1)
go func() {
- evt, err := testharness.GetNextEventOfType(session, copilot.SessionIdle, 60*time.Second)
+ evt, err := testharness.GetNextEventOfType(session, copilot.SessionEventTypeSessionIdle, 60*time.Second)
if err != nil {
sessionIdleErrCh <- err
} else {
@@ -563,7 +610,7 @@ func TestSession(t *testing.T) {
// Verify messages contain an abort event
hasAbortEvent := false
for _, msg := range messages {
- if msg.Type == copilot.Abort {
+ if msg.Type == copilot.SessionEventTypeAbort {
hasAbortEvent = true
break
}
@@ -586,11 +633,31 @@ func TestSession(t *testing.T) {
t.Run("should receive session events", func(t *testing.T) {
ctx.ConfigureForTest(t)
- session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll})
+ // Use OnEvent to capture events dispatched during session creation.
+ // session.start is emitted during the session.create RPC; with channel-based
+ // dispatch it may not have been delivered by the time CreateSession returns.
+ sessionStartCh := make(chan bool, 1)
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ OnEvent: func(event copilot.SessionEvent) {
+ if event.Type == "session.start" {
+ select {
+ case sessionStartCh <- true:
+ default:
+ }
+ }
+ },
+ })
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
+ select {
+ case <-sessionStartCh:
+ case <-time.After(5 * time.Second):
+ t.Error("Expected session.start event via OnEvent during creation")
+ }
+
var receivedEvents []copilot.SessionEvent
idle := make(chan bool)
@@ -735,10 +802,10 @@ func TestSession(t *testing.T) {
// Verify both sessions are in the list
if !contains(sessionIDs, session1.SessionID) {
- t.Errorf("Expected session1 ID %s to be in sessions list", session1.SessionID)
+ t.Errorf("Expected session1 ID %s to be in sessions list %v", session1.SessionID, sessionIDs)
}
if !contains(sessionIDs, session2.SessionID) {
- t.Errorf("Expected session2 ID %s to be in sessions list", session2.SessionID)
+ t.Errorf("Expected session2 ID %s to be in sessions list %v", session2.SessionID, sessionIDs)
}
// Verify session metadata structure
@@ -873,6 +940,91 @@ func getSystemMessage(exchange testharness.ParsedHttpExchange) string {
return ""
}
+func TestSetModelWithReasoningEffort(t *testing.T) {
+ ctx := testharness.NewTestContext(t)
+ client := ctx.NewClient()
+ t.Cleanup(func() { client.ForceStop() })
+
+ if err := client.Start(t.Context()); err != nil {
+ t.Fatalf("Failed to start client: %v", err)
+ }
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ modelChanged := make(chan copilot.SessionEvent, 1)
+ session.On(func(event copilot.SessionEvent) {
+ if event.Type == copilot.SessionEventTypeSessionModelChange {
+ select {
+ case modelChanged <- event:
+ default:
+ }
+ }
+ })
+
+ if err := session.SetModel(t.Context(), "gpt-4.1", copilot.SetModelOptions{ReasoningEffort: "high"}); err != nil {
+ t.Fatalf("SetModel returned error: %v", err)
+ }
+
+ select {
+ case evt := <-modelChanged:
+ if evt.Data.NewModel == nil || *evt.Data.NewModel != "gpt-4.1" {
+ t.Errorf("Expected newModel 'gpt-4.1', got %v", evt.Data.NewModel)
+ }
+ if evt.Data.ReasoningEffort == nil || *evt.Data.ReasoningEffort != "high" {
+ t.Errorf("Expected reasoningEffort 'high', got %v", evt.Data.ReasoningEffort)
+ }
+ case <-time.After(30 * time.Second):
+ t.Fatal("Timed out waiting for session.model_change event")
+ }
+}
+
+func TestSessionBlobAttachment(t *testing.T) {
+ ctx := testharness.NewTestContext(t)
+ client := ctx.NewClient()
+ t.Cleanup(func() { client.ForceStop() })
+
+ if err := client.Start(t.Context()); err != nil {
+ t.Fatalf("Failed to start client: %v", err)
+ }
+
+ t.Run("should accept blob attachments", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ data := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
+ mimeType := "image/png"
+ displayName := "test-pixel.png"
+ _, err = session.Send(t.Context(), copilot.MessageOptions{
+ Prompt: "Describe this image",
+ Attachments: []copilot.Attachment{
+ {
+ Type: copilot.AttachmentTypeBlob,
+ Data: &data,
+ MIMEType: &mimeType,
+ DisplayName: &displayName,
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("Send with blob attachment failed: %v", err)
+ }
+
+ // Just verify send doesn't error — blob attachment support varies by runtime
+ session.Disconnect()
+ })
+}
+
func getToolNames(exchange testharness.ParsedHttpExchange) []string {
var names []string
for _, tool := range exchange.Request.Tools {
@@ -889,3 +1041,105 @@ func contains(slice []string, item string) bool {
}
return false
}
+
+func TestSessionLog(t *testing.T) {
+ ctx := testharness.NewTestContext(t)
+ client := ctx.NewClient()
+ t.Cleanup(func() { client.ForceStop() })
+
+ if err := client.Start(t.Context()); err != nil {
+ t.Fatalf("Failed to start client: %v", err)
+ }
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ // Collect events
+ var events []copilot.SessionEvent
+ var mu sync.Mutex
+ unsubscribe := session.On(func(event copilot.SessionEvent) {
+ mu.Lock()
+ defer mu.Unlock()
+ events = append(events, event)
+ })
+ defer unsubscribe()
+
+ t.Run("should log info message (default level)", func(t *testing.T) {
+ if err := session.Log(t.Context(), "Info message", nil); err != nil {
+ t.Fatalf("Log failed: %v", err)
+ }
+
+ evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionInfo, "Info message", 5*time.Second)
+ if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" {
+ t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType)
+ }
+ if evt.Data.Message == nil || *evt.Data.Message != "Info message" {
+ t.Errorf("Expected message 'Info message', got %v", evt.Data.Message)
+ }
+ })
+
+ t.Run("should log warning message", func(t *testing.T) {
+ if err := session.Log(t.Context(), "Warning message", &copilot.LogOptions{Level: rpc.LevelWarning}); err != nil {
+ t.Fatalf("Log failed: %v", err)
+ }
+
+ evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionWarning, "Warning message", 5*time.Second)
+ if evt.Data.WarningType == nil || *evt.Data.WarningType != "notification" {
+ t.Errorf("Expected warningType 'notification', got %v", evt.Data.WarningType)
+ }
+ if evt.Data.Message == nil || *evt.Data.Message != "Warning message" {
+ t.Errorf("Expected message 'Warning message', got %v", evt.Data.Message)
+ }
+ })
+
+ t.Run("should log error message", func(t *testing.T) {
+ if err := session.Log(t.Context(), "Error message", &copilot.LogOptions{Level: rpc.LevelError}); err != nil {
+ t.Fatalf("Log failed: %v", err)
+ }
+
+ evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionError, "Error message", 5*time.Second)
+ if evt.Data.ErrorType == nil || *evt.Data.ErrorType != "notification" {
+ t.Errorf("Expected errorType 'notification', got %v", evt.Data.ErrorType)
+ }
+ if evt.Data.Message == nil || *evt.Data.Message != "Error message" {
+ t.Errorf("Expected message 'Error message', got %v", evt.Data.Message)
+ }
+ })
+
+ t.Run("should log ephemeral message", func(t *testing.T) {
+ if err := session.Log(t.Context(), "Ephemeral message", &copilot.LogOptions{Ephemeral: copilot.Bool(true)}); err != nil {
+ t.Fatalf("Log failed: %v", err)
+ }
+
+ evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionInfo, "Ephemeral message", 5*time.Second)
+ if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" {
+ t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType)
+ }
+ if evt.Data.Message == nil || *evt.Data.Message != "Ephemeral message" {
+ t.Errorf("Expected message 'Ephemeral message', got %v", evt.Data.Message)
+ }
+ })
+}
+
+// waitForEvent polls the collected events for a matching event type and message.
+func waitForEvent(t *testing.T, mu *sync.Mutex, events *[]copilot.SessionEvent, eventType copilot.SessionEventType, message string, timeout time.Duration) copilot.SessionEvent {
+ t.Helper()
+ deadline := time.Now().Add(timeout)
+ for time.Now().Before(deadline) {
+ mu.Lock()
+ for _, evt := range *events {
+ if evt.Type == eventType && evt.Data.Message != nil && *evt.Data.Message == message {
+ mu.Unlock()
+ return evt
+ }
+ }
+ mu.Unlock()
+ time.Sleep(50 * time.Millisecond)
+ }
+ t.Fatalf("Timed out waiting for %s event with message %q", eventType, message)
+ return copilot.SessionEvent{} // unreachable
+}
diff --git a/go/internal/e2e/system_message_transform_test.go b/go/internal/e2e/system_message_transform_test.go
new file mode 100644
index 0000000000..2d62b01cfb
--- /dev/null
+++ b/go/internal/e2e/system_message_transform_test.go
@@ -0,0 +1,189 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+package e2e
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+
+ copilot "github.com/github/copilot-sdk/go"
+ "github.com/github/copilot-sdk/go/internal/e2e/testharness"
+)
+
+func TestSystemMessageTransform(t *testing.T) {
+ ctx := testharness.NewTestContext(t)
+ client := ctx.NewClient()
+ t.Cleanup(func() { client.ForceStop() })
+
+ t.Run("should_invoke_transform_callbacks_with_section_content", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ var identityContent string
+ var toneContent string
+ var mu sync.Mutex
+ identityCalled := false
+ toneCalled := false
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ "identity": {
+ Transform: func(currentContent string) (string, error) {
+ mu.Lock()
+ identityCalled = true
+ identityContent = currentContent
+ mu.Unlock()
+ return currentContent, nil
+ },
+ },
+ "tone": {
+ Transform: func(currentContent string) (string, error) {
+ mu.Lock()
+ toneCalled = true
+ toneContent = currentContent
+ mu.Unlock()
+ return currentContent, nil
+ },
+ },
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ testFile := filepath.Join(ctx.WorkDir, "test.txt")
+ err = os.WriteFile(testFile, []byte("Hello transform!"), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write test file: %v", err)
+ }
+
+ _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{
+ Prompt: "Read the contents of test.txt and tell me what it says",
+ })
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+
+ if !identityCalled {
+ t.Error("Expected identity transform callback to be invoked")
+ }
+ if !toneCalled {
+ t.Error("Expected tone transform callback to be invoked")
+ }
+ if identityContent == "" {
+ t.Error("Expected identity transform to receive non-empty content")
+ }
+ if toneContent == "" {
+ t.Error("Expected tone transform to receive non-empty content")
+ }
+ })
+
+ t.Run("should_apply_transform_modifications_to_section_content", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ "identity": {
+ Transform: func(currentContent string) (string, error) {
+ return currentContent + "\nAlways end your reply with TRANSFORM_MARKER", nil
+ },
+ },
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ testFile := filepath.Join(ctx.WorkDir, "hello.txt")
+ err = os.WriteFile(testFile, []byte("Hello!"), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write test file: %v", err)
+ }
+
+ assistantMessage, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
+ Prompt: "Read the contents of hello.txt",
+ })
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ // Verify the transform result was actually applied to the system message
+ traffic, err := ctx.GetExchanges()
+ if err != nil {
+ t.Fatalf("Failed to get exchanges: %v", err)
+ }
+ if len(traffic) == 0 {
+ t.Fatal("Expected at least one exchange")
+ }
+ systemMessage := getSystemMessage(traffic[0])
+ if !strings.Contains(systemMessage, "TRANSFORM_MARKER") {
+ t.Errorf("Expected system message to contain TRANSFORM_MARKER, got %q", systemMessage)
+ }
+
+ _ = assistantMessage
+ })
+
+ t.Run("should_work_with_static_overrides_and_transforms_together", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ var mu sync.Mutex
+ transformCalled := false
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ "safety": {
+ Action: copilot.SectionActionRemove,
+ },
+ "identity": {
+ Transform: func(currentContent string) (string, error) {
+ mu.Lock()
+ transformCalled = true
+ mu.Unlock()
+ return currentContent, nil
+ },
+ },
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ testFile := filepath.Join(ctx.WorkDir, "combo.txt")
+ err = os.WriteFile(testFile, []byte("Combo test!"), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write test file: %v", err)
+ }
+
+ _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{
+ Prompt: "Read the contents of combo.txt and tell me what it says",
+ })
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+
+ if !transformCalled {
+ t.Error("Expected identity transform callback to be invoked")
+ }
+ })
+}
diff --git a/go/internal/e2e/testharness/helper.go b/go/internal/e2e/testharness/helper.go
index 05947c8061..3b521f3303 100644
--- a/go/internal/e2e/testharness/helper.go
+++ b/go/internal/e2e/testharness/helper.go
@@ -67,7 +67,7 @@ func GetNextEventOfType(session *copilot.Session, eventType copilot.SessionEvent
case result <- &event:
default:
}
- case copilot.SessionError:
+ case copilot.SessionEventTypeSessionError:
msg := "session error"
if event.Data.Message != nil {
msg = *event.Data.Message
diff --git a/go/internal/e2e/tools_test.go b/go/internal/e2e/tools_test.go
index 83f3780c15..c9676363f1 100644
--- a/go/internal/e2e/tools_test.go
+++ b/go/internal/e2e/tools_test.go
@@ -264,6 +264,52 @@ func TestTools(t *testing.T) {
}
})
+ t.Run("skipPermission sent in tool definition", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ type LookupParams struct {
+ ID string `json:"id" jsonschema:"ID to look up"`
+ }
+
+ safeLookupTool := copilot.DefineTool("safe_lookup", "A safe lookup that skips permission",
+ func(params LookupParams, inv copilot.ToolInvocation) (string, error) {
+ return "RESULT: " + params.ID, nil
+ })
+ safeLookupTool.SkipPermission = true
+
+ didRunPermissionRequest := false
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
+ didRunPermissionRequest = true
+ return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil
+ },
+ Tools: []copilot.Tool{
+ safeLookupTool,
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Use safe_lookup to look up 'test123'"})
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ answer, err := testharness.GetFinalAssistantMessage(t.Context(), session)
+ if err != nil {
+ t.Fatalf("Failed to get assistant message: %v", err)
+ }
+
+ if answer.Data.Content == nil || !strings.Contains(*answer.Data.Content, "RESULT: test123") {
+ t.Errorf("Expected answer to contain 'RESULT: test123', got %v", answer.Data.Content)
+ }
+
+ if didRunPermissionRequest {
+ t.Errorf("Expected permission handler to NOT be called for skipPermission tool")
+ }
+ })
+
t.Run("overrides built-in tool with custom tool", func(t *testing.T) {
ctx.ConfigureForTest(t)
diff --git a/go/internal/jsonrpc2/jsonrpc2.go b/go/internal/jsonrpc2/jsonrpc2.go
index 09505c06d8..fbc5b931c4 100644
--- a/go/internal/jsonrpc2/jsonrpc2.go
+++ b/go/internal/jsonrpc2/jsonrpc2.go
@@ -4,8 +4,10 @@ import (
"bufio"
"crypto/rand"
"encoding/json"
+ "errors"
"fmt"
"io"
+ "os"
"reflect"
"sync"
"sync/atomic"
@@ -61,6 +63,7 @@ type Client struct {
processDone chan struct{} // closed when the underlying process exits
processError error // set before processDone is closed
processErrorMu sync.RWMutex // protects processError
+ onClose func() // called when the read loop exits unexpectedly
}
// NewClient creates a new JSON-RPC client
@@ -293,9 +296,22 @@ func (c *Client) sendMessage(message any) error {
return nil
}
+// SetOnClose sets a callback invoked when the read loop exits unexpectedly
+// (e.g. the underlying connection or process was lost).
+func (c *Client) SetOnClose(fn func()) {
+ c.onClose = fn
+}
+
// readLoop reads messages from stdout in a background goroutine
func (c *Client) readLoop() {
defer c.wg.Done()
+ defer func() {
+ // If still running, the read loop exited unexpectedly (process died or
+ // connection dropped). Notify the caller so it can update its state.
+ if c.onClose != nil && c.running.Load() {
+ c.onClose()
+ }
+ }()
reader := bufio.NewReader(c.stdout)
@@ -306,7 +322,7 @@ func (c *Client) readLoop() {
line, err := reader.ReadString('\n')
if err != nil {
// Only log unexpected errors (not EOF or closed pipe during shutdown)
- if err != io.EOF && c.running.Load() {
+ if err != io.EOF && !errors.Is(err, os.ErrClosed) && c.running.Load() {
fmt.Printf("Error reading header: %v\n", err)
}
return
@@ -331,7 +347,10 @@ func (c *Client) readLoop() {
// Read message body
body := make([]byte, contentLength)
if _, err := io.ReadFull(reader, body); err != nil {
- fmt.Printf("Error reading body: %v\n", err)
+ // Only log unexpected errors (not EOF or closed pipe during shutdown)
+ if err != io.EOF && !errors.Is(err, os.ErrClosed) && c.running.Load() {
+ fmt.Printf("Error reading body: %v\n", err)
+ }
return
}
diff --git a/go/internal/jsonrpc2/jsonrpc2_test.go b/go/internal/jsonrpc2/jsonrpc2_test.go
new file mode 100644
index 0000000000..9f542049d5
--- /dev/null
+++ b/go/internal/jsonrpc2/jsonrpc2_test.go
@@ -0,0 +1,69 @@
+package jsonrpc2
+
+import (
+ "io"
+ "sync"
+ "testing"
+ "time"
+)
+
+func TestOnCloseCalledOnUnexpectedExit(t *testing.T) {
+ stdinR, stdinW := io.Pipe()
+ stdoutR, stdoutW := io.Pipe()
+ defer stdinR.Close()
+
+ client := NewClient(stdinW, stdoutR)
+
+ var called bool
+ var mu sync.Mutex
+ client.SetOnClose(func() {
+ mu.Lock()
+ called = true
+ mu.Unlock()
+ })
+
+ client.Start()
+
+ // Simulate unexpected process death by closing the stdout writer
+ stdoutW.Close()
+
+ // Wait for readLoop to detect the close and invoke the callback
+ time.Sleep(200 * time.Millisecond)
+
+ mu.Lock()
+ defer mu.Unlock()
+ if !called {
+ t.Error("expected onClose to be called when read loop exits unexpectedly")
+ }
+}
+
+func TestOnCloseNotCalledOnIntentionalStop(t *testing.T) {
+ stdinR, stdinW := io.Pipe()
+ stdoutR, stdoutW := io.Pipe()
+ defer stdinR.Close()
+ defer stdoutW.Close()
+
+ client := NewClient(stdinW, stdoutR)
+
+ var called bool
+ var mu sync.Mutex
+ client.SetOnClose(func() {
+ mu.Lock()
+ called = true
+ mu.Unlock()
+ })
+
+ client.Start()
+
+ // Intentional stop — should set running=false before closing stdout,
+ // so the readLoop should NOT invoke onClose.
+ client.Stop()
+
+ time.Sleep(200 * time.Millisecond)
+
+ mu.Lock()
+ defer mu.Unlock()
+ if called {
+ t.Error("onClose should not be called on intentional Stop()")
+ }
+}
diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go
index 67a3542029..b9ba408b53 100644
--- a/go/rpc/generated_rpc.go
+++ b/go/rpc/generated_rpc.go
@@ -48,30 +48,41 @@ type Model struct {
// Billing information
type Billing struct {
+ // Billing cost multiplier relative to the base rate
Multiplier float64 `json:"multiplier"`
}
// Model capabilities and limits
type Capabilities struct {
- Limits Limits `json:"limits"`
+ // Token limits for prompts, outputs, and context window
+ Limits Limits `json:"limits"`
+ // Feature flags indicating what the model supports
Supports Supports `json:"supports"`
}
+// Token limits for prompts, outputs, and context window
type Limits struct {
- MaxContextWindowTokens float64 `json:"max_context_window_tokens"`
- MaxOutputTokens *float64 `json:"max_output_tokens,omitempty"`
- MaxPromptTokens *float64 `json:"max_prompt_tokens,omitempty"`
+ // Maximum total context window size in tokens
+ MaxContextWindowTokens float64 `json:"max_context_window_tokens"`
+ // Maximum number of output/completion tokens
+ MaxOutputTokens *float64 `json:"max_output_tokens,omitempty"`
+ // Maximum number of prompt/input tokens
+ MaxPromptTokens *float64 `json:"max_prompt_tokens,omitempty"`
}
+// Feature flags indicating what the model supports
type Supports struct {
// Whether this model supports reasoning effort configuration
ReasoningEffort *bool `json:"reasoningEffort,omitempty"`
- Vision *bool `json:"vision,omitempty"`
+ // Whether this model supports vision/image input
+ Vision *bool `json:"vision,omitempty"`
}
// Policy state (if applicable)
type Policy struct {
+ // Current policy state for this model
State string `json:"state"`
+ // Usage terms or conditions for this model
Terms string `json:"terms"`
}
@@ -121,15 +132,20 @@ type QuotaSnapshot struct {
}
type SessionModelGetCurrentResult struct {
+ // Currently active model identifier
ModelID *string `json:"modelId,omitempty"`
}
type SessionModelSwitchToResult struct {
+ // Currently active model identifier after the switch
ModelID *string `json:"modelId,omitempty"`
}
type SessionModelSwitchToParams struct {
+ // Model identifier to switch to
ModelID string `json:"modelId"`
+ // Reasoning effort level to use for the model
+ ReasoningEffort *string `json:"reasoningEffort,omitempty"`
}
type SessionModeGetResult struct {
@@ -192,22 +208,25 @@ type SessionWorkspaceCreateFileParams struct {
Path string `json:"path"`
}
+// Experimental: SessionFleetStartResult is part of an experimental API and may change or be removed.
type SessionFleetStartResult struct {
// Whether fleet mode was successfully activated
Started bool `json:"started"`
}
+// Experimental: SessionFleetStartParams is part of an experimental API and may change or be removed.
type SessionFleetStartParams struct {
// Optional user prompt to combine with fleet instructions
Prompt *string `json:"prompt,omitempty"`
}
+// Experimental: SessionAgentListResult is part of an experimental API and may change or be removed.
type SessionAgentListResult struct {
// Available custom agents
- Agents []AgentElement `json:"agents"`
+ Agents []SessionAgentListResultAgent `json:"agents"`
}
-type AgentElement struct {
+type SessionAgentListResultAgent struct {
// Description of the agent's purpose
Description string `json:"description"`
// Human-readable display name
@@ -216,6 +235,7 @@ type AgentElement struct {
Name string `json:"name"`
}
+// Experimental: SessionAgentGetCurrentResult is part of an experimental API and may change or be removed.
type SessionAgentGetCurrentResult struct {
// Currently selected custom agent, or null if using the default agent
Agent *SessionAgentGetCurrentResultAgent `json:"agent"`
@@ -230,6 +250,7 @@ type SessionAgentGetCurrentResultAgent struct {
Name string `json:"name"`
}
+// Experimental: SessionAgentSelectResult is part of an experimental API and may change or be removed.
type SessionAgentSelectResult struct {
// The newly selected custom agent
Agent SessionAgentSelectResultAgent `json:"agent"`
@@ -245,14 +266,172 @@ type SessionAgentSelectResultAgent struct {
Name string `json:"name"`
}
+// Experimental: SessionAgentSelectParams is part of an experimental API and may change or be removed.
type SessionAgentSelectParams struct {
// Name of the custom agent to select
Name string `json:"name"`
}
+// Experimental: SessionAgentDeselectResult is part of an experimental API and may change or be removed.
type SessionAgentDeselectResult struct {
}
+// Experimental: SessionAgentReloadResult is part of an experimental API and may change or be removed.
+type SessionAgentReloadResult struct {
+ // Reloaded custom agents
+ Agents []SessionAgentReloadResultAgent `json:"agents"`
+}
+
+type SessionAgentReloadResultAgent struct {
+ // Description of the agent's purpose
+ Description string `json:"description"`
+ // Human-readable display name
+ DisplayName string `json:"displayName"`
+ // Unique identifier of the custom agent
+ Name string `json:"name"`
+}
+
+// Experimental: SessionSkillsListResult is part of an experimental API and may change or be removed.
+type SessionSkillsListResult struct {
+ // Available skills
+ Skills []Skill `json:"skills"`
+}
+
+type Skill struct {
+ // Description of what the skill does
+ Description string `json:"description"`
+ // Whether the skill is currently enabled
+ Enabled bool `json:"enabled"`
+ // Unique identifier for the skill
+ Name string `json:"name"`
+ // Absolute path to the skill file
+ Path *string `json:"path,omitempty"`
+ // Source location type (e.g., project, personal, plugin)
+ Source string `json:"source"`
+ // Whether the skill can be invoked by the user as a slash command
+ UserInvocable bool `json:"userInvocable"`
+}
+
+// Experimental: SessionSkillsEnableResult is part of an experimental API and may change or be removed.
+type SessionSkillsEnableResult struct {
+}
+
+// Experimental: SessionSkillsEnableParams is part of an experimental API and may change or be removed.
+type SessionSkillsEnableParams struct {
+ // Name of the skill to enable
+ Name string `json:"name"`
+}
+
+// Experimental: SessionSkillsDisableResult is part of an experimental API and may change or be removed.
+type SessionSkillsDisableResult struct {
+}
+
+// Experimental: SessionSkillsDisableParams is part of an experimental API and may change or be removed.
+type SessionSkillsDisableParams struct {
+ // Name of the skill to disable
+ Name string `json:"name"`
+}
+
+// Experimental: SessionSkillsReloadResult is part of an experimental API and may change or be removed.
+type SessionSkillsReloadResult struct {
+}
+
+type SessionMCPListResult struct {
+ // Configured MCP servers
+ Servers []Server `json:"servers"`
+}
+
+type Server struct {
+ // Error message if the server failed to connect
+ Error *string `json:"error,omitempty"`
+ // Server name (config key)
+ Name string `json:"name"`
+ // Configuration source: user, workspace, plugin, or builtin
+ Source *string `json:"source,omitempty"`
+ // Connection status: connected, failed, pending, disabled, or not_configured
+ Status ServerStatus `json:"status"`
+}
+
+type SessionMCPEnableResult struct {
+}
+
+type SessionMCPEnableParams struct {
+ // Name of the MCP server to enable
+ ServerName string `json:"serverName"`
+}
+
+type SessionMCPDisableResult struct {
+}
+
+type SessionMCPDisableParams struct {
+ // Name of the MCP server to disable
+ ServerName string `json:"serverName"`
+}
+
+type SessionMCPReloadResult struct {
+}
+
+// Experimental: SessionPluginsListResult is part of an experimental API and may change or be removed.
+type SessionPluginsListResult struct {
+ // Installed plugins
+ Plugins []Plugin `json:"plugins"`
+}
+
+type Plugin struct {
+ // Whether the plugin is currently enabled
+ Enabled bool `json:"enabled"`
+ // Marketplace the plugin came from
+ Marketplace string `json:"marketplace"`
+ // Plugin name
+ Name string `json:"name"`
+ // Installed version
+ Version *string `json:"version,omitempty"`
+}
+
+// Experimental: SessionExtensionsListResult is part of an experimental API and may change or be removed.
+type SessionExtensionsListResult struct {
+ // Discovered extensions and their current status
+ Extensions []Extension `json:"extensions"`
+}
+
+type Extension struct {
+ // Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper')
+ ID string `json:"id"`
+ // Extension name (directory name)
+ Name string `json:"name"`
+ // Process ID if the extension is running
+ PID *int64 `json:"pid,omitempty"`
+ // Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)
+ Source Source `json:"source"`
+ // Current status: running, disabled, failed, or starting
+ Status ExtensionStatus `json:"status"`
+}
+
+// Experimental: SessionExtensionsEnableResult is part of an experimental API and may change or be removed.
+type SessionExtensionsEnableResult struct {
+}
+
+// Experimental: SessionExtensionsEnableParams is part of an experimental API and may change or be removed.
+type SessionExtensionsEnableParams struct {
+ // Source-qualified extension ID to enable
+ ID string `json:"id"`
+}
+
+// Experimental: SessionExtensionsDisableResult is part of an experimental API and may change or be removed.
+type SessionExtensionsDisableResult struct {
+}
+
+// Experimental: SessionExtensionsDisableParams is part of an experimental API and may change or be removed.
+type SessionExtensionsDisableParams struct {
+ // Source-qualified extension ID to disable
+ ID string `json:"id"`
+}
+
+// Experimental: SessionExtensionsReloadResult is part of an experimental API and may change or be removed.
+type SessionExtensionsReloadResult struct {
+}
+
+// Experimental: SessionCompactionCompactResult is part of an experimental API and may change or be removed.
type SessionCompactionCompactResult struct {
// Number of messages removed during compaction
MessagesRemoved float64 `json:"messagesRemoved"`
@@ -263,6 +442,7 @@ type SessionCompactionCompactResult struct {
}
type SessionToolsHandlePendingToolCallResult struct {
+ // Whether the tool call result was handled successfully
Success bool `json:"success"`
}
@@ -279,7 +459,77 @@ type ResultResult struct {
ToolTelemetry map[string]interface{} `json:"toolTelemetry,omitempty"`
}
+type SessionCommandsHandlePendingCommandResult struct {
+ Success bool `json:"success"`
+}
+
+type SessionCommandsHandlePendingCommandParams struct {
+ // Error message if the command handler failed
+ Error *string `json:"error,omitempty"`
+ // Request ID from the command invocation event
+ RequestID string `json:"requestId"`
+}
+
+type SessionUIElicitationResult struct {
+ // The user's response: accept (submitted), decline (rejected), or cancel (dismissed)
+ Action Action `json:"action"`
+ // The form values submitted by the user (present when action is 'accept')
+ Content map[string]*Content `json:"content,omitempty"`
+}
+
+type SessionUIElicitationParams struct {
+ // Message describing what information is needed from the user
+ Message string `json:"message"`
+ // JSON Schema describing the form fields to present to the user
+ RequestedSchema RequestedSchema `json:"requestedSchema"`
+}
+
+// JSON Schema describing the form fields to present to the user
+type RequestedSchema struct {
+ // Form field definitions, keyed by field name
+ Properties map[string]Property `json:"properties"`
+ // List of required field names
+ Required []string `json:"required,omitempty"`
+ // Schema type indicator (always 'object')
+ Type RequestedSchemaType `json:"type"`
+}
+
+type Property struct {
+ Default *Content `json:"default"`
+ Description *string `json:"description,omitempty"`
+ Enum []string `json:"enum,omitempty"`
+ EnumNames []string `json:"enumNames,omitempty"`
+ Title *string `json:"title,omitempty"`
+ Type PropertyType `json:"type"`
+ OneOf []OneOf `json:"oneOf,omitempty"`
+ Items *Items `json:"items,omitempty"`
+ MaxItems *float64 `json:"maxItems,omitempty"`
+ MinItems *float64 `json:"minItems,omitempty"`
+ Format *Format `json:"format,omitempty"`
+ MaxLength *float64 `json:"maxLength,omitempty"`
+ MinLength *float64 `json:"minLength,omitempty"`
+ Maximum *float64 `json:"maximum,omitempty"`
+ Minimum *float64 `json:"minimum,omitempty"`
+}
+
+type Items struct {
+ Enum []string `json:"enum,omitempty"`
+ Type *ItemsType `json:"type,omitempty"`
+ AnyOf []AnyOf `json:"anyOf,omitempty"`
+}
+
+type AnyOf struct {
+ Const string `json:"const"`
+ Title string `json:"title"`
+}
+
+type OneOf struct {
+ Const string `json:"const"`
+ Title string `json:"title"`
+}
+
type SessionPermissionsHandlePendingPermissionRequestResult struct {
+ // Whether the permission request was handled successfully
Success bool `json:"success"`
}
@@ -296,6 +546,49 @@ type SessionPermissionsHandlePendingPermissionRequestParamsResult struct {
Path *string `json:"path,omitempty"`
}
+type SessionLogResult struct {
+ // The unique identifier of the emitted session event
+ EventID string `json:"eventId"`
+}
+
+type SessionLogParams struct {
+ // When true, the message is transient and not persisted to the session event log on disk
+ Ephemeral *bool `json:"ephemeral,omitempty"`
+ // Log severity level. Determines how the message is displayed in the timeline. Defaults to
+ // "info".
+ Level *Level `json:"level,omitempty"`
+ // Human-readable message
+ Message string `json:"message"`
+ // Optional URL the user can open in their browser for more details
+ URL *string `json:"url,omitempty"`
+}
+
+type SessionShellExecResult struct {
+ // Unique identifier for tracking streamed output
+ ProcessID string `json:"processId"`
+}
+
+type SessionShellExecParams struct {
+ // Shell command to execute
+ Command string `json:"command"`
+ // Working directory (defaults to session working directory)
+ Cwd *string `json:"cwd,omitempty"`
+ // Timeout in milliseconds (default: 30000)
+ Timeout *float64 `json:"timeout,omitempty"`
+}
+
+type SessionShellKillResult struct {
+ // Whether the signal was sent successfully
+ Killed bool `json:"killed"`
+}
+
+type SessionShellKillParams struct {
+ // Process identifier returned by shell.exec
+ ProcessID string `json:"processId"`
+ // Signal to send (default: SIGTERM)
+ Signal *Signal `json:"signal,omitempty"`
+}
+
// The current agent mode.
//
// The agent mode after switching.
@@ -304,19 +597,107 @@ type SessionPermissionsHandlePendingPermissionRequestParamsResult struct {
type Mode string
const (
- Autopilot Mode = "autopilot"
- Interactive Mode = "interactive"
- Plan Mode = "plan"
+ ModeAutopilot Mode = "autopilot"
+ ModeInteractive Mode = "interactive"
+ ModePlan Mode = "plan"
+)
+
+// Connection status: connected, failed, pending, disabled, or not_configured
+type ServerStatus string
+
+const (
+ ServerStatusConnected ServerStatus = "connected"
+ ServerStatusNotConfigured ServerStatus = "not_configured"
+ ServerStatusPending ServerStatus = "pending"
+ ServerStatusDisabled ServerStatus = "disabled"
+ ServerStatusFailed ServerStatus = "failed"
+)
+
+// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)
+type Source string
+
+const (
+ SourceProject Source = "project"
+ SourceUser Source = "user"
+)
+
+// Current status: running, disabled, failed, or starting
+type ExtensionStatus string
+
+const (
+ ExtensionStatusDisabled ExtensionStatus = "disabled"
+ ExtensionStatusFailed ExtensionStatus = "failed"
+ ExtensionStatusRunning ExtensionStatus = "running"
+ ExtensionStatusStarting ExtensionStatus = "starting"
+)
+
+// The user's response: accept (submitted), decline (rejected), or cancel (dismissed)
+type Action string
+
+const (
+ ActionAccept Action = "accept"
+ ActionCancel Action = "cancel"
+ ActionDecline Action = "decline"
+)
+
+type Format string
+
+const (
+ FormatDate Format = "date"
+ FormatDateTime Format = "date-time"
+ FormatEmail Format = "email"
+ FormatUri Format = "uri"
+)
+
+type ItemsType string
+
+const (
+ ItemsTypeString ItemsType = "string"
+)
+
+type PropertyType string
+
+const (
+ PropertyTypeArray PropertyType = "array"
+ PropertyTypeBoolean PropertyType = "boolean"
+ PropertyTypeString PropertyType = "string"
+ PropertyTypeInteger PropertyType = "integer"
+ PropertyTypeNumber PropertyType = "number"
+)
+
+type RequestedSchemaType string
+
+const (
+ RequestedSchemaTypeObject RequestedSchemaType = "object"
)
type Kind string
const (
- Approved Kind = "approved"
- DeniedByContentExclusionPolicy Kind = "denied-by-content-exclusion-policy"
- DeniedByRules Kind = "denied-by-rules"
- DeniedInteractivelyByUser Kind = "denied-interactively-by-user"
- DeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user"
+ KindApproved Kind = "approved"
+ KindDeniedByContentExclusionPolicy Kind = "denied-by-content-exclusion-policy"
+ KindDeniedByRules Kind = "denied-by-rules"
+ KindDeniedInteractivelyByUser Kind = "denied-interactively-by-user"
+ KindDeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user"
+)
+
+// Log severity level. Determines how the message is displayed in the timeline. Defaults to
+// "info".
+type Level string
+
+const (
+ LevelError Level = "error"
+ LevelInfo Level = "info"
+ LevelWarning Level = "warning"
+)
+
+// Signal to send (default: SIGTERM)
+type Signal string
+
+const (
+ SignalSIGINT Signal = "SIGINT"
+ SignalSIGKILL Signal = "SIGKILL"
+ SignalSIGTERM Signal = "SIGTERM"
)
type ResultUnion struct {
@@ -324,7 +705,16 @@ type ResultUnion struct {
String *string
}
-type ServerModelsRpcApi struct{ client *jsonrpc2.Client }
+type Content struct {
+ Bool *bool
+ Double *float64
+ String *string
+ StringArray []string
+}
+
+type ServerModelsRpcApi struct {
+ client *jsonrpc2.Client
+}
func (a *ServerModelsRpcApi) List(ctx context.Context) (*ModelsListResult, error) {
raw, err := a.client.Request("models.list", map[string]interface{}{})
@@ -338,7 +728,9 @@ func (a *ServerModelsRpcApi) List(ctx context.Context) (*ModelsListResult, error
return &result, nil
}
-type ServerToolsRpcApi struct{ client *jsonrpc2.Client }
+type ServerToolsRpcApi struct {
+ client *jsonrpc2.Client
+}
func (a *ServerToolsRpcApi) List(ctx context.Context, params *ToolsListParams) (*ToolsListResult, error) {
raw, err := a.client.Request("tools.list", params)
@@ -352,7 +744,9 @@ func (a *ServerToolsRpcApi) List(ctx context.Context, params *ToolsListParams) (
return &result, nil
}
-type ServerAccountRpcApi struct{ client *jsonrpc2.Client }
+type ServerAccountRpcApi struct {
+ client *jsonrpc2.Client
+}
func (a *ServerAccountRpcApi) GetQuota(ctx context.Context) (*AccountGetQuotaResult, error) {
raw, err := a.client.Request("account.getQuota", map[string]interface{}{})
@@ -416,6 +810,9 @@ func (a *ModelRpcApi) SwitchTo(ctx context.Context, params *SessionModelSwitchTo
req := map[string]interface{}{"sessionId": a.sessionID}
if params != nil {
req["modelId"] = params.ModelID
+ if params.ReasoningEffort != nil {
+ req["reasoningEffort"] = *params.ReasoningEffort
+ }
}
raw, err := a.client.Request("session.model.switchTo", req)
if err != nil {
@@ -560,6 +957,7 @@ func (a *WorkspaceRpcApi) CreateFile(ctx context.Context, params *SessionWorkspa
return &result, nil
}
+// Experimental: FleetRpcApi contains experimental APIs that may change or be removed.
type FleetRpcApi struct {
client *jsonrpc2.Client
sessionID string
@@ -583,6 +981,7 @@ func (a *FleetRpcApi) Start(ctx context.Context, params *SessionFleetStartParams
return &result, nil
}
+// Experimental: AgentRpcApi contains experimental APIs that may change or be removed.
type AgentRpcApi struct {
client *jsonrpc2.Client
sessionID string
@@ -643,6 +1042,231 @@ func (a *AgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult
return &result, nil
}
+func (a *AgentRpcApi) Reload(ctx context.Context) (*SessionAgentReloadResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.agent.reload", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionAgentReloadResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: SkillsRpcApi contains experimental APIs that may change or be removed.
+type SkillsRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *SkillsRpcApi) List(ctx context.Context) (*SessionSkillsListResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.skills.list", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionSkillsListResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *SkillsRpcApi) Enable(ctx context.Context, params *SessionSkillsEnableParams) (*SessionSkillsEnableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["name"] = params.Name
+ }
+ raw, err := a.client.Request("session.skills.enable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionSkillsEnableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *SkillsRpcApi) Disable(ctx context.Context, params *SessionSkillsDisableParams) (*SessionSkillsDisableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["name"] = params.Name
+ }
+ raw, err := a.client.Request("session.skills.disable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionSkillsDisableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *SkillsRpcApi) Reload(ctx context.Context) (*SessionSkillsReloadResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.skills.reload", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionSkillsReloadResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: McpRpcApi contains experimental APIs that may change or be removed.
+type McpRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *McpRpcApi) List(ctx context.Context) (*SessionMCPListResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.mcp.list", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionMCPListResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *McpRpcApi) Enable(ctx context.Context, params *SessionMCPEnableParams) (*SessionMCPEnableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["serverName"] = params.ServerName
+ }
+ raw, err := a.client.Request("session.mcp.enable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionMCPEnableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *McpRpcApi) Disable(ctx context.Context, params *SessionMCPDisableParams) (*SessionMCPDisableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["serverName"] = params.ServerName
+ }
+ raw, err := a.client.Request("session.mcp.disable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionMCPDisableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *McpRpcApi) Reload(ctx context.Context) (*SessionMCPReloadResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.mcp.reload", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionMCPReloadResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: PluginsRpcApi contains experimental APIs that may change or be removed.
+type PluginsRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *PluginsRpcApi) List(ctx context.Context) (*SessionPluginsListResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.plugins.list", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionPluginsListResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: ExtensionsRpcApi contains experimental APIs that may change or be removed.
+type ExtensionsRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *ExtensionsRpcApi) List(ctx context.Context) (*SessionExtensionsListResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.extensions.list", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionExtensionsListResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *ExtensionsRpcApi) Enable(ctx context.Context, params *SessionExtensionsEnableParams) (*SessionExtensionsEnableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["id"] = params.ID
+ }
+ raw, err := a.client.Request("session.extensions.enable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionExtensionsEnableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *ExtensionsRpcApi) Disable(ctx context.Context, params *SessionExtensionsDisableParams) (*SessionExtensionsDisableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["id"] = params.ID
+ }
+ raw, err := a.client.Request("session.extensions.disable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionExtensionsDisableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *ExtensionsRpcApi) Reload(ctx context.Context) (*SessionExtensionsReloadResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.extensions.reload", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionExtensionsReloadResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: CompactionRpcApi contains experimental APIs that may change or be removed.
type CompactionRpcApi struct {
client *jsonrpc2.Client
sessionID string
@@ -688,6 +1312,52 @@ func (a *ToolsRpcApi) HandlePendingToolCall(ctx context.Context, params *Session
return &result, nil
}
+type CommandsRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *CommandsRpcApi) HandlePendingCommand(ctx context.Context, params *SessionCommandsHandlePendingCommandParams) (*SessionCommandsHandlePendingCommandResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["requestId"] = params.RequestID
+ if params.Error != nil {
+ req["error"] = *params.Error
+ }
+ }
+ raw, err := a.client.Request("session.commands.handlePendingCommand", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionCommandsHandlePendingCommandResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+type UiRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *UiRpcApi) Elicitation(ctx context.Context, params *SessionUIElicitationParams) (*SessionUIElicitationResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["message"] = params.Message
+ req["requestedSchema"] = params.RequestedSchema
+ }
+ raw, err := a.client.Request("session.ui.elicitation", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionUIElicitationResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
type PermissionsRpcApi struct {
client *jsonrpc2.Client
sessionID string
@@ -710,6 +1380,52 @@ func (a *PermissionsRpcApi) HandlePendingPermissionRequest(ctx context.Context,
return &result, nil
}
+type ShellRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *ShellRpcApi) Exec(ctx context.Context, params *SessionShellExecParams) (*SessionShellExecResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["command"] = params.Command
+ if params.Cwd != nil {
+ req["cwd"] = *params.Cwd
+ }
+ if params.Timeout != nil {
+ req["timeout"] = *params.Timeout
+ }
+ }
+ raw, err := a.client.Request("session.shell.exec", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionShellExecResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *ShellRpcApi) Kill(ctx context.Context, params *SessionShellKillParams) (*SessionShellKillResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["processId"] = params.ProcessID
+ if params.Signal != nil {
+ req["signal"] = *params.Signal
+ }
+ }
+ raw, err := a.client.Request("session.shell.kill", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionShellKillResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
// SessionRpc provides typed session-scoped RPC methods.
type SessionRpc struct {
client *jsonrpc2.Client
@@ -720,9 +1436,41 @@ type SessionRpc struct {
Workspace *WorkspaceRpcApi
Fleet *FleetRpcApi
Agent *AgentRpcApi
+ Skills *SkillsRpcApi
+ Mcp *McpRpcApi
+ Plugins *PluginsRpcApi
+ Extensions *ExtensionsRpcApi
Compaction *CompactionRpcApi
Tools *ToolsRpcApi
+ Commands *CommandsRpcApi
+ Ui *UiRpcApi
Permissions *PermissionsRpcApi
+ Shell *ShellRpcApi
+}
+
+func (a *SessionRpc) Log(ctx context.Context, params *SessionLogParams) (*SessionLogResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["message"] = params.Message
+ if params.Level != nil {
+ req["level"] = *params.Level
+ }
+ if params.Ephemeral != nil {
+ req["ephemeral"] = *params.Ephemeral
+ }
+ if params.URL != nil {
+ req["url"] = *params.URL
+ }
+ }
+ raw, err := a.client.Request("session.log", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionLogResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
}
func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc {
@@ -733,8 +1481,15 @@ func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc {
Workspace: &WorkspaceRpcApi{client: client, sessionID: sessionID},
Fleet: &FleetRpcApi{client: client, sessionID: sessionID},
Agent: &AgentRpcApi{client: client, sessionID: sessionID},
+ Skills: &SkillsRpcApi{client: client, sessionID: sessionID},
+ Mcp: &McpRpcApi{client: client, sessionID: sessionID},
+ Plugins: &PluginsRpcApi{client: client, sessionID: sessionID},
+ Extensions: &ExtensionsRpcApi{client: client, sessionID: sessionID},
Compaction: &CompactionRpcApi{client: client, sessionID: sessionID},
Tools: &ToolsRpcApi{client: client, sessionID: sessionID},
+ Commands: &CommandsRpcApi{client: client, sessionID: sessionID},
+ Ui: &UiRpcApi{client: client, sessionID: sessionID},
Permissions: &PermissionsRpcApi{client: client, sessionID: sessionID},
+ Shell: &ShellRpcApi{client: client, sessionID: sessionID},
}
}
diff --git a/go/samples/chat.go b/go/samples/chat.go
index f984f758ae..4d5e98d7de 100644
--- a/go/samples/chat.go
+++ b/go/samples/chat.go
@@ -35,11 +35,11 @@ func main() {
session.On(func(event copilot.SessionEvent) {
var output string
switch event.Type {
- case copilot.AssistantReasoning:
+ case copilot.SessionEventTypeAssistantReasoning:
if event.Data.Content != nil {
output = fmt.Sprintf("[reasoning: %s]", *event.Data.Content)
}
- case copilot.ToolExecutionStart:
+ case copilot.SessionEventTypeToolExecutionStart:
if event.Data.ToolName != nil {
output = fmt.Sprintf("[tool: %s]", *event.Data.ToolName)
}
diff --git a/go/session.go b/go/session.go
index c06a8e1ec3..3a94a818ed 100644
--- a/go/session.go
+++ b/go/session.go
@@ -50,20 +50,27 @@ type sessionHandler struct {
// })
type Session struct {
// SessionID is the unique identifier for this session.
- SessionID string
- workspacePath string
- client *jsonrpc2.Client
- handlers []sessionHandler
- nextHandlerID uint64
- handlerMutex sync.RWMutex
- toolHandlers map[string]ToolHandler
- toolHandlersM sync.RWMutex
- permissionHandler PermissionHandlerFunc
- permissionMux sync.RWMutex
- userInputHandler UserInputHandler
- userInputMux sync.RWMutex
- hooks *SessionHooks
- hooksMux sync.RWMutex
+ SessionID string
+ workspacePath string
+ client *jsonrpc2.Client
+ handlers []sessionHandler
+ nextHandlerID uint64
+ handlerMutex sync.RWMutex
+ toolHandlers map[string]ToolHandler
+ toolHandlersM sync.RWMutex
+ permissionHandler PermissionHandlerFunc
+ permissionMux sync.RWMutex
+ userInputHandler UserInputHandler
+ userInputMux sync.RWMutex
+ hooks *SessionHooks
+ hooksMux sync.RWMutex
+ transformCallbacks map[string]SectionTransformFn
+ transformMu sync.Mutex
+
+ // eventCh serializes user event handler dispatch. dispatchEvent enqueues;
+ // a single goroutine (processEvents) dequeues and invokes handlers in FIFO order.
+ eventCh chan SessionEvent
+ closeOnce sync.Once // guards eventCh close so Disconnect is safe to call more than once
// RPC provides typed session-scoped RPC methods.
RPC *rpc.SessionRpc
@@ -78,14 +85,17 @@ func (s *Session) WorkspacePath() string {
// newSession creates a new session wrapper with the given session ID and client.
func newSession(sessionID string, client *jsonrpc2.Client, workspacePath string) *Session {
- return &Session{
+ s := &Session{
SessionID: sessionID,
workspacePath: workspacePath,
client: client,
handlers: make([]sessionHandler, 0),
toolHandlers: make(map[string]ToolHandler),
+ eventCh: make(chan SessionEvent, 128),
RPC: rpc.NewSessionRpc(client, sessionID),
}
+ go s.processEvents()
+ return s
}
// Send sends a message to this session and waits for the response.
@@ -111,11 +121,14 @@ func newSession(sessionID string, client *jsonrpc2.Client, workspacePath string)
// log.Printf("Failed to send message: %v", err)
// }
func (s *Session) Send(ctx context.Context, options MessageOptions) (string, error) {
+ traceparent, tracestate := getTraceContext(ctx)
req := sessionSendRequest{
SessionID: s.SessionID,
Prompt: options.Prompt,
Attachments: options.Attachments,
Mode: options.Mode,
+ Traceparent: traceparent,
+ Tracestate: tracestate,
}
result, err := s.client.Request("session.send", req)
@@ -171,17 +184,17 @@ func (s *Session) SendAndWait(ctx context.Context, options MessageOptions) (*Ses
unsubscribe := s.On(func(event SessionEvent) {
switch event.Type {
- case AssistantMessage:
+ case SessionEventTypeAssistantMessage:
mu.Lock()
eventCopy := event
lastAssistantMessage = &eventCopy
mu.Unlock()
- case SessionIdle:
+ case SessionEventTypeSessionIdle:
select {
case idleCh <- struct{}{}:
default:
}
- case SessionError:
+ case SessionEventTypeSessionError:
errMsg := "session error"
if event.Data.Message != nil {
errMsg = *event.Data.Message
@@ -435,39 +448,112 @@ func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) (
}
}
-// dispatchEvent dispatches an event to all registered handlers.
-// This is an internal method; handlers are called synchronously and any panics
-// are recovered to prevent crashing the event dispatcher.
-func (s *Session) dispatchEvent(event SessionEvent) {
- // Handle broadcast request events internally (fire-and-forget)
- s.handleBroadcastEvent(event)
+// registerTransformCallbacks registers transform callbacks for this session.
+//
+// Transform callbacks are invoked when the CLI requests system message section
+// transforms. This method is internal and typically called when creating a session.
+func (s *Session) registerTransformCallbacks(callbacks map[string]SectionTransformFn) {
+ s.transformMu.Lock()
+ defer s.transformMu.Unlock()
+ s.transformCallbacks = callbacks
+}
+
+type systemMessageTransformSection struct {
+ Content string `json:"content"`
+}
+
+type systemMessageTransformRequest struct {
+ SessionID string `json:"sessionId"`
+ Sections map[string]systemMessageTransformSection `json:"sections"`
+}
- s.handlerMutex.RLock()
- handlers := make([]SessionEventHandler, 0, len(s.handlers))
- for _, h := range s.handlers {
- handlers = append(handlers, h.fn)
+type systemMessageTransformResponse struct {
+ Sections map[string]systemMessageTransformSection `json:"sections"`
+}
+
+// handleSystemMessageTransform handles a system message transform request from the Copilot CLI.
+// This is an internal method called by the SDK when the CLI requests section transforms.
+func (s *Session) handleSystemMessageTransform(sections map[string]systemMessageTransformSection) (systemMessageTransformResponse, error) {
+ s.transformMu.Lock()
+ callbacks := s.transformCallbacks
+ s.transformMu.Unlock()
+
+ result := make(map[string]systemMessageTransformSection)
+ for sectionID, data := range sections {
+ var callback SectionTransformFn
+ if callbacks != nil {
+ callback = callbacks[sectionID]
+ }
+ if callback != nil {
+ transformed, err := callback(data.Content)
+ if err != nil {
+ result[sectionID] = systemMessageTransformSection{Content: data.Content}
+ } else {
+ result[sectionID] = systemMessageTransformSection{Content: transformed}
+ }
+ } else {
+ result[sectionID] = systemMessageTransformSection{Content: data.Content}
+ }
}
- s.handlerMutex.RUnlock()
-
- for _, handler := range handlers {
- // Call handler - don't let panics crash the dispatcher
- func() {
- defer func() {
- if r := recover(); r != nil {
- fmt.Printf("Error in session event handler: %v\n", r)
- }
+ return systemMessageTransformResponse{Sections: result}, nil
+}
+
+// dispatchEvent enqueues an event for delivery to user handlers and fires
+// broadcast handlers concurrently.
+//
+// Broadcast work (tool calls, permission requests) is fired in a separate
+// goroutine so it does not block the JSON-RPC read loop. User event handlers
+// are delivered by a single consumer goroutine (processEvents), guaranteeing
+// serial, FIFO dispatch without blocking the read loop.
+func (s *Session) dispatchEvent(event SessionEvent) {
+ go s.handleBroadcastEvent(event)
+
+ // Send to the event channel in a closure with a recover guard.
+ // Disconnect closes eventCh, and in Go sending on a closed channel
+ // panics — there is no non-panicking send primitive. We only want
+ // to suppress that specific panic; other panics are not expected here.
+ func() {
+ defer func() { recover() }()
+ s.eventCh <- event
+ }()
+}
+
+// processEvents is the single consumer goroutine for the event channel.
+// It invokes user handlers serially, in arrival order. Panics in individual
+// handlers are recovered so that one misbehaving handler does not prevent
+// others from receiving the event.
+func (s *Session) processEvents() {
+ for event := range s.eventCh {
+ s.handlerMutex.RLock()
+ handlers := make([]SessionEventHandler, 0, len(s.handlers))
+ for _, h := range s.handlers {
+ handlers = append(handlers, h.fn)
+ }
+ s.handlerMutex.RUnlock()
+
+ for _, handler := range handlers {
+ func() {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Printf("Error in session event handler: %v\n", r)
+ }
+ }()
+ handler(event)
}()
- handler(event)
- }()
+ }
}
}
// handleBroadcastEvent handles broadcast request events by executing local handlers
// and responding via RPC. This implements the protocol v3 broadcast model where tool
// calls and permission requests are broadcast as session events to all clients.
+//
+// Handlers are executed in their own goroutine (not the JSON-RPC read loop or the
+// event consumer loop) so that a stalled handler does not block event delivery or
+// cause RPC deadlocks.
func (s *Session) handleBroadcastEvent(event SessionEvent) {
switch event.Type {
- case ExternalToolRequested:
+ case SessionEventTypeExternalToolRequested:
requestID := event.Data.RequestID
toolName := event.Data.ToolName
if requestID == nil || toolName == nil {
@@ -481,9 +567,16 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) {
if event.Data.ToolCallID != nil {
toolCallID = *event.Data.ToolCallID
}
- go s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler)
+ var tp, ts string
+ if event.Data.Traceparent != nil {
+ tp = *event.Data.Traceparent
+ }
+ if event.Data.Tracestate != nil {
+ ts = *event.Data.Tracestate
+ }
+ s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler, tp, ts)
- case PermissionRequested:
+ case SessionEventTypePermissionRequested:
requestID := event.Data.RequestID
if requestID == nil || event.Data.PermissionRequest == nil {
return
@@ -492,16 +585,17 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) {
if handler == nil {
return
}
- go s.executePermissionAndRespond(*requestID, *event.Data.PermissionRequest, handler)
+ s.executePermissionAndRespond(*requestID, *event.Data.PermissionRequest, handler)
}
}
// executeToolAndRespond executes a tool handler and sends the result back via RPC.
-func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, arguments any, handler ToolHandler) {
+func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, arguments any, handler ToolHandler, traceparent, tracestate string) {
+ ctx := contextWithTraceParent(context.Background(), traceparent, tracestate)
defer func() {
if r := recover(); r != nil {
errMsg := fmt.Sprintf("tool panic: %v", r)
- s.RPC.Tools.HandlePendingToolCall(context.Background(), &rpc.SessionToolsHandlePendingToolCallParams{
+ s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.SessionToolsHandlePendingToolCallParams{
RequestID: requestID,
Error: &errMsg,
})
@@ -509,16 +603,17 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string,
}()
invocation := ToolInvocation{
- SessionID: s.SessionID,
- ToolCallID: toolCallID,
- ToolName: toolName,
- Arguments: arguments,
+ SessionID: s.SessionID,
+ ToolCallID: toolCallID,
+ ToolName: toolName,
+ Arguments: arguments,
+ TraceContext: ctx,
}
result, err := handler(invocation)
if err != nil {
errMsg := err.Error()
- s.RPC.Tools.HandlePendingToolCall(context.Background(), &rpc.SessionToolsHandlePendingToolCallParams{
+ s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.SessionToolsHandlePendingToolCallParams{
RequestID: requestID,
Error: &errMsg,
})
@@ -529,7 +624,7 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string,
if resultStr == "" {
resultStr = fmt.Sprintf("%v", result)
}
- s.RPC.Tools.HandlePendingToolCall(context.Background(), &rpc.SessionToolsHandlePendingToolCallParams{
+ s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.SessionToolsHandlePendingToolCallParams{
RequestID: requestID,
Result: &rpc.ResultUnion{String: &resultStr},
})
@@ -542,7 +637,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques
s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{
RequestID: requestID,
Result: rpc.SessionPermissionsHandlePendingPermissionRequestParamsResult{
- Kind: rpc.DeniedNoApprovalRuleAndCouldNotRequestFromUser,
+ Kind: rpc.KindDeniedNoApprovalRuleAndCouldNotRequestFromUser,
},
})
}
@@ -557,11 +652,14 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques
s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{
RequestID: requestID,
Result: rpc.SessionPermissionsHandlePendingPermissionRequestParamsResult{
- Kind: rpc.DeniedNoApprovalRuleAndCouldNotRequestFromUser,
+ Kind: rpc.KindDeniedNoApprovalRuleAndCouldNotRequestFromUser,
},
})
return
}
+ if result.Kind == "no-result" {
+ return
+ }
s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{
RequestID: requestID,
@@ -610,6 +708,10 @@ func (s *Session) GetMessages(ctx context.Context) ([]SessionEvent, error) {
// Disconnect closes this session and releases all in-memory resources (event
// handlers, tool handlers, permission handlers).
//
+// The caller should ensure the session is idle (e.g., [Session.SendAndWait] has
+// returned) before disconnecting. If the session is not idle, in-flight event
+// handlers or tool handlers may observe failures.
+//
// Session state on disk (conversation history, planning state, artifacts) is
// preserved, so the conversation can be resumed later by calling
// [Client.ResumeSession] with the session ID. To permanently remove all
@@ -631,6 +733,8 @@ func (s *Session) Disconnect() error {
return fmt.Errorf("failed to disconnect session: %w", err)
}
+ s.closeOnce.Do(func() { close(s.eventCh) })
+
// Clear handlers
s.handlerMutex.Lock()
s.handlers = nil
@@ -685,6 +789,12 @@ func (s *Session) Abort(ctx context.Context) error {
return nil
}
+// SetModelOptions configures optional parameters for SetModel.
+type SetModelOptions struct {
+ // ReasoningEffort sets the reasoning effort level for the new model (e.g., "low", "medium", "high", "xhigh").
+ ReasoningEffort string
+}
+
// SetModel changes the model for this session.
// The new model takes effect for the next message. Conversation history is preserved.
//
@@ -693,11 +803,66 @@ func (s *Session) Abort(ctx context.Context) error {
// if err := session.SetModel(context.Background(), "gpt-4.1"); err != nil {
// log.Printf("Failed to set model: %v", err)
// }
-func (s *Session) SetModel(ctx context.Context, model string) error {
- _, err := s.RPC.Model.SwitchTo(ctx, &rpc.SessionModelSwitchToParams{ModelID: model})
+// if err := session.SetModel(context.Background(), "claude-sonnet-4.6", SetModelOptions{ReasoningEffort: "high"}); err != nil {
+// log.Printf("Failed to set model: %v", err)
+// }
+func (s *Session) SetModel(ctx context.Context, model string, opts ...SetModelOptions) error {
+ params := &rpc.SessionModelSwitchToParams{ModelID: model}
+ if len(opts) > 0 && opts[0].ReasoningEffort != "" {
+ re := opts[0].ReasoningEffort
+ params.ReasoningEffort = &re
+ }
+ _, err := s.RPC.Model.SwitchTo(ctx, params)
if err != nil {
return fmt.Errorf("failed to set model: %w", err)
}
return nil
}
+
+// LogOptions configures optional parameters for [Session.Log].
+type LogOptions struct {
+ // Level sets the log severity. Valid values are [rpc.LevelInfo] (default),
+ // [rpc.LevelWarning], and [rpc.LevelError].
+ Level rpc.Level
+ // Ephemeral marks the message as transient so it is not persisted
+ // to the session event log on disk. When nil the server decides the
+ // default; set to a non-nil value to explicitly control persistence.
+ Ephemeral *bool
+}
+
+// Log sends a log message to the session timeline.
+// The message appears in the session event stream and is visible to SDK consumers
+// and (for non-ephemeral messages) persisted to the session event log on disk.
+//
+// Pass nil for opts to use defaults (info level, non-ephemeral).
+//
+// Example:
+//
+// // Simple info message
+// session.Log(ctx, "Processing started")
+//
+// // Warning with options
+// session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.LevelWarning})
+//
+// // Ephemeral message (not persisted)
+// session.Log(ctx, "Working...", &copilot.LogOptions{Ephemeral: copilot.Bool(true)})
+func (s *Session) Log(ctx context.Context, message string, opts *LogOptions) error {
+ params := &rpc.SessionLogParams{Message: message}
+
+ if opts != nil {
+ if opts.Level != "" {
+ params.Level = &opts.Level
+ }
+ if opts.Ephemeral != nil {
+ params.Ephemeral = opts.Ephemeral
+ }
+ }
+
+ _, err := s.RPC.Log(ctx, params)
+ if err != nil {
+ return fmt.Errorf("failed to log message: %w", err)
+ }
+
+ return nil
+}
diff --git a/go/session_test.go b/go/session_test.go
index 40874a6540..664c06e558 100644
--- a/go/session_test.go
+++ b/go/session_test.go
@@ -2,21 +2,36 @@ package copilot
import (
"sync"
+ "sync/atomic"
"testing"
+ "time"
)
+// newTestSession creates a session with an event channel and starts the consumer goroutine.
+// Returns a cleanup function that closes the channel (stopping the consumer).
+func newTestSession() (*Session, func()) {
+ s := &Session{
+ handlers: make([]sessionHandler, 0),
+ eventCh: make(chan SessionEvent, 128),
+ }
+ go s.processEvents()
+ return s, func() { close(s.eventCh) }
+}
+
func TestSession_On(t *testing.T) {
t.Run("multiple handlers all receive events", func(t *testing.T) {
- session := &Session{
- handlers: make([]sessionHandler, 0),
- }
+ session, cleanup := newTestSession()
+ defer cleanup()
+ var wg sync.WaitGroup
+ wg.Add(3)
var received1, received2, received3 bool
- session.On(func(event SessionEvent) { received1 = true })
- session.On(func(event SessionEvent) { received2 = true })
- session.On(func(event SessionEvent) { received3 = true })
+ session.On(func(event SessionEvent) { received1 = true; wg.Done() })
+ session.On(func(event SessionEvent) { received2 = true; wg.Done() })
+ session.On(func(event SessionEvent) { received3 = true; wg.Done() })
session.dispatchEvent(SessionEvent{Type: "test"})
+ wg.Wait()
if !received1 || !received2 || !received3 {
t.Errorf("Expected all handlers to receive event, got received1=%v, received2=%v, received3=%v",
@@ -25,68 +40,81 @@ func TestSession_On(t *testing.T) {
})
t.Run("unsubscribing one handler does not affect others", func(t *testing.T) {
- session := &Session{
- handlers: make([]sessionHandler, 0),
- }
+ session, cleanup := newTestSession()
+ defer cleanup()
+
+ var count1, count2, count3 atomic.Int32
+ var wg sync.WaitGroup
- var count1, count2, count3 int
- session.On(func(event SessionEvent) { count1++ })
- unsub2 := session.On(func(event SessionEvent) { count2++ })
- session.On(func(event SessionEvent) { count3++ })
+ wg.Add(3)
+ session.On(func(event SessionEvent) { count1.Add(1); wg.Done() })
+ unsub2 := session.On(func(event SessionEvent) { count2.Add(1); wg.Done() })
+ session.On(func(event SessionEvent) { count3.Add(1); wg.Done() })
// First event - all handlers receive it
session.dispatchEvent(SessionEvent{Type: "test"})
+ wg.Wait()
// Unsubscribe handler 2
unsub2()
// Second event - only handlers 1 and 3 should receive it
+ wg.Add(2)
session.dispatchEvent(SessionEvent{Type: "test"})
+ wg.Wait()
- if count1 != 2 {
- t.Errorf("Expected handler 1 to receive 2 events, got %d", count1)
+ if count1.Load() != 2 {
+ t.Errorf("Expected handler 1 to receive 2 events, got %d", count1.Load())
}
- if count2 != 1 {
- t.Errorf("Expected handler 2 to receive 1 event (before unsubscribe), got %d", count2)
+ if count2.Load() != 1 {
+ t.Errorf("Expected handler 2 to receive 1 event (before unsubscribe), got %d", count2.Load())
}
- if count3 != 2 {
- t.Errorf("Expected handler 3 to receive 2 events, got %d", count3)
+ if count3.Load() != 2 {
+ t.Errorf("Expected handler 3 to receive 2 events, got %d", count3.Load())
}
})
t.Run("calling unsubscribe multiple times is safe", func(t *testing.T) {
- session := &Session{
- handlers: make([]sessionHandler, 0),
- }
+ session, cleanup := newTestSession()
+ defer cleanup()
+
+ var count atomic.Int32
+ var wg sync.WaitGroup
- var count int
- unsub := session.On(func(event SessionEvent) { count++ })
+ wg.Add(1)
+ unsub := session.On(func(event SessionEvent) { count.Add(1); wg.Done() })
session.dispatchEvent(SessionEvent{Type: "test"})
+ wg.Wait()
- // Call unsubscribe multiple times - should not panic
unsub()
unsub()
unsub()
+ // Dispatch again and wait for it to be processed via a sentinel handler
+ wg.Add(1)
+ session.On(func(event SessionEvent) { wg.Done() })
session.dispatchEvent(SessionEvent{Type: "test"})
+ wg.Wait()
- if count != 1 {
- t.Errorf("Expected handler to receive 1 event, got %d", count)
+ if count.Load() != 1 {
+ t.Errorf("Expected handler to receive 1 event, got %d", count.Load())
}
})
t.Run("handlers are called in registration order", func(t *testing.T) {
- session := &Session{
- handlers: make([]sessionHandler, 0),
- }
+ session, cleanup := newTestSession()
+ defer cleanup()
var order []int
- session.On(func(event SessionEvent) { order = append(order, 1) })
- session.On(func(event SessionEvent) { order = append(order, 2) })
- session.On(func(event SessionEvent) { order = append(order, 3) })
+ var wg sync.WaitGroup
+ wg.Add(3)
+ session.On(func(event SessionEvent) { order = append(order, 1); wg.Done() })
+ session.On(func(event SessionEvent) { order = append(order, 2); wg.Done() })
+ session.On(func(event SessionEvent) { order = append(order, 3); wg.Done() })
session.dispatchEvent(SessionEvent{Type: "test"})
+ wg.Wait()
if len(order) != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3 {
t.Errorf("Expected handlers to be called in order [1,2,3], got %v", order)
@@ -94,9 +122,8 @@ func TestSession_On(t *testing.T) {
})
t.Run("concurrent subscribe and unsubscribe is safe", func(t *testing.T) {
- session := &Session{
- handlers: make([]sessionHandler, 0),
- }
+ session, cleanup := newTestSession()
+ defer cleanup()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
@@ -109,7 +136,6 @@ func TestSession_On(t *testing.T) {
}
wg.Wait()
- // Should not panic and handlers should be empty
session.handlerMutex.RLock()
count := len(session.handlers)
session.handlerMutex.RUnlock()
@@ -118,4 +144,63 @@ func TestSession_On(t *testing.T) {
t.Errorf("Expected 0 handlers after all unsubscribes, got %d", count)
}
})
+
+ t.Run("events are dispatched serially", func(t *testing.T) {
+ session, cleanup := newTestSession()
+ defer cleanup()
+
+ var concurrentCount atomic.Int32
+ var maxConcurrent atomic.Int32
+ var done sync.WaitGroup
+ const totalEvents = 5
+ done.Add(totalEvents)
+
+ session.On(func(event SessionEvent) {
+ current := concurrentCount.Add(1)
+ if current > maxConcurrent.Load() {
+ maxConcurrent.Store(current)
+ }
+
+ time.Sleep(10 * time.Millisecond)
+
+ concurrentCount.Add(-1)
+ done.Done()
+ })
+
+ for i := 0; i < totalEvents; i++ {
+ session.dispatchEvent(SessionEvent{Type: "test"})
+ }
+
+ done.Wait()
+
+ if max := maxConcurrent.Load(); max != 1 {
+ t.Errorf("Expected max concurrent count of 1, got %d", max)
+ }
+ })
+
+ t.Run("handler panic does not halt delivery", func(t *testing.T) {
+ session, cleanup := newTestSession()
+ defer cleanup()
+
+ var eventCount atomic.Int32
+ var done sync.WaitGroup
+ done.Add(2)
+
+ session.On(func(event SessionEvent) {
+ count := eventCount.Add(1)
+ defer done.Done()
+ if count == 1 {
+ panic("boom")
+ }
+ })
+
+ session.dispatchEvent(SessionEvent{Type: "test"})
+ session.dispatchEvent(SessionEvent{Type: "test"})
+
+ done.Wait()
+
+ if eventCount.Load() != 2 {
+ t.Errorf("Expected 2 events dispatched, got %d", eventCount.Load())
+ }
+ })
}
diff --git a/go/telemetry.go b/go/telemetry.go
new file mode 100644
index 0000000000..b9a480b871
--- /dev/null
+++ b/go/telemetry.go
@@ -0,0 +1,31 @@
+package copilot
+
+import (
+ "context"
+
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/propagation"
+)
+
+// getTraceContext extracts the current W3C Trace Context (traceparent/tracestate)
+// from the Go context using the global OTel propagator.
+func getTraceContext(ctx context.Context) (traceparent, tracestate string) {
+ carrier := propagation.MapCarrier{}
+ otel.GetTextMapPropagator().Inject(ctx, carrier)
+ return carrier.Get("traceparent"), carrier.Get("tracestate")
+}
+
+// contextWithTraceParent returns a new context with trace context extracted from
+// the provided W3C traceparent and tracestate headers.
+func contextWithTraceParent(ctx context.Context, traceparent, tracestate string) context.Context {
+ if traceparent == "" {
+ return ctx
+ }
+ carrier := propagation.MapCarrier{
+ "traceparent": traceparent,
+ }
+ if tracestate != "" {
+ carrier["tracestate"] = tracestate
+ }
+ return otel.GetTextMapPropagator().Extract(ctx, carrier)
+}
diff --git a/go/telemetry_test.go b/go/telemetry_test.go
new file mode 100644
index 0000000000..827623fce8
--- /dev/null
+++ b/go/telemetry_test.go
@@ -0,0 +1,86 @@
+package copilot
+
+import (
+ "context"
+ "testing"
+
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/propagation"
+ "go.opentelemetry.io/otel/trace"
+)
+
+func TestGetTraceContextEmpty(t *testing.T) {
+ // Without any propagator configured, should return empty strings
+ tp, ts := getTraceContext(context.Background())
+ if tp != "" || ts != "" {
+ t.Errorf("expected empty trace context, got traceparent=%q tracestate=%q", tp, ts)
+ }
+}
+
+func TestGetTraceContextWithPropagator(t *testing.T) {
+ // Set up W3C propagator
+ otel.SetTextMapPropagator(propagation.TraceContext{})
+ defer otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator())
+
+ // Inject known trace context
+ carrier := propagation.MapCarrier{
+ "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
+ }
+ ctx := otel.GetTextMapPropagator().Extract(context.Background(), carrier)
+
+ tp, ts := getTraceContext(ctx)
+ if tp == "" {
+ t.Error("expected non-empty traceparent")
+ }
+ _ = ts // tracestate may be empty
+}
+
+func TestContextWithTraceParentEmpty(t *testing.T) {
+ ctx := contextWithTraceParent(context.Background(), "", "")
+ if ctx == nil {
+ t.Error("expected non-nil context")
+ }
+}
+
+func TestContextWithTraceParentValid(t *testing.T) {
+ otel.SetTextMapPropagator(propagation.TraceContext{})
+ defer otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator())
+
+ ctx := contextWithTraceParent(context.Background(),
+ "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "")
+
+ // Verify the context has trace info by extracting it back
+ carrier := propagation.MapCarrier{}
+ otel.GetTextMapPropagator().Inject(ctx, carrier)
+ if carrier.Get("traceparent") == "" {
+ t.Error("expected traceparent to be set in context")
+ }
+}
+
+func TestToolInvocationTraceContext(t *testing.T) {
+ otel.SetTextMapPropagator(propagation.TraceContext{})
+ defer otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator())
+
+ traceparent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
+ ctx := contextWithTraceParent(context.Background(), traceparent, "")
+
+ inv := ToolInvocation{
+ SessionID: "sess-1",
+ ToolCallID: "call-1",
+ ToolName: "my_tool",
+ Arguments: nil,
+ TraceContext: ctx,
+ }
+
+ // The TraceContext should carry the remote span context
+ sc := trace.SpanContextFromContext(inv.TraceContext)
+ if !sc.IsValid() {
+ t.Fatal("expected valid span context on ToolInvocation.TraceContext")
+ }
+ if sc.TraceID().String() != "4bf92f3577b34da6a3ce929d0e0e4736" {
+ t.Errorf("unexpected trace ID: %s", sc.TraceID())
+ }
+ if sc.SpanID().String() != "00f067aa0ba902b7" {
+ t.Errorf("unexpected span ID: %s", sc.SpanID())
+ }
+}
diff --git a/go/types.go b/go/types.go
index d749de74a8..502d61c1c6 100644
--- a/go/types.go
+++ b/go/types.go
@@ -1,6 +1,9 @@
package copilot
-import "encoding/json"
+import (
+ "context"
+ "encoding/json"
+)
// ConnectionState represents the client connection state
type ConnectionState string
@@ -35,8 +38,7 @@ type ClientOptions struct {
// AutoStart automatically starts the CLI server on first use (default: true).
// Use Bool(false) to disable.
AutoStart *bool
- // AutoRestart automatically restarts the CLI server if it crashes (default: true).
- // Use Bool(false) to disable.
+ // Deprecated: AutoRestart has no effect and will be removed in a future release.
AutoRestart *bool
// Env is the environment variables for the CLI process (default: inherits from current process).
// Each entry is of the form "key=value".
@@ -54,10 +56,45 @@ type ClientOptions struct {
// Default: true (but defaults to false when GitHubToken is provided).
// Use Bool(false) to explicitly disable.
UseLoggedInUser *bool
+ // OnListModels is a custom handler for listing available models.
+ // When provided, client.ListModels() calls this handler instead of
+ // querying the CLI server. Useful in BYOK mode to return models
+ // available from your custom provider.
+ OnListModels func(ctx context.Context) ([]ModelInfo, error)
+ // Telemetry configures OpenTelemetry integration for the Copilot CLI process.
+ // When non-nil, COPILOT_OTEL_ENABLED=true is set and any populated fields
+ // are mapped to the corresponding environment variables.
+ Telemetry *TelemetryConfig
+}
+
+// TelemetryConfig configures OpenTelemetry integration for the Copilot CLI process.
+type TelemetryConfig struct {
+ // OTLPEndpoint is the OTLP HTTP endpoint URL for trace/metric export.
+ // Sets OTEL_EXPORTER_OTLP_ENDPOINT.
+ OTLPEndpoint string
+
+ // FilePath is the file path for JSON-lines trace output.
+ // Sets COPILOT_OTEL_FILE_EXPORTER_PATH.
+ FilePath string
+
+ // ExporterType is the exporter backend type: "otlp-http" or "file".
+ // Sets COPILOT_OTEL_EXPORTER_TYPE.
+ ExporterType string
+
+ // SourceName is the instrumentation scope name.
+ // Sets COPILOT_OTEL_SOURCE_NAME.
+ SourceName string
+
+ // CaptureContent controls whether to capture message content (prompts, responses).
+ // Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.
+ CaptureContent *bool
}
// Bool returns a pointer to the given bool value.
-// Use for setting AutoStart or AutoRestart: AutoStart: Bool(false)
+// Use for option fields such as AutoStart, AutoRestart, or LogOptions.Ephemeral:
+//
+// AutoStart: Bool(false)
+// Ephemeral: Bool(true)
func Bool(v bool) *bool {
return &v
}
@@ -74,6 +111,51 @@ func Float64(v float64) *float64 {
return &v
}
+// Known system prompt section identifiers for the "customize" mode.
+const (
+ SectionIdentity = "identity"
+ SectionTone = "tone"
+ SectionToolEfficiency = "tool_efficiency"
+ SectionEnvironmentContext = "environment_context"
+ SectionCodeChangeRules = "code_change_rules"
+ SectionGuidelines = "guidelines"
+ SectionSafety = "safety"
+ SectionToolInstructions = "tool_instructions"
+ SectionCustomInstructions = "custom_instructions"
+ SectionLastInstructions = "last_instructions"
+)
+
+// SectionOverrideAction represents the action to perform on a system prompt section.
+type SectionOverrideAction string
+
+const (
+ // SectionActionReplace replaces section content entirely.
+ SectionActionReplace SectionOverrideAction = "replace"
+ // SectionActionRemove removes the section.
+ SectionActionRemove SectionOverrideAction = "remove"
+ // SectionActionAppend appends to existing section content.
+ SectionActionAppend SectionOverrideAction = "append"
+ // SectionActionPrepend prepends to existing section content.
+ SectionActionPrepend SectionOverrideAction = "prepend"
+)
+
+// SectionTransformFn is a callback that receives the current content of a system prompt section
+// and returns the transformed content. Used with the "transform" action to read-then-write
+// modify sections at runtime.
+type SectionTransformFn func(currentContent string) (string, error)
+
+// SectionOverride defines an override operation for a single system prompt section.
+type SectionOverride struct {
+ // Action is the operation to perform: "replace", "remove", "append", "prepend", or "transform".
+ Action SectionOverrideAction `json:"action,omitempty"`
+ // Content for the override. Optional for all actions. Ignored for "remove".
+ Content string `json:"content,omitempty"`
+ // Transform is a callback invoked when Action is "transform".
+ // The runtime calls this with the current section content and uses the returned string.
+ // Excluded from JSON serialization; the SDK registers it as an RPC callback internally.
+ Transform SectionTransformFn `json:"-"`
+}
+
// SystemMessageAppendConfig is append mode: use CLI foundation with optional appended content.
type SystemMessageAppendConfig struct {
// Mode is optional, defaults to "append"
@@ -92,11 +174,15 @@ type SystemMessageReplaceConfig struct {
}
// SystemMessageConfig represents system message configuration for session creation.
-// Use SystemMessageAppendConfig for default behavior, SystemMessageReplaceConfig for full control.
-// In Go, use one struct or the other based on your needs.
+// - Append mode (default): SDK foundation + optional custom content
+// - Replace mode: Full control, caller provides entire system message
+// - Customize mode: Section-level overrides with graceful fallback
+//
+// In Go, use one struct and set fields appropriate for the desired mode.
type SystemMessageConfig struct {
- Mode string `json:"mode,omitempty"`
- Content string `json:"content,omitempty"`
+ Mode string `json:"mode,omitempty"`
+ Content string `json:"content,omitempty"`
+ Sections map[string]SectionOverride `json:"sections,omitempty"`
}
// PermissionRequestResultKind represents the kind of a permission request result.
@@ -115,6 +201,9 @@ const (
// PermissionRequestResultKindDeniedInteractivelyByUser indicates the permission was denied interactively by the user.
PermissionRequestResultKindDeniedInteractivelyByUser PermissionRequestResultKind = "denied-interactively-by-user"
+
+ // PermissionRequestResultKindNoResult indicates no permission decision was made.
+ PermissionRequestResultKindNoResult PermissionRequestResultKind = "no-result"
)
// PermissionRequestResult represents the result of a permission request
@@ -384,6 +473,9 @@ type SessionConfig struct {
MCPServers map[string]MCPServerConfig
// CustomAgents configures custom agents for the session
CustomAgents []CustomAgentConfig
+ // Agent is the name of the custom agent to activate when the session starts.
+ // Must match the Name of one of the agents in CustomAgents.
+ Agent string
// SkillDirectories is a list of directories to load skills from
SkillDirectories []string
// DisabledSkills is a list of skill names to disable
@@ -391,14 +483,19 @@ type SessionConfig struct {
// InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction.
// When enabled (default), sessions automatically manage context limits and persist state.
InfiniteSessions *InfiniteSessionConfig
+ // OnEvent is an optional event handler that is registered on the session before
+ // the session.create RPC is issued. This guarantees that early events emitted
+ // by the CLI during session creation (e.g. session.start) are delivered to the
+ // handler. Equivalent to calling session.On(handler) immediately after creation,
+ // but executes earlier in the lifecycle so no events are missed.
+ OnEvent SessionEventHandler
}
-
-// Tool describes a caller-implemented tool that can be invoked by Copilot
type Tool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parameters map[string]any `json:"parameters,omitempty"`
OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"`
+ SkipPermission bool `json:"skipPermission,omitempty"`
Handler ToolHandler `json:"-"`
}
@@ -408,6 +505,12 @@ type ToolInvocation struct {
ToolCallID string
ToolName string
Arguments any
+
+ // TraceContext carries the W3C Trace Context propagated from the CLI's
+ // execute_tool span. Pass this to OpenTelemetry-aware code so that
+ // child spans created inside the handler are parented to the CLI span.
+ // When no trace context is available this will be context.Background().
+ TraceContext context.Context
}
// ToolHandler executes a tool invocation.
@@ -467,6 +570,9 @@ type ResumeSessionConfig struct {
MCPServers map[string]MCPServerConfig
// CustomAgents configures custom agents for the session
CustomAgents []CustomAgentConfig
+ // Agent is the name of the custom agent to activate when the session starts.
+ // Must match the Name of one of the agents in CustomAgents.
+ Agent string
// SkillDirectories is a list of directories to load skills from
SkillDirectories []string
// DisabledSkills is a list of skill names to disable
@@ -476,9 +582,10 @@ type ResumeSessionConfig struct {
// DisableResume, when true, skips emitting the session.resume event.
// Useful for reconnecting to a session without triggering resume-related side effects.
DisableResume bool
+ // OnEvent is an optional event handler registered before the session.resume RPC
+ // is issued, ensuring early events are delivered. See SessionConfig.OnEvent.
+ OnEvent SessionEventHandler
}
-
-// ProviderConfig configures a custom model provider
type ProviderConfig struct {
// Type is the provider type: "openai", "azure", or "anthropic". Defaults to "openai".
Type string `json:"type,omitempty"`
@@ -652,10 +759,13 @@ type createSessionRequest struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
+ Agent string `json:"agent,omitempty"`
ConfigDir string `json:"configDir,omitempty"`
SkillDirectories []string `json:"skillDirectories,omitempty"`
DisabledSkills []string `json:"disabledSkills,omitempty"`
InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"`
+ Traceparent string `json:"traceparent,omitempty"`
+ Tracestate string `json:"tracestate,omitempty"`
}
// createSessionResponse is the response from session.create
@@ -685,9 +795,12 @@ type resumeSessionRequest struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
+ Agent string `json:"agent,omitempty"`
SkillDirectories []string `json:"skillDirectories,omitempty"`
DisabledSkills []string `json:"disabledSkills,omitempty"`
InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"`
+ Traceparent string `json:"traceparent,omitempty"`
+ Tracestate string `json:"tracestate,omitempty"`
}
// resumeSessionResponse is the response from session.resume
@@ -816,6 +929,8 @@ type sessionSendRequest struct {
Prompt string `json:"prompt"`
Attachments []Attachment `json:"attachments,omitempty"`
Mode string `json:"mode,omitempty"`
+ Traceparent string `json:"traceparent,omitempty"`
+ Tracestate string `json:"tracestate,omitempty"`
}
// sessionSendResponse is the response from session.send
diff --git a/go/types_test.go b/go/types_test.go
index 190cd913d8..80b0cc5458 100644
--- a/go/types_test.go
+++ b/go/types_test.go
@@ -15,6 +15,7 @@ func TestPermissionRequestResultKind_Constants(t *testing.T) {
{"DeniedByRules", PermissionRequestResultKindDeniedByRules, "denied-by-rules"},
{"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser, "denied-no-approval-rule-and-could-not-request-from-user"},
{"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser, "denied-interactively-by-user"},
+ {"NoResult", PermissionRequestResultKind("no-result"), "no-result"},
}
for _, tt := range tests {
@@ -42,6 +43,7 @@ func TestPermissionRequestResult_JSONRoundTrip(t *testing.T) {
{"DeniedByRules", PermissionRequestResultKindDeniedByRules},
{"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser},
{"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser},
+ {"NoResult", PermissionRequestResultKind("no-result")},
{"Custom", PermissionRequestResultKind("custom")},
}
diff --git a/justfile b/justfile
index 85cd8c61bf..fd7fc3adbe 100644
--- a/justfile
+++ b/justfile
@@ -71,15 +71,34 @@ test-dotnet:
@echo "=== Testing .NET code ==="
@cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj
-# Install all dependencies
-install:
- @echo "=== Installing dependencies ==="
- @cd nodejs && npm ci
- @cd python && uv pip install -e ".[dev]"
+# Install all dependencies across all languages
+install: install-go install-python install-nodejs install-dotnet
+ @echo "✅ All dependencies installed"
+
+# Install Go dependencies and prerequisites for tests
+install-go: install-nodejs install-test-harness
+ @echo "=== Installing Go dependencies ==="
@cd go && go mod download
+
+# Install Python dependencies and prerequisites for tests
+install-python: install-nodejs install-test-harness
+ @echo "=== Installing Python dependencies ==="
+ @cd python && uv pip install -e ".[dev]"
+
+# Install .NET dependencies and prerequisites for tests
+install-dotnet: install-nodejs install-test-harness
+ @echo "=== Installing .NET dependencies ==="
@cd dotnet && dotnet restore
+
+# Install Node.js dependencies
+install-nodejs:
+ @echo "=== Installing Node.js dependencies ==="
+ @cd nodejs && npm ci
+
+# Install test harness dependencies (used by E2E tests in all languages)
+install-test-harness:
+ @echo "=== Installing test harness dependencies ==="
@cd test/harness && npm ci --ignore-scripts
- @echo "✅ All dependencies installed"
# Run interactive SDK playground
playground:
diff --git a/nodejs/README.md b/nodejs/README.md
index 78a535b76a..cc5d624165 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -26,15 +26,16 @@ npm start
## Quick Start
```typescript
-import { CopilotClient } from "@github/copilot-sdk";
+import { CopilotClient, approveAll } from "@github/copilot-sdk";
// Create and start client
const client = new CopilotClient();
await client.start();
-// Create a session
+// Create a session (onPermissionRequest is required)
const session = await client.createSession({
model: "gpt-5",
+ onPermissionRequest: approveAll,
});
// Wait for response using typed event handlers
@@ -59,7 +60,7 @@ await client.stop();
Sessions also support `Symbol.asyncDispose` for use with [`await using`](https://github.com/tc39/proposal-explicit-resource-management) (TypeScript 5.2+/Node.js 18.0+):
```typescript
-await using session = await client.createSession({ model: "gpt-5" });
+await using session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll });
// session is automatically disconnected when leaving scope
```
@@ -82,9 +83,10 @@ new CopilotClient(options?: CopilotClientOptions)
- `useStdio?: boolean` - Use stdio transport instead of TCP (default: true)
- `logLevel?: string` - Log level (default: "info")
- `autoStart?: boolean` - Auto-start server (default: true)
-- `autoRestart?: boolean` - Auto-restart on crash (default: true)
- `githubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods.
- `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `githubToken` is provided). Cannot be used with `cliUrl`.
+- `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the CLI process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below.
+- `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the CLI's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below.
#### Methods
@@ -113,6 +115,7 @@ Create a new conversation session.
- `systemMessage?: SystemMessageConfig` - System message customization (see below)
- `infiniteSessions?: InfiniteSessionConfig` - Configure automatic context compaction (see below)
- `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section.
+- `onPermissionRequest: PermissionHandler` - **Required.** Handler called before each tool execution to approve or deny it. Use `approveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
- `onUserInputRequest?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section.
- `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
@@ -297,9 +300,10 @@ See `SessionEvent` type in the source for full details.
## Image Support
-The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path:
+The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment:
```typescript
+// File attachment — runtime reads from disk
await session.send({
prompt: "What's in this image?",
attachments: [
@@ -309,6 +313,18 @@ await session.send({
},
],
});
+
+// Blob attachment — provide base64 data directly
+await session.send({
+ prompt: "What's in this image?",
+ attachments: [
+ {
+ type: "blob",
+ data: base64ImageData,
+ mimeType: "image/png",
+ },
+ ],
+});
```
Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like:
@@ -426,6 +442,19 @@ defineTool("edit_file", {
})
```
+#### Skipping Permission Prompts
+
+Set `skipPermission: true` on a tool definition to allow it to execute without triggering a permission prompt:
+
+```ts
+defineTool("safe_lookup", {
+ description: "A read-only lookup that needs no confirmation",
+ parameters: z.object({ id: z.string() }),
+ skipPermission: true,
+ handler: async ({ id }) => { /* your logic */ },
+})
+```
+
### System Message Customization
Control the system prompt using `systemMessage` in session config:
@@ -444,7 +473,45 @@ const session = await client.createSession({
});
```
-The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"`.
+The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"` or `mode: "customize"`.
+
+#### Customize Mode
+
+Use `mode: "customize"` to selectively override individual sections of the prompt while preserving the rest:
+
+```typescript
+import { SYSTEM_PROMPT_SECTIONS } from "@github/copilot-sdk";
+import type { SectionOverride, SystemPromptSection } from "@github/copilot-sdk";
+
+const session = await client.createSession({
+ model: "gpt-5",
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ // Replace the tone/style section
+ tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." },
+ // Remove coding-specific rules
+ code_change_rules: { action: "remove" },
+ // Append to existing guidelines
+ guidelines: { action: "append", content: "\n* Always cite data sources" },
+ },
+ // Additional instructions appended after all sections
+ content: "Focus on financial analysis and reporting.",
+ },
+});
+```
+
+Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`. Use the `SYSTEM_PROMPT_SECTIONS` constant for descriptions of each section.
+
+Each section override supports four actions:
+- **`replace`** — Replace the section content entirely
+- **`remove`** — Remove the section from the prompt
+- **`append`** — Add content after the existing section
+- **`prepend`** — Add content before the existing section
+
+Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
+
+#### Replace Mode
For full control (removes all guardrails), use `mode: "replace"`:
@@ -589,6 +656,127 @@ const session = await client.createSession({
> - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`.
> - The `baseUrl` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically.
+## Telemetry
+
+The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export from the CLI process — this is all most users need:
+
+```typescript
+const client = new CopilotClient({
+ telemetry: {
+ otlpEndpoint: "http://localhost:4318",
+ },
+});
+```
+
+With just this configuration, the CLI emits spans for every session, message, and tool call to your collector. No additional dependencies or setup required.
+
+**TelemetryConfig options:**
+
+- `otlpEndpoint?: string` - OTLP HTTP endpoint URL
+- `filePath?: string` - File path for JSON-lines trace output
+- `exporterType?: string` - `"otlp-http"` or `"file"`
+- `sourceName?: string` - Instrumentation scope name
+- `captureContent?: boolean` - Whether to capture message content
+
+### Advanced: Trace Context Propagation
+
+> **You don't need this for normal telemetry collection.** The `telemetry` config above is sufficient to get full traces from the CLI.
+
+`onGetTraceContext` is only needed if your application creates its own OpenTelemetry spans and you want them to appear in the **same distributed trace** as the CLI's spans — for example, to nest a "handle tool call" span inside the CLI's "execute tool" span, or to show the SDK call as a child of your application's request-handling span.
+
+If you're already using `@opentelemetry/api` in your app and want this linkage, provide a callback:
+
+```typescript
+import { propagation, context } from "@opentelemetry/api";
+
+const client = new CopilotClient({
+ telemetry: { otlpEndpoint: "http://localhost:4318" },
+ onGetTraceContext: () => {
+ const carrier: Record = {};
+ propagation.inject(context.active(), carrier);
+ return carrier;
+ },
+});
+```
+
+Inbound trace context from the CLI is available on the `ToolInvocation` object passed to tool handlers as `traceparent` and `tracestate` fields. See the [OpenTelemetry guide](../docs/observability/opentelemetry.md) for a full wire-up example.
+
+## Permission Handling
+
+An `onPermissionRequest` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision.
+
+### Approve All (simplest)
+
+Use the built-in `approveAll` helper to allow every tool call without any checks:
+
+```typescript
+import { CopilotClient, approveAll } from "@github/copilot-sdk";
+
+const session = await client.createSession({
+ model: "gpt-5",
+ onPermissionRequest: approveAll,
+});
+```
+
+### Custom Permission Handler
+
+Provide your own function to inspect each request and apply custom logic:
+
+```typescript
+import type { PermissionRequest, PermissionRequestResult } from "@github/copilot-sdk";
+
+const session = await client.createSession({
+ model: "gpt-5",
+ onPermissionRequest: (request: PermissionRequest, invocation): PermissionRequestResult => {
+ // request.kind — what type of operation is being requested:
+ // "shell" — executing a shell command
+ // "write" — writing or editing a file
+ // "read" — reading a file
+ // "mcp" — calling an MCP tool
+ // "custom-tool" — calling one of your registered tools
+ // "url" — fetching a URL
+ // "memory" — storing or retrieving persistent session memory
+ // "hook" — invoking a server-side hook or integration
+ // (additional kinds may be added; include a default case in handlers)
+ // request.toolCallId — the tool call that triggered this request
+ // request.toolName — name of the tool (for custom-tool / mcp)
+ // request.fileName — file being written (for write)
+ // request.fullCommandText — full shell command (for shell)
+
+ if (request.kind === "shell") {
+ // Deny shell commands
+ return { kind: "denied-interactively-by-user" };
+ }
+
+ return { kind: "approved" };
+ },
+});
+```
+
+### Permission Result Kinds
+
+| Kind | Meaning |
+|------|---------|
+| `"approved"` | Allow the tool to run |
+| `"denied-interactively-by-user"` | User explicitly denied the request |
+| `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked |
+| `"denied-by-rules"` | Denied by a policy rule |
+| `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy |
+| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) |
+### Resuming Sessions
+
+Pass `onPermissionRequest` when resuming a session too — it is required:
+
+```typescript
+const session = await client.resumeSession("session-id", {
+ onPermissionRequest: approveAll,
+});
+```
+
+### Per-Tool Skip Permission
+
+To let a specific custom tool bypass the permission prompt entirely, set `skipPermission: true` on the tool definition. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools.
+
## User Input Requests
Enable the agent to ask questions to the user using the `ask_user` tool by providing an `onUserInputRequest` handler:
diff --git a/nodejs/docs/agent-author.md b/nodejs/docs/agent-author.md
new file mode 100644
index 0000000000..8b3d935939
--- /dev/null
+++ b/nodejs/docs/agent-author.md
@@ -0,0 +1,263 @@
+# Agent Extension Authoring Guide
+
+A precise, step-by-step reference for agents writing Copilot CLI extensions programmatically.
+
+## Workflow
+
+### Step 1: Scaffold the extension
+
+Use the `extensions_manage` tool with `operation: "scaffold"`:
+
+```
+extensions_manage({ operation: "scaffold", name: "my-extension" })
+```
+
+This creates `.github/extensions/my-extension/extension.mjs` with a working skeleton.
+For user-scoped extensions (persist across all repos), add `location: "user"`.
+
+### Step 2: Edit the extension file
+
+Modify the generated `extension.mjs` using `edit` or `create` tools. The file must:
+- Be named `extension.mjs` (only `.mjs` is supported)
+- Use ES module syntax (`import`/`export`)
+- Call `joinSession({ ... })`
+
+### Step 3: Reload extensions
+
+```
+extensions_reload({})
+```
+
+This stops all running extensions and re-discovers/re-launches them. New tools are available immediately in the same turn (mid-turn refresh).
+
+### Step 4: Verify
+
+```
+extensions_manage({ operation: "list" })
+extensions_manage({ operation: "inspect", name: "my-extension" })
+```
+
+Check that the extension loaded successfully and isn't marked as "failed".
+
+---
+
+## File Structure
+
+```
+.github/extensions//extension.mjs
+```
+
+Discovery rules:
+- The CLI scans `.github/extensions/` relative to the git root
+- It also scans the user's copilot config extensions directory
+- Only immediate subdirectories are checked (not recursive)
+- Each subdirectory must contain a file named `extension.mjs`
+- Project extensions shadow user extensions on name collision
+
+---
+
+## Minimal Skeleton
+
+```js
+import { joinSession } from "@github/copilot-sdk/extension";
+
+await joinSession({
+ tools: [], // Optional — custom tools
+ hooks: {}, // Optional — lifecycle hooks
+});
+```
+
+---
+
+## Registering Tools
+
+```js
+tools: [
+ {
+ name: "tool_name", // Required. Must be globally unique across all extensions.
+ description: "What it does", // Required. Shown to the agent in tool descriptions.
+ parameters: { // Optional. JSON Schema for the arguments.
+ type: "object",
+ properties: {
+ arg1: { type: "string", description: "..." },
+ },
+ required: ["arg1"],
+ },
+ handler: async (args, invocation) => {
+ // args: parsed arguments matching the schema
+ // invocation.sessionId: current session ID
+ // invocation.toolCallId: unique call ID
+ // invocation.toolName: this tool's name
+ //
+ // Return value: string or ToolResultObject
+ // string → treated as success
+ // { textResultForLlm, resultType } → structured result
+ // resultType: "success" | "failure" | "rejected" | "denied"
+ return `Result: ${args.arg1}`;
+ },
+ },
+]
+```
+
+**Constraints:**
+- Tool names must be unique across ALL loaded extensions. Collisions cause the second extension to fail to load.
+- Handler must return a string or `{ textResultForLlm: string, resultType?: string }`.
+- Handler receives `(args, invocation)` — the second argument has `sessionId`, `toolCallId`, `toolName`.
+- Use `session.log()` to surface messages to the user. Don't use `console.log()` (stdout is reserved for JSON-RPC).
+
+---
+
+## Registering Hooks
+
+```js
+hooks: {
+ onUserPromptSubmitted: async (input, invocation) => { ... },
+ onPreToolUse: async (input, invocation) => { ... },
+ onPostToolUse: async (input, invocation) => { ... },
+ onSessionStart: async (input, invocation) => { ... },
+ onSessionEnd: async (input, invocation) => { ... },
+ onErrorOccurred: async (input, invocation) => { ... },
+}
+```
+
+All hook inputs include `timestamp` (unix ms) and `cwd` (working directory).
+All handlers receive `invocation: { sessionId: string }` as the second argument.
+All handlers may return `void`/`undefined` (no-op) or an output object.
+
+### onUserPromptSubmitted
+
+**Input:** `{ prompt: string, timestamp, cwd }`
+
+**Output (all fields optional):**
+| Field | Type | Effect |
+|-------|------|--------|
+| `modifiedPrompt` | `string` | Replaces the user's prompt |
+| `additionalContext` | `string` | Appended as hidden context the agent sees |
+
+### onPreToolUse
+
+**Input:** `{ toolName: string, toolArgs: unknown, timestamp, cwd }`
+
+**Output (all fields optional):**
+| Field | Type | Effect |
+|-------|------|--------|
+| `permissionDecision` | `"allow" \| "deny" \| "ask"` | Override the permission check |
+| `permissionDecisionReason` | `string` | Shown to user if denied |
+| `modifiedArgs` | `unknown` | Replaces the tool arguments |
+| `additionalContext` | `string` | Injected into the conversation |
+
+### onPostToolUse
+
+**Input:** `{ toolName: string, toolArgs: unknown, toolResult: ToolResultObject, timestamp, cwd }`
+
+**Output (all fields optional):**
+| Field | Type | Effect |
+|-------|------|--------|
+| `modifiedResult` | `ToolResultObject` | Replaces the tool result |
+| `additionalContext` | `string` | Injected into the conversation |
+
+### onSessionStart
+
+**Input:** `{ source: "startup" \| "resume" \| "new", initialPrompt?: string, timestamp, cwd }`
+
+**Output (all fields optional):**
+| Field | Type | Effect |
+|-------|------|--------|
+| `additionalContext` | `string` | Injected as initial context |
+
+### onSessionEnd
+
+**Input:** `{ reason: "complete" \| "error" \| "abort" \| "timeout" \| "user_exit", finalMessage?: string, error?: string, timestamp, cwd }`
+
+**Output (all fields optional):**
+| Field | Type | Effect |
+|-------|------|--------|
+| `sessionSummary` | `string` | Summary for session persistence |
+| `cleanupActions` | `string[]` | Cleanup descriptions |
+
+### onErrorOccurred
+
+**Input:** `{ error: string, errorContext: "model_call" \| "tool_execution" \| "system" \| "user_input", recoverable: boolean, timestamp, cwd }`
+
+**Output (all fields optional):**
+| Field | Type | Effect |
+|-------|------|--------|
+| `errorHandling` | `"retry" \| "skip" \| "abort"` | How to handle the error |
+| `retryCount` | `number` | Max retries (when errorHandling is "retry") |
+| `userNotification` | `string` | Message shown to the user |
+
+---
+
+## Session Object
+
+After `joinSession()`, the returned `session` provides:
+
+### session.send(options)
+
+Send a message programmatically:
+```js
+await session.send({ prompt: "Analyze the test results." });
+await session.send({
+ prompt: "Review this file",
+ attachments: [{ type: "file", path: "./src/index.ts" }],
+});
+```
+
+### session.sendAndWait(options, timeout?)
+
+Send and block until the agent finishes (resolves on `session.idle`):
+```js
+const response = await session.sendAndWait({ prompt: "What is 2+2?" });
+// response?.data.content contains the agent's reply
+```
+
+### session.log(message, options?)
+
+Log to the CLI timeline:
+```js
+await session.log("Extension ready");
+await session.log("Rate limit approaching", { level: "warning" });
+await session.log("Connection failed", { level: "error" });
+await session.log("Processing...", { ephemeral: true }); // transient, not persisted
+```
+
+### session.on(eventType, handler)
+
+Subscribe to session events. Returns an unsubscribe function.
+```js
+const unsub = session.on("tool.execution_complete", (event) => {
+ // event.data.toolName, event.data.success, event.data.result
+});
+```
+
+### Key Event Types
+
+| Event | Key Data Fields |
+|-------|----------------|
+| `assistant.message` | `content`, `messageId` |
+| `tool.execution_start` | `toolCallId`, `toolName`, `arguments` |
+| `tool.execution_complete` | `toolCallId`, `toolName`, `success`, `result`, `error` |
+| `user.message` | `content`, `attachments`, `source` |
+| `session.idle` | `backgroundTasks` |
+| `session.error` | `errorType`, `message`, `stack` |
+| `permission.requested` | `requestId`, `permissionRequest.kind` |
+| `session.shutdown` | `shutdownType`, `totalPremiumRequests` |
+
+### session.workspacePath
+
+Path to the session workspace directory (checkpoints, plan.md, files/). `undefined` if infinite sessions disabled.
+
+### session.rpc
+
+Low-level typed RPC access to all session APIs (model, mode, plan, workspace, etc.).
+
+---
+
+## Gotchas
+
+- **stdout is reserved for JSON-RPC.** Don't use `console.log()` — it will corrupt the protocol. Use `session.log()` to surface messages to the user.
+- **Tool name collisions are fatal.** If two extensions register the same tool name, the second extension fails to initialize.
+- **Don't call `session.send()` synchronously from `onUserPromptSubmitted`.** Use `setTimeout(() => session.send(...), 0)` to avoid infinite loops.
+- **Extensions are reloaded on `/clear`.** Any in-memory state is lost between sessions.
+- **Only `.mjs` is supported.** TypeScript (`.ts`) is not yet supported.
+- **The handler's return value is the tool result.** Returning `undefined` sends an empty success. Throwing sends a failure with the error message.
diff --git a/nodejs/docs/examples.md b/nodejs/docs/examples.md
new file mode 100644
index 0000000000..1461a2f392
--- /dev/null
+++ b/nodejs/docs/examples.md
@@ -0,0 +1,668 @@
+# Copilot CLI Extension Examples
+
+A practical guide to writing extensions using the `@github/copilot-sdk` extension API.
+
+## Extension Skeleton
+
+Every extension starts with the same boilerplate:
+
+```js
+import { joinSession } from "@github/copilot-sdk/extension";
+
+const session = await joinSession({
+ hooks: { /* ... */ },
+ tools: [ /* ... */ ],
+});
+```
+
+`joinSession` returns a `CopilotSession` object you can use to send messages and subscribe to events.
+
+> **Platform notes (Windows vs macOS/Linux):**
+> - Use `process.platform === "win32"` to detect Windows at runtime.
+> - Clipboard: `pbcopy` on macOS, `clip` on Windows.
+> - Use `exec()` instead of `execFile()` for `.cmd` scripts like `code`, `npx`, `npm` on Windows.
+> - PowerShell stderr redirection uses `*>&1` instead of `2>&1`.
+
+---
+
+## Logging to the Timeline
+
+Use `session.log()` to surface messages to the user in the CLI timeline:
+
+```js
+const session = await joinSession({
+ hooks: {
+ onSessionStart: async () => {
+ await session.log("My extension loaded");
+ },
+ onPreToolUse: async (input) => {
+ if (input.toolName === "bash") {
+ await session.log(`Running: ${input.toolArgs?.command}`, { ephemeral: true });
+ }
+ },
+ },
+ tools: [],
+});
+```
+
+Levels: `"info"` (default), `"warning"`, `"error"`. Set `ephemeral: true` for transient messages that aren't persisted.
+
+---
+
+## Registering Custom Tools
+
+Tools are functions the agent can call. Define them with a name, description, JSON Schema parameters, and a handler.
+
+### Basic tool
+
+```js
+tools: [
+ {
+ name: "my_tool",
+ description: "Does something useful",
+ parameters: {
+ type: "object",
+ properties: {
+ input: { type: "string", description: "The input value" },
+ },
+ required: ["input"],
+ },
+ handler: async (args) => {
+ return `Processed: ${args.input}`;
+ },
+ },
+]
+```
+
+### Tool that invokes an external shell command
+
+```js
+import { execFile } from "node:child_process";
+
+{
+ name: "run_command",
+ description: "Runs a shell command and returns its output",
+ parameters: {
+ type: "object",
+ properties: {
+ command: { type: "string", description: "The command to run" },
+ },
+ required: ["command"],
+ },
+ handler: async (args) => {
+ const isWindows = process.platform === "win32";
+ const shell = isWindows ? "powershell" : "bash";
+ const shellArgs = isWindows
+ ? ["-NoProfile", "-Command", args.command]
+ : ["-c", args.command];
+ return new Promise((resolve) => {
+ execFile(shell, shellArgs, (err, stdout, stderr) => {
+ if (err) resolve(`Error: ${stderr || err.message}`);
+ else resolve(stdout);
+ });
+ });
+ },
+}
+```
+
+### Tool that calls an external API
+
+```js
+{
+ name: "fetch_data",
+ description: "Fetches data from an API endpoint",
+ parameters: {
+ type: "object",
+ properties: {
+ url: { type: "string", description: "The URL to fetch" },
+ },
+ required: ["url"],
+ },
+ handler: async (args) => {
+ const res = await fetch(args.url);
+ if (!res.ok) return `Error: HTTP ${res.status}`;
+ return await res.text();
+ },
+}
+```
+
+### Tool handler invocation context
+
+The handler receives a second argument with invocation metadata:
+
+```js
+handler: async (args, invocation) => {
+ // invocation.sessionId — current session ID
+ // invocation.toolCallId — unique ID for this tool call
+ // invocation.toolName — name of the tool being called
+ return "done";
+}
+```
+
+---
+
+## Hooks
+
+Hooks intercept and modify behavior at key lifecycle points. Register them in the `hooks` option.
+
+### Available Hooks
+
+| Hook | Fires When | Can Modify |
+|------|-----------|------------|
+| `onUserPromptSubmitted` | User sends a message | The prompt text, add context |
+| `onPreToolUse` | Before a tool executes | Tool args, permission decision, add context |
+| `onPostToolUse` | After a tool executes | Tool result, add context |
+| `onSessionStart` | Session starts or resumes | Add context, modify config |
+| `onSessionEnd` | Session ends | Cleanup actions, summary |
+| `onErrorOccurred` | An error occurs | Error handling strategy (retry/skip/abort) |
+
+All hook inputs include `timestamp` (unix ms) and `cwd` (working directory).
+
+### Modifying the user's message
+
+Use `onUserPromptSubmitted` to rewrite or augment what the user typed before the agent sees it.
+
+```js
+hooks: {
+ onUserPromptSubmitted: async (input) => {
+ // Rewrite the prompt
+ return { modifiedPrompt: input.prompt.toUpperCase() };
+ },
+}
+```
+
+### Injecting additional context into every message
+
+Return `additionalContext` to silently append instructions the agent will follow.
+
+```js
+hooks: {
+ onUserPromptSubmitted: async (input) => {
+ return {
+ additionalContext: "Always respond in bullet points. Follow our team coding standards.",
+ };
+ },
+}
+```
+
+### Sending a follow-up message based on a keyword
+
+Use `session.send()` to programmatically inject a new user message.
+
+```js
+hooks: {
+ onUserPromptSubmitted: async (input) => {
+ if (/\\burgent\\b/i.test(input.prompt)) {
+ // Fire-and-forget a follow-up message
+ setTimeout(() => session.send({ prompt: "Please prioritize this." }), 0);
+ }
+ },
+}
+```
+
+> **Tip:** Guard against infinite loops if your follow-up message could re-trigger the same hook.
+
+### Blocking dangerous tool calls
+
+Use `onPreToolUse` to inspect and optionally deny tool execution.
+
+```js
+hooks: {
+ onPreToolUse: async (input) => {
+ if (input.toolName === "bash") {
+ const cmd = String(input.toolArgs?.command || "");
+ if (/rm\\s+-rf/i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) {
+ return {
+ permissionDecision: "deny",
+ permissionDecisionReason: "Destructive commands are not allowed.",
+ };
+ }
+ }
+ // Allow everything else
+ return { permissionDecision: "allow" };
+ },
+}
+```
+
+### Modifying tool arguments before execution
+
+```js
+hooks: {
+ onPreToolUse: async (input) => {
+ if (input.toolName === "bash") {
+ const redirect = process.platform === "win32" ? "*>&1" : "2>&1";
+ return {
+ modifiedArgs: {
+ ...input.toolArgs,
+ command: `${input.toolArgs.command} ${redirect}`,
+ },
+ };
+ }
+ },
+}
+```
+
+### Reacting when the agent creates or edits a file
+
+Use `onPostToolUse` to run side effects after a tool completes.
+
+```js
+import { exec } from "node:child_process";
+
+hooks: {
+ onPostToolUse: async (input) => {
+ if (input.toolName === "create" || input.toolName === "edit") {
+ const filePath = input.toolArgs?.path;
+ if (filePath) {
+ // Open the file in VS Code
+ exec(`code "${filePath}"`, () => {});
+ }
+ }
+ },
+}
+```
+
+### Augmenting tool results with extra context
+
+```js
+hooks: {
+ onPostToolUse: async (input) => {
+ if (input.toolName === "bash" && input.toolResult?.resultType === "failure") {
+ return {
+ additionalContext: "The command failed. Try a different approach.",
+ };
+ }
+ },
+}
+```
+
+### Running a linter after every file edit
+
+```js
+import { exec } from "node:child_process";
+
+hooks: {
+ onPostToolUse: async (input) => {
+ if (input.toolName === "edit") {
+ const filePath = input.toolArgs?.path;
+ if (filePath?.endsWith(".ts")) {
+ const result = await new Promise((resolve) => {
+ exec(`npx eslint "${filePath}"`, (err, stdout) => {
+ resolve(err ? stdout : "No lint errors.");
+ });
+ });
+ return { additionalContext: `Lint result: ${result}` };
+ }
+ }
+ },
+}
+```
+
+### Handling errors with retry logic
+
+```js
+hooks: {
+ onErrorOccurred: async (input) => {
+ if (input.recoverable && input.errorContext === "model_call") {
+ return { errorHandling: "retry", retryCount: 2 };
+ }
+ return {
+ errorHandling: "abort",
+ userNotification: `An error occurred: ${input.error}`,
+ };
+ },
+}
+```
+
+### Session lifecycle hooks
+
+```js
+hooks: {
+ onSessionStart: async (input) => {
+ // input.source is "startup", "resume", or "new"
+ return { additionalContext: "Remember to write tests for all changes." };
+ },
+ onSessionEnd: async (input) => {
+ // input.reason is "complete", "error", "abort", "timeout", or "user_exit"
+ },
+}
+```
+
+---
+
+## Session Events
+
+After calling `joinSession`, use `session.on()` to react to events in real time.
+
+### Listening to a specific event type
+
+```js
+session.on("assistant.message", (event) => {
+ // event.data.content has the agent's response text
+});
+```
+
+### Listening to all events
+
+```js
+session.on((event) => {
+ // event.type and event.data are available for all events
+});
+```
+
+### Unsubscribing from events
+
+`session.on()` returns an unsubscribe function:
+
+```js
+const unsubscribe = session.on("tool.execution_complete", (event) => {
+ // event.data.toolName, event.data.success, event.data.result, event.data.error
+});
+
+// Later, stop listening
+unsubscribe();
+```
+
+### Example: Auto-copy agent responses to clipboard
+
+Combine a hook (to detect a keyword) with a session event (to capture the response):
+
+```js
+import { execFile } from "node:child_process";
+
+let copyNextResponse = false;
+
+function copyToClipboard(text) {
+ const cmd = process.platform === "win32" ? "clip" : "pbcopy";
+ const proc = execFile(cmd, [], () => {});
+ proc.stdin.write(text);
+ proc.stdin.end();
+}
+
+const session = await joinSession({
+ hooks: {
+ onUserPromptSubmitted: async (input) => {
+ if (/\\bcopy\\b/i.test(input.prompt)) {
+ copyNextResponse = true;
+ }
+ },
+ },
+ tools: [],
+});
+
+session.on("assistant.message", (event) => {
+ if (copyNextResponse) {
+ copyNextResponse = false;
+ copyToClipboard(event.data.content);
+ }
+});
+```
+
+### Top 10 Most Useful Event Types
+
+| Event Type | Description | Key Data Fields |
+|-----------|-------------|-----------------|
+| `assistant.message` | Agent's final response | `content`, `messageId`, `toolRequests` |
+| `assistant.streaming_delta` | Token-by-token streaming (ephemeral) | `totalResponseSizeBytes` |
+| `tool.execution_start` | A tool is about to run | `toolCallId`, `toolName`, `arguments` |
+| `tool.execution_complete` | A tool finished running | `toolCallId`, `toolName`, `success`, `result`, `error` |
+| `user.message` | User sent a message | `content`, `attachments`, `source` |
+| `session.idle` | Session finished processing a turn | `backgroundTasks` |
+| `session.error` | An error occurred | `errorType`, `message`, `stack` |
+| `permission.requested` | Agent needs permission (shell, file write, etc.) | `requestId`, `permissionRequest.kind` |
+| `session.shutdown` | Session is ending | `shutdownType`, `totalPremiumRequests`, `codeChanges` |
+| `assistant.turn_start` | Agent begins a new thinking/response cycle | `turnId` |
+
+### Example: Detecting when the plan file is created or edited
+
+Use `session.workspacePath` to locate the session's `plan.md`, then `fs.watchFile` to detect changes.
+Correlate `tool.execution_start` / `tool.execution_complete` events by `toolCallId` to distinguish agent edits from user edits.
+
+```js
+import { existsSync, watchFile, readFileSync } from "node:fs";
+import { join } from "node:path";
+import { joinSession } from "@github/copilot-sdk/extension";
+
+const agentEdits = new Set(); // toolCallIds for in-flight agent edits
+const recentAgentPaths = new Set(); // paths recently written by the agent
+
+const session = await joinSession();
+
+const workspace = session.workspacePath; // e.g. ~/.copilot/session-state/
+if (workspace) {
+ const planPath = join(workspace, "plan.md");
+ let lastContent = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null;
+
+ // Track agent edits to suppress false triggers
+ session.on("tool.execution_start", (event) => {
+ if ((event.data.toolName === "edit" || event.data.toolName === "create")
+ && String(event.data.arguments?.path || "").endsWith("plan.md")) {
+ agentEdits.add(event.data.toolCallId);
+ recentAgentPaths.add(planPath);
+ }
+ });
+ session.on("tool.execution_complete", (event) => {
+ if (agentEdits.delete(event.data.toolCallId)) {
+ setTimeout(() => {
+ recentAgentPaths.delete(planPath);
+ lastContent = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null;
+ }, 2000);
+ }
+ });
+
+ watchFile(planPath, { interval: 1000 }, () => {
+ if (recentAgentPaths.has(planPath) || agentEdits.size > 0) return;
+ const content = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null;
+ if (content === lastContent) return;
+ const wasCreated = lastContent === null && content !== null;
+ lastContent = content;
+ if (content !== null) {
+ session.send({
+ prompt: `The plan was ${wasCreated ? "created" : "edited"} by the user.`,
+ });
+ }
+ });
+}
+```
+
+### Example: Reacting when the user manually edits any file in the repo
+
+Use `fs.watch` with `recursive: true` on `process.cwd()` to detect file changes.
+Filter out agent edits by tracking `tool.execution_start` / `tool.execution_complete` events.
+
+```js
+import { watch, readFileSync, statSync } from "node:fs";
+import { join, relative, resolve } from "node:path";
+import { joinSession } from "@github/copilot-sdk/extension";
+
+const agentEditPaths = new Set();
+
+const session = await joinSession();
+
+const cwd = process.cwd();
+const IGNORE = new Set(["node_modules", ".git", "dist"]);
+
+// Track agent file edits
+session.on("tool.execution_start", (event) => {
+ if (event.data.toolName === "edit" || event.data.toolName === "create") {
+ const p = String(event.data.arguments?.path || "");
+ if (p) agentEditPaths.add(resolve(p));
+ }
+});
+session.on("tool.execution_complete", (event) => {
+ // Clear after a delay to avoid race with fs.watch
+ const p = [...agentEditPaths].find((x) => x); // any tracked path
+ setTimeout(() => agentEditPaths.clear(), 3000);
+});
+
+const debounce = new Map();
+
+watch(cwd, { recursive: true }, (eventType, filename) => {
+ if (!filename || eventType !== "change") return;
+ if (filename.split(/[\\\\\\/]/).some((p) => IGNORE.has(p))) return;
+
+ if (debounce.has(filename)) clearTimeout(debounce.get(filename));
+ debounce.set(filename, setTimeout(() => {
+ debounce.delete(filename);
+ const fullPath = join(cwd, filename);
+ if (agentEditPaths.has(resolve(fullPath))) return;
+
+ try { if (!statSync(fullPath).isFile()) return; } catch { return; }
+ const relPath = relative(cwd, fullPath);
+ session.send({
+ prompt: `The user edited \\`${relPath}\\`.`,
+ attachments: [{ type: "file", path: fullPath }],
+ });
+ }, 500));
+});
+```
+
+---
+
+## Sending Messages Programmatically
+
+### Fire-and-forget
+
+```js
+await session.send({ prompt: "Analyze the test results." });
+```
+
+### Send and wait for the response
+
+```js
+const response = await session.sendAndWait({ prompt: "What is 2 + 2?" });
+// response?.data.content contains the agent's reply
+```
+
+### Send with file attachments
+
+```js
+await session.send({
+ prompt: "Review this file",
+ attachments: [
+ { type: "file", path: "./src/index.ts" },
+ ],
+});
+```
+
+---
+
+## Permission and User Input Handlers
+
+### Custom permission logic
+
+```js
+const session = await joinSession({
+ onPermissionRequest: async (request) => {
+ if (request.kind === "shell") {
+ // request.fullCommandText has the shell command
+ return { kind: "approved" };
+ }
+ if (request.kind === "write") {
+ return { kind: "approved" };
+ }
+ return { kind: "denied-by-rules" };
+ },
+});
+```
+
+### Handling agent questions (ask_user)
+
+Register `onUserInputRequest` to enable the agent's `ask_user` tool:
+
+```js
+const session = await joinSession({
+ onUserInputRequest: async (request) => {
+ // request.question has the agent's question
+ // request.choices has the options (if multiple choice)
+ return { answer: "yes", wasFreeform: false };
+ },
+});
+```
+
+---
+
+## Complete Example: Multi-Feature Extension
+
+An extension that combines tools, hooks, and events.
+
+```js
+import { execFile, exec } from "node:child_process";
+import { joinSession } from "@github/copilot-sdk/extension";
+
+const isWindows = process.platform === "win32";
+let copyNextResponse = false;
+
+function copyToClipboard(text) {
+ const proc = execFile(isWindows ? "clip" : "pbcopy", [], () => {});
+ proc.stdin.write(text);
+ proc.stdin.end();
+}
+
+function openInEditor(filePath) {
+ if (isWindows) exec(`code "${filePath}"`, () => {});
+ else execFile("code", [filePath], () => {});
+}
+
+const session = await joinSession({
+ hooks: {
+ onUserPromptSubmitted: async (input) => {
+ if (/\\bcopy this\\b/i.test(input.prompt)) {
+ copyNextResponse = true;
+ }
+ return {
+ additionalContext: "Follow our team style guide. Use 4-space indentation.",
+ };
+ },
+ onPreToolUse: async (input) => {
+ if (input.toolName === "bash") {
+ const cmd = String(input.toolArgs?.command || "");
+ if (/rm\\s+-rf\\s+\\//i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) {
+ return { permissionDecision: "deny" };
+ }
+ }
+ },
+ onPostToolUse: async (input) => {
+ if (input.toolName === "create" || input.toolName === "edit") {
+ const filePath = input.toolArgs?.path;
+ if (filePath) openInEditor(filePath);
+ }
+ },
+ },
+ tools: [
+ {
+ name: "copy_to_clipboard",
+ description: "Copies text to the system clipboard.",
+ parameters: {
+ type: "object",
+ properties: {
+ text: { type: "string", description: "Text to copy" },
+ },
+ required: ["text"],
+ },
+ handler: async (args) => {
+ return new Promise((resolve) => {
+ const proc = execFile(isWindows ? "clip" : "pbcopy", [], (err) => {
+ if (err) resolve(`Error: ${err.message}`);
+ else resolve("Copied to clipboard.");
+ });
+ proc.stdin.write(args.text);
+ proc.stdin.end();
+ });
+ },
+ },
+ ],
+});
+
+session.on("assistant.message", (event) => {
+ if (copyNextResponse) {
+ copyNextResponse = false;
+ copyToClipboard(event.data.content);
+ }
+});
+
+session.on("tool.execution_complete", (event) => {
+ // event.data.success, event.data.toolName, event.data.result
+});
+```
+
diff --git a/nodejs/docs/extensions.md b/nodejs/docs/extensions.md
new file mode 100644
index 0000000000..8b36de8a50
--- /dev/null
+++ b/nodejs/docs/extensions.md
@@ -0,0 +1,59 @@
+# Copilot CLI Extensions
+
+Extensions add custom tools, hooks, and behaviors to the Copilot CLI. They run as separate Node.js processes that communicate with the CLI over JSON-RPC via stdio.
+
+## How Extensions Work
+
+```
+┌─────────────────────┐ JSON-RPC / stdio ┌──────────────────────┐
+│ Copilot CLI │ ◄──────────────────────────────────► │ Extension Process │
+│ (parent process) │ tool calls, events, hooks │ (forked child) │
+│ │ │ │
+│ • Discovers exts │ │ • Registers tools │
+│ • Forks processes │ │ • Registers hooks │
+│ • Routes tool calls │ │ • Listens to events │
+│ • Manages lifecycle │ │ • Uses SDK APIs │
+└─────────────────────┘ └──────────────────────┘
+```
+
+1. **Discovery**: The CLI scans `.github/extensions/` (project) and the user's copilot config extensions directory for subdirectories containing `extension.mjs`.
+2. **Launch**: Each extension is forked as a child process with `@github/copilot-sdk` available via an automatic module resolver.
+3. **Connection**: The extension calls `joinSession()` which establishes a JSON-RPC connection over stdio to the CLI and attaches to the user's current foreground session.
+4. **Registration**: Tools and hooks declared in the session options are registered with the CLI and become available to the agent.
+5. **Lifecycle**: Extensions are reloaded on `/clear` (or if the foreground session is replaced) and stopped on CLI exit (SIGTERM, then SIGKILL after 5s).
+
+## File Structure
+
+```
+.github/extensions/
+ my-extension/
+ extension.mjs ← Entry point (required, must be .mjs)
+```
+
+- Only `.mjs` files are supported (ES modules). The file must be named `extension.mjs`.
+- Each extension lives in its own subdirectory.
+- The `@github/copilot-sdk` import is resolved automatically — you don't install it.
+
+## The SDK
+
+Extensions use `@github/copilot-sdk` for all interactions with the CLI:
+
+```js
+import { joinSession } from "@github/copilot-sdk/extension";
+
+const session = await joinSession({
+ tools: [
+ /* ... */
+ ],
+ hooks: {
+ /* ... */
+ },
+});
+```
+
+The `session` object provides methods for sending messages, logging to the timeline, listening to events, and accessing the RPC API. See the `.d.ts` files in the SDK package for full type information.
+
+## Further Reading
+
+- `examples.md` — Practical code examples for tools, hooks, events, and complete extensions
+- `agent-author.md` — Step-by-step workflow for agents authoring extensions programmatically
diff --git a/nodejs/esbuild-copilotsdk-nodejs.ts b/nodejs/esbuild-copilotsdk-nodejs.ts
index 059b8cfa60..f65a47236f 100644
--- a/nodejs/esbuild-copilotsdk-nodejs.ts
+++ b/nodejs/esbuild-copilotsdk-nodejs.ts
@@ -4,6 +4,7 @@ import { execSync } from "child_process";
const entryPoints = globSync("src/**/*.ts");
+// ESM build
await esbuild.build({
entryPoints,
outbase: "src",
@@ -15,5 +16,22 @@ await esbuild.build({
outExtension: { ".js": ".js" },
});
+// CJS build — uses .js extension with a "type":"commonjs" package.json marker
+await esbuild.build({
+ entryPoints,
+ outbase: "src",
+ outdir: "dist/cjs",
+ format: "cjs",
+ platform: "node",
+ target: "es2022",
+ sourcemap: false,
+ outExtension: { ".js": ".js" },
+ logOverride: { "empty-import-meta": "silent" },
+});
+
+// Mark the CJS directory so Node treats .js files as CommonJS
+import { writeFileSync } from "fs";
+writeFileSync("dist/cjs/package.json", JSON.stringify({ type: "commonjs" }) + "\n");
+
// Generate .d.ts files
execSync("tsc", { stdio: "inherit" });
diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json
index 78aacd1c0b..52a84fc9d2 100644
--- a/nodejs/package-lock.json
+++ b/nodejs/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.1.8",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^1.0.2",
+ "@github/copilot": "^1.0.10",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
@@ -662,26 +662,26 @@
}
},
"node_modules/@github/copilot": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.2.tgz",
- "integrity": "sha512-716SIZMYftldVcJay2uZOzsa9ROGGb2Mh2HnxbDxoisFsWNNgZlQXlV7A+PYoGsnAo2Zk/8e1i5SPTscGf2oww==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10.tgz",
+ "integrity": "sha512-RpHYMXYpyAgQLYQ3MB8ubV8zMn/zDatwaNmdxcC8ws7jqM+Ojy7Dz4KFKzyT0rCrWoUCAEBXsXoPbP0LY0FgLw==",
"license": "SEE LICENSE IN LICENSE.md",
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
- "@github/copilot-darwin-arm64": "1.0.2",
- "@github/copilot-darwin-x64": "1.0.2",
- "@github/copilot-linux-arm64": "1.0.2",
- "@github/copilot-linux-x64": "1.0.2",
- "@github/copilot-win32-arm64": "1.0.2",
- "@github/copilot-win32-x64": "1.0.2"
+ "@github/copilot-darwin-arm64": "1.0.10",
+ "@github/copilot-darwin-x64": "1.0.10",
+ "@github/copilot-linux-arm64": "1.0.10",
+ "@github/copilot-linux-x64": "1.0.10",
+ "@github/copilot-win32-arm64": "1.0.10",
+ "@github/copilot-win32-x64": "1.0.10"
}
},
"node_modules/@github/copilot-darwin-arm64": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.2.tgz",
- "integrity": "sha512-dYoeaTidsphRXyMjvAgpjEbBV41ipICnXURrLFEiATcjC4IY6x2BqPOocrExBYW/Tz2VZvDw51iIZaf6GXrTmw==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10.tgz",
+ "integrity": "sha512-MNlzwkTQ9iUgHQ+2Z25D0KgYZDEl4riEa1Z4/UCNpHXmmBiIY8xVRbXZTNMB69cnagjQ5Z8D2QM2BjI0kqeFPg==",
"cpu": [
"arm64"
],
@@ -695,9 +695,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.2.tgz",
- "integrity": "sha512-8+Z9dYigEfXf0wHl9c2tgFn8Cr6v4RAY8xTgHMI9mZInjQyxVeBXCxbE2VgzUtDUD3a705Ka2d8ZOz05aYtGsg==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10.tgz",
+ "integrity": "sha512-zAQBCbEue/n4xHBzE9T03iuupVXvLtu24MDMeXXtIC0d4O+/WV6j1zVJrp9Snwr0MBWYH+wUrV74peDDdd1VOQ==",
"cpu": [
"x64"
],
@@ -711,9 +711,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.2.tgz",
- "integrity": "sha512-ik0Y5aTXOFRPLFrNjZJdtfzkozYqYeJjVXGBAH3Pp1nFZRu/pxJnrnQ1HrqO/LEgQVbJzAjQmWEfMbXdQIxE4Q==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10.tgz",
+ "integrity": "sha512-7mJ3uLe7ITyRi2feM1rMLQ5d0bmUGTUwV1ZxKZwSzWCYmuMn05pg4fhIUdxZZZMkLbOl3kG/1J7BxMCTdS2w7A==",
"cpu": [
"arm64"
],
@@ -727,9 +727,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.2.tgz",
- "integrity": "sha512-mHSPZjH4nU9rwbfwLxYJ7CQ90jK/Qu1v2CmvBCUPfmuGdVwrpGPHB5FrB+f+b0NEXjmemDWstk2zG53F7ppHfw==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10.tgz",
+ "integrity": "sha512-66NPaxroRScNCs6TZGX3h1RSKtzew0tcHBkj4J1AHkgYLjNHMdjjBwokGtKeMxzYOCAMBbmJkUDdNGkqsKIKUA==",
"cpu": [
"x64"
],
@@ -743,9 +743,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.2.tgz",
- "integrity": "sha512-tLW2CY/vg0fYLp8EuiFhWIHBVzbFCDDpohxT/F/XyMAdTVSZLnopCcxQHv2BOu0CVGrYjlf7YOIwPfAKYml1FA==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10.tgz",
+ "integrity": "sha512-WC5M+M75sxLn4lvZ1wPA1Lrs/vXFisPXJPCKbKOMKqzwMLX/IbuybTV4dZDIyGEN591YmOdRIylUF0tVwO8Zmw==",
"cpu": [
"arm64"
],
@@ -759,9 +759,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.2.tgz",
- "integrity": "sha512-cFlc3xMkKKFRIYR00EEJ2XlYAemeh5EZHsGA8Ir2G0AH+DOevJbomdP1yyCC5gaK/7IyPkHX3sGie5sER2yPvQ==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10.tgz",
+ "integrity": "sha512-tUfIwyamd0zpm9DVTtbjIWF6j3zrA5A5IkkiuRgsy0HRJPQpeAV7ZYaHEZteHrynaULpl1Gn/Dq0IB4hYc4QtQ==",
"cpu": [
"x64"
],
diff --git a/nodejs/package.json b/nodejs/package.json
index ccd63582a4..7bde33b80d 100644
--- a/nodejs/package.json
+++ b/nodejs/package.json
@@ -6,16 +6,28 @@
},
"version": "0.1.8",
"description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC",
- "main": "./dist/index.js",
+ "main": "./dist/cjs/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
- "import": "./dist/index.js",
- "types": "./dist/index.d.ts"
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "require": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/cjs/index.js"
+ }
},
"./extension": {
- "import": "./dist/extension.js",
- "types": "./dist/extension.d.ts"
+ "import": {
+ "types": "./dist/extension.d.ts",
+ "default": "./dist/extension.js"
+ },
+ "require": {
+ "types": "./dist/extension.d.ts",
+ "default": "./dist/cjs/extension.js"
+ }
}
},
"type": "module",
@@ -44,7 +56,7 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^1.0.2",
+ "@github/copilot": "^1.0.10",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
@@ -70,6 +82,7 @@
},
"files": [
"dist/**/*",
+ "docs/**/*",
"README.md"
]
}
diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json
index 36f8b70390..a05be5360a 100644
--- a/nodejs/samples/package-lock.json
+++ b/nodejs/samples/package-lock.json
@@ -18,7 +18,7 @@
"version": "0.1.8",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^0.0.421",
+ "@github/copilot": "^1.0.10",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
diff --git a/nodejs/scripts/update-protocol-version.ts b/nodejs/scripts/update-protocol-version.ts
index 46f6189e86..a18a560c75 100644
--- a/nodejs/scripts/update-protocol-version.ts
+++ b/nodejs/scripts/update-protocol-version.ts
@@ -8,7 +8,7 @@
* Reads from sdk-protocol-version.json and generates:
* - nodejs/src/sdkProtocolVersion.ts
* - go/sdk_protocol_version.go
- * - python/copilot/sdk_protocol_version.py
+ * - python/copilot/_sdk_protocol_version.py
* - dotnet/src/SdkProtocolVersion.cs
*
* Run this script whenever the protocol version changes.
@@ -89,8 +89,8 @@ def get_sdk_protocol_version() -> int:
"""
return SDK_PROTOCOL_VERSION
`;
-fs.writeFileSync(path.join(rootDir, "python", "copilot", "sdk_protocol_version.py"), pythonCode);
-console.log(" ✓ python/copilot/sdk_protocol_version.py");
+fs.writeFileSync(path.join(rootDir, "python", "copilot", "_sdk_protocol_version.py"), pythonCode);
+console.log(" ✓ python/copilot/_sdk_protocol_version.py");
// Generate C#
const csharpCode = `// Code generated by update-protocol-version.ts. DO NOT EDIT.
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index de5f1856eb..9b8af3dd1c 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -12,7 +12,9 @@
*/
import { spawn, type ChildProcess } from "node:child_process";
+import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
+import { createRequire } from "node:module";
import { Socket } from "node:net";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -24,7 +26,8 @@ import {
} from "vscode-jsonrpc/node.js";
import { createServerRpc } from "./generated/rpc.js";
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
-import { CopilotSession } from "./session.js";
+import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
+import { getTraceContext } from "./telemetry.js";
import type {
ConnectionState,
CopilotClientOptions,
@@ -33,6 +36,7 @@ import type {
GetStatusResponse,
ModelInfo,
ResumeSessionConfig,
+ SectionTransformFn,
SessionConfig,
SessionContext,
SessionEvent,
@@ -41,10 +45,13 @@ import type {
SessionLifecycleHandler,
SessionListFilter,
SessionMetadata,
+ SystemMessageCustomizeConfig,
+ TelemetryConfig,
Tool,
ToolCallRequestPayload,
ToolCallResponsePayload,
ToolResultObject,
+ TraceContextProvider,
TypedSessionLifecycleHandler,
} from "./types.js";
@@ -77,6 +84,86 @@ function toJsonSchema(parameters: Tool["parameters"]): Record |
return parameters;
}
+/**
+ * Extract transform callbacks from a system message config and prepare the wire payload.
+ * Function-valued actions are replaced with `{ action: "transform" }` for serialization,
+ * and the original callbacks are returned in a separate map.
+ */
+function extractTransformCallbacks(systemMessage: SessionConfig["systemMessage"]): {
+ wirePayload: SessionConfig["systemMessage"];
+ transformCallbacks: Map | undefined;
+} {
+ if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
+ return { wirePayload: systemMessage, transformCallbacks: undefined };
+ }
+
+ const transformCallbacks = new Map();
+ const wireSections: Record = {};
+
+ for (const [sectionId, override] of Object.entries(systemMessage.sections)) {
+ if (!override) continue;
+
+ if (typeof override.action === "function") {
+ transformCallbacks.set(sectionId, override.action);
+ wireSections[sectionId] = { action: "transform" };
+ } else {
+ wireSections[sectionId] = { action: override.action, content: override.content };
+ }
+ }
+
+ if (transformCallbacks.size === 0) {
+ return { wirePayload: systemMessage, transformCallbacks: undefined };
+ }
+
+ const wirePayload: SystemMessageCustomizeConfig = {
+ ...systemMessage,
+ sections: wireSections as SystemMessageCustomizeConfig["sections"],
+ };
+
+ return { wirePayload, transformCallbacks };
+}
+
+function getNodeExecPath(): string {
+ if (process.versions.bun) {
+ return "node";
+ }
+ return process.execPath;
+}
+
+/**
+ * Gets the path to the bundled CLI from the @github/copilot package.
+ * Uses index.js directly rather than npm-loader.js (which spawns the native binary).
+ *
+ * In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions
+ * bundled with esbuild format:"cjs"), import.meta is empty so we fall back to
+ * walking node_modules to find the package.
+ */
+function getBundledCliPath(): string {
+ if (typeof import.meta.resolve === "function") {
+ // ESM: resolve via import.meta.resolve
+ const sdkUrl = import.meta.resolve("@github/copilot/sdk");
+ const sdkPath = fileURLToPath(sdkUrl);
+ // sdkPath is like .../node_modules/@github/copilot/sdk/index.js
+ // Go up two levels to get the package root, then append index.js
+ return join(dirname(dirname(sdkPath)), "index.js");
+ }
+
+ // CJS fallback: the @github/copilot package has ESM-only exports so
+ // require.resolve cannot reach it. Walk the module search paths instead.
+ const req = createRequire(__filename);
+ const searchPaths = req.resolve.paths("@github/copilot") ?? [];
+ for (const base of searchPaths) {
+ const candidate = join(base, "@github", "copilot", "index.js");
+ if (existsSync(candidate)) {
+ return candidate;
+ }
+ }
+ throw new Error(
+ `Could not find @github/copilot package. Searched ${searchPaths.length} paths. ` +
+ `Ensure it is installed, or pass cliPath/cliUrl to CopilotClient.`
+ );
+}
+
/**
* Main client for interacting with the Copilot CLI.
*
@@ -110,27 +197,6 @@ function toJsonSchema(parameters: Tool["parameters"]): Record |
* await client.stop();
* ```
*/
-
-function getNodeExecPath(): string {
- if (process.versions.bun) {
- return "node";
- }
- return process.execPath;
-}
-
-/**
- * Gets the path to the bundled CLI from the @github/copilot package.
- * Uses index.js directly rather than npm-loader.js (which spawns the native binary).
- */
-function getBundledCliPath(): string {
- // Find the actual location of the @github/copilot package by resolving its sdk export
- const sdkUrl = import.meta.resolve("@github/copilot/sdk");
- const sdkPath = fileURLToPath(sdkUrl);
- // sdkPath is like .../node_modules/@github/copilot/sdk/index.js
- // Go up two levels to get the package root, then append index.js
- return join(dirname(dirname(sdkPath)), "index.js");
-}
-
export class CopilotClient {
private cliProcess: ChildProcess | null = null;
private connection: MessageConnection | null = null;
@@ -141,14 +207,27 @@ export class CopilotClient {
private sessions: Map = new Map();
private stderrBuffer: string = ""; // Captures CLI stderr for error messages
private options: Required<
- Omit
+ Omit<
+ CopilotClientOptions,
+ | "cliPath"
+ | "cliUrl"
+ | "githubToken"
+ | "useLoggedInUser"
+ | "onListModels"
+ | "telemetry"
+ | "onGetTraceContext"
+ >
> & {
+ cliPath?: string;
cliUrl?: string;
githubToken?: string;
useLoggedInUser?: boolean;
+ telemetry?: TelemetryConfig;
};
private isExternalServer: boolean = false;
private forceStopping: boolean = false;
+ private onListModels?: () => Promise | ModelInfo[];
+ private onGetTraceContext?: TraceContextProvider;
private modelsCache: ModelInfo[] | null = null;
private modelsCacheLock: Promise = Promise.resolve();
private sessionLifecycleHandlers: Set = new Set();
@@ -226,8 +305,11 @@ export class CopilotClient {
this.isExternalServer = true;
}
+ this.onListModels = options.onListModels;
+ this.onGetTraceContext = options.onGetTraceContext;
+
this.options = {
- cliPath: options.cliPath || getBundledCliPath(),
+ cliPath: options.cliUrl ? undefined : options.cliPath || getBundledCliPath(),
cliArgs: options.cliArgs ?? [],
cwd: options.cwd ?? process.cwd(),
port: options.port || 0,
@@ -236,11 +318,13 @@ export class CopilotClient {
cliUrl: options.cliUrl,
logLevel: options.logLevel || "debug",
autoStart: options.autoStart ?? true,
- autoRestart: options.autoRestart ?? true,
+ autoRestart: false,
+
env: options.env ?? process.env,
githubToken: options.githubToken,
// Default useLoggedInUser to false when githubToken is provided, otherwise true
useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true),
+ telemetry: options.telemetry,
};
}
@@ -544,40 +628,16 @@ export class CopilotClient {
}
}
- const response = await this.connection!.sendRequest("session.create", {
- model: config.model,
- sessionId: config.sessionId,
- clientName: config.clientName,
- reasoningEffort: config.reasoningEffort,
- tools: config.tools?.map((tool) => ({
- name: tool.name,
- description: tool.description,
- parameters: toJsonSchema(tool.parameters),
- overridesBuiltInTool: tool.overridesBuiltInTool,
- })),
- systemMessage: config.systemMessage,
- availableTools: config.availableTools,
- excludedTools: config.excludedTools,
- provider: config.provider,
- requestPermission: true,
- requestUserInput: !!config.onUserInputRequest,
- hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
- workingDirectory: config.workingDirectory,
- streaming: config.streaming,
- mcpServers: config.mcpServers,
- envValueMode: "direct",
- customAgents: config.customAgents,
- configDir: config.configDir,
- skillDirectories: config.skillDirectories,
- disabledSkills: config.disabledSkills,
- infiniteSessions: config.infiniteSessions,
- });
+ const sessionId = config.sessionId ?? randomUUID();
- const { sessionId, workspacePath } = response as {
- sessionId: string;
- workspacePath?: string;
- };
- const session = new CopilotSession(sessionId, this.connection!, workspacePath);
+ // Create and register the session before issuing the RPC so that
+ // events emitted by the CLI (e.g. session.start) are not dropped.
+ const session = new CopilotSession(
+ sessionId,
+ this.connection!,
+ undefined,
+ this.onGetTraceContext
+ );
session.registerTools(config.tools);
session.registerPermissionHandler(config.onPermissionRequest);
if (config.onUserInputRequest) {
@@ -586,8 +646,63 @@ export class CopilotClient {
if (config.hooks) {
session.registerHooks(config.hooks);
}
+
+ // Extract transform callbacks from system message config before serialization.
+ const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
+ config.systemMessage
+ );
+ if (transformCallbacks) {
+ session.registerTransformCallbacks(transformCallbacks);
+ }
+
+ if (config.onEvent) {
+ session.on(config.onEvent);
+ }
this.sessions.set(sessionId, session);
+ try {
+ const response = await this.connection!.sendRequest("session.create", {
+ ...(await getTraceContext(this.onGetTraceContext)),
+ model: config.model,
+ sessionId,
+ clientName: config.clientName,
+ reasoningEffort: config.reasoningEffort,
+ tools: config.tools?.map((tool) => ({
+ name: tool.name,
+ description: tool.description,
+ parameters: toJsonSchema(tool.parameters),
+ overridesBuiltInTool: tool.overridesBuiltInTool,
+ skipPermission: tool.skipPermission,
+ })),
+ systemMessage: wireSystemMessage,
+ availableTools: config.availableTools,
+ excludedTools: config.excludedTools,
+ provider: config.provider,
+ requestPermission: true,
+ requestUserInput: !!config.onUserInputRequest,
+ hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
+ workingDirectory: config.workingDirectory,
+ streaming: config.streaming,
+ mcpServers: config.mcpServers,
+ envValueMode: "direct",
+ customAgents: config.customAgents,
+ agent: config.agent,
+ configDir: config.configDir,
+ skillDirectories: config.skillDirectories,
+ disabledSkills: config.disabledSkills,
+ infiniteSessions: config.infiniteSessions,
+ });
+
+ const { workspacePath } = response as {
+ sessionId: string;
+ workspacePath?: string;
+ };
+ session["_workspacePath"] = workspacePath;
+ } catch (e) {
+ this.sessions.delete(sessionId);
+ throw e;
+ }
+
return session;
}
@@ -630,41 +745,14 @@ export class CopilotClient {
}
}
- const response = await this.connection!.sendRequest("session.resume", {
+ // Create and register the session before issuing the RPC so that
+ // events emitted by the CLI (e.g. session.start) are not dropped.
+ const session = new CopilotSession(
sessionId,
- clientName: config.clientName,
- model: config.model,
- reasoningEffort: config.reasoningEffort,
- systemMessage: config.systemMessage,
- availableTools: config.availableTools,
- excludedTools: config.excludedTools,
- tools: config.tools?.map((tool) => ({
- name: tool.name,
- description: tool.description,
- parameters: toJsonSchema(tool.parameters),
- overridesBuiltInTool: tool.overridesBuiltInTool,
- })),
- provider: config.provider,
- requestPermission: true,
- requestUserInput: !!config.onUserInputRequest,
- hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
- workingDirectory: config.workingDirectory,
- configDir: config.configDir,
- streaming: config.streaming,
- mcpServers: config.mcpServers,
- envValueMode: "direct",
- customAgents: config.customAgents,
- skillDirectories: config.skillDirectories,
- disabledSkills: config.disabledSkills,
- infiniteSessions: config.infiniteSessions,
- disableResume: config.disableResume,
- });
-
- const { sessionId: resumedSessionId, workspacePath } = response as {
- sessionId: string;
- workspacePath?: string;
- };
- const session = new CopilotSession(resumedSessionId, this.connection!, workspacePath);
+ this.connection!,
+ undefined,
+ this.onGetTraceContext
+ );
session.registerTools(config.tools);
session.registerPermissionHandler(config.onPermissionRequest);
if (config.onUserInputRequest) {
@@ -673,7 +761,63 @@ export class CopilotClient {
if (config.hooks) {
session.registerHooks(config.hooks);
}
- this.sessions.set(resumedSessionId, session);
+
+ // Extract transform callbacks from system message config before serialization.
+ const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
+ config.systemMessage
+ );
+ if (transformCallbacks) {
+ session.registerTransformCallbacks(transformCallbacks);
+ }
+
+ if (config.onEvent) {
+ session.on(config.onEvent);
+ }
+ this.sessions.set(sessionId, session);
+
+ try {
+ const response = await this.connection!.sendRequest("session.resume", {
+ ...(await getTraceContext(this.onGetTraceContext)),
+ sessionId,
+ clientName: config.clientName,
+ model: config.model,
+ reasoningEffort: config.reasoningEffort,
+ systemMessage: wireSystemMessage,
+ availableTools: config.availableTools,
+ excludedTools: config.excludedTools,
+ tools: config.tools?.map((tool) => ({
+ name: tool.name,
+ description: tool.description,
+ parameters: toJsonSchema(tool.parameters),
+ overridesBuiltInTool: tool.overridesBuiltInTool,
+ skipPermission: tool.skipPermission,
+ })),
+ provider: config.provider,
+ requestPermission: true,
+ requestUserInput: !!config.onUserInputRequest,
+ hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
+ workingDirectory: config.workingDirectory,
+ configDir: config.configDir,
+ streaming: config.streaming,
+ mcpServers: config.mcpServers,
+ envValueMode: "direct",
+ customAgents: config.customAgents,
+ agent: config.agent,
+ skillDirectories: config.skillDirectories,
+ disabledSkills: config.disabledSkills,
+ infiniteSessions: config.infiniteSessions,
+ disableResume: config.disableResume,
+ });
+
+ const { workspacePath } = response as {
+ sessionId: string;
+ workspacePath?: string;
+ };
+ session["_workspacePath"] = workspacePath;
+ } catch (e) {
+ this.sessions.delete(sessionId);
+ throw e;
+ }
return session;
}
@@ -749,16 +893,15 @@ export class CopilotClient {
/**
* List available models with their metadata.
*
+ * If an `onListModels` handler was provided in the client options,
+ * it is called instead of querying the CLI server.
+ *
* Results are cached after the first successful call to avoid rate limiting.
* The cache is cleared when the client disconnects.
*
- * @throws Error if not authenticated
+ * @throws Error if not connected (when no custom handler is set)
*/
async listModels(): Promise {
- if (!this.connection) {
- throw new Error("Client not connected");
- }
-
// Use promise-based locking to prevent race condition with concurrent calls
await this.modelsCacheLock;
@@ -773,13 +916,22 @@ export class CopilotClient {
return [...this.modelsCache]; // Return a copy to prevent cache mutation
}
- // Cache miss - fetch from backend while holding lock
- const result = await this.connection.sendRequest("models.list", {});
- const response = result as { models: ModelInfo[] };
- const models = response.models;
+ let models: ModelInfo[];
+ if (this.onListModels) {
+ // Use custom handler instead of CLI RPC
+ models = await this.onListModels();
+ } else {
+ if (!this.connection) {
+ throw new Error("Client not connected");
+ }
+ // Cache miss - fetch from backend while holding lock
+ const result = await this.connection.sendRequest("models.list", {});
+ const response = result as { models: ModelInfo[] };
+ models = response.models;
+ }
- // Update cache before releasing lock
- this.modelsCache = models;
+ // Update cache before releasing lock (copy to prevent external mutation)
+ this.modelsCache = [...models];
return [...models]; // Return a copy to prevent cache mutation
} finally {
@@ -1096,6 +1248,30 @@ export class CopilotClient {
envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.githubToken;
}
+ if (!this.options.cliPath) {
+ throw new Error(
+ "Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI."
+ );
+ }
+
+ // Set OpenTelemetry environment variables if telemetry is configured
+ if (this.options.telemetry) {
+ const t = this.options.telemetry;
+ envWithoutNodeDebug.COPILOT_OTEL_ENABLED = "true";
+ if (t.otlpEndpoint !== undefined)
+ envWithoutNodeDebug.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint;
+ if (t.filePath !== undefined)
+ envWithoutNodeDebug.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath;
+ if (t.exporterType !== undefined)
+ envWithoutNodeDebug.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType;
+ if (t.sourceName !== undefined)
+ envWithoutNodeDebug.COPILOT_OTEL_SOURCE_NAME = t.sourceName;
+ if (t.captureContent !== undefined)
+ envWithoutNodeDebug.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String(
+ t.captureContent
+ );
+ }
+
// Verify CLI exists before attempting to spawn
if (!existsSync(this.options.cliPath)) {
throw new Error(
@@ -1210,8 +1386,6 @@ export class CopilotClient {
} else {
reject(new Error(`CLI server exited with code ${code}`));
}
- } else if (this.options.autoRestart && this.state === "connected") {
- void this.reconnect();
}
});
@@ -1362,14 +1536,21 @@ export class CopilotClient {
}): Promise<{ output?: unknown }> => await this.handleHooksInvoke(params)
);
+ this.connection.onRequest(
+ "systemMessage.transform",
+ async (params: {
+ sessionId: string;
+ sections: Record;
+ }): Promise<{ sections: Record }> =>
+ await this.handleSystemMessageTransform(params)
+ );
+
this.connection.onClose(() => {
- if (this.state === "connected" && this.options.autoRestart) {
- void this.reconnect();
- }
+ this.state = "disconnected";
});
this.connection.onError((_error) => {
- // Connection errors are handled via autoRestart if enabled
+ this.state = "disconnected";
});
}
@@ -1475,6 +1656,27 @@ export class CopilotClient {
return { output };
}
+ private async handleSystemMessageTransform(params: {
+ sessionId: string;
+ sections: Record;
+ }): Promise<{ sections: Record }> {
+ if (
+ !params ||
+ typeof params.sessionId !== "string" ||
+ !params.sections ||
+ typeof params.sections !== "object"
+ ) {
+ throw new Error("Invalid systemMessage.transform payload");
+ }
+
+ const session = this.sessions.get(params.sessionId);
+ if (!session) {
+ throw new Error(`Session not found: ${params.sessionId}`);
+ }
+
+ return await session._handleSystemMessageTransform(params.sections);
+ }
+
// ========================================================================
// Protocol v2 backward-compatibility adapters
// ========================================================================
@@ -1514,11 +1716,15 @@ export class CopilotClient {
}
try {
+ const traceparent = (params as { traceparent?: string }).traceparent;
+ const tracestate = (params as { tracestate?: string }).tracestate;
const invocation = {
sessionId: params.sessionId,
toolCallId: params.toolCallId,
toolName: params.toolName,
arguments: params.arguments,
+ traceparent,
+ tracestate,
};
const result = await handler(params.arguments, invocation);
return { result: this.normalizeToolResultV2(result) };
@@ -1555,7 +1761,10 @@ export class CopilotClient {
try {
const result = await session._handlePermissionRequestV2(params.permissionRequest);
return { result };
- } catch (_error) {
+ } catch (error) {
+ if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
+ throw error;
+ }
return {
result: {
kind: "denied-no-approval-rule-and-could-not-request-from-user",
@@ -1595,17 +1804,4 @@ export class CopilotClient {
"resultType" in value
);
}
-
- /**
- * Attempt to reconnect to the server
- */
- private async reconnect(): Promise {
- this.state = "disconnected";
- try {
- await this.stop();
- await this.start();
- } catch (_error) {
- // Reconnection failed
- }
- }
}
diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts
index b84fb2b6fb..b7c2da3a84 100644
--- a/nodejs/src/extension.ts
+++ b/nodejs/src/extension.ts
@@ -3,5 +3,42 @@
*--------------------------------------------------------------------------------------------*/
import { CopilotClient } from "./client.js";
+import type { CopilotSession } from "./session.js";
+import type { PermissionHandler, PermissionRequestResult, ResumeSessionConfig } from "./types.js";
-export const extension = new CopilotClient({ isChildProcess: true });
+const defaultJoinSessionPermissionHandler: PermissionHandler = (): PermissionRequestResult => ({
+ kind: "no-result",
+});
+
+export type JoinSessionConfig = Omit & {
+ onPermissionRequest?: PermissionHandler;
+};
+
+/**
+ * Joins the current foreground session.
+ *
+ * @param config - Configuration to add to the session
+ * @returns A promise that resolves with the joined session
+ *
+ * @example
+ * ```typescript
+ * import { joinSession } from "@github/copilot-sdk/extension";
+ *
+ * const session = await joinSession({ tools: [myTool] });
+ * ```
+ */
+export async function joinSession(config: JoinSessionConfig = {}): Promise {
+ const sessionId = process.env.SESSION_ID;
+ if (!sessionId) {
+ throw new Error(
+ "joinSession() is intended for extensions running as child processes of the Copilot CLI."
+ );
+ }
+
+ const client = new CopilotClient({ isChildProcess: true });
+ return client.resumeSession(sessionId, {
+ ...config,
+ onPermissionRequest: config.onPermissionRequest ?? defaultJoinSessionPermissionHandler,
+ disableResume: config.disableResume ?? true,
+ });
+}
diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts
index c230348e09..dadb9e79dc 100644
--- a/nodejs/src/generated/rpc.ts
+++ b/nodejs/src/generated/rpc.ts
@@ -44,16 +44,34 @@ export interface ModelsListResult {
* Model capabilities and limits
*/
capabilities: {
+ /**
+ * Feature flags indicating what the model supports
+ */
supports: {
+ /**
+ * Whether this model supports vision/image input
+ */
vision?: boolean;
/**
* Whether this model supports reasoning effort configuration
*/
reasoningEffort?: boolean;
};
+ /**
+ * Token limits for prompts, outputs, and context window
+ */
limits: {
+ /**
+ * Maximum number of prompt/input tokens
+ */
max_prompt_tokens?: number;
+ /**
+ * Maximum number of output/completion tokens
+ */
max_output_tokens?: number;
+ /**
+ * Maximum total context window size in tokens
+ */
max_context_window_tokens: number;
};
};
@@ -61,13 +79,22 @@ export interface ModelsListResult {
* Policy state (if applicable)
*/
policy?: {
+ /**
+ * Current policy state for this model
+ */
state: string;
+ /**
+ * Usage terms or conditions for this model
+ */
terms: string;
};
/**
* Billing information
*/
billing?: {
+ /**
+ * Billing cost multiplier relative to the base rate
+ */
multiplier: number;
};
/**
@@ -153,6 +180,9 @@ export interface AccountGetQuotaResult {
}
export interface SessionModelGetCurrentResult {
+ /**
+ * Currently active model identifier
+ */
modelId?: string;
}
@@ -164,6 +194,9 @@ export interface SessionModelGetCurrentParams {
}
export interface SessionModelSwitchToResult {
+ /**
+ * Currently active model identifier after the switch
+ */
modelId?: string;
}
@@ -172,7 +205,14 @@ export interface SessionModelSwitchToParams {
* Target session identifier
*/
sessionId: string;
+ /**
+ * Model identifier to switch to
+ */
modelId: string;
+ /**
+ * Reasoning effort level to use for the model
+ */
+ reasoningEffort?: string;
}
export interface SessionModeGetResult {
@@ -300,6 +340,7 @@ export interface SessionWorkspaceCreateFileParams {
content: string;
}
+/** @experimental */
export interface SessionFleetStartResult {
/**
* Whether fleet mode was successfully activated
@@ -307,6 +348,7 @@ export interface SessionFleetStartResult {
started: boolean;
}
+/** @experimental */
export interface SessionFleetStartParams {
/**
* Target session identifier
@@ -318,6 +360,7 @@ export interface SessionFleetStartParams {
prompt?: string;
}
+/** @experimental */
export interface SessionAgentListResult {
/**
* Available custom agents
@@ -338,6 +381,7 @@ export interface SessionAgentListResult {
}[];
}
+/** @experimental */
export interface SessionAgentListParams {
/**
* Target session identifier
@@ -345,6 +389,7 @@ export interface SessionAgentListParams {
sessionId: string;
}
+/** @experimental */
export interface SessionAgentGetCurrentResult {
/**
* Currently selected custom agent, or null if using the default agent
@@ -365,6 +410,7 @@ export interface SessionAgentGetCurrentResult {
} | null;
}
+/** @experimental */
export interface SessionAgentGetCurrentParams {
/**
* Target session identifier
@@ -372,6 +418,7 @@ export interface SessionAgentGetCurrentParams {
sessionId: string;
}
+/** @experimental */
export interface SessionAgentSelectResult {
/**
* The newly selected custom agent
@@ -392,6 +439,7 @@ export interface SessionAgentSelectResult {
};
}
+/** @experimental */
export interface SessionAgentSelectParams {
/**
* Target session identifier
@@ -403,8 +451,10 @@ export interface SessionAgentSelectParams {
name: string;
}
+/** @experimental */
export interface SessionAgentDeselectResult {}
+/** @experimental */
export interface SessionAgentDeselectParams {
/**
* Target session identifier
@@ -412,6 +462,303 @@ export interface SessionAgentDeselectParams {
sessionId: string;
}
+/** @experimental */
+export interface SessionAgentReloadResult {
+ /**
+ * Reloaded custom agents
+ */
+ agents: {
+ /**
+ * Unique identifier of the custom agent
+ */
+ name: string;
+ /**
+ * Human-readable display name
+ */
+ displayName: string;
+ /**
+ * Description of the agent's purpose
+ */
+ description: string;
+ }[];
+}
+
+/** @experimental */
+export interface SessionAgentReloadParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionSkillsListResult {
+ /**
+ * Available skills
+ */
+ skills: {
+ /**
+ * Unique identifier for the skill
+ */
+ name: string;
+ /**
+ * Description of what the skill does
+ */
+ description: string;
+ /**
+ * Source location type (e.g., project, personal, plugin)
+ */
+ source: string;
+ /**
+ * Whether the skill can be invoked by the user as a slash command
+ */
+ userInvocable: boolean;
+ /**
+ * Whether the skill is currently enabled
+ */
+ enabled: boolean;
+ /**
+ * Absolute path to the skill file
+ */
+ path?: string;
+ }[];
+}
+
+/** @experimental */
+export interface SessionSkillsListParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionSkillsEnableResult {}
+
+/** @experimental */
+export interface SessionSkillsEnableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the skill to enable
+ */
+ name: string;
+}
+
+/** @experimental */
+export interface SessionSkillsDisableResult {}
+
+/** @experimental */
+export interface SessionSkillsDisableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the skill to disable
+ */
+ name: string;
+}
+
+/** @experimental */
+export interface SessionSkillsReloadResult {}
+
+/** @experimental */
+export interface SessionSkillsReloadParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionMcpListResult {
+ /**
+ * Configured MCP servers
+ */
+ servers: {
+ /**
+ * Server name (config key)
+ */
+ name: string;
+ /**
+ * Connection status: connected, failed, pending, disabled, or not_configured
+ */
+ status: "connected" | "failed" | "pending" | "disabled" | "not_configured";
+ /**
+ * Configuration source: user, workspace, plugin, or builtin
+ */
+ source?: string;
+ /**
+ * Error message if the server failed to connect
+ */
+ error?: string;
+ }[];
+}
+
+/** @experimental */
+export interface SessionMcpListParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionMcpEnableResult {}
+
+/** @experimental */
+export interface SessionMcpEnableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the MCP server to enable
+ */
+ serverName: string;
+}
+
+/** @experimental */
+export interface SessionMcpDisableResult {}
+
+/** @experimental */
+export interface SessionMcpDisableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the MCP server to disable
+ */
+ serverName: string;
+}
+
+/** @experimental */
+export interface SessionMcpReloadResult {}
+
+/** @experimental */
+export interface SessionMcpReloadParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionPluginsListResult {
+ /**
+ * Installed plugins
+ */
+ plugins: {
+ /**
+ * Plugin name
+ */
+ name: string;
+ /**
+ * Marketplace the plugin came from
+ */
+ marketplace: string;
+ /**
+ * Installed version
+ */
+ version?: string;
+ /**
+ * Whether the plugin is currently enabled
+ */
+ enabled: boolean;
+ }[];
+}
+
+/** @experimental */
+export interface SessionPluginsListParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionExtensionsListResult {
+ /**
+ * Discovered extensions and their current status
+ */
+ extensions: {
+ /**
+ * Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper')
+ */
+ id: string;
+ /**
+ * Extension name (directory name)
+ */
+ name: string;
+ /**
+ * Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)
+ */
+ source: "project" | "user";
+ /**
+ * Current status: running, disabled, failed, or starting
+ */
+ status: "running" | "disabled" | "failed" | "starting";
+ /**
+ * Process ID if the extension is running
+ */
+ pid?: number;
+ }[];
+}
+
+/** @experimental */
+export interface SessionExtensionsListParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionExtensionsEnableResult {}
+
+/** @experimental */
+export interface SessionExtensionsEnableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Source-qualified extension ID to enable
+ */
+ id: string;
+}
+
+/** @experimental */
+export interface SessionExtensionsDisableResult {}
+
+/** @experimental */
+export interface SessionExtensionsDisableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Source-qualified extension ID to disable
+ */
+ id: string;
+}
+
+/** @experimental */
+export interface SessionExtensionsReloadResult {}
+
+/** @experimental */
+export interface SessionExtensionsReloadParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
export interface SessionCompactionCompactResult {
/**
* Whether compaction completed successfully
@@ -427,6 +774,7 @@ export interface SessionCompactionCompactResult {
messagesRemoved: number;
}
+/** @experimental */
export interface SessionCompactionCompactParams {
/**
* Target session identifier
@@ -435,6 +783,9 @@ export interface SessionCompactionCompactParams {
}
export interface SessionToolsHandlePendingToolCallResult {
+ /**
+ * Whether the tool call result was handled successfully
+ */
success: boolean;
}
@@ -457,7 +808,139 @@ export interface SessionToolsHandlePendingToolCallParams {
error?: string;
}
+export interface SessionCommandsHandlePendingCommandResult {
+ success: boolean;
+}
+
+export interface SessionCommandsHandlePendingCommandParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Request ID from the command invocation event
+ */
+ requestId: string;
+ /**
+ * Error message if the command handler failed
+ */
+ error?: string;
+}
+
+export interface SessionUiElicitationResult {
+ /**
+ * The user's response: accept (submitted), decline (rejected), or cancel (dismissed)
+ */
+ action: "accept" | "decline" | "cancel";
+ /**
+ * The form values submitted by the user (present when action is 'accept')
+ */
+ content?: {
+ [k: string]: string | number | boolean | string[];
+ };
+}
+
+export interface SessionUiElicitationParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Message describing what information is needed from the user
+ */
+ message: string;
+ /**
+ * JSON Schema describing the form fields to present to the user
+ */
+ requestedSchema: {
+ /**
+ * Schema type indicator (always 'object')
+ */
+ type: "object";
+ /**
+ * Form field definitions, keyed by field name
+ */
+ properties: {
+ [k: string]:
+ | {
+ type: "string";
+ title?: string;
+ description?: string;
+ enum: string[];
+ enumNames?: string[];
+ default?: string;
+ }
+ | {
+ type: "string";
+ title?: string;
+ description?: string;
+ oneOf: {
+ const: string;
+ title: string;
+ }[];
+ default?: string;
+ }
+ | {
+ type: "array";
+ title?: string;
+ description?: string;
+ minItems?: number;
+ maxItems?: number;
+ items: {
+ type: "string";
+ enum: string[];
+ };
+ default?: string[];
+ }
+ | {
+ type: "array";
+ title?: string;
+ description?: string;
+ minItems?: number;
+ maxItems?: number;
+ items: {
+ anyOf: {
+ const: string;
+ title: string;
+ }[];
+ };
+ default?: string[];
+ }
+ | {
+ type: "boolean";
+ title?: string;
+ description?: string;
+ default?: boolean;
+ }
+ | {
+ type: "string";
+ title?: string;
+ description?: string;
+ minLength?: number;
+ maxLength?: number;
+ format?: "email" | "uri" | "date" | "date-time";
+ default?: string;
+ }
+ | {
+ type: "number" | "integer";
+ title?: string;
+ description?: string;
+ minimum?: number;
+ maximum?: number;
+ default?: number;
+ };
+ };
+ /**
+ * List of required field names
+ */
+ required?: string[];
+ };
+}
+
export interface SessionPermissionsHandlePendingPermissionRequestResult {
+ /**
+ * Whether the permission request was handled successfully
+ */
success: boolean;
}
@@ -489,6 +972,84 @@ export interface SessionPermissionsHandlePendingPermissionRequestParams {
};
}
+export interface SessionLogResult {
+ /**
+ * The unique identifier of the emitted session event
+ */
+ eventId: string;
+}
+
+export interface SessionLogParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Human-readable message
+ */
+ message: string;
+ /**
+ * Log severity level. Determines how the message is displayed in the timeline. Defaults to "info".
+ */
+ level?: "info" | "warning" | "error";
+ /**
+ * When true, the message is transient and not persisted to the session event log on disk
+ */
+ ephemeral?: boolean;
+ /**
+ * Optional URL the user can open in their browser for more details
+ */
+ url?: string;
+}
+
+export interface SessionShellExecResult {
+ /**
+ * Unique identifier for tracking streamed output
+ */
+ processId: string;
+}
+
+export interface SessionShellExecParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Shell command to execute
+ */
+ command: string;
+ /**
+ * Working directory (defaults to session working directory)
+ */
+ cwd?: string;
+ /**
+ * Timeout in milliseconds (default: 30000)
+ */
+ timeout?: number;
+}
+
+export interface SessionShellKillResult {
+ /**
+ * Whether the signal was sent successfully
+ */
+ killed: boolean;
+}
+
+export interface SessionShellKillParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Process identifier returned by shell.exec
+ */
+ processId: string;
+ /**
+ * Signal to send (default: SIGTERM)
+ */
+ signal?: "SIGTERM" | "SIGKILL" | "SIGINT";
+}
+
/** Create typed server-scoped RPC methods (no session required). */
export function createServerRpc(connection: MessageConnection) {
return {
@@ -540,10 +1101,12 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
createFile: async (params: Omit): Promise =>
connection.sendRequest("session.workspace.createFile", { sessionId, ...params }),
},
+ /** @experimental */
fleet: {
start: async (params: Omit): Promise =>
connection.sendRequest("session.fleet.start", { sessionId, ...params }),
},
+ /** @experimental */
agent: {
list: async (): Promise =>
connection.sendRequest("session.agent.list", { sessionId }),
@@ -553,7 +1116,48 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
connection.sendRequest("session.agent.select", { sessionId, ...params }),
deselect: async (): Promise =>
connection.sendRequest("session.agent.deselect", { sessionId }),
+ reload: async (): Promise =>
+ connection.sendRequest("session.agent.reload", { sessionId }),
+ },
+ /** @experimental */
+ skills: {
+ list: async (): Promise =>
+ connection.sendRequest("session.skills.list", { sessionId }),
+ enable: async (params: Omit): Promise =>
+ connection.sendRequest("session.skills.enable", { sessionId, ...params }),
+ disable: async (params: Omit): Promise =>
+ connection.sendRequest("session.skills.disable", { sessionId, ...params }),
+ reload: async (): Promise =>
+ connection.sendRequest("session.skills.reload", { sessionId }),
+ },
+ /** @experimental */
+ mcp: {
+ list: async (): Promise =>
+ connection.sendRequest("session.mcp.list", { sessionId }),
+ enable: async (params: Omit): Promise =>
+ connection.sendRequest("session.mcp.enable", { sessionId, ...params }),
+ disable: async (params: Omit): Promise =>
+ connection.sendRequest("session.mcp.disable", { sessionId, ...params }),
+ reload: async (): Promise =>
+ connection.sendRequest("session.mcp.reload", { sessionId }),
+ },
+ /** @experimental */
+ plugins: {
+ list: async (): Promise =>
+ connection.sendRequest("session.plugins.list", { sessionId }),
},
+ /** @experimental */
+ extensions: {
+ list: async (): Promise =>
+ connection.sendRequest("session.extensions.list", { sessionId }),
+ enable: async (params: Omit): Promise =>
+ connection.sendRequest("session.extensions.enable", { sessionId, ...params }),
+ disable: async (params: Omit): Promise =>
+ connection.sendRequest("session.extensions.disable", { sessionId, ...params }),
+ reload: async (): Promise =>
+ connection.sendRequest("session.extensions.reload", { sessionId }),
+ },
+ /** @experimental */
compaction: {
compact: async (): Promise =>
connection.sendRequest("session.compaction.compact", { sessionId }),
@@ -562,9 +1166,25 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
handlePendingToolCall: async (params: Omit): Promise =>
connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params }),
},
+ commands: {
+ handlePendingCommand: async (params: Omit): Promise =>
+ connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params }),
+ },
+ ui: {
+ elicitation: async (params: Omit): Promise =>
+ connection.sendRequest("session.ui.elicitation", { sessionId, ...params }),
+ },
permissions: {
handlePendingPermissionRequest: async (params: Omit): Promise =>
connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }),
},
+ log: async (params: Omit): Promise =>
+ connection.sendRequest("session.log", { sessionId, ...params }),
+ shell: {
+ exec: async (params: Omit): Promise =>
+ connection.sendRequest("session.shell.exec", { sessionId, ...params }),
+ kill: async (params: Omit): Promise =>
+ connection.sendRequest("session.shell.kill", { sessionId, ...params }),
+ },
};
}
diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts
index cf87e10251..3453f01917 100644
--- a/nodejs/src/generated/session-events.ts
+++ b/nodejs/src/generated/session-events.ts
@@ -22,6 +22,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.start";
+ /**
+ * Session initialization metadata including context and configuration
+ */
data: {
/**
* Unique identifier for the session
@@ -47,6 +50,10 @@ export type SessionEvent =
* Model selected at session creation time, if any
*/
selectedModel?: string;
+ /**
+ * Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh")
+ */
+ reasoningEffort?: string;
/**
* Working directory and git context at session start
*/
@@ -60,14 +67,30 @@ export type SessionEvent =
*/
gitRoot?: string;
/**
- * Repository identifier in "owner/name" format, derived from the git remote URL
+ * Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps)
*/
repository?: string;
+ /**
+ * Hosting platform type of the repository (github or ado)
+ */
+ hostType?: "github" | "ado";
/**
* Current git branch name
*/
branch?: string;
+ /**
+ * Head commit of current git branch at session start time
+ */
+ headCommit?: string;
+ /**
+ * Base commit of current git branch at session start time
+ */
+ baseCommit?: string;
};
+ /**
+ * Whether the session was already in use by another client at start time
+ */
+ alreadyInUse?: boolean;
};
}
| {
@@ -88,6 +111,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.resume";
+ /**
+ * Session resume metadata including current context and event count
+ */
data: {
/**
* ISO 8601 timestamp when the session was resumed
@@ -97,6 +123,14 @@ export type SessionEvent =
* Total number of persisted events in the session at the time of resume
*/
eventCount: number;
+ /**
+ * Model currently selected at resume time
+ */
+ selectedModel?: string;
+ /**
+ * Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh")
+ */
+ reasoningEffort?: string;
/**
* Updated working directory and git context at resume time
*/
@@ -110,14 +144,30 @@ export type SessionEvent =
*/
gitRoot?: string;
/**
- * Repository identifier in "owner/name" format, derived from the git remote URL
+ * Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps)
*/
repository?: string;
+ /**
+ * Hosting platform type of the repository (github or ado)
+ */
+ hostType?: "github" | "ado";
/**
* Current git branch name
*/
branch?: string;
+ /**
+ * Head commit of current git branch at session start time
+ */
+ headCommit?: string;
+ /**
+ * Base commit of current git branch at session start time
+ */
+ baseCommit?: string;
};
+ /**
+ * Whether the session was already in use by another client at resume time
+ */
+ alreadyInUse?: boolean;
};
}
| {
@@ -138,6 +188,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.error";
+ /**
+ * Error details for timeline display including message and optional diagnostic information
+ */
data: {
/**
* Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "query")
@@ -159,6 +212,10 @@ export type SessionEvent =
* GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs
*/
providerCallId?: string;
+ /**
+ * Optional URL associated with this error that the user can open in a browser
+ */
+ url?: string;
};
}
| {
@@ -232,6 +289,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "session.title_changed";
+ /**
+ * Session title change payload containing the new display title
+ */
data: {
/**
* The new display title for the session
@@ -257,6 +317,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.info";
+ /**
+ * Informational message for timeline display with categorization
+ */
data: {
/**
* Category of informational message (e.g., "notification", "timing", "context_window", "mcp", "snapshot", "configuration", "authentication", "model")
@@ -266,6 +329,10 @@ export type SessionEvent =
* Human-readable informational message for display in the timeline
*/
message: string;
+ /**
+ * Optional URL associated with this message that the user can open in a browser
+ */
+ url?: string;
};
}
| {
@@ -286,6 +353,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.warning";
+ /**
+ * Warning message for timeline display with categorization
+ */
data: {
/**
* Category of warning (e.g., "subscription", "policy", "mcp")
@@ -295,6 +365,10 @@ export type SessionEvent =
* Human-readable warning message for display in the timeline
*/
message: string;
+ /**
+ * Optional URL associated with this warning that the user can open in a browser
+ */
+ url?: string;
};
}
| {
@@ -315,6 +389,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.model_change";
+ /**
+ * Model change details including previous and new model identifiers
+ */
data: {
/**
* Model that was previously selected, if any
@@ -324,6 +401,14 @@ export type SessionEvent =
* Newly selected model identifier
*/
newModel: string;
+ /**
+ * Reasoning effort level before the model change, if applicable
+ */
+ previousReasoningEffort?: string;
+ /**
+ * Reasoning effort level after the model change, if applicable
+ */
+ reasoningEffort?: string;
};
}
| {
@@ -344,6 +429,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.mode_changed";
+ /**
+ * Agent mode change details including previous and new modes
+ */
data: {
/**
* Agent mode before the change (e.g., "interactive", "plan", "autopilot")
@@ -373,6 +461,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.plan_changed";
+ /**
+ * Plan file operation details indicating what changed
+ */
data: {
/**
* The type of operation performed on the plan file
@@ -398,6 +489,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.workspace_file_changed";
+ /**
+ * Workspace file change details including path and operation type
+ */
data: {
/**
* Relative path within the session workspace files directory
@@ -427,6 +521,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.handoff";
+ /**
+ * Session handoff metadata including source, context, and repository information
+ */
data: {
/**
* ISO 8601 timestamp when the handoff occurred
@@ -465,6 +562,10 @@ export type SessionEvent =
* Session ID of the remote session being handed off
*/
remoteSessionId?: string;
+ /**
+ * GitHub host URL for the source session (e.g., https://github.com or https://tenant.ghe.com)
+ */
+ host?: string;
};
}
| {
@@ -485,6 +586,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.truncation";
+ /**
+ * Conversation truncation statistics including token counts and removed content metrics
+ */
data: {
/**
* Maximum token count for the model's context window
@@ -535,6 +639,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "session.snapshot_rewind";
+ /**
+ * Session rewind details including target event and count of removed events
+ */
data: {
/**
* Event ID that was rewound to; all events after this one were removed
@@ -564,6 +671,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.shutdown";
+ /**
+ * Session termination metrics including usage statistics, code changes, and shutdown reason
+ */
data: {
/**
* Whether the session ended normally ("routine") or due to a crash/fatal error ("error")
@@ -647,6 +757,22 @@ export type SessionEvent =
* Model that was selected at the time of shutdown
*/
currentModel?: string;
+ /**
+ * Total tokens in context window at shutdown
+ */
+ currentTokens?: number;
+ /**
+ * System message token count at shutdown
+ */
+ systemTokens?: number;
+ /**
+ * Non-system message token count at shutdown
+ */
+ conversationTokens?: number;
+ /**
+ * Tool definitions token count at shutdown
+ */
+ toolDefinitionsTokens?: number;
};
}
| {
@@ -667,6 +793,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.context_changed";
+ /**
+ * Updated working directory and git context after the change
+ */
data: {
/**
* Current working directory path
@@ -677,13 +806,25 @@ export type SessionEvent =
*/
gitRoot?: string;
/**
- * Repository identifier in "owner/name" format, derived from the git remote URL
+ * Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps)
*/
repository?: string;
+ /**
+ * Hosting platform type of the repository (github or ado)
+ */
+ hostType?: "github" | "ado";
/**
* Current git branch name
*/
branch?: string;
+ /**
+ * Head commit of current git branch at session start time
+ */
+ headCommit?: string;
+ /**
+ * Base commit of current git branch at session start time
+ */
+ baseCommit?: string;
};
}
| {
@@ -701,6 +842,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "session.usage_info";
+ /**
+ * Current context window usage statistics including token and message counts
+ */
data: {
/**
* Maximum token count for the model's context window
@@ -714,6 +858,22 @@ export type SessionEvent =
* Current number of messages in the conversation
*/
messagesLength: number;
+ /**
+ * Token count from system message(s)
+ */
+ systemTokens?: number;
+ /**
+ * Token count from non-system messages (user, assistant, tool)
+ */
+ conversationTokens?: number;
+ /**
+ * Token count from tool definitions
+ */
+ toolDefinitionsTokens?: number;
+ /**
+ * Whether this is the first usage_info event emitted in this session
+ */
+ isInitial?: boolean;
};
}
| {
@@ -735,9 +895,22 @@ export type SessionEvent =
ephemeral?: boolean;
type: "session.compaction_start";
/**
- * Empty payload; the event signals that LLM-powered conversation compaction has begun
+ * Context window breakdown at the start of LLM-powered conversation compaction
*/
- data: {};
+ data: {
+ /**
+ * Token count from system message(s) at compaction start
+ */
+ systemTokens?: number;
+ /**
+ * Token count from non-system messages (user, assistant, tool) at compaction start
+ */
+ conversationTokens?: number;
+ /**
+ * Token count from tool definitions at compaction start
+ */
+ toolDefinitionsTokens?: number;
+ };
}
| {
/**
@@ -757,6 +930,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.compaction_complete";
+ /**
+ * Conversation compaction results including success status, metrics, and optional error details
+ */
data: {
/**
* Whether compaction completed successfully
@@ -819,6 +995,18 @@ export type SessionEvent =
* GitHub request tracing ID (x-github-request-id header) for the compaction LLM call
*/
requestId?: string;
+ /**
+ * Token count from system message(s) after compaction
+ */
+ systemTokens?: number;
+ /**
+ * Token count from non-system messages (user, assistant, tool) after compaction
+ */
+ conversationTokens?: number;
+ /**
+ * Token count from tool definitions after compaction
+ */
+ toolDefinitionsTokens?: number;
};
}
| {
@@ -839,11 +1027,18 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "session.task_complete";
+ /**
+ * Task completion notification with summary from the agent
+ */
data: {
/**
- * Optional summary of the completed task, provided by the agent
+ * Summary of the completed task, provided by the agent
*/
summary?: string;
+ /**
+ * Whether the tool call succeeded. False when validation failed (e.g., invalid arguments)
+ */
+ success?: boolean;
};
}
| {
@@ -878,9 +1073,12 @@ export type SessionEvent =
*/
attachments?: (
| {
+ /**
+ * Attachment type discriminator
+ */
type: "file";
/**
- * Absolute file or directory path
+ * Absolute file path
*/
path: string;
/**
@@ -902,28 +1100,18 @@ export type SessionEvent =
};
}
| {
+ /**
+ * Attachment type discriminator
+ */
type: "directory";
/**
- * Absolute file or directory path
+ * Absolute directory path
*/
path: string;
/**
* User-facing display name for the attachment
*/
displayName: string;
- /**
- * Optional line range to scope the attachment to a specific section of the file
- */
- lineRange?: {
- /**
- * Start line number (1-based)
- */
- start: number;
- /**
- * End line number (1-based, inclusive)
- */
- end: number;
- };
}
| {
/**
@@ -946,6 +1134,9 @@ export type SessionEvent =
* Position range of the selection within the file
*/
selection: {
+ /**
+ * Start position of the selection
+ */
start: {
/**
* Start line number (0-based)
@@ -956,6 +1147,9 @@ export type SessionEvent =
*/
character: number;
};
+ /**
+ * End position of the selection
+ */
end: {
/**
* End line number (0-based)
@@ -994,6 +1188,24 @@ export type SessionEvent =
*/
url: string;
}
+ | {
+ /**
+ * Attachment type discriminator
+ */
+ type: "blob";
+ /**
+ * Base64-encoded content
+ */
+ data: string;
+ /**
+ * MIME type of the inline data
+ */
+ mimeType: string;
+ /**
+ * User-facing display name for the attachment
+ */
+ displayName?: string;
+ }
)[];
/**
* Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user)
@@ -1047,6 +1259,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "assistant.turn_start";
+ /**
+ * Turn initialization metadata including identifier and interaction tracking
+ */
data: {
/**
* Identifier for this turn within the agentic loop, typically a stringified turn number
@@ -1073,6 +1288,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "assistant.intent";
+ /**
+ * Agent intent description for current activity or plan
+ */
data: {
/**
* Short description of what the agent is currently doing or planning to do
@@ -1098,6 +1316,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "assistant.reasoning";
+ /**
+ * Assistant reasoning content for timeline display with complete thinking text
+ */
data: {
/**
* Unique identifier for this reasoning block
@@ -1124,6 +1345,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "assistant.reasoning_delta";
+ /**
+ * Streaming reasoning delta for incremental extended thinking updates
+ */
data: {
/**
* Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning event
@@ -1150,6 +1374,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "assistant.streaming_delta";
+ /**
+ * Streaming response progress with cumulative byte count
+ */
data: {
/**
* Cumulative total bytes received from the streaming response so far
@@ -1175,6 +1402,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "assistant.message";
+ /**
+ * Assistant response containing text content, optional tool requests, and interaction metadata
+ */
data: {
/**
* Unique identifier for this assistant message
@@ -1206,6 +1436,14 @@ export type SessionEvent =
* Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent.
*/
type?: "function" | "custom";
+ /**
+ * Human-readable display title for the tool
+ */
+ toolTitle?: string;
+ /**
+ * Resolved intention summary describing what this specific call does
+ */
+ intentionSummary?: string | null;
}[];
/**
* Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume.
@@ -1252,6 +1490,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "assistant.message_delta";
+ /**
+ * Streaming assistant message delta for incremental response updates
+ */
data: {
/**
* Message ID this delta belongs to, matching the corresponding assistant.message event
@@ -1285,6 +1526,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "assistant.turn_end";
+ /**
+ * Turn completion metadata including the turn identifier
+ */
data: {
/**
* Identifier of the turn that has ended, matching the corresponding assistant.turn_start event
@@ -1307,6 +1551,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "assistant.usage";
+ /**
+ * LLM API call usage metrics including tokens, costs, quotas, and billing information
+ */
data: {
/**
* Model identifier used for this API call
@@ -1421,6 +1668,10 @@ export type SessionEvent =
*/
totalNanoAiu: number;
};
+ /**
+ * Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh")
+ */
+ reasoningEffort?: string;
};
}
| {
@@ -1441,6 +1692,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "abort";
+ /**
+ * Turn abort information including the reason for termination
+ */
data: {
/**
* Reason the current turn was aborted (e.g., "user initiated")
@@ -1466,6 +1720,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "tool.user_requested";
+ /**
+ * User-initiated tool invocation request with tool name and arguments
+ */
data: {
/**
* Unique identifier for this tool call
@@ -1501,6 +1758,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "tool.execution_start";
+ /**
+ * Tool execution startup details including MCP server information when applicable
+ */
data: {
/**
* Unique identifier for this tool call
@@ -1545,6 +1805,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "tool.execution_partial_result";
+ /**
+ * Streaming tool execution output for incremental result display
+ */
data: {
/**
* Tool call ID this partial result belongs to
@@ -1571,6 +1834,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "tool.execution_progress";
+ /**
+ * Tool execution progress notification with status message
+ */
data: {
/**
* Tool call ID this progress notification belongs to
@@ -1600,6 +1866,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "tool.execution_complete";
+ /**
+ * Tool execution completion results including success status, detailed output, and error information
+ */
data: {
/**
* Unique identifier for the completed tool call
@@ -1827,6 +2096,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "skill.invoked";
+ /**
+ * Skill invocation details including content, allowed tools, and plugin metadata
+ */
data: {
/**
* Name of the invoked skill
@@ -1872,6 +2144,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "subagent.started";
+ /**
+ * Sub-agent startup details including parent tool call and agent information
+ */
data: {
/**
* Tool call ID of the parent tool invocation that spawned this sub-agent
@@ -1909,6 +2184,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "subagent.completed";
+ /**
+ * Sub-agent completion details for successful execution
+ */
data: {
/**
* Tool call ID of the parent tool invocation that spawned this sub-agent
@@ -1942,6 +2220,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "subagent.failed";
+ /**
+ * Sub-agent failure details including error message and agent information
+ */
data: {
/**
* Tool call ID of the parent tool invocation that spawned this sub-agent
@@ -1979,6 +2260,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "subagent.selected";
+ /**
+ * Custom agent selection details including name and available tools
+ */
data: {
/**
* Internal name of the selected custom agent
@@ -2035,6 +2319,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "hook.start";
+ /**
+ * Hook invocation start details including type and input data
+ */
data: {
/**
* Unique identifier for this hook invocation
@@ -2070,6 +2357,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "hook.end";
+ /**
+ * Hook invocation completion details including output, success status, and error information
+ */
data: {
/**
* Identifier matching the corresponding hook.start event
@@ -2122,6 +2412,9 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "system.message";
+ /**
+ * System or developer message content with role and optional template metadata
+ */
data: {
/**
* The system or developer prompt text
@@ -2165,84 +2458,183 @@ export type SessionEvent =
* ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
*/
parentId: string | null;
- ephemeral: true;
- type: "permission.requested";
+ /**
+ * When true, the event is transient and not persisted to the session event log on disk
+ */
+ ephemeral?: boolean;
+ type: "system.notification";
+ /**
+ * System-generated notification for runtime events like background task completion
+ */
data: {
/**
- * Unique identifier for this permission request; used to respond via session.respondToPermission()
+ * The notification text, typically wrapped in XML tags
*/
- requestId: string;
+ content: string;
/**
- * Details of the permission being requested
+ * Structured metadata identifying what triggered this notification
*/
- permissionRequest:
+ kind:
| {
+ type: "agent_completed";
/**
- * Permission kind discriminator
+ * Unique identifier of the background agent
*/
- kind: "shell";
+ agentId: string;
/**
- * Tool call ID that triggered this permission request
+ * Type of the agent (e.g., explore, task, general-purpose)
*/
- toolCallId?: string;
+ agentType: string;
/**
- * The complete shell command text to be executed
+ * Whether the agent completed successfully or failed
*/
- fullCommandText: string;
+ status: "completed" | "failed";
/**
- * Human-readable description of what the command intends to do
+ * Human-readable description of the agent task
*/
- intention: string;
+ description?: string;
/**
- * Parsed command identifiers found in the command text
+ * The full prompt given to the background agent
*/
- commands: {
- /**
- * Command identifier (e.g., executable name)
- */
- identifier: string;
- /**
- * Whether this command is read-only (no side effects)
- */
- readOnly: boolean;
- }[];
+ prompt?: string;
+ }
+ | {
+ type: "agent_idle";
/**
- * File paths that may be read or written by the command
+ * Unique identifier of the background agent
*/
- possiblePaths: string[];
+ agentId: string;
/**
- * URLs that may be accessed by the command
+ * Type of the agent (e.g., explore, task, general-purpose)
*/
- possibleUrls: {
- /**
- * URL that may be accessed by the command
- */
- url: string;
- }[];
+ agentType: string;
/**
- * Whether the command includes a file write redirection (e.g., > or >>)
+ * Human-readable description of the agent task
*/
- hasWriteFileRedirection: boolean;
+ description?: string;
+ }
+ | {
+ type: "shell_completed";
/**
- * Whether the UI can offer session-wide approval for this command pattern
+ * Unique identifier of the shell session
*/
- canOfferSessionApproval: boolean;
+ shellId: string;
/**
- * Optional warning message about risks of running this command
+ * Exit code of the shell command, if available
*/
- warning?: string;
+ exitCode?: number;
+ /**
+ * Human-readable description of the command
+ */
+ description?: string;
}
| {
+ type: "shell_detached_completed";
/**
- * Permission kind discriminator
+ * Unique identifier of the detached shell session
*/
- kind: "write";
+ shellId: string;
/**
- * Tool call ID that triggered this permission request
+ * Human-readable description of the command
*/
- toolCallId?: string;
- /**
- * Human-readable description of the intended file change
+ description?: string;
+ };
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "permission.requested";
+ /**
+ * Permission request notification requiring client approval with request details
+ */
+ data: {
+ /**
+ * Unique identifier for this permission request; used to respond via session.respondToPermission()
+ */
+ requestId: string;
+ /**
+ * Details of the permission being requested
+ */
+ permissionRequest:
+ | {
+ /**
+ * Permission kind discriminator
+ */
+ kind: "shell";
+ /**
+ * Tool call ID that triggered this permission request
+ */
+ toolCallId?: string;
+ /**
+ * The complete shell command text to be executed
+ */
+ fullCommandText: string;
+ /**
+ * Human-readable description of what the command intends to do
+ */
+ intention: string;
+ /**
+ * Parsed command identifiers found in the command text
+ */
+ commands: {
+ /**
+ * Command identifier (e.g., executable name)
+ */
+ identifier: string;
+ /**
+ * Whether this command is read-only (no side effects)
+ */
+ readOnly: boolean;
+ }[];
+ /**
+ * File paths that may be read or written by the command
+ */
+ possiblePaths: string[];
+ /**
+ * URLs that may be accessed by the command
+ */
+ possibleUrls: {
+ /**
+ * URL that may be accessed by the command
+ */
+ url: string;
+ }[];
+ /**
+ * Whether the command includes a file write redirection (e.g., > or >>)
+ */
+ hasWriteFileRedirection: boolean;
+ /**
+ * Whether the UI can offer session-wide approval for this command pattern
+ */
+ canOfferSessionApproval: boolean;
+ /**
+ * Optional warning message about risks of running this command
+ */
+ warning?: string;
+ }
+ | {
+ /**
+ * Permission kind discriminator
+ */
+ kind: "write";
+ /**
+ * Tool call ID that triggered this permission request
+ */
+ toolCallId?: string;
+ /**
+ * Human-readable description of the intended file change
*/
intention: string;
/**
@@ -2371,6 +2763,30 @@ export type SessionEvent =
args?: {
[k: string]: unknown;
};
+ }
+ | {
+ /**
+ * Permission kind discriminator
+ */
+ kind: "hook";
+ /**
+ * Tool call ID that triggered this permission request
+ */
+ toolCallId?: string;
+ /**
+ * Name of the tool the hook is gating
+ */
+ toolName: string;
+ /**
+ * Arguments of the tool call being gated
+ */
+ toolArgs?: {
+ [k: string]: unknown;
+ };
+ /**
+ * Optional message from the hook explaining why confirmation is needed
+ */
+ hookMessage?: string;
};
};
}
@@ -2389,6 +2805,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "permission.completed";
+ /**
+ * Permission request completion notification signaling UI dismissal
+ */
data: {
/**
* Request ID of the resolved permission request; clients should dismiss any UI for this request
@@ -2425,6 +2844,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "user_input.requested";
+ /**
+ * User input request notification with question and optional predefined choices
+ */
data: {
/**
* Unique identifier for this input request; used to respond via session.respondToUserInput()
@@ -2442,6 +2864,10 @@ export type SessionEvent =
* Whether the user can provide a free-form text response in addition to predefined choices
*/
allowFreeform?: boolean;
+ /**
+ * The LLM-assigned tool call ID that triggered this request; used by remote UIs to correlate responses
+ */
+ toolCallId?: string;
};
}
| {
@@ -2459,6 +2885,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "user_input.completed";
+ /**
+ * User input request completion notification signaling UI dismissal
+ */
data: {
/**
* Request ID of the resolved user input request; clients should dismiss any UI for this request
@@ -2481,23 +2910,37 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "elicitation.requested";
+ /**
+ * Elicitation request; may be form-based (structured input) or URL-based (browser redirect)
+ */
data: {
/**
* Unique identifier for this elicitation request; used to respond via session.respondToElicitation()
*/
requestId: string;
+ /**
+ * Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id for remote UIs
+ */
+ toolCallId?: string;
+ /**
+ * The source that initiated the request (MCP server name, or absent for agent-initiated)
+ */
+ elicitationSource?: string;
/**
* Message describing what information is needed from the user
*/
message: string;
/**
- * Elicitation mode; currently only "form" is supported. Defaults to "form" when absent.
+ * Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent.
*/
- mode?: "form";
+ mode?: "form" | "url";
/**
- * JSON Schema describing the form fields to present to the user
+ * JSON Schema describing the form fields to present to the user (form mode only)
*/
- requestedSchema: {
+ requestedSchema?: {
+ /**
+ * Schema type indicator (always 'object')
+ */
type: "object";
/**
* Form field definitions, keyed by field name
@@ -2510,6 +2953,10 @@ export type SessionEvent =
*/
required?: string[];
};
+ /**
+ * URL to open in the user's browser (url mode only)
+ */
+ url?: string;
[k: string]: unknown;
};
}
@@ -2528,6 +2975,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "elicitation.completed";
+ /**
+ * Elicitation request completion notification signaling UI dismissal
+ */
data: {
/**
* Request ID of the resolved elicitation request; clients should dismiss any UI for this request
@@ -2535,6 +2985,77 @@ export type SessionEvent =
requestId: string;
};
}
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "mcp.oauth_required";
+ /**
+ * OAuth authentication request for an MCP server
+ */
+ data: {
+ /**
+ * Unique identifier for this OAuth request; used to respond via session.respondToMcpOAuth()
+ */
+ requestId: string;
+ /**
+ * Display name of the MCP server that requires OAuth
+ */
+ serverName: string;
+ /**
+ * URL of the MCP server that requires OAuth
+ */
+ serverUrl: string;
+ /**
+ * Static OAuth client configuration, if the server specifies one
+ */
+ staticClientConfig?: {
+ /**
+ * OAuth client ID for the server
+ */
+ clientId: string;
+ /**
+ * Whether this is a public OAuth client
+ */
+ publicClient?: boolean;
+ };
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "mcp.oauth_completed";
+ /**
+ * MCP OAuth request completion notification
+ */
+ data: {
+ /**
+ * Request ID of the resolved OAuth request
+ */
+ requestId: string;
+ };
+ }
| {
/**
* Unique event identifier (UUID v4), generated when the event is emitted
@@ -2550,6 +3071,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "external_tool.requested";
+ /**
+ * External tool invocation request for client-side tool execution
+ */
data: {
/**
* Unique identifier for this request; used to respond via session.respondToExternalTool()
@@ -2573,6 +3097,14 @@ export type SessionEvent =
arguments?: {
[k: string]: unknown;
};
+ /**
+ * W3C Trace Context traceparent header for the execute_tool span
+ */
+ traceparent?: string;
+ /**
+ * W3C Trace Context tracestate header for the execute_tool span
+ */
+ tracestate?: string;
};
}
| {
@@ -2590,6 +3122,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "external_tool.completed";
+ /**
+ * External tool completion notification signaling UI dismissal
+ */
data: {
/**
* Request ID of the resolved external tool request; clients should dismiss any UI for this request
@@ -2612,6 +3147,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "command.queued";
+ /**
+ * Queued slash command dispatch request for client execution
+ */
data: {
/**
* Unique identifier for this request; used to respond via session.respondToQueuedCommand()
@@ -2623,6 +3161,43 @@ export type SessionEvent =
command: string;
};
}
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "command.execute";
+ /**
+ * Registered command dispatch request routed to the owning client
+ */
+ data: {
+ /**
+ * Unique identifier; used to respond via session.commands.handlePendingCommand()
+ */
+ requestId: string;
+ /**
+ * The full command text (e.g., /deploy production)
+ */
+ command: string;
+ /**
+ * Command name without leading /
+ */
+ commandName: string;
+ /**
+ * Raw argument string after the command name
+ */
+ args: string;
+ };
+ }
| {
/**
* Unique event identifier (UUID v4), generated when the event is emitted
@@ -2638,6 +3213,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "command.completed";
+ /**
+ * Queued command completion notification signaling UI dismissal
+ */
data: {
/**
* Request ID of the resolved command request; clients should dismiss any UI for this request
@@ -2645,6 +3223,34 @@ export type SessionEvent =
requestId: string;
};
}
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "commands.changed";
+ /**
+ * SDK command registration change notification
+ */
+ data: {
+ /**
+ * Current list of registered SDK commands
+ */
+ commands: {
+ name: string;
+ description?: string;
+ }[];
+ };
+ }
| {
/**
* Unique event identifier (UUID v4), generated when the event is emitted
@@ -2660,6 +3266,9 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "exit_plan_mode.requested";
+ /**
+ * Plan approval request with plan content and available user actions
+ */
data: {
/**
* Unique identifier for this request; used to respond via session.respondToExitPlanMode()
@@ -2698,10 +3307,200 @@ export type SessionEvent =
parentId: string | null;
ephemeral: true;
type: "exit_plan_mode.completed";
+ /**
+ * Plan mode exit completion notification signaling UI dismissal
+ */
data: {
/**
* Request ID of the resolved exit plan mode request; clients should dismiss any UI for this request
*/
requestId: string;
};
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.tools_updated";
+ data: {
+ model: string;
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.background_tasks_changed";
+ data: {};
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.skills_loaded";
+ data: {
+ /**
+ * Array of resolved skill metadata
+ */
+ skills: {
+ /**
+ * Unique identifier for the skill
+ */
+ name: string;
+ /**
+ * Description of what the skill does
+ */
+ description: string;
+ /**
+ * Source location type of the skill (e.g., project, personal, plugin)
+ */
+ source: string;
+ /**
+ * Whether the skill can be invoked by the user as a slash command
+ */
+ userInvocable: boolean;
+ /**
+ * Whether the skill is currently enabled
+ */
+ enabled: boolean;
+ /**
+ * Absolute path to the skill file, if available
+ */
+ path?: string;
+ }[];
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.mcp_servers_loaded";
+ data: {
+ /**
+ * Array of MCP server status summaries
+ */
+ servers: {
+ /**
+ * Server name (config key)
+ */
+ name: string;
+ /**
+ * Connection status: connected, failed, pending, disabled, or not_configured
+ */
+ status: "connected" | "failed" | "pending" | "disabled" | "not_configured";
+ /**
+ * Configuration source: user, workspace, plugin, or builtin
+ */
+ source?: string;
+ /**
+ * Error message if the server failed to connect
+ */
+ error?: string;
+ }[];
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.mcp_server_status_changed";
+ data: {
+ /**
+ * Name of the MCP server whose status changed
+ */
+ serverName: string;
+ /**
+ * New connection status: connected, failed, pending, disabled, or not_configured
+ */
+ status: "connected" | "failed" | "pending" | "disabled" | "not_configured";
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.extensions_loaded";
+ data: {
+ /**
+ * Array of discovered extensions and their status
+ */
+ extensions: {
+ /**
+ * Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')
+ */
+ id: string;
+ /**
+ * Extension name (directory name)
+ */
+ name: string;
+ /**
+ * Discovery source
+ */
+ source: "project" | "user";
+ /**
+ * Current status: running, disabled, failed, or starting
+ */
+ status: "running" | "disabled" | "failed" | "starting";
+ }[];
+ };
};
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index f2655f2fcd..f3788e168a 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -10,7 +10,7 @@
export { CopilotClient } from "./client.js";
export { CopilotSession, type AssistantMessageEvent } from "./session.js";
-export { defineTool, approveAll } from "./types.js";
+export { defineTool, approveAll, SYSTEM_PROMPT_SECTIONS } from "./types.js";
export type {
ConnectionState,
CopilotClientOptions,
@@ -31,6 +31,9 @@ export type {
PermissionRequest,
PermissionRequestResult,
ResumeSessionConfig,
+ SectionOverride,
+ SectionOverrideAction,
+ SectionTransformFn,
SessionConfig,
SessionEvent,
SessionEventHandler,
@@ -44,7 +47,12 @@ export type {
SessionMetadata,
SystemMessageAppendConfig,
SystemMessageConfig,
+ SystemMessageCustomizeConfig,
SystemMessageReplaceConfig,
+ SystemPromptSection,
+ TelemetryConfig,
+ TraceContext,
+ TraceContextProvider,
Tool,
ToolHandler,
ToolInvocation,
diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts
index 181d1a9615..122f4ece83 100644
--- a/nodejs/src/session.ts
+++ b/nodejs/src/session.ts
@@ -7,14 +7,17 @@
* @module session
*/
-import type { MessageConnection } from "vscode-jsonrpc/node";
-import { ConnectionError, ResponseError } from "vscode-jsonrpc/node";
+import type { MessageConnection } from "vscode-jsonrpc/node.js";
+import { ConnectionError, ResponseError } from "vscode-jsonrpc/node.js";
import { createSessionRpc } from "./generated/rpc.js";
+import { getTraceContext } from "./telemetry.js";
import type {
MessageOptions,
PermissionHandler,
PermissionRequest,
PermissionRequestResult,
+ ReasoningEffort,
+ SectionTransformFn,
SessionEvent,
SessionEventHandler,
SessionEventPayload,
@@ -22,12 +25,16 @@ import type {
SessionHooks,
Tool,
ToolHandler,
+ TraceContextProvider,
TypedSessionEventHandler,
UserInputHandler,
UserInputRequest,
UserInputResponse,
} from "./types.js";
+export const NO_RESULT_PERMISSION_V2_ERROR =
+ "Permission handlers cannot return 'no-result' when connected to a protocol v2 server.";
+
/** Assistant message event - the final response from the assistant. */
export type AssistantMessageEvent = Extract;
@@ -64,7 +71,9 @@ export class CopilotSession {
private permissionHandler?: PermissionHandler;
private userInputHandler?: UserInputHandler;
private hooks?: SessionHooks;
+ private transformCallbacks?: Map;
private _rpc: ReturnType | null = null;
+ private traceContextProvider?: TraceContextProvider;
/**
* Creates a new CopilotSession instance.
@@ -72,13 +81,17 @@ export class CopilotSession {
* @param sessionId - The unique identifier for this session
* @param connection - The JSON-RPC message connection to the Copilot CLI
* @param workspacePath - Path to the session workspace directory (when infinite sessions enabled)
+ * @param traceContextProvider - Optional callback to get W3C Trace Context for outbound RPCs
* @internal This constructor is internal. Use {@link CopilotClient.createSession} to create sessions.
*/
constructor(
public readonly sessionId: string,
private connection: MessageConnection,
- private readonly _workspacePath?: string
- ) {}
+ private _workspacePath?: string,
+ traceContextProvider?: TraceContextProvider
+ ) {
+ this.traceContextProvider = traceContextProvider;
+ }
/**
* Typed session-scoped RPC methods.
@@ -119,6 +132,7 @@ export class CopilotSession {
*/
async send(options: MessageOptions): Promise {
const response = await this.connection.sendRequest("session.send", {
+ ...(await getTraceContext(this.traceContextProvider)),
sessionId: this.sessionId,
prompt: options.prompt,
attachments: options.attachments,
@@ -333,9 +347,19 @@ export class CopilotSession {
};
const args = (event.data as { arguments: unknown }).arguments;
const toolCallId = (event.data as { toolCallId: string }).toolCallId;
+ const traceparent = (event.data as { traceparent?: string }).traceparent;
+ const tracestate = (event.data as { tracestate?: string }).tracestate;
const handler = this.toolHandlers.get(toolName);
if (handler) {
- void this._executeToolAndRespond(requestId, toolName, toolCallId, args, handler);
+ void this._executeToolAndRespond(
+ requestId,
+ toolName,
+ toolCallId,
+ args,
+ handler,
+ traceparent,
+ tracestate
+ );
}
} else if (event.type === "permission.requested") {
const { requestId, permissionRequest } = event.data as {
@@ -357,7 +381,9 @@ export class CopilotSession {
toolName: string,
toolCallId: string,
args: unknown,
- handler: ToolHandler
+ handler: ToolHandler,
+ traceparent?: string,
+ tracestate?: string
): Promise {
try {
const rawResult = await handler(args, {
@@ -365,6 +391,8 @@ export class CopilotSession {
toolCallId,
toolName,
arguments: args,
+ traceparent,
+ tracestate,
});
let result: string;
if (rawResult == null) {
@@ -400,6 +428,9 @@ export class CopilotSession {
const result = await this.permissionHandler!(permissionRequest, {
sessionId: this.sessionId,
});
+ if (result.kind === "no-result") {
+ return;
+ }
await this.rpc.permissions.handlePendingPermissionRequest({ requestId, result });
} catch (_error) {
try {
@@ -488,6 +519,48 @@ export class CopilotSession {
this.hooks = hooks;
}
+ /**
+ * Registers transform callbacks for system message sections.
+ *
+ * @param callbacks - Map of section ID to transform callback, or undefined to clear
+ * @internal This method is typically called internally when creating a session.
+ */
+ registerTransformCallbacks(callbacks?: Map): void {
+ this.transformCallbacks = callbacks;
+ }
+
+ /**
+ * Handles a systemMessage.transform request from the runtime.
+ * Dispatches each section to its registered transform callback.
+ *
+ * @param sections - Map of section IDs to their current rendered content
+ * @returns A promise that resolves with the transformed sections
+ * @internal This method is for internal use by the SDK.
+ */
+ async _handleSystemMessageTransform(
+ sections: Record
+ ): Promise<{ sections: Record }> {
+ const result: Record = {};
+
+ for (const [sectionId, { content }] of Object.entries(sections)) {
+ const callback = this.transformCallbacks?.get(sectionId);
+ if (callback) {
+ try {
+ const transformed = await callback(content);
+ result[sectionId] = { content: transformed };
+ } catch (_error) {
+ // Callback failed — return original content
+ result[sectionId] = { content };
+ }
+ } else {
+ // No callback for this section — pass through unchanged
+ result[sectionId] = { content };
+ }
+ }
+
+ return { sections: result };
+ }
+
/**
* Handles a permission request in the v2 protocol format (synchronous RPC).
* Used as a back-compat adapter when connected to a v2 server.
@@ -505,8 +578,14 @@ export class CopilotSession {
const result = await this.permissionHandler(request as PermissionRequest, {
sessionId: this.sessionId,
});
+ if (result.kind === "no-result") {
+ throw new Error(NO_RESULT_PERMISSION_V2_ERROR);
+ }
return result;
- } catch (_error) {
+ } catch (error) {
+ if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
+ throw error;
+ }
return { kind: "denied-no-approval-rule-and-could-not-request-from-user" };
}
}
@@ -684,13 +763,38 @@ export class CopilotSession {
* The new model takes effect for the next message. Conversation history is preserved.
*
* @param model - Model ID to switch to
+ * @param options - Optional settings for the new model
*
* @example
* ```typescript
* await session.setModel("gpt-4.1");
+ * await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" });
* ```
*/
- async setModel(model: string): Promise {
- await this.rpc.model.switchTo({ modelId: model });
+ async setModel(model: string, options?: { reasoningEffort?: ReasoningEffort }): Promise {
+ await this.rpc.model.switchTo({ modelId: model, ...options });
+ }
+
+ /**
+ * Log a message to the session timeline.
+ * The message appears in the session event stream and is visible to SDK consumers
+ * and (for non-ephemeral messages) persisted to the session event log on disk.
+ *
+ * @param message - Human-readable message text
+ * @param options - Optional log level and ephemeral flag
+ *
+ * @example
+ * ```typescript
+ * await session.log("Processing started");
+ * await session.log("Disk usage high", { level: "warning" });
+ * await session.log("Connection failed", { level: "error" });
+ * await session.log("Debug info", { ephemeral: true });
+ * ```
+ */
+ async log(
+ message: string,
+ options?: { level?: "info" | "warning" | "error"; ephemeral?: boolean }
+ ): Promise {
+ await this.rpc.log({ message, ...options });
}
}
diff --git a/nodejs/src/telemetry.ts b/nodejs/src/telemetry.ts
new file mode 100644
index 0000000000..f9d3316781
--- /dev/null
+++ b/nodejs/src/telemetry.ts
@@ -0,0 +1,27 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * Trace-context helpers.
+ *
+ * The SDK does not depend on any OpenTelemetry packages. Instead, users
+ * provide an {@link TraceContextProvider} callback via client options.
+ *
+ * @module telemetry
+ */
+
+import type { TraceContext, TraceContextProvider } from "./types.js";
+
+/**
+ * Calls the user-provided {@link TraceContextProvider} to obtain the current
+ * W3C Trace Context. Returns `{}` when no provider is configured.
+ */
+export async function getTraceContext(provider?: TraceContextProvider): Promise {
+ if (!provider) return {};
+ try {
+ return (await provider()) ?? {};
+ } catch {
+ return {};
+ }
+}
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 7eef940972..992dbdb9d4 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -13,6 +13,41 @@ export type SessionEvent = GeneratedSessionEvent;
/**
* Options for creating a CopilotClient
*/
+/**
+ * W3C Trace Context headers used for distributed trace propagation.
+ */
+export interface TraceContext {
+ traceparent?: string;
+ tracestate?: string;
+}
+
+/**
+ * Callback that returns the current W3C Trace Context.
+ * Wire this up to your OpenTelemetry (or other tracing) SDK to enable
+ * distributed trace propagation between your app and the Copilot CLI.
+ */
+export type TraceContextProvider = () => TraceContext | Promise;
+
+/**
+ * Configuration for OpenTelemetry instrumentation.
+ *
+ * When provided via {@link CopilotClientOptions.telemetry}, the SDK sets
+ * the corresponding environment variables on the spawned CLI process so
+ * that the CLI's built-in OTel exporter is configured automatically.
+ */
+export interface TelemetryConfig {
+ /** OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT. */
+ otlpEndpoint?: string;
+ /** File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH. */
+ filePath?: string;
+ /** Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE. */
+ exporterType?: string;
+ /** Instrumentation scope name. Sets COPILOT_OTEL_SOURCE_NAME. */
+ sourceName?: string;
+ /** Whether to capture message content (prompts, responses). Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. */
+ captureContent?: boolean;
+}
+
export interface CopilotClientOptions {
/**
* Path to the CLI executable or JavaScript entry point.
@@ -72,8 +107,7 @@ export interface CopilotClientOptions {
autoStart?: boolean;
/**
- * Auto-restart the CLI server if it crashes
- * @default true
+ * @deprecated This option has no effect and will be removed in a future release.
*/
autoRestart?: boolean;
@@ -96,6 +130,47 @@ export interface CopilotClientOptions {
* @default true (but defaults to false when githubToken is provided)
*/
useLoggedInUser?: boolean;
+
+ /**
+ * Custom handler for listing available models.
+ * When provided, client.listModels() calls this handler instead of
+ * querying the CLI server. Useful in BYOK mode to return models
+ * available from your custom provider.
+ */
+ onListModels?: () => Promise | ModelInfo[];
+
+ /**
+ * OpenTelemetry configuration for the CLI process.
+ * When provided, the corresponding OTel environment variables are set
+ * on the spawned CLI server.
+ */
+ telemetry?: TelemetryConfig;
+
+ /**
+ * Advanced: callback that returns the current W3C Trace Context for distributed
+ * trace propagation. Most users do not need this — the {@link telemetry} config
+ * alone is sufficient to collect traces from the CLI.
+ *
+ * This callback is only useful when your application creates its own
+ * OpenTelemetry spans and you want them to appear in the **same** distributed
+ * trace as the CLI's spans. The SDK calls this before `session.create`,
+ * `session.resume`, and `session.send` RPCs to inject `traceparent`/`tracestate`
+ * into the request.
+ *
+ * @example
+ * ```typescript
+ * import { propagation, context } from "@opentelemetry/api";
+ *
+ * const client = new CopilotClient({
+ * onGetTraceContext: () => {
+ * const carrier: Record = {};
+ * propagation.inject(context.active(), carrier);
+ * return carrier;
+ * },
+ * });
+ * ```
+ */
+ onGetTraceContext?: TraceContextProvider;
}
/**
@@ -126,6 +201,10 @@ export interface ToolInvocation {
toolCallId: string;
toolName: string;
arguments: unknown;
+ /** W3C Trace Context traceparent from the CLI's execute_tool span. */
+ traceparent?: string;
+ /** W3C Trace Context tracestate from the CLI's execute_tool span. */
+ tracestate?: string;
}
export type ToolHandler = (
@@ -159,6 +238,10 @@ export interface Tool {
* will return an error.
*/
overridesBuiltInTool?: boolean;
+ /**
+ * When true, the tool can execute without a permission prompt.
+ */
+ skipPermission?: boolean;
}
/**
@@ -172,6 +255,7 @@ export function defineTool(
parameters?: ZodSchema | Record;
handler: ToolHandler;
overridesBuiltInTool?: boolean;
+ skipPermission?: boolean;
}
): Tool {
return { name, ...config };
@@ -188,6 +272,79 @@ export interface ToolCallResponsePayload {
result: ToolResult;
}
+/**
+ * Known system prompt section identifiers for the "customize" mode.
+ * Each section corresponds to a distinct part of the system prompt.
+ */
+export type SystemPromptSection =
+ | "identity"
+ | "tone"
+ | "tool_efficiency"
+ | "environment_context"
+ | "code_change_rules"
+ | "guidelines"
+ | "safety"
+ | "tool_instructions"
+ | "custom_instructions"
+ | "last_instructions";
+
+/** Section metadata for documentation and tooling. */
+export const SYSTEM_PROMPT_SECTIONS: Record = {
+ identity: { description: "Agent identity preamble and mode statement" },
+ tone: { description: "Response style, conciseness rules, output formatting preferences" },
+ tool_efficiency: { description: "Tool usage patterns, parallel calling, batching guidelines" },
+ environment_context: { description: "CWD, OS, git root, directory listing, available tools" },
+ code_change_rules: { description: "Coding rules, linting/testing, ecosystem tools, style" },
+ guidelines: { description: "Tips, behavioral best practices, behavioral guidelines" },
+ safety: { description: "Environment limitations, prohibited actions, security policies" },
+ tool_instructions: { description: "Per-tool usage instructions" },
+ custom_instructions: { description: "Repository and organization custom instructions" },
+ last_instructions: {
+ description:
+ "End-of-prompt instructions: parallel tool calling, persistence, task completion",
+ },
+};
+
+/**
+ * Transform callback for a single section: receives current content, returns new content.
+ */
+export type SectionTransformFn = (currentContent: string) => string | Promise;
+
+/**
+ * Override action: a string literal for static overrides, or a callback for transforms.
+ *
+ * - `"replace"`: Replace section content entirely
+ * - `"remove"`: Remove the section
+ * - `"append"`: Append to existing section content
+ * - `"prepend"`: Prepend to existing section content
+ * - `function`: Transform callback — receives current section content, returns new content
+ */
+export type SectionOverrideAction =
+ | "replace"
+ | "remove"
+ | "append"
+ | "prepend"
+ | SectionTransformFn;
+
+/**
+ * Override operation for a single system prompt section.
+ */
+export interface SectionOverride {
+ /**
+ * The operation to perform on this section.
+ * Can be a string action or a transform callback function.
+ */
+ action: SectionOverrideAction;
+
+ /**
+ * Content for the override. Optional for all actions.
+ * - For replace, omitting content replaces with an empty string.
+ * - For append/prepend, content is added before/after the existing section.
+ * - Ignored for the remove action.
+ */
+ content?: string;
+}
+
/**
* Append mode: Use CLI foundation with optional appended content (default).
*/
@@ -214,12 +371,37 @@ export interface SystemMessageReplaceConfig {
content: string;
}
+/**
+ * Customize mode: Override individual sections of the system prompt.
+ * Keeps the SDK-managed prompt structure while allowing targeted modifications.
+ */
+export interface SystemMessageCustomizeConfig {
+ mode: "customize";
+
+ /**
+ * Override specific sections of the system prompt by section ID.
+ * Unknown section IDs gracefully fall back: content-bearing overrides are appended
+ * to additional instructions, and "remove" on unknown sections is a silent no-op.
+ */
+ sections?: Partial>;
+
+ /**
+ * Additional content appended after all sections.
+ * Equivalent to append mode's content field — provided for convenience.
+ */
+ content?: string;
+}
+
/**
* System message configuration for session creation.
* - Append mode (default): SDK foundation + optional custom content
* - Replace mode: Full control, caller provides entire system message
+ * - Customize mode: Section-level overrides with graceful fallback
*/
-export type SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig;
+export type SystemMessageConfig =
+ | SystemMessageAppendConfig
+ | SystemMessageReplaceConfig
+ | SystemMessageCustomizeConfig;
/**
* Permission request types from the server
@@ -233,7 +415,8 @@ export interface PermissionRequest {
import type { SessionPermissionsHandlePendingPermissionRequestParams } from "./generated/rpc.js";
export type PermissionRequestResult =
- SessionPermissionsHandlePendingPermissionRequestParams["result"];
+ | SessionPermissionsHandlePendingPermissionRequestParams["result"]
+ | { kind: "no-result" };
export type PermissionHandler = (
request: PermissionRequest,
@@ -725,6 +908,13 @@ export interface SessionConfig {
*/
customAgents?: CustomAgentConfig[];
+ /**
+ * Name of the custom agent to activate when the session starts.
+ * Must match the `name` of one of the agents in `customAgents`.
+ * Equivalent to calling `session.rpc.agent.select({ name })` after creation.
+ */
+ agent?: string;
+
/**
* Directories to load skills from.
*/
@@ -741,6 +931,17 @@ export interface SessionConfig {
* Set to `{ enabled: false }` to disable.
*/
infiniteSessions?: InfiniteSessionConfig;
+
+ /**
+ * Optional event handler that is registered on the session before the
+ * session.create RPC is issued. This guarantees that early events emitted
+ * by the CLI during session creation (e.g. session.start) are delivered to
+ * the handler.
+ *
+ * Equivalent to calling `session.on(handler)` immediately after creation,
+ * but executes earlier in the lifecycle so no events are missed.
+ */
+ onEvent?: SessionEventHandler;
}
/**
@@ -764,9 +965,11 @@ export type ResumeSessionConfig = Pick<
| "configDir"
| "mcpServers"
| "customAgents"
+ | "agent"
| "skillDirectories"
| "disabledSkills"
| "infiniteSessions"
+ | "onEvent"
> & {
/**
* When true, skips emitting the session.resume event.
@@ -828,7 +1031,7 @@ export interface MessageOptions {
prompt: string;
/**
- * File, directory, or selection attachments
+ * File, directory, selection, or blob attachments
*/
attachments?: Array<
| {
@@ -851,6 +1054,12 @@ export interface MessageOptions {
};
text?: string;
}
+ | {
+ type: "blob";
+ data: string;
+ mimeType: string;
+ displayName?: string;
+ }
>;
/**
diff --git a/nodejs/test/cjs-compat.test.ts b/nodejs/test/cjs-compat.test.ts
new file mode 100644
index 0000000000..f574037256
--- /dev/null
+++ b/nodejs/test/cjs-compat.test.ts
@@ -0,0 +1,72 @@
+/**
+ * Dual ESM/CJS build compatibility tests
+ *
+ * Verifies that both the ESM and CJS builds exist and work correctly,
+ * so consumers using either module system get a working package.
+ *
+ * See: https://github.com/github/copilot-sdk/issues/528
+ */
+
+import { describe, expect, it } from "vitest";
+import { existsSync } from "node:fs";
+import { execFileSync } from "node:child_process";
+import { join } from "node:path";
+
+const distDir = join(import.meta.dirname, "../dist");
+
+describe("Dual ESM/CJS build (#528)", () => {
+ it("ESM dist file should exist", () => {
+ expect(existsSync(join(distDir, "index.js"))).toBe(true);
+ });
+
+ it("CJS dist file should exist", () => {
+ expect(existsSync(join(distDir, "cjs/index.js"))).toBe(true);
+ });
+
+ it("CJS build is requireable and exports CopilotClient", () => {
+ const script = `
+ const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))});
+ if (typeof sdk.CopilotClient !== 'function') {
+ console.error('CopilotClient is not a function');
+ process.exit(1);
+ }
+ console.log('CJS require: OK');
+ `;
+ const output = execFileSync(process.execPath, ["--eval", script], {
+ encoding: "utf-8",
+ timeout: 10000,
+ cwd: join(import.meta.dirname, ".."),
+ });
+ expect(output).toContain("CJS require: OK");
+ });
+
+ it("CJS build resolves bundled CLI path", () => {
+ const script = `
+ const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))});
+ const client = new sdk.CopilotClient({ autoStart: false });
+ console.log('CJS CLI resolved: OK');
+ `;
+ const output = execFileSync(process.execPath, ["--eval", script], {
+ encoding: "utf-8",
+ timeout: 10000,
+ cwd: join(import.meta.dirname, ".."),
+ });
+ expect(output).toContain("CJS CLI resolved: OK");
+ });
+
+ it("ESM build resolves bundled CLI path", () => {
+ const esmPath = join(distDir, "index.js");
+ const script = `
+ import { pathToFileURL } from 'node:url';
+ const sdk = await import(pathToFileURL(${JSON.stringify(esmPath)}).href);
+ const client = new sdk.CopilotClient({ autoStart: false });
+ console.log('ESM CLI resolved: OK');
+ `;
+ const output = execFileSync(process.execPath, ["--input-type=module", "--eval", script], {
+ encoding: "utf-8",
+ timeout: 10000,
+ cwd: join(import.meta.dirname, ".."),
+ });
+ expect(output).toContain("ESM CLI resolved: OK");
+ });
+});
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index b7dd343956..3d13d27ffc 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, expect, it, onTestFinished, vi } from "vitest";
-import { approveAll, CopilotClient } from "../src/index.js";
+import { approveAll, CopilotClient, type ModelInfo } from "../src/index.js";
// This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead
@@ -26,6 +26,38 @@ describe("CopilotClient", () => {
);
});
+ it("does not respond to v3 permission requests when handler returns no-result", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const session = await client.createSession({
+ onPermissionRequest: () => ({ kind: "no-result" }),
+ });
+ const spy = vi.spyOn(session.rpc.permissions, "handlePendingPermissionRequest");
+
+ await (session as any)._executePermissionAndRespond("request-1", { kind: "write" });
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it("throws when a v2 permission handler returns no-result", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const session = await client.createSession({
+ onPermissionRequest: () => ({ kind: "no-result" }),
+ });
+
+ await expect(
+ (client as any).handlePermissionRequestV2({
+ sessionId: session.sessionId,
+ permissionRequest: { kind: "write" },
+ })
+ ).rejects.toThrow(/protocol v2 server/);
+ });
+
it("forwards clientName in session.create request", async () => {
const client = new CopilotClient();
await client.start();
@@ -91,6 +123,31 @@ describe("CopilotClient", () => {
spy.mockRestore();
});
+ it("sends reasoningEffort with session.model.switchTo when provided", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+
+ const spy = vi
+ .spyOn((client as any).connection!, "sendRequest")
+ .mockImplementation(async (method: string, _params: any) => {
+ if (method === "session.model.switchTo") return {};
+ throw new Error(`Unexpected method: ${method}`);
+ });
+
+ await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" });
+
+ expect(spy).toHaveBeenCalledWith("session.model.switchTo", {
+ sessionId: session.sessionId,
+ modelId: "claude-sonnet-4.6",
+ reasoningEffort: "high",
+ });
+
+ spy.mockRestore();
+ });
+
describe("URL parsing", () => {
it("should parse port-only URL format", () => {
const client = new CopilotClient({
@@ -210,6 +267,15 @@ describe("CopilotClient", () => {
expect((client as any).isExternalServer).toBe(true);
});
+
+ it("should not resolve cliPath when cliUrl is provided", () => {
+ const client = new CopilotClient({
+ cliUrl: "localhost:8080",
+ logLevel: "error",
+ });
+
+ expect(client["options"].cliPath).toBeUndefined();
+ });
});
describe("Auth options", () => {
@@ -336,4 +402,252 @@ describe("CopilotClient", () => {
spy.mockRestore();
});
});
+
+ describe("agent parameter in session creation", () => {
+ it("forwards agent in session.create request", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const spy = vi.spyOn((client as any).connection!, "sendRequest");
+ await client.createSession({
+ onPermissionRequest: approveAll,
+ customAgents: [
+ {
+ name: "test-agent",
+ prompt: "You are a test agent.",
+ },
+ ],
+ agent: "test-agent",
+ });
+
+ const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any;
+ expect(payload.agent).toBe("test-agent");
+ expect(payload.customAgents).toEqual([expect.objectContaining({ name: "test-agent" })]);
+ });
+
+ it("forwards agent in session.resume request", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+ const spy = vi
+ .spyOn((client as any).connection!, "sendRequest")
+ .mockImplementation(async (method: string, params: any) => {
+ if (method === "session.resume") return { sessionId: params.sessionId };
+ throw new Error(`Unexpected method: ${method}`);
+ });
+ await client.resumeSession(session.sessionId, {
+ onPermissionRequest: approveAll,
+ customAgents: [
+ {
+ name: "test-agent",
+ prompt: "You are a test agent.",
+ },
+ ],
+ agent: "test-agent",
+ });
+
+ const payload = spy.mock.calls.find((c) => c[0] === "session.resume")![1] as any;
+ expect(payload.agent).toBe("test-agent");
+ spy.mockRestore();
+ });
+ });
+
+ describe("onListModels", () => {
+ it("calls onListModels handler instead of RPC when provided", async () => {
+ const customModels: ModelInfo[] = [
+ {
+ id: "my-custom-model",
+ name: "My Custom Model",
+ capabilities: {
+ supports: { vision: false, reasoningEffort: false },
+ limits: { max_context_window_tokens: 128000 },
+ },
+ },
+ ];
+
+ const handler = vi.fn().mockReturnValue(customModels);
+ const client = new CopilotClient({ onListModels: handler });
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const models = await client.listModels();
+ expect(handler).toHaveBeenCalledTimes(1);
+ expect(models).toEqual(customModels);
+ });
+
+ it("caches onListModels results on subsequent calls", async () => {
+ const customModels: ModelInfo[] = [
+ {
+ id: "cached-model",
+ name: "Cached Model",
+ capabilities: {
+ supports: { vision: false, reasoningEffort: false },
+ limits: { max_context_window_tokens: 128000 },
+ },
+ },
+ ];
+
+ const handler = vi.fn().mockReturnValue(customModels);
+ const client = new CopilotClient({ onListModels: handler });
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ await client.listModels();
+ await client.listModels();
+ expect(handler).toHaveBeenCalledTimes(1); // Only called once due to caching
+ });
+
+ it("supports async onListModels handler", async () => {
+ const customModels: ModelInfo[] = [
+ {
+ id: "async-model",
+ name: "Async Model",
+ capabilities: {
+ supports: { vision: false, reasoningEffort: false },
+ limits: { max_context_window_tokens: 128000 },
+ },
+ },
+ ];
+
+ const handler = vi.fn().mockResolvedValue(customModels);
+ const client = new CopilotClient({ onListModels: handler });
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const models = await client.listModels();
+ expect(models).toEqual(customModels);
+ });
+
+ it("does not require client.start when onListModels is provided", async () => {
+ const customModels: ModelInfo[] = [
+ {
+ id: "no-start-model",
+ name: "No Start Model",
+ capabilities: {
+ supports: { vision: false, reasoningEffort: false },
+ limits: { max_context_window_tokens: 128000 },
+ },
+ },
+ ];
+
+ const handler = vi.fn().mockReturnValue(customModels);
+ const client = new CopilotClient({ onListModels: handler });
+
+ const models = await client.listModels();
+ expect(handler).toHaveBeenCalledTimes(1);
+ expect(models).toEqual(customModels);
+ });
+ });
+
+ describe("unexpected disconnection", () => {
+ it("transitions to disconnected when child process is killed", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ expect(client.getState()).toBe("connected");
+
+ // Kill the child process to simulate unexpected termination
+ const proc = (client as any).cliProcess as import("node:child_process").ChildProcess;
+ proc.kill();
+
+ // Wait for the connection.onClose handler to fire
+ await vi.waitFor(() => {
+ expect(client.getState()).toBe("disconnected");
+ });
+ });
+ });
+
+ describe("onGetTraceContext", () => {
+ it("includes trace context from callback in session.create request", async () => {
+ const traceContext = {
+ traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01",
+ tracestate: "vendor=opaque",
+ };
+ const provider = vi.fn().mockReturnValue(traceContext);
+ const client = new CopilotClient({ onGetTraceContext: provider });
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const spy = vi.spyOn((client as any).connection!, "sendRequest");
+ await client.createSession({ onPermissionRequest: approveAll });
+
+ expect(provider).toHaveBeenCalled();
+ expect(spy).toHaveBeenCalledWith(
+ "session.create",
+ expect.objectContaining({
+ traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01",
+ tracestate: "vendor=opaque",
+ })
+ );
+ });
+
+ it("includes trace context from callback in session.resume request", async () => {
+ const traceContext = {
+ traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01",
+ };
+ const provider = vi.fn().mockReturnValue(traceContext);
+ const client = new CopilotClient({ onGetTraceContext: provider });
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+ const spy = vi
+ .spyOn((client as any).connection!, "sendRequest")
+ .mockImplementation(async (method: string, params: any) => {
+ if (method === "session.resume") return { sessionId: params.sessionId };
+ throw new Error(`Unexpected method: ${method}`);
+ });
+ await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll });
+
+ expect(spy).toHaveBeenCalledWith(
+ "session.resume",
+ expect.objectContaining({
+ traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01",
+ })
+ );
+ });
+
+ it("includes trace context from callback in session.send request", async () => {
+ const traceContext = {
+ traceparent: "00-fedcba0987654321fedcba0987654321-abcdef1234567890-01",
+ };
+ const provider = vi.fn().mockReturnValue(traceContext);
+ const client = new CopilotClient({ onGetTraceContext: provider });
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+ const spy = vi
+ .spyOn((client as any).connection!, "sendRequest")
+ .mockImplementation(async (method: string) => {
+ if (method === "session.send") return { responseId: "r1" };
+ throw new Error(`Unexpected method: ${method}`);
+ });
+ await session.send({ prompt: "hello" });
+
+ expect(spy).toHaveBeenCalledWith(
+ "session.send",
+ expect.objectContaining({
+ traceparent: "00-fedcba0987654321fedcba0987654321-abcdef1234567890-01",
+ })
+ );
+ });
+
+ it("does not include trace context when no callback is provided", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const spy = vi.spyOn((client as any).connection!, "sendRequest");
+ await client.createSession({ onPermissionRequest: approveAll });
+
+ const [, params] = spy.mock.calls.find(([method]) => method === "session.create")!;
+ expect(params.traceparent).toBeUndefined();
+ expect(params.tracestate).toBeUndefined();
+ });
+ });
});
diff --git a/nodejs/test/e2e/client.test.ts b/nodejs/test/e2e/client.test.ts
index 9d71ee726a..594607cd13 100644
--- a/nodejs/test/e2e/client.test.ts
+++ b/nodejs/test/e2e/client.test.ts
@@ -43,27 +43,32 @@ describe("Client", () => {
expect(client.getState()).toBe("disconnected");
});
- it.skipIf(process.platform === "darwin")("should return errors on failed cleanup", async () => {
- // Use TCP mode to avoid stdin stream destruction issues
- // Without this, on macOS there are intermittent test failures
- // saying "Cannot call write after a stream was destroyed"
- // because the JSON-RPC logic is still trying to write to stdin after
- // the process has exited.
- const client = new CopilotClient({ useStdio: false });
-
- await client.createSession({ onPermissionRequest: approveAll });
-
- // Kill the server processto force cleanup to fail
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const cliProcess = (client as any).cliProcess as ChildProcess;
- expect(cliProcess).toBeDefined();
- cliProcess.kill("SIGKILL");
- await new Promise((resolve) => setTimeout(resolve, 100));
-
- const errors = await client.stop();
- expect(errors.length).toBeGreaterThan(0);
- expect(errors[0].message).toContain("Failed to disconnect session");
- });
+ it.skipIf(process.platform === "darwin")(
+ "should stop cleanly when the server exits during cleanup",
+ async () => {
+ // Use TCP mode to avoid stdin stream destruction issues
+ // Without this, on macOS there are intermittent test failures
+ // saying "Cannot call write after a stream was destroyed"
+ // because the JSON-RPC logic is still trying to write to stdin after
+ // the process has exited.
+ const client = new CopilotClient({ useStdio: false });
+
+ await client.createSession({ onPermissionRequest: approveAll });
+
+ // Kill the server processto force cleanup to fail
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const cliProcess = (client as any).cliProcess as ChildProcess;
+ expect(cliProcess).toBeDefined();
+ cliProcess.kill("SIGKILL");
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ const errors = await client.stop();
+ expect(client.getState()).toBe("disconnected");
+ if (errors.length > 0) {
+ expect(errors[0].message).toContain("Failed to disconnect session");
+ }
+ }
+ );
it("should forceStop without cleanup", async () => {
const client = new CopilotClient({});
diff --git a/nodejs/test/e2e/rpc.test.ts b/nodejs/test/e2e/rpc.test.ts
index 62a885d050..d4d732efd1 100644
--- a/nodejs/test/e2e/rpc.test.ts
+++ b/nodejs/test/e2e/rpc.test.ts
@@ -92,8 +92,11 @@ describe("Session RPC", async () => {
const before = await session.rpc.model.getCurrent();
expect(before.modelId).toBeDefined();
- // Switch to a different model
- const result = await session.rpc.model.switchTo({ modelId: "gpt-4.1" });
+ // Switch to a different model with reasoning effort
+ const result = await session.rpc.model.switchTo({
+ modelId: "gpt-4.1",
+ reasoningEffort: "high",
+ });
expect(result.modelId).toBe("gpt-4.1");
// Verify the switch persisted
diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts
index e988e62c81..dbcbed8bb7 100644
--- a/nodejs/test/e2e/session.test.ts
+++ b/nodejs/test/e2e/session.test.ts
@@ -1,5 +1,5 @@
import { rm } from "fs/promises";
-import { describe, expect, it, onTestFinished } from "vitest";
+import { describe, expect, it, onTestFinished, vi } from "vitest";
import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js";
import { CopilotClient, approveAll } from "../../src/index.js";
import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js";
@@ -96,6 +96,33 @@ describe("Sessions", async () => {
expect(systemMessage).toEqual(testSystemMessage); // Exact match
});
+ it("should create a session with customized systemMessage config", async () => {
+ const customTone = "Respond in a warm, professional tone. Be thorough in explanations.";
+ const appendedContent = "Always mention quarterly earnings.";
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ tone: { action: "replace", content: customTone },
+ code_change_rules: { action: "remove" },
+ },
+ content: appendedContent,
+ },
+ });
+
+ const assistantMessage = await session.sendAndWait({ prompt: "Who are you?" });
+ expect(assistantMessage?.data.content).toBeDefined();
+
+ // Validate the system message sent to the model
+ const traffic = await openAiEndpoint.getExchanges();
+ const systemMessage = getSystemMessage(traffic[0]);
+ expect(systemMessage).toContain(customTone);
+ expect(systemMessage).toContain(appendedContent);
+ // The code_change_rules section should have been removed
+ expect(systemMessage).not.toContain("");
+ });
+
it("should create a session with availableTools", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
@@ -297,7 +324,19 @@ describe("Sessions", async () => {
});
it("should receive session events", async () => {
- const session = await client.createSession({ onPermissionRequest: approveAll });
+ // Use onEvent to capture events dispatched during session creation.
+ // session.start is emitted during the session.create RPC; if the session
+ // weren't registered in the sessions map before the RPC, it would be dropped.
+ const earlyEvents: Array<{ type: string }> = [];
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ onEvent: (event) => {
+ earlyEvents.push(event);
+ },
+ });
+
+ expect(earlyEvents.some((e) => e.type === "session.start")).toBe(true);
+
const receivedEvents: Array<{ type: string }> = [];
session.on((event) => {
@@ -334,6 +373,57 @@ describe("Sessions", async () => {
const assistantMessage = await getFinalAssistantMessage(session);
expect(assistantMessage.data.content).toContain("2");
});
+
+ it("should log messages at all levels and emit matching session events", async () => {
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+
+ const events: Array<{ type: string; id?: string; data?: Record }> = [];
+ session.on((event) => {
+ events.push(event as (typeof events)[number]);
+ });
+
+ await session.log("Info message");
+ await session.log("Warning message", { level: "warning" });
+ await session.log("Error message", { level: "error" });
+ await session.log("Ephemeral message", { ephemeral: true });
+
+ await vi.waitFor(
+ () => {
+ const notifications = events.filter(
+ (e) =>
+ e.data &&
+ ("infoType" in e.data || "warningType" in e.data || "errorType" in e.data)
+ );
+ expect(notifications).toHaveLength(4);
+ },
+ { timeout: 10_000 }
+ );
+
+ const byMessage = (msg: string) => events.find((e) => e.data?.message === msg)!;
+ expect(byMessage("Info message").type).toBe("session.info");
+ expect(byMessage("Info message").data).toEqual({
+ infoType: "notification",
+ message: "Info message",
+ });
+
+ expect(byMessage("Warning message").type).toBe("session.warning");
+ expect(byMessage("Warning message").data).toEqual({
+ warningType: "notification",
+ message: "Warning message",
+ });
+
+ expect(byMessage("Error message").type).toBe("session.error");
+ expect(byMessage("Error message").data).toEqual({
+ errorType: "notification",
+ message: "Error message",
+ });
+
+ expect(byMessage("Ephemeral message").type).toBe("session.info");
+ expect(byMessage("Ephemeral message").data).toEqual({
+ infoType: "notification",
+ message: "Ephemeral message",
+ });
+ });
});
function getSystemMessage(exchange: ParsedHttpExchange): string | undefined {
@@ -398,4 +488,16 @@ describe("Send Blocking Behavior", async () => {
session.sendAndWait({ prompt: "Run 'sleep 2 && echo done'" }, 100)
).rejects.toThrow(/Timeout after 100ms/);
});
+
+ it("should set model with reasoningEffort", async () => {
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+
+ const modelChangePromise = getNextEventOfType(session, "session.model_change");
+
+ await session.setModel("gpt-4.1", { reasoningEffort: "high" });
+
+ const event = await modelChangePromise;
+ expect(event.data.newModel).toBe("gpt-4.1");
+ expect(event.data.reasoningEffort).toBe("high");
+ });
});
diff --git a/nodejs/test/e2e/session_config.test.ts b/nodejs/test/e2e/session_config.test.ts
index 2984c3c04c..e27421ebf8 100644
--- a/nodejs/test/e2e/session_config.test.ts
+++ b/nodejs/test/e2e/session_config.test.ts
@@ -43,6 +43,25 @@ describe("Session Configuration", async () => {
}
});
+ it("should accept blob attachments", async () => {
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+
+ await session.send({
+ prompt: "Describe this image",
+ attachments: [
+ {
+ type: "blob",
+ data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
+ mimeType: "image/png",
+ displayName: "test-pixel.png",
+ },
+ ],
+ });
+
+ // Just verify send doesn't throw — blob attachment support varies by runtime
+ await session.disconnect();
+ });
+
it("should accept message attachments", async () => {
await writeFile(join(workDir, "attached.txt"), "This file is attached");
diff --git a/nodejs/test/e2e/system_message_transform.test.ts b/nodejs/test/e2e/system_message_transform.test.ts
new file mode 100644
index 0000000000..ef37c39e9a
--- /dev/null
+++ b/nodejs/test/e2e/system_message_transform.test.ts
@@ -0,0 +1,125 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+import { writeFile } from "fs/promises";
+import { join } from "path";
+import { describe, expect, it } from "vitest";
+import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js";
+import { approveAll } from "../../src/index.js";
+import { createSdkTestContext } from "./harness/sdkTestContext.js";
+
+describe("System message transform", async () => {
+ const { copilotClient: client, openAiEndpoint, workDir } = await createSdkTestContext();
+
+ it("should invoke transform callbacks with section content", async () => {
+ const transformedSections: Record = {};
+
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ identity: {
+ action: (content: string) => {
+ transformedSections["identity"] = content;
+ // Pass through unchanged
+ return content;
+ },
+ },
+ tone: {
+ action: (content: string) => {
+ transformedSections["tone"] = content;
+ return content;
+ },
+ },
+ },
+ },
+ });
+
+ await writeFile(join(workDir, "test.txt"), "Hello transform!");
+
+ await session.sendAndWait({
+ prompt: "Read the contents of test.txt and tell me what it says",
+ });
+
+ // Transform callbacks should have been invoked with real section content
+ expect(Object.keys(transformedSections).length).toBe(2);
+ expect(transformedSections["identity"]).toBeDefined();
+ expect(transformedSections["identity"]!.length).toBeGreaterThan(0);
+ expect(transformedSections["tone"]).toBeDefined();
+ expect(transformedSections["tone"]!.length).toBeGreaterThan(0);
+
+ await session.disconnect();
+ });
+
+ it("should apply transform modifications to section content", async () => {
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ identity: {
+ action: (content: string) => {
+ return content + "\nTRANSFORM_MARKER";
+ },
+ },
+ },
+ },
+ });
+
+ await writeFile(join(workDir, "hello.txt"), "Hello!");
+
+ await session.sendAndWait({
+ prompt: "Read the contents of hello.txt",
+ });
+
+ // Verify the transform result was actually applied to the system message
+ const traffic = await openAiEndpoint.getExchanges();
+ const systemMessage = getSystemMessage(traffic[0]);
+ expect(systemMessage).toContain("TRANSFORM_MARKER");
+
+ await session.disconnect();
+ });
+
+ it("should work with static overrides and transforms together", async () => {
+ const transformedSections: Record = {};
+
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ // Static override
+ safety: { action: "remove" },
+ // Transform
+ identity: {
+ action: (content: string) => {
+ transformedSections["identity"] = content;
+ return content;
+ },
+ },
+ },
+ },
+ });
+
+ await writeFile(join(workDir, "combo.txt"), "Combo test!");
+
+ await session.sendAndWait({
+ prompt: "Read the contents of combo.txt and tell me what it says",
+ });
+
+ // Transform should have been invoked
+ expect(transformedSections["identity"]).toBeDefined();
+ expect(transformedSections["identity"]!.length).toBeGreaterThan(0);
+
+ await session.disconnect();
+ });
+});
+
+function getSystemMessage(exchange: ParsedHttpExchange): string | undefined {
+ const systemMessage = exchange.request.messages.find((m) => m.role === "system") as
+ | { role: "system"; content: string }
+ | undefined;
+ return systemMessage?.content;
+}
diff --git a/nodejs/test/e2e/tools.test.ts b/nodejs/test/e2e/tools.test.ts
index 3f5c3e09fb..83d7336862 100644
--- a/nodejs/test/e2e/tools.test.ts
+++ b/nodejs/test/e2e/tools.test.ts
@@ -159,6 +159,32 @@ describe("Custom tools", async () => {
expect(customToolRequests[0].toolName).toBe("encrypt_string");
});
+ it("skipPermission sent in tool definition", async () => {
+ let didRunPermissionRequest = false;
+ const session = await client.createSession({
+ onPermissionRequest: () => {
+ didRunPermissionRequest = true;
+ return { kind: "no-result" };
+ },
+ tools: [
+ defineTool("safe_lookup", {
+ description: "A safe lookup that skips permission",
+ parameters: z.object({
+ id: z.string().describe("ID to look up"),
+ }),
+ handler: ({ id }) => `RESULT: ${id}`,
+ skipPermission: true,
+ }),
+ ],
+ });
+
+ const assistantMessage = await session.sendAndWait({
+ prompt: "Use safe_lookup to look up 'test123'",
+ });
+ expect(assistantMessage?.data.content).toContain("RESULT: test123");
+ expect(didRunPermissionRequest).toBe(false);
+ });
+
it("overrides built-in tool with custom tool", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
diff --git a/nodejs/test/extension.test.ts b/nodejs/test/extension.test.ts
new file mode 100644
index 0000000000..d9fcf8dfdb
--- /dev/null
+++ b/nodejs/test/extension.test.ts
@@ -0,0 +1,47 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { CopilotClient } from "../src/client.js";
+import { approveAll } from "../src/index.js";
+import { joinSession } from "../src/extension.js";
+
+describe("joinSession", () => {
+ const originalSessionId = process.env.SESSION_ID;
+
+ afterEach(() => {
+ if (originalSessionId === undefined) {
+ delete process.env.SESSION_ID;
+ } else {
+ process.env.SESSION_ID = originalSessionId;
+ }
+ vi.restoreAllMocks();
+ });
+
+ it("defaults onPermissionRequest to no-result", async () => {
+ process.env.SESSION_ID = "session-123";
+ const resumeSession = vi
+ .spyOn(CopilotClient.prototype, "resumeSession")
+ .mockResolvedValue({} as any);
+
+ await joinSession({ tools: [] });
+
+ const [, config] = resumeSession.mock.calls[0]!;
+ expect(config.onPermissionRequest).toBeDefined();
+ const result = await Promise.resolve(
+ config.onPermissionRequest!({ kind: "write" }, { sessionId: "session-123" })
+ );
+ expect(result).toEqual({ kind: "no-result" });
+ expect(config.disableResume).toBe(true);
+ });
+
+ it("preserves an explicit onPermissionRequest handler", async () => {
+ process.env.SESSION_ID = "session-123";
+ const resumeSession = vi
+ .spyOn(CopilotClient.prototype, "resumeSession")
+ .mockResolvedValue({} as any);
+
+ await joinSession({ onPermissionRequest: approveAll, disableResume: false });
+
+ const [, config] = resumeSession.mock.calls[0]!;
+ expect(config.onPermissionRequest).toBe(approveAll);
+ expect(config.disableResume).toBe(false);
+ });
+});
diff --git a/nodejs/test/telemetry.test.ts b/nodejs/test/telemetry.test.ts
new file mode 100644
index 0000000000..9ad97b63a1
--- /dev/null
+++ b/nodejs/test/telemetry.test.ts
@@ -0,0 +1,133 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { describe, expect, it } from "vitest";
+import { getTraceContext } from "../src/telemetry.js";
+import type { TraceContextProvider } from "../src/types.js";
+
+describe("telemetry", () => {
+ describe("getTraceContext", () => {
+ it("returns empty object when no provider is given", async () => {
+ const ctx = await getTraceContext();
+ expect(ctx).toEqual({});
+ });
+
+ it("returns empty object when provider is undefined", async () => {
+ const ctx = await getTraceContext(undefined);
+ expect(ctx).toEqual({});
+ });
+
+ it("calls provider and returns trace context", async () => {
+ const provider: TraceContextProvider = () => ({
+ traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
+ tracestate: "congo=t61rcWkgMzE",
+ });
+ const ctx = await getTraceContext(provider);
+ expect(ctx).toEqual({
+ traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
+ tracestate: "congo=t61rcWkgMzE",
+ });
+ });
+
+ it("supports async providers", async () => {
+ const provider: TraceContextProvider = async () => ({
+ traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01",
+ });
+ const ctx = await getTraceContext(provider);
+ expect(ctx).toEqual({
+ traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01",
+ });
+ });
+
+ it("returns empty object when provider throws", async () => {
+ const provider: TraceContextProvider = () => {
+ throw new Error("boom");
+ };
+ const ctx = await getTraceContext(provider);
+ expect(ctx).toEqual({});
+ });
+
+ it("returns empty object when async provider rejects", async () => {
+ const provider: TraceContextProvider = async () => {
+ throw new Error("boom");
+ };
+ const ctx = await getTraceContext(provider);
+ expect(ctx).toEqual({});
+ });
+
+ it("returns empty object when provider returns null", async () => {
+ const provider = (() => null) as unknown as TraceContextProvider;
+ const ctx = await getTraceContext(provider);
+ expect(ctx).toEqual({});
+ });
+ });
+
+ describe("TelemetryConfig env var mapping", () => {
+ it("sets correct env vars for full telemetry config", async () => {
+ const telemetry = {
+ otlpEndpoint: "http://localhost:4318",
+ filePath: "/tmp/traces.jsonl",
+ exporterType: "otlp-http",
+ sourceName: "my-app",
+ captureContent: true,
+ };
+
+ const env: Record = {};
+
+ if (telemetry) {
+ const t = telemetry;
+ env.COPILOT_OTEL_ENABLED = "true";
+ if (t.otlpEndpoint !== undefined) env.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint;
+ if (t.filePath !== undefined) env.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath;
+ if (t.exporterType !== undefined) env.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType;
+ if (t.sourceName !== undefined) env.COPILOT_OTEL_SOURCE_NAME = t.sourceName;
+ if (t.captureContent !== undefined)
+ env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String(
+ t.captureContent
+ );
+ }
+
+ expect(env).toEqual({
+ COPILOT_OTEL_ENABLED: "true",
+ OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318",
+ COPILOT_OTEL_FILE_EXPORTER_PATH: "/tmp/traces.jsonl",
+ COPILOT_OTEL_EXPORTER_TYPE: "otlp-http",
+ COPILOT_OTEL_SOURCE_NAME: "my-app",
+ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "true",
+ });
+ });
+
+ it("only sets COPILOT_OTEL_ENABLED for empty telemetry config", async () => {
+ const telemetry = {};
+ const env: Record = {};
+
+ if (telemetry) {
+ const t = telemetry as any;
+ env.COPILOT_OTEL_ENABLED = "true";
+ if (t.otlpEndpoint !== undefined) env.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint;
+ if (t.filePath !== undefined) env.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath;
+ if (t.exporterType !== undefined) env.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType;
+ if (t.sourceName !== undefined) env.COPILOT_OTEL_SOURCE_NAME = t.sourceName;
+ if (t.captureContent !== undefined)
+ env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String(
+ t.captureContent
+ );
+ }
+
+ expect(env).toEqual({
+ COPILOT_OTEL_ENABLED: "true",
+ });
+ });
+
+ it("converts captureContent false to string 'false'", async () => {
+ const telemetry = { captureContent: false };
+ const env: Record = {};
+
+ env.COPILOT_OTEL_ENABLED = "true";
+ if (telemetry.captureContent !== undefined)
+ env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String(
+ telemetry.captureContent
+ );
+
+ expect(env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT).toBe("false");
+ });
+ });
+});
diff --git a/python/README.md b/python/README.md
index 5b87bb04e9..139098fa39 100644
--- a/python/README.md
+++ b/python/README.md
@@ -7,9 +7,9 @@ Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC.
## Installation
```bash
-pip install -e ".[dev]"
+pip install -e ".[telemetry,dev]"
# or
-uv pip install -e ".[dev]"
+uv pip install -e ".[telemetry,dev]"
```
## Run the Sample
@@ -25,15 +25,15 @@ python chat.py
```python
import asyncio
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
async def main():
# Create and start client
client = CopilotClient()
await client.start()
- # Create a session
- session = await client.create_session({"model": "gpt-5"})
+ # Create a session (on_permission_request is required)
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5")
# Wait for response using session.idle event
done = asyncio.Event()
@@ -47,7 +47,7 @@ async def main():
session.on(on_event)
# Send a message and wait for completion
- await session.send({"prompt": "What is 2+2?"})
+ await session.send("What is 2+2?")
await done.wait()
# Clean up
@@ -60,8 +60,8 @@ asyncio.run(main())
Sessions also support the `async with` context manager pattern for automatic cleanup:
```python
-async with await client.create_session({"model": "gpt-5"}) as session:
- await session.send({"prompt": "What is 2+2?"})
+async with await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") as session:
+ await session.send("What is 2+2?")
# session is automatically disconnected when leaving the block
```
@@ -79,22 +79,19 @@ async with await client.create_session({"model": "gpt-5"}) as session:
### CopilotClient
```python
-client = CopilotClient({
- "cli_path": "copilot", # Optional: path to CLI executable
- "cli_url": None, # Optional: URL of existing server (e.g., "localhost:8080")
- "log_level": "info", # Optional: log level (default: "info")
- "auto_start": True, # Optional: auto-start server (default: True)
- "auto_restart": True, # Optional: auto-restart on crash (default: True)
-})
+from copilot import CopilotClient, SubprocessConfig
+
+# Spawn a local CLI process (default)
+client = CopilotClient() # uses bundled CLI, stdio transport
await client.start()
-session = await client.create_session({"model": "gpt-5"})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5")
def on_event(event):
print(f"Event: {event['type']}")
session.on(on_event)
-await session.send({"prompt": "Hello!"})
+await session.send("Hello!")
# ... wait for events ...
@@ -102,31 +99,97 @@ await session.disconnect()
await client.stop()
```
-**CopilotClient Options:**
+```python
+from copilot import CopilotClient, ExternalServerConfig
-- `cli_path` (str): Path to CLI executable (default: "copilot" or `COPILOT_CLI_PATH` env var)
-- `cli_url` (str): URL of existing CLI server (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). When provided, the client will not spawn a CLI process.
-- `cwd` (str): Working directory for CLI process
-- `port` (int): Server port for TCP mode (default: 0 for random)
+# Connect to an existing CLI server
+client = CopilotClient(ExternalServerConfig(url="localhost:3000"))
+```
+
+**CopilotClient Constructor:**
+
+```python
+CopilotClient(
+ config=None, # SubprocessConfig | ExternalServerConfig | None
+ *,
+ auto_start=True, # auto-start server on first use
+ on_list_models=None, # custom handler for list_models()
+)
+```
+
+**SubprocessConfig** — spawn a local CLI process:
+
+- `cli_path` (str | None): Path to CLI executable (default: bundled binary)
+- `cli_args` (list[str]): Extra arguments for the CLI executable
+- `cwd` (str | None): Working directory for CLI process (default: current dir)
- `use_stdio` (bool): Use stdio transport instead of TCP (default: True)
+- `port` (int): Server port for TCP mode (default: 0 for random)
- `log_level` (str): Log level (default: "info")
-- `auto_start` (bool): Auto-start server on first use (default: True)
-- `auto_restart` (bool): Auto-restart on crash (default: True)
-- `github_token` (str): GitHub token for authentication. When provided, takes priority over other auth methods.
-- `use_logged_in_user` (bool): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). Cannot be used with `cli_url`.
+- `env` (dict | None): Environment variables for the CLI process
+- `github_token` (str | None): GitHub token for authentication. When provided, takes priority over other auth methods.
+- `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided).
+- `telemetry` (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below.
+
+**ExternalServerConfig** — connect to an existing CLI server:
+
+- `url` (str): Server URL (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`).
+
+**`create_session` Parameters:**
-**SessionConfig Options (for `create_session`):**
+All parameters are keyword-only:
-- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.**
+- `on_permission_request` (callable): **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
+- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.).
+- `session_id` (str): Custom session ID for resuming or identifying sessions.
+- `client_name` (str): Client name to identify the application using the SDK. Included in the User-Agent header for API requests.
- `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `list_models()` to check which models support this option.
-- `session_id` (str): Custom session ID
-- `tools` (list): Custom tools exposed to the CLI
-- `system_message` (dict): System message configuration
-- `streaming` (bool): Enable streaming delta events
-- `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section.
-- `infinite_sessions` (dict): Automatic context compaction configuration
+- `tools` (list): Custom tools exposed to the CLI.
+- `system_message` (dict): System message configuration. Supports three modes:
+ - **append** (default): Appends `content` after the SDK-managed prompt
+ - **replace**: Replaces the entire prompt with `content`
+ - **customize**: Selectively override individual sections via `sections` dict (keys: `"identity"`, `"tone"`, `"tool_efficiency"`, `"environment_context"`, `"code_change_rules"`, `"guidelines"`, `"safety"`, `"tool_instructions"`, `"custom_instructions"`, `"last_instructions"`; values: `SectionOverride` with `action` and optional `content`)
+- `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`.
+- `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set.
- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `hooks` (dict): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
+- `working_directory` (str): Working directory for the session. Tool operations will be relative to this directory.
+- `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section.
+- `streaming` (bool): Enable streaming delta events.
+- `mcp_servers` (dict): MCP server configurations for the session.
+- `custom_agents` (list): Custom agent configurations for the session.
+- `config_dir` (str): Override the default configuration directory location.
+- `skill_directories` (list[str]): Directories to load skills from.
+- `disabled_skills` (list[str]): List of skill names to disable.
+- `infinite_sessions` (dict): Automatic context compaction configuration.
+
+**`resume_session` Parameters:**
+
+- `session_id` (str): **Required.** The ID of the session to resume.
+
+The parameters below are keyword-only:
+
+- `on_permission_request` (callable): **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
+- `model` (str): Model to use (can change the model when resuming).
+- `client_name` (str): Client name to identify the application using the SDK.
+- `reasoning_effort` (str): Reasoning effort level ("low", "medium", "high", "xhigh").
+- `tools` (list): Custom tools exposed to the CLI.
+- `system_message` (dict): System message configuration.
+- `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`.
+- `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set.
+- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool).
+- `hooks` (dict): Hook handlers for session lifecycle events.
+- `working_directory` (str): Working directory for the session.
+- `provider` (dict): Custom API provider configuration (BYOK).
+- `streaming` (bool): Enable streaming delta events.
+- `mcp_servers` (dict): MCP server configurations for the session.
+- `custom_agents` (list): Custom agent configurations for the session.
+- `agent` (str): Name of the custom agent to activate when the session starts.
+- `config_dir` (str): Override the default configuration directory location.
+- `skill_directories` (list[str]): Directories to load skills from.
+- `disabled_skills` (list[str]): List of skill names to disable.
+- `infinite_sessions` (dict): Automatic context compaction configuration.
+- `disable_resume` (bool): Skip emitting the session.resume event (default: False).
+- `on_event` (callable): Event handler registered before the session.resume RPC.
**Session Lifecycle Methods:**
@@ -157,13 +220,61 @@ unsubscribe()
- `session.foreground` - A session became the foreground session in TUI
- `session.background` - A session is no longer the foreground session
+### System Message Customization
+
+Control the system prompt using `system_message` in session config:
+
+```python
+session = await client.create_session(
+ system_message={
+ "content": "Always check for security vulnerabilities before suggesting changes."
+ }
+)
+```
+
+The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"` or `mode: "customize"`.
+
+#### Customize Mode
+
+Use `mode: "customize"` to selectively override individual sections of the prompt while preserving the rest:
+
+```python
+from copilot import SYSTEM_PROMPT_SECTIONS
+
+session = await client.create_session(
+ system_message={
+ "mode": "customize",
+ "sections": {
+ # Replace the tone/style section
+ "tone": {"action": "replace", "content": "Respond in a warm, professional tone. Be thorough in explanations."},
+ # Remove coding-specific rules
+ "code_change_rules": {"action": "remove"},
+ # Append to existing guidelines
+ "guidelines": {"action": "append", "content": "\n* Always cite data sources"},
+ },
+ # Additional instructions appended after all sections
+ "content": "Focus on financial analysis and reporting.",
+ }
+)
+```
+
+Available section IDs: `"identity"`, `"tone"`, `"tool_efficiency"`, `"environment_context"`, `"code_change_rules"`, `"guidelines"`, `"safety"`, `"tool_instructions"`, `"custom_instructions"`, `"last_instructions"`. Use the `SYSTEM_PROMPT_SECTIONS` dict for descriptions of each section.
+
+Each section override supports four actions:
+- **`replace`** — Replace the section content entirely
+- **`remove`** — Remove the section from the prompt
+- **`append`** — Add content after the existing section
+- **`prepend`** — Add content before the existing section
+
+Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
+
### Tools
Define tools with automatic JSON schema generation using the `@define_tool` decorator and Pydantic models:
```python
from pydantic import BaseModel, Field
-from copilot import CopilotClient, define_tool
+from copilot import CopilotClient, define_tool, PermissionHandler
class LookupIssueParams(BaseModel):
id: str = Field(description="Issue identifier")
@@ -173,10 +284,11 @@ async def lookup_issue(params: LookupIssueParams) -> str:
issue = await fetch_issue(params.id)
return issue.summary
-session = await client.create_session({
- "model": "gpt-5",
- "tools": [lookup_issue],
-})
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-5",
+ tools=[lookup_issue],
+)
```
> **Note:** When using `from __future__ import annotations`, define Pydantic models at module level (not inside functions).
@@ -186,7 +298,7 @@ session = await client.create_session({
For users who prefer manual schema definition:
```python
-from copilot import CopilotClient, Tool
+from copilot import CopilotClient, Tool, PermissionHandler
async def lookup_issue(invocation):
issue_id = invocation["arguments"]["id"]
@@ -197,9 +309,10 @@ async def lookup_issue(invocation):
"sessionLog": f"Fetched issue {issue_id}",
}
-session = await client.create_session({
- "model": "gpt-5",
- "tools": [
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-5",
+ tools=[
Tool(
name="lookup_issue",
description="Fetch issue details from our tracker",
@@ -213,7 +326,7 @@ session = await client.create_session({
handler=lookup_issue,
)
],
-})
+)
```
The SDK automatically handles `tool.call`, executes your handler (sync or async), and responds with the final result when the tool completes.
@@ -232,26 +345,49 @@ async def edit_file(params: EditFileParams) -> str:
# your logic
```
+#### Skipping Permission Prompts
+
+Set `skip_permission=True` on a tool definition to allow it to execute without triggering a permission prompt:
+
+```python
+@define_tool(name="safe_lookup", description="A read-only lookup that needs no confirmation", skip_permission=True)
+async def safe_lookup(params: LookupParams) -> str:
+ # your logic
+```
+
## Image Support
-The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path:
+The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment:
```python
-await session.send({
- "prompt": "What's in this image?",
- "attachments": [
+# File attachment — runtime reads from disk
+await session.send(
+ "What's in this image?",
+ attachments=[
{
"type": "file",
"path": "/path/to/image.jpg",
}
- ]
-})
+ ],
+)
+
+# Blob attachment — provide base64 data directly
+await session.send(
+ "What's in this image?",
+ attachments=[
+ {
+ "type": "blob",
+ "data": base64_image_data,
+ "mimeType": "image/png",
+ }
+ ],
+)
```
Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like:
```python
-await session.send({"prompt": "What does the most recent jpg in this directory portray?"})
+await session.send("What does the most recent jpg in this directory portray?")
```
## Streaming
@@ -260,16 +396,17 @@ Enable streaming to receive assistant response chunks as they're generated:
```python
import asyncio
-from copilot import CopilotClient
+from copilot import CopilotClient, PermissionHandler
async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session({
- "model": "gpt-5",
- "streaming": True
- })
+ session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-5",
+ streaming=True,
+ )
# Use asyncio.Event to wait for completion
done = asyncio.Event()
@@ -296,7 +433,7 @@ async def main():
done.set()
session.on(on_event)
- await session.send({"prompt": "Tell me a short story"})
+ await session.send("Tell me a short story")
await done.wait() # Wait for streaming to complete
await session.disconnect()
@@ -320,27 +457,29 @@ By default, sessions use **infinite sessions** which automatically manage contex
```python
# Default: infinite sessions enabled with default thresholds
-session = await client.create_session({"model": "gpt-5"})
+session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5")
# Access the workspace path for checkpoints and files
print(session.workspace_path)
# => ~/.copilot/session-state/{session_id}/
# Custom thresholds
-session = await client.create_session({
- "model": "gpt-5",
- "infinite_sessions": {
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-5",
+ infinite_sessions={
"enabled": True,
"background_compaction_threshold": 0.80, # Start compacting at 80% context usage
"buffer_exhaustion_threshold": 0.95, # Block at 95% until compaction completes
},
-})
+)
# Disable infinite sessions
-session = await client.create_session({
- "model": "gpt-5",
- "infinite_sessions": {"enabled": False},
-})
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-5",
+ infinite_sessions={"enabled": False},
+)
```
When enabled, sessions emit compaction events:
@@ -364,16 +503,17 @@ The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own K
**Example with Ollama:**
```python
-session = await client.create_session({
- "model": "deepseek-coder-v2:16b", # Required when using custom provider
- "provider": {
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="deepseek-coder-v2:16b", # Model to use with the custom provider
+ provider={
"type": "openai",
"base_url": "http://localhost:11434/v1", # Ollama endpoint
# api_key not required for Ollama
},
-})
+)
-await session.send({"prompt": "Hello!"})
+await session.send("Hello!")
```
**Example with custom OpenAI-compatible API:**
@@ -381,14 +521,15 @@ await session.send({"prompt": "Hello!"})
```python
import os
-session = await client.create_session({
- "model": "gpt-4",
- "provider": {
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-4",
+ provider={
"type": "openai",
"base_url": "https://my-api.example.com/v1",
"api_key": os.environ["MY_API_KEY"],
},
-})
+)
```
**Example with Azure OpenAI:**
@@ -396,9 +537,10 @@ session = await client.create_session({
```python
import os
-session = await client.create_session({
- "model": "gpt-4",
- "provider": {
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-4",
+ provider={
"type": "azure", # Must be "azure" for Azure endpoints, NOT "openai"
"base_url": "https://my-resource.openai.azure.com", # Just the host, no path
"api_key": os.environ["AZURE_OPENAI_KEY"],
@@ -406,14 +548,124 @@ session = await client.create_session({
"api_version": "2024-10-21",
},
},
-})
+)
```
> **Important notes:**
-> - When using a custom provider, the `model` parameter is **required**. The SDK will throw an error if no model is specified.
> - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`.
> - The `base_url` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically.
+## Telemetry
+
+The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export and automatic W3C Trace Context propagation.
+
+```python
+from copilot import CopilotClient, SubprocessConfig
+
+client = CopilotClient(SubprocessConfig(
+ telemetry={
+ "otlp_endpoint": "http://localhost:4318",
+ },
+))
+```
+
+**TelemetryConfig options:**
+
+- `otlp_endpoint` (str): OTLP HTTP endpoint URL
+- `file_path` (str): File path for JSON-lines trace output
+- `exporter_type` (str): `"otlp-http"` or `"file"`
+- `source_name` (str): Instrumentation scope name
+- `capture_content` (bool): Whether to capture message content
+
+Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `create_session`, `resume_session`, and `send` calls, and inbound when the CLI invokes tool handlers.
+
+Install with telemetry extras: `pip install copilot-sdk[telemetry]` (provides `opentelemetry-api`)
+
+## Permission Handling
+
+An `on_permission_request` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision.
+
+### Approve All (simplest)
+
+Use the built-in `PermissionHandler.approve_all` helper to allow every tool call without any checks:
+
+```python
+from copilot import CopilotClient, PermissionHandler
+
+session = await client.create_session({
+ "model": "gpt-5",
+ "on_permission_request": PermissionHandler.approve_all,
+})
+```
+
+### Custom Permission Handler
+
+Provide your own function to inspect each request and apply custom logic (sync or async):
+
+```python
+from copilot import PermissionRequest, PermissionRequestResult
+
+def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
+ # request.kind — what type of operation is being requested:
+ # "shell" — executing a shell command
+ # "write" — writing or editing a file
+ # "read" — reading a file
+ # "mcp" — calling an MCP tool
+ # "custom-tool" — calling one of your registered tools
+ # "url" — fetching a URL
+ # "memory" — accessing or updating session/workspace memory
+ # "hook" — invoking a registered hook
+ # request.tool_call_id — the tool call that triggered this request
+ # request.tool_name — name of the tool (for custom-tool / mcp)
+ # request.file_name — file being written (for write)
+ # request.full_command_text — full shell command (for shell)
+
+ if request.kind.value == "shell":
+ # Deny shell commands
+ return PermissionRequestResult(kind="denied-interactively-by-user")
+
+ return PermissionRequestResult(kind="approved")
+
+session = await client.create_session({
+ "model": "gpt-5",
+ "on_permission_request": on_permission_request,
+})
+```
+
+Async handlers are also supported:
+
+```python
+async def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
+ # Simulate an async approval check (e.g., prompting a user over a network)
+ await asyncio.sleep(0)
+ return PermissionRequestResult(kind="approved")
+```
+
+### Permission Result Kinds
+
+| `kind` value | Meaning |
+|---|---------|
+| `"approved"` | Allow the tool to run |
+| `"denied-interactively-by-user"` | User explicitly denied the request |
+| `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked (default when no kind is specified) |
+| `"denied-by-rules"` | Denied by a policy rule |
+| `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy |
+| `"no-result"` | Leave the request unanswered (not allowed for protocol v2 permission requests) |
+
+### Resuming Sessions
+
+Pass `on_permission_request` when resuming a session too — it is required:
+
+```python
+session = await client.resume_session("session-id", {
+ "on_permission_request": PermissionHandler.approve_all,
+})
+```
+
+### Per-Tool Skip Permission
+
+To let a specific custom tool bypass the permission prompt entirely, set `skip_permission=True` on the tool definition. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools.
+
## User Input Requests
Enable the agent to ask questions to the user using the `ask_user` tool by providing an `on_user_input_request` handler:
@@ -434,10 +686,11 @@ async def handle_user_input(request, invocation):
"wasFreeform": True, # Whether the answer was freeform (not from choices)
}
-session = await client.create_session({
- "model": "gpt-5",
- "on_user_input_request": handle_user_input,
-})
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-5",
+ on_user_input_request=handle_user_input,
+)
```
## Session Hooks
@@ -481,9 +734,10 @@ async def on_error_occurred(input, invocation):
"errorHandling": "retry", # "retry", "skip", or "abort"
}
-session = await client.create_session({
- "model": "gpt-5",
- "hooks": {
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ model="gpt-5",
+ hooks={
"on_pre_tool_use": on_pre_tool_use,
"on_post_tool_use": on_post_tool_use,
"on_user_prompt_submitted": on_user_prompt_submitted,
@@ -491,7 +745,7 @@ session = await client.create_session({
"on_session_end": on_session_end,
"on_error_occurred": on_error_occurred,
},
-})
+)
```
**Available hooks:**
diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py
index f5f7ed0b18..6a007afa30 100644
--- a/python/copilot/__init__.py
+++ b/python/copilot/__init__.py
@@ -8,15 +8,20 @@
from .session import CopilotSession
from .tools import define_tool
from .types import (
+ SYSTEM_PROMPT_SECTIONS,
+ Attachment,
AzureProviderOptions,
+ BlobAttachment,
ConnectionState,
CustomAgentConfig,
+ DirectoryAttachment,
+ ExternalServerConfig,
+ FileAttachment,
GetAuthStatusResponse,
GetStatusResponse,
MCPLocalServerConfig,
MCPRemoteServerConfig,
MCPServerConfig,
- MessageOptions,
ModelBilling,
ModelCapabilities,
ModelInfo,
@@ -26,13 +31,22 @@
PermissionRequestResult,
PingResponse,
ProviderConfig,
- ResumeSessionConfig,
- SessionConfig,
+ SectionOverride,
+ SectionOverrideAction,
+ SectionTransformFn,
+ SelectionAttachment,
SessionContext,
SessionEvent,
SessionListFilter,
SessionMetadata,
StopError,
+ SubprocessConfig,
+ SystemMessageAppendConfig,
+ SystemMessageConfig,
+ SystemMessageCustomizeConfig,
+ SystemMessageReplaceConfig,
+ SystemPromptSection,
+ TelemetryConfig,
Tool,
ToolHandler,
ToolInvocation,
@@ -42,17 +56,21 @@
__version__ = "0.1.0"
__all__ = [
+ "Attachment",
"AzureProviderOptions",
+ "BlobAttachment",
"CopilotClient",
"CopilotSession",
"ConnectionState",
"CustomAgentConfig",
+ "DirectoryAttachment",
+ "ExternalServerConfig",
+ "FileAttachment",
"GetAuthStatusResponse",
"GetStatusResponse",
"MCPLocalServerConfig",
"MCPRemoteServerConfig",
"MCPServerConfig",
- "MessageOptions",
"ModelBilling",
"ModelCapabilities",
"ModelInfo",
@@ -62,13 +80,23 @@
"PermissionRequestResult",
"PingResponse",
"ProviderConfig",
- "ResumeSessionConfig",
- "SessionConfig",
+ "SectionOverride",
+ "SectionOverrideAction",
+ "SectionTransformFn",
+ "SelectionAttachment",
"SessionContext",
"SessionEvent",
"SessionListFilter",
"SessionMetadata",
"StopError",
+ "SYSTEM_PROMPT_SECTIONS",
+ "SubprocessConfig",
+ "SystemMessageAppendConfig",
+ "SystemMessageConfig",
+ "SystemMessageCustomizeConfig",
+ "SystemMessageReplaceConfig",
+ "SystemPromptSection",
+ "TelemetryConfig",
"Tool",
"ToolHandler",
"ToolInvocation",
diff --git a/python/copilot/jsonrpc.py b/python/copilot/_jsonrpc.py
similarity index 99%
rename from python/copilot/jsonrpc.py
rename to python/copilot/_jsonrpc.py
index fc8255274d..287f1b965d 100644
--- a/python/copilot/jsonrpc.py
+++ b/python/copilot/_jsonrpc.py
@@ -60,6 +60,7 @@ def __init__(self, process):
self._process_exit_error: str | None = None
self._stderr_output: list[str] = []
self._stderr_lock = threading.Lock()
+ self.on_close: Callable[[], None] | None = None
def start(self, loop: asyncio.AbstractEventLoop | None = None):
"""Start listening for messages in background thread"""
@@ -211,6 +212,8 @@ def _read_loop(self):
# Process exited or read failed - fail all pending requests
if self._running:
self._fail_pending_requests()
+ if self.on_close is not None:
+ self.on_close()
def _fail_pending_requests(self):
"""Fail all pending requests when process exits"""
diff --git a/python/copilot/sdk_protocol_version.py b/python/copilot/_sdk_protocol_version.py
similarity index 100%
rename from python/copilot/sdk_protocol_version.py
rename to python/copilot/_sdk_protocol_version.py
diff --git a/python/copilot/_telemetry.py b/python/copilot/_telemetry.py
new file mode 100644
index 0000000000..caa27a4e7c
--- /dev/null
+++ b/python/copilot/_telemetry.py
@@ -0,0 +1,48 @@
+"""OpenTelemetry trace context helpers for Copilot SDK."""
+
+from __future__ import annotations
+
+from collections.abc import Generator
+from contextlib import contextmanager
+
+
+def get_trace_context() -> dict[str, str]:
+ """Get the current W3C Trace Context (traceparent/tracestate) if OpenTelemetry is available."""
+ try:
+ from opentelemetry import context, propagate
+ except ImportError:
+ return {}
+
+ carrier: dict[str, str] = {}
+ propagate.inject(carrier, context=context.get_current())
+ result: dict[str, str] = {}
+ if "traceparent" in carrier:
+ result["traceparent"] = carrier["traceparent"]
+ if "tracestate" in carrier:
+ result["tracestate"] = carrier["tracestate"]
+ return result
+
+
+@contextmanager
+def trace_context(traceparent: str | None, tracestate: str | None) -> Generator[None, None, None]:
+ """Context manager that sets the trace context from W3C headers for the block's duration."""
+ try:
+ from opentelemetry import context, propagate
+ except ImportError:
+ yield
+ return
+
+ if not traceparent:
+ yield
+ return
+
+ carrier: dict[str, str] = {"traceparent": traceparent}
+ if tracestate:
+ carrier["tracestate"] = tracestate
+
+ ctx = propagate.extract(carrier, context=context.get_current())
+ token = context.attach(ctx)
+ try:
+ yield
+ finally:
+ context.detach(token)
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 7ea4e97a1a..e9dd98d35d 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -5,48 +5,68 @@
to the Copilot CLI server and provides session management capabilities.
Example:
- >>> from copilot import CopilotClient
+ >>> from copilot import CopilotClient, PermissionHandler
>>>
>>> async with CopilotClient() as client:
- ... session = await client.create_session()
- ... await session.send({"prompt": "Hello!"})
+ ... session = await client.create_session(
+ ... on_permission_request=PermissionHandler.approve_all
+ ... )
+ ... await session.send("Hello!")
"""
import asyncio
import inspect
import os
import re
+import shutil
import subprocess
import sys
import threading
-from collections.abc import Callable
+import uuid
+from collections.abc import Awaitable, Callable
from pathlib import Path
-from typing import Any, cast
+from typing import Any, cast, overload
+from ._jsonrpc import JsonRpcClient, ProcessExitedError
+from ._sdk_protocol_version import get_sdk_protocol_version
+from ._telemetry import get_trace_context, trace_context
from .generated.rpc import ServerRpc
from .generated.session_events import PermissionRequest, session_event_from_dict
-from .jsonrpc import JsonRpcClient, ProcessExitedError
-from .sdk_protocol_version import get_sdk_protocol_version
from .session import CopilotSession
from .types import (
ConnectionState,
- CopilotClientOptions,
CustomAgentConfig,
+ ExternalServerConfig,
GetAuthStatusResponse,
GetStatusResponse,
+ InfiniteSessionConfig,
+ MCPServerConfig,
ModelInfo,
PingResponse,
ProviderConfig,
- ResumeSessionConfig,
- SessionConfig,
+ ReasoningEffort,
+ SectionTransformFn,
+ SessionEvent,
+ SessionHooks,
SessionLifecycleEvent,
SessionLifecycleEventType,
SessionLifecycleHandler,
SessionListFilter,
SessionMetadata,
StopError,
+ SubprocessConfig,
+ SystemMessageConfig,
+ Tool,
ToolInvocation,
ToolResult,
+ UserInputHandler,
+ _PermissionHandlerFn,
+)
+
+HandlerUnsubcribe = Callable[[], None]
+
+NO_RESULT_PERMISSION_V2_ERROR = (
+ "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."
)
# Minimum protocol version this SDK can communicate with.
@@ -54,6 +74,40 @@
MIN_PROTOCOL_VERSION = 2
+def _extract_transform_callbacks(
+ system_message: dict | None,
+) -> tuple[dict | None, dict[str, SectionTransformFn] | None]:
+ """Extract function-valued actions from system message config.
+
+ Returns a wire-safe payload (with callable actions replaced by ``"transform"``)
+ and a dict of transform callbacks keyed by section ID.
+ """
+ if (
+ not system_message
+ or system_message.get("mode") != "customize"
+ or not system_message.get("sections")
+ ):
+ return system_message, None
+
+ callbacks: dict[str, SectionTransformFn] = {}
+ wire_sections: dict[str, dict] = {}
+ for section_id, override in system_message["sections"].items():
+ if not override:
+ continue
+ action = override.get("action")
+ if callable(action):
+ callbacks[section_id] = action
+ wire_sections[section_id] = {"action": "transform"}
+ else:
+ wire_sections[section_id] = override
+
+ if not callbacks:
+ return system_message, None
+
+ wire_payload = {**system_message, "sections": wire_sections}
+ return wire_payload, callbacks
+
+
def _get_bundled_cli_path() -> str | None:
"""Get the path to the bundled CLI binary, if available."""
# The binary is bundled in copilot/bin/ within the package
@@ -85,120 +139,92 @@ class CopilotClient:
The client supports both stdio (default) and TCP transport modes for
communication with the CLI server.
- Attributes:
- options: The configuration options for the client.
-
Example:
>>> # Create a client with default options (spawns CLI server)
>>> client = CopilotClient()
>>> await client.start()
>>>
>>> # Create a session and send a message
- >>> session = await client.create_session({
- ... "on_permission_request": PermissionHandler.approve_all,
- ... "model": "gpt-4",
- ... })
+ >>> session = await client.create_session(
+ ... PermissionHandler.approve_all,
+ ... "gpt-4",
+ ... )
>>> session.on(lambda event: print(event.type))
- >>> await session.send({"prompt": "Hello!"})
+ >>> await session.send("Hello!")
>>>
>>> # Clean up
>>> await session.disconnect()
>>> await client.stop()
>>> # Or connect to an existing server
- >>> client = CopilotClient({"cli_url": "localhost:3000"})
+ >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000"))
"""
- def __init__(self, options: CopilotClientOptions | None = None):
+ def __init__(
+ self,
+ config: SubprocessConfig | ExternalServerConfig | None = None,
+ *,
+ auto_start: bool = True,
+ on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None,
+ ):
"""
Initialize a new CopilotClient.
Args:
- options: Optional configuration options for the client. If not provided,
- default options are used (spawns CLI server using stdio).
-
- Raises:
- ValueError: If mutually exclusive options are provided (e.g., cli_url
- with use_stdio or cli_path).
+ config: Connection configuration. Pass a :class:`SubprocessConfig` to
+ spawn a local CLI process, or an :class:`ExternalServerConfig` to
+ connect to an existing server. Defaults to ``SubprocessConfig()``.
+ auto_start: Automatically start the connection on first use
+ (default: ``True``).
+ on_list_models: Custom handler for :meth:`list_models`. When provided,
+ the handler is called instead of querying the CLI server.
Example:
- >>> # Default options - spawns CLI server using stdio
+ >>> # Default — spawns CLI server using stdio
>>> client = CopilotClient()
>>>
>>> # Connect to an existing server
- >>> client = CopilotClient({"cli_url": "localhost:3000"})
+ >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000"))
>>>
>>> # Custom CLI path with specific log level
- >>> client = CopilotClient({
- ... "cli_path": "/usr/local/bin/copilot",
- ... "log_level": "debug"
- ... })
+ >>> client = CopilotClient(
+ ... SubprocessConfig(
+ ... cli_path="/usr/local/bin/copilot",
+ ... log_level="debug",
+ ... )
+ ... )
"""
- opts = options or {}
+ if config is None:
+ config = SubprocessConfig()
- # Validate mutually exclusive options
- if opts.get("cli_url") and (opts.get("use_stdio") or opts.get("cli_path")):
- raise ValueError("cli_url is mutually exclusive with use_stdio and cli_path")
+ self._config: SubprocessConfig | ExternalServerConfig = config
+ self._auto_start = auto_start
+ self._on_list_models = on_list_models
- # Validate auth options with external server
- if opts.get("cli_url") and (
- opts.get("github_token") or opts.get("use_logged_in_user") is not None
- ):
- raise ValueError(
- "github_token and use_logged_in_user cannot be used with cli_url "
- "(external server manages its own auth)"
- )
-
- # Parse cli_url if provided
+ # Resolve connection-mode-specific state
self._actual_host: str = "localhost"
- self._is_external_server: bool = False
- if opts.get("cli_url"):
- self._actual_host, actual_port = self._parse_cli_url(opts["cli_url"])
+ self._is_external_server: bool = isinstance(config, ExternalServerConfig)
+
+ if isinstance(config, ExternalServerConfig):
+ self._actual_host, actual_port = self._parse_cli_url(config.url)
self._actual_port: int | None = actual_port
- self._is_external_server = True
else:
self._actual_port = None
- # Determine CLI path: explicit option > bundled binary
- # Not needed when connecting to external server via cli_url
- if opts.get("cli_url"):
- default_cli_path = "" # Not used for external server
- elif opts.get("cli_path"):
- default_cli_path = opts["cli_path"]
- else:
- bundled_path = _get_bundled_cli_path()
- if bundled_path:
- default_cli_path = bundled_path
- else:
- raise RuntimeError(
- "Copilot CLI not found. The bundled CLI binary is not available. "
- "Ensure you installed a platform-specific wheel, or provide cli_path."
- )
+ # Resolve CLI path: explicit > bundled binary
+ if config.cli_path is None:
+ bundled_path = _get_bundled_cli_path()
+ if bundled_path:
+ config.cli_path = bundled_path
+ else:
+ raise RuntimeError(
+ "Copilot CLI not found. The bundled CLI binary is not available. "
+ "Ensure you installed a platform-specific wheel, or provide cli_path."
+ )
- # Default use_logged_in_user to False when github_token is provided
- github_token = opts.get("github_token")
- use_logged_in_user = opts.get("use_logged_in_user")
- if use_logged_in_user is None:
- use_logged_in_user = False if github_token else True
-
- self.options: CopilotClientOptions = {
- "cli_path": default_cli_path,
- "cwd": opts.get("cwd", os.getcwd()),
- "port": opts.get("port", 0),
- "use_stdio": False if opts.get("cli_url") else opts.get("use_stdio", True),
- "log_level": opts.get("log_level", "info"),
- "auto_start": opts.get("auto_start", True),
- "auto_restart": opts.get("auto_restart", True),
- "use_logged_in_user": use_logged_in_user,
- }
- if opts.get("cli_args"):
- self.options["cli_args"] = opts["cli_args"]
- if opts.get("cli_url"):
- self.options["cli_url"] = opts["cli_url"]
- if opts.get("env"):
- self.options["env"] = opts["env"]
- if github_token:
- self.options["github_token"] = github_token
+ # Resolve use_logged_in_user default
+ if config.use_logged_in_user is None:
+ config.use_logged_in_user = not bool(config.github_token)
self._process: subprocess.Popen | None = None
self._client: JsonRpcClient | None = None
@@ -280,8 +306,9 @@ async def start(self) -> None:
"""
Start the CLI server and establish a connection.
- If connecting to an external server (via cli_url), only establishes the
- connection. Otherwise, spawns the CLI server process and then connects.
+ If connecting to an external server (via :class:`ExternalServerConfig`),
+ only establishes the connection. Otherwise, spawns the CLI server process
+ and then connects.
This method is called automatically when creating a session if ``auto_start``
is True (default).
@@ -290,7 +317,7 @@ async def start(self) -> None:
RuntimeError: If the server fails to start or the connection fails.
Example:
- >>> client = CopilotClient({"auto_start": False})
+ >>> client = CopilotClient(auto_start=False)
>>> await client.start()
>>> # Now ready to create sessions
"""
@@ -443,7 +470,32 @@ async def force_stop(self) -> None:
if not self._is_external_server:
self._actual_port = None
- async def create_session(self, config: SessionConfig) -> CopilotSession:
+ async def create_session(
+ self,
+ *,
+ on_permission_request: _PermissionHandlerFn,
+ model: str | None = None,
+ session_id: str | None = None,
+ client_name: str | None = None,
+ reasoning_effort: ReasoningEffort | None = None,
+ tools: list[Tool] | None = None,
+ system_message: SystemMessageConfig | None = None,
+ available_tools: list[str] | None = None,
+ excluded_tools: list[str] | None = None,
+ on_user_input_request: UserInputHandler | None = None,
+ hooks: SessionHooks | None = None,
+ working_directory: str | None = None,
+ provider: ProviderConfig | None = None,
+ streaming: bool | None = None,
+ mcp_servers: dict[str, MCPServerConfig] | None = None,
+ custom_agents: list[CustomAgentConfig] | None = None,
+ agent: str | None = None,
+ config_dir: str | None = None,
+ skill_directories: list[str] | None = None,
+ disabled_skills: list[str] | None = None,
+ infinite_sessions: InfiniteSessionConfig | None = None,
+ on_event: Callable[[SessionEvent], None] | None = None,
+ ) -> CopilotSession:
"""
Create a new conversation session with the Copilot CLI.
@@ -452,8 +504,29 @@ async def create_session(self, config: SessionConfig) -> CopilotSession:
automatically start the connection.
Args:
- config: Optional configuration for the session, including model selection,
- custom tools, system messages, and more.
+ on_permission_request: Handler for permission requests from the server.
+ model: Model to use for this session.
+ session_id: Custom session ID.
+ client_name: Client name to identify the application using the SDK.
+ reasoning_effort: Reasoning effort level ("low", "medium", "high", "xhigh").
+ tools: Custom tools exposed to the CLI.
+ system_message: System message configuration.
+ available_tools: List of tool names to allow (takes precedence over excluded_tools).
+ excluded_tools: List of tool names to disable (ignored if available_tools is set).
+ on_user_input_request: Handler for user input requests (enables ask_user tool).
+ hooks: Hook handlers for intercepting session lifecycle events.
+ working_directory: Working directory for the session.
+ provider: Custom provider configuration (BYOK - Bring Your Own Key).
+ streaming: Enable streaming of assistant message and reasoning chunks.
+ mcp_servers: MCP server configurations for the session.
+ custom_agents: Custom agent configurations for the session.
+ agent: Name of the custom agent to activate when the session starts.
+ config_dir: Override the default configuration directory location.
+ skill_directories: Directories to load skills from.
+ disabled_skills: List of skill names to disable.
+ infinite_sessions: Infinite session configuration for persistent workspaces.
+ on_event: Event handler registered before the session.create RPC, ensuring
+ early events (e.g. session.start) are not missed.
Returns:
A :class:`CopilotSession` instance for the new session.
@@ -462,34 +535,30 @@ async def create_session(self, config: SessionConfig) -> CopilotSession:
RuntimeError: If the client is not connected and auto_start is disabled.
Example:
- >>> # Basic session
- >>> config = {"on_permission_request": PermissionHandler.approve_all}
- >>> session = await client.create_session(config)
+ >>> session = await client.create_session(
+ ... on_permission_request=PermissionHandler.approve_all,
+ ... )
>>>
>>> # Session with model and streaming
- >>> session = await client.create_session({
- ... "on_permission_request": PermissionHandler.approve_all,
- ... "model": "gpt-4",
- ... "streaming": True
- ... })
+ >>> session = await client.create_session(
+ ... on_permission_request=PermissionHandler.approve_all,
+ ... model="gpt-4",
+ ... streaming=True,
+ ... )
"""
+ if not on_permission_request or not callable(on_permission_request):
+ raise ValueError(
+ "A valid on_permission_request handler is required. "
+ "Use PermissionHandler.approve_all or provide a custom handler."
+ )
+
if not self._client:
- if self.options["auto_start"]:
+ if self._auto_start:
await self.start()
else:
raise RuntimeError("Client not connected. Call start() first.")
- cfg = config
-
- if not cfg.get("on_permission_request"):
- raise ValueError(
- "An on_permission_request handler is required when creating a session. "
- "For example, to allow all permissions, use "
- '{"on_permission_request": PermissionHandler.approve_all}.'
- )
-
tool_defs = []
- tools = cfg.get("tools")
if tools:
for tool in tools:
definition: dict[str, Any] = {
@@ -500,92 +569,67 @@ async def create_session(self, config: SessionConfig) -> CopilotSession:
definition["parameters"] = tool.parameters
if tool.overrides_built_in_tool:
definition["overridesBuiltInTool"] = True
+ if tool.skip_permission:
+ definition["skipPermission"] = True
tool_defs.append(definition)
payload: dict[str, Any] = {}
- if cfg.get("model"):
- payload["model"] = cfg["model"]
- if cfg.get("session_id"):
- payload["sessionId"] = cfg["session_id"]
- if cfg.get("client_name"):
- payload["clientName"] = cfg["client_name"]
- if cfg.get("reasoning_effort"):
- payload["reasoningEffort"] = cfg["reasoning_effort"]
+ if model:
+ payload["model"] = model
+ if client_name:
+ payload["clientName"] = client_name
+ if reasoning_effort:
+ payload["reasoningEffort"] = reasoning_effort
if tool_defs:
payload["tools"] = tool_defs
- # Add system message configuration if provided
- system_message = cfg.get("system_message")
- if system_message:
- payload["systemMessage"] = system_message
+ wire_system_message, transform_callbacks = _extract_transform_callbacks(system_message)
+ if wire_system_message:
+ payload["systemMessage"] = wire_system_message
- # Add tool filtering options
- available_tools = cfg.get("available_tools")
if available_tools is not None:
payload["availableTools"] = available_tools
- excluded_tools = cfg.get("excluded_tools")
if excluded_tools is not None:
payload["excludedTools"] = excluded_tools
- # Always enable permission request callback (deny by default if no handler provided)
- on_permission_request = cfg.get("on_permission_request")
payload["requestPermission"] = True
- # Enable user input request callback if handler provided
- on_user_input_request = cfg.get("on_user_input_request")
if on_user_input_request:
payload["requestUserInput"] = True
- # Enable hooks callback if any hook handler provided
- hooks = cfg.get("hooks")
if hooks and any(hooks.values()):
payload["hooks"] = True
- # Add working directory if provided
- working_directory = cfg.get("working_directory")
if working_directory:
payload["workingDirectory"] = working_directory
- # Add streaming option if provided
- streaming = cfg.get("streaming")
if streaming is not None:
payload["streaming"] = streaming
- # Add provider configuration if provided
- provider = cfg.get("provider")
if provider:
payload["provider"] = self._convert_provider_to_wire_format(provider)
- # Add MCP servers configuration if provided
- mcp_servers = cfg.get("mcp_servers")
if mcp_servers:
payload["mcpServers"] = mcp_servers
payload["envValueMode"] = "direct"
- # Add custom agents configuration if provided
- custom_agents = cfg.get("custom_agents")
if custom_agents:
payload["customAgents"] = [
- self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents
+ self._convert_custom_agent_to_wire_format(ca) for ca in custom_agents
]
- # Add config directory override if provided
- config_dir = cfg.get("config_dir")
+ if agent:
+ payload["agent"] = agent
+
if config_dir:
payload["configDir"] = config_dir
- # Add skill directories configuration if provided
- skill_directories = cfg.get("skill_directories")
if skill_directories:
payload["skillDirectories"] = skill_directories
- # Add disabled skills configuration if provided
- disabled_skills = cfg.get("disabled_skills")
if disabled_skills:
payload["disabledSkills"] = disabled_skills
- # Add infinite sessions configuration if provided
- infinite_sessions = cfg.get("infinite_sessions")
if infinite_sessions:
wire_config: dict[str, Any] = {}
if "enabled" in infinite_sessions:
@@ -602,23 +646,67 @@ async def create_session(self, config: SessionConfig) -> CopilotSession:
if not self._client:
raise RuntimeError("Client not connected")
- response = await self._client.request("session.create", payload)
- session_id = response["sessionId"]
- workspace_path = response.get("workspacePath")
- session = CopilotSession(session_id, self._client, workspace_path)
+ session_id = session_id or str(uuid.uuid4())
+ payload["sessionId"] = session_id
+
+ # Propagate W3C Trace Context to CLI if OpenTelemetry is active
+ trace_ctx = get_trace_context()
+ payload.update(trace_ctx)
+
+ # Create and register the session before issuing the RPC so that
+ # events emitted by the CLI (e.g. session.start) are not dropped.
+ session = CopilotSession(session_id, self._client, None)
session._register_tools(tools)
session._register_permission_handler(on_permission_request)
if on_user_input_request:
session._register_user_input_handler(on_user_input_request)
if hooks:
session._register_hooks(hooks)
+ if transform_callbacks:
+ session._register_transform_callbacks(transform_callbacks)
+ if on_event:
+ session.on(on_event)
with self._sessions_lock:
self._sessions[session_id] = session
+ try:
+ response = await self._client.request("session.create", payload)
+ session._workspace_path = response.get("workspacePath")
+ except BaseException:
+ with self._sessions_lock:
+ self._sessions.pop(session_id, None)
+ raise
+
return session
- async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> CopilotSession:
+ async def resume_session(
+ self,
+ session_id: str,
+ *,
+ on_permission_request: _PermissionHandlerFn,
+ model: str | None = None,
+ client_name: str | None = None,
+ reasoning_effort: ReasoningEffort | None = None,
+ tools: list[Tool] | None = None,
+ system_message: SystemMessageConfig | None = None,
+ available_tools: list[str] | None = None,
+ excluded_tools: list[str] | None = None,
+ on_user_input_request: UserInputHandler | None = None,
+ hooks: SessionHooks | None = None,
+ working_directory: str | None = None,
+ provider: ProviderConfig | None = None,
+ streaming: bool | None = None,
+ mcp_servers: dict[str, MCPServerConfig] | None = None,
+ custom_agents: list[CustomAgentConfig] | None = None,
+ agent: str | None = None,
+ config_dir: str | None = None,
+ skill_directories: list[str] | None = None,
+ disabled_skills: list[str] | None = None,
+ infinite_sessions: InfiniteSessionConfig | None = None,
+ disable_resume: bool = False,
+ on_event: Callable[[SessionEvent], None] | None = None,
+ ) -> CopilotSession:
"""
Resume an existing conversation session by its ID.
@@ -628,7 +716,30 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) ->
Args:
session_id: The ID of the session to resume.
- config: Optional configuration for the resumed session.
+ on_permission_request: Handler for permission requests from the server.
+ model: Model to use for this session. Can change the model when resuming.
+ client_name: Client name to identify the application using the SDK.
+ reasoning_effort: Reasoning effort level ("low", "medium", "high", "xhigh").
+ tools: Custom tools exposed to the CLI.
+ system_message: System message configuration.
+ available_tools: List of tool names to allow (takes precedence over excluded_tools).
+ excluded_tools: List of tool names to disable (ignored if available_tools is set).
+ on_user_input_request: Handler for user input requests (enables ask_user tool).
+ hooks: Hook handlers for intercepting session lifecycle events.
+ working_directory: Working directory for the session.
+ provider: Custom provider configuration (BYOK - Bring Your Own Key).
+ streaming: Enable streaming of assistant message and reasoning chunks.
+ mcp_servers: MCP server configurations for the session.
+ custom_agents: Custom agent configurations for the session.
+ agent: Name of the custom agent to activate when the session starts.
+ config_dir: Override the default configuration directory location.
+ skill_directories: Directories to load skills from.
+ disabled_skills: List of skill names to disable.
+ infinite_sessions: Infinite session configuration for persistent workspaces.
+ disable_resume: When True, skips emitting the session.resume event.
+ Useful for reconnecting without triggering resume-related side effects.
+ on_event: Event handler registered before the session.resume RPC, ensuring
+ early events (e.g. session.start) are not missed.
Returns:
A :class:`CopilotSession` instance for the resumed session.
@@ -637,33 +748,32 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) ->
RuntimeError: If the session does not exist or the client is not connected.
Example:
- >>> # Resume a previous session
- >>> config = {"on_permission_request": PermissionHandler.approve_all}
- >>> session = await client.resume_session("session-123", config)
+ >>> session = await client.resume_session(
+ ... "session-123",
+ ... on_permission_request=PermissionHandler.approve_all,
+ ... )
>>>
- >>> # Resume with new tools
- >>> session = await client.resume_session("session-123", {
- ... "on_permission_request": PermissionHandler.approve_all,
- ... "tools": [my_new_tool]
- ... })
+ >>> # Resume with model and streaming
+ >>> session = await client.resume_session(
+ ... "session-123",
+ ... on_permission_request=PermissionHandler.approve_all,
+ ... model="gpt-4",
+ ... streaming=True,
+ ... )
"""
+ if not on_permission_request or not callable(on_permission_request):
+ raise ValueError(
+ "A valid on_permission_request handler is required. "
+ "Use PermissionHandler.approve_all or provide a custom handler."
+ )
+
if not self._client:
- if self.options["auto_start"]:
+ if self._auto_start:
await self.start()
else:
raise RuntimeError("Client not connected. Call start() first.")
- cfg = config
-
- if not cfg.get("on_permission_request"):
- raise ValueError(
- "An on_permission_request handler is required when resuming a session. "
- "For example, to allow all permissions, use "
- '{"on_permission_request": PermissionHandler.approve_all}.'
- )
-
tool_defs = []
- tools = cfg.get("tools")
if tools:
for tool in tools:
definition: dict[str, Any] = {
@@ -674,102 +784,70 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) ->
definition["parameters"] = tool.parameters
if tool.overrides_built_in_tool:
definition["overridesBuiltInTool"] = True
+ if tool.skip_permission:
+ definition["skipPermission"] = True
tool_defs.append(definition)
payload: dict[str, Any] = {"sessionId": session_id}
-
- # Add client name if provided
- client_name = cfg.get("client_name")
- if client_name:
- payload["clientName"] = client_name
-
- # Add model if provided
- model = cfg.get("model")
if model:
payload["model"] = model
-
- if cfg.get("reasoning_effort"):
- payload["reasoningEffort"] = cfg["reasoning_effort"]
+ if client_name:
+ payload["clientName"] = client_name
+ if reasoning_effort:
+ payload["reasoningEffort"] = reasoning_effort
if tool_defs:
payload["tools"] = tool_defs
- # Add system message configuration if provided
- system_message = cfg.get("system_message")
- if system_message:
- payload["systemMessage"] = system_message
+ wire_system_message, transform_callbacks = _extract_transform_callbacks(system_message)
+ if wire_system_message:
+ payload["systemMessage"] = wire_system_message
- # Add available/excluded tools if provided
- available_tools = cfg.get("available_tools")
if available_tools is not None:
payload["availableTools"] = available_tools
-
- excluded_tools = cfg.get("excluded_tools")
if excluded_tools is not None:
payload["excludedTools"] = excluded_tools
- provider = cfg.get("provider")
- if provider:
- payload["provider"] = self._convert_provider_to_wire_format(provider)
-
- # Add streaming option if provided
- streaming = cfg.get("streaming")
- if streaming is not None:
- payload["streaming"] = streaming
-
- # Always enable permission request callback (deny by default if no handler provided)
- on_permission_request = cfg.get("on_permission_request")
payload["requestPermission"] = True
- # Enable user input request callback if handler provided
- on_user_input_request = cfg.get("on_user_input_request")
if on_user_input_request:
payload["requestUserInput"] = True
- # Enable hooks callback if any hook handler provided
- hooks = cfg.get("hooks")
if hooks and any(hooks.values()):
payload["hooks"] = True
- # Add working directory if provided
- working_directory = cfg.get("working_directory")
if working_directory:
payload["workingDirectory"] = working_directory
- # Add config directory if provided
- config_dir = cfg.get("config_dir")
- if config_dir:
- payload["configDir"] = config_dir
+ if streaming is not None:
+ payload["streaming"] = streaming
- # Add disable resume flag if provided
- disable_resume = cfg.get("disable_resume")
- if disable_resume:
- payload["disableResume"] = True
+ if provider:
+ payload["provider"] = self._convert_provider_to_wire_format(provider)
- # Add MCP servers configuration if provided
- mcp_servers = cfg.get("mcp_servers")
if mcp_servers:
payload["mcpServers"] = mcp_servers
payload["envValueMode"] = "direct"
- # Add custom agents configuration if provided
- custom_agents = cfg.get("custom_agents")
if custom_agents:
payload["customAgents"] = [
- self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents
+ self._convert_custom_agent_to_wire_format(ca) for ca in custom_agents
]
- # Add skill directories configuration if provided
- skill_directories = cfg.get("skill_directories")
+ if agent:
+ payload["agent"] = agent
+
+ if config_dir:
+ payload["configDir"] = config_dir
+
+ if disable_resume:
+ payload["disableResume"] = True
+
if skill_directories:
payload["skillDirectories"] = skill_directories
- # Add disabled skills configuration if provided
- disabled_skills = cfg.get("disabled_skills")
if disabled_skills:
payload["disabledSkills"] = disabled_skills
- # Add infinite sessions configuration if provided
- infinite_sessions = cfg.get("infinite_sessions")
if infinite_sessions:
wire_config: dict[str, Any] = {}
if "enabled" in infinite_sessions:
@@ -786,19 +864,34 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) ->
if not self._client:
raise RuntimeError("Client not connected")
- response = await self._client.request("session.resume", payload)
- resumed_session_id = response["sessionId"]
- workspace_path = response.get("workspacePath")
- session = CopilotSession(resumed_session_id, self._client, workspace_path)
- session._register_tools(cfg.get("tools"))
+ # Propagate W3C Trace Context to CLI if OpenTelemetry is active
+ trace_ctx = get_trace_context()
+ payload.update(trace_ctx)
+
+ # Create and register the session before issuing the RPC so that
+ # events emitted by the CLI (e.g. session.start) are not dropped.
+ session = CopilotSession(session_id, self._client, None)
+ session._register_tools(tools)
session._register_permission_handler(on_permission_request)
if on_user_input_request:
session._register_user_input_handler(on_user_input_request)
if hooks:
session._register_hooks(hooks)
+ if transform_callbacks:
+ session._register_transform_callbacks(transform_callbacks)
+ if on_event:
+ session.on(on_event)
with self._sessions_lock:
- self._sessions[resumed_session_id] = session
+ self._sessions[session_id] = session
+
+ try:
+ response = await self._client.request("session.resume", payload)
+ session._workspace_path = response.get("workspacePath")
+ except BaseException:
+ with self._sessions_lock:
+ self._sessions.pop(session_id, None)
+ raise
return session
@@ -887,11 +980,15 @@ async def list_models(self) -> list["ModelInfo"]:
Results are cached after the first successful call to avoid rate limiting.
The cache is cleared when the client disconnects.
+ If a custom ``on_list_models`` handler was provided in the client options,
+ it is called instead of querying the CLI server. The handler may be sync
+ or async.
+
Returns:
A list of ModelInfo objects with model details.
Raises:
- RuntimeError: If the client is not connected.
+ RuntimeError: If the client is not connected (when no custom handler is set).
Exception: If not authenticated.
Example:
@@ -899,22 +996,30 @@ async def list_models(self) -> list["ModelInfo"]:
>>> for model in models:
... print(f"{model.id}: {model.name}")
"""
- if not self._client:
- raise RuntimeError("Client not connected")
-
# Use asyncio lock to prevent race condition with concurrent calls
async with self._models_cache_lock:
# Check cache (already inside lock)
if self._models_cache is not None:
return list(self._models_cache) # Return a copy to prevent cache mutation
- # Cache miss - fetch from backend while holding lock
- response = await self._client.request("models.list", {})
- models_data = response.get("models", [])
- models = [ModelInfo.from_dict(model) for model in models_data]
+ if self._on_list_models:
+ # Use custom handler instead of CLI RPC
+ result = self._on_list_models()
+ if inspect.isawaitable(result):
+ models = await result
+ else:
+ models = result
+ else:
+ if not self._client:
+ raise RuntimeError("Client not connected")
+
+ # Cache miss - fetch from backend while holding lock
+ response = await self._client.request("models.list", {})
+ models_data = response.get("models", [])
+ models = [ModelInfo.from_dict(model) for model in models_data]
- # Update cache before releasing lock
- self._models_cache = models
+ # Update cache before releasing lock (copy to prevent external mutation)
+ self._models_cache = list(models)
return list(models) # Return a copy to prevent cache mutation
@@ -1004,8 +1109,7 @@ async def get_last_session_id(self) -> str | None:
Example:
>>> last_id = await client.get_last_session_id()
>>> if last_id:
- ... config = {"on_permission_request": PermissionHandler.approve_all}
- ... session = await client.resume_session(last_id, config)
+ ... session = await client.resume_session(last_id, PermissionHandler.approve_all)
"""
if not self._client:
raise RuntimeError("Client not connected")
@@ -1063,11 +1167,20 @@ async def set_foreground_session_id(self, session_id: str) -> None:
error = response.get("error", "Unknown error")
raise RuntimeError(f"Failed to set foreground session: {error}")
+ @overload
+ def on(self, handler: SessionLifecycleHandler, /) -> HandlerUnsubcribe: ...
+
+ @overload
+ def on(
+ self, event_type: SessionLifecycleEventType, /, handler: SessionLifecycleHandler
+ ) -> HandlerUnsubcribe: ...
+
def on(
self,
event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler,
+ /,
handler: SessionLifecycleHandler | None = None,
- ) -> Callable[[], None]:
+ ) -> HandlerUnsubcribe:
"""
Subscribe to session lifecycle events.
@@ -1236,25 +1349,30 @@ async def _start_cli_server(self) -> None:
Raises:
RuntimeError: If the server fails to start or times out.
"""
- cli_path = self.options["cli_path"]
+ assert isinstance(self._config, SubprocessConfig)
+ cfg = self._config
+
+ cli_path = cfg.cli_path
+ assert cli_path is not None # resolved in __init__
# Verify CLI exists
if not os.path.exists(cli_path):
- raise RuntimeError(f"Copilot CLI not found at {cli_path}")
+ original_path = cli_path
+ if (cli_path := shutil.which(cli_path)) is None:
+ raise RuntimeError(f"Copilot CLI not found at {original_path}")
# Start with user-provided cli_args, then add SDK-managed args
- cli_args = self.options.get("cli_args") or []
- args = list(cli_args) + [
+ args = list(cfg.cli_args) + [
"--headless",
"--no-auto-update",
"--log-level",
- self.options["log_level"],
+ cfg.log_level,
]
# Add auth-related flags
- if self.options.get("github_token"):
+ if cfg.github_token:
args.extend(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"])
- if not self.options.get("use_logged_in_user", True):
+ if not cfg.use_logged_in_user:
args.append("--no-auto-login")
# If cli_path is a .js file, run it with node
@@ -1265,21 +1383,39 @@ async def _start_cli_server(self) -> None:
args = [cli_path] + args
# Get environment variables
- env = self.options.get("env")
- if env is None:
+ if cfg.env is None:
env = dict(os.environ)
else:
- env = dict(env)
+ env = dict(cfg.env)
# Set auth token in environment if provided
- if self.options.get("github_token"):
- env["COPILOT_SDK_AUTH_TOKEN"] = self.options["github_token"]
+ if cfg.github_token:
+ env["COPILOT_SDK_AUTH_TOKEN"] = cfg.github_token
+
+ # Set OpenTelemetry environment variables if telemetry config is provided
+ telemetry = cfg.telemetry
+ if telemetry is not None:
+ env["COPILOT_OTEL_ENABLED"] = "true"
+ if "otlp_endpoint" in telemetry:
+ env["OTEL_EXPORTER_OTLP_ENDPOINT"] = telemetry["otlp_endpoint"]
+ if "file_path" in telemetry:
+ env["COPILOT_OTEL_FILE_EXPORTER_PATH"] = telemetry["file_path"]
+ if "exporter_type" in telemetry:
+ env["COPILOT_OTEL_EXPORTER_TYPE"] = telemetry["exporter_type"]
+ if "source_name" in telemetry:
+ env["COPILOT_OTEL_SOURCE_NAME"] = telemetry["source_name"]
+ if "capture_content" in telemetry:
+ env["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = str(
+ telemetry["capture_content"]
+ ).lower()
# On Windows, hide the console window to avoid distracting users in GUI apps
creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
+ cwd = cfg.cwd or os.getcwd()
+
# Choose transport mode
- if self.options["use_stdio"]:
+ if cfg.use_stdio:
args.append("--stdio")
# Use regular Popen with pipes (buffering=0 for unbuffered)
self._process = subprocess.Popen(
@@ -1288,25 +1424,25 @@ async def _start_cli_server(self) -> None:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=0,
- cwd=self.options["cwd"],
+ cwd=cwd,
env=env,
creationflags=creationflags,
)
else:
- if self.options["port"] > 0:
- args.extend(["--port", str(self.options["port"])])
+ if cfg.port > 0:
+ args.extend(["--port", str(cfg.port)])
self._process = subprocess.Popen(
args,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
- cwd=self.options["cwd"],
+ cwd=cwd,
env=env,
creationflags=creationflags,
)
# For stdio mode, we're ready immediately
- if self.options["use_stdio"]:
+ if cfg.use_stdio:
return
# For TCP mode, wait for port announcement
@@ -1341,7 +1477,8 @@ async def _connect_to_server(self) -> None:
Raises:
RuntimeError: If the connection fails.
"""
- if self.options["use_stdio"]:
+ use_stdio = isinstance(self._config, SubprocessConfig) and self._config.use_stdio
+ if use_stdio:
await self._connect_via_stdio()
else:
await self._connect_via_tcp()
@@ -1360,6 +1497,7 @@ async def _connect_via_stdio(self) -> None:
# Create JSON-RPC client with the process
self._client = JsonRpcClient(self._process)
+ self._client.on_close = lambda: setattr(self, "_state", "disconnected")
self._rpc = ServerRpc(self._client)
# Set up notification handler for session events
@@ -1388,6 +1526,9 @@ def handle_notification(method: str, params: dict):
self._client.set_request_handler("permission.request", self._handle_permission_request_v2)
self._client.set_request_handler("userInput.request", self._handle_user_input_request)
self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke)
+ self._client.set_request_handler(
+ "systemMessage.transform", self._handle_system_message_transform
+ )
# Start listening for messages
loop = asyncio.get_running_loop()
@@ -1447,6 +1588,7 @@ def wait(self, timeout=None):
self._process = SocketWrapper(sock_file, sock) # type: ignore
self._client = JsonRpcClient(self._process)
+ self._client.on_close = lambda: setattr(self, "_state", "disconnected")
self._rpc = ServerRpc(self._client)
# Set up notification handler for session events
@@ -1472,6 +1614,9 @@ def handle_notification(method: str, params: dict):
self._client.set_request_handler("permission.request", self._handle_permission_request_v2)
self._client.set_request_handler("userInput.request", self._handle_user_input_request)
self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke)
+ self._client.set_request_handler(
+ "systemMessage.transform", self._handle_system_message_transform
+ )
# Start listening for messages
loop = asyncio.get_running_loop()
@@ -1532,6 +1677,32 @@ async def _handle_hooks_invoke(self, params: dict) -> dict:
output = await session._handle_hooks_invoke(hook_type, input_data)
return {"output": output}
+ async def _handle_system_message_transform(self, params: dict) -> dict:
+ """
+ Handle a systemMessage.transform request from the CLI server.
+
+ Args:
+ params: The transform parameters from the server.
+
+ Returns:
+ A dict containing the transformed sections.
+
+ Raises:
+ ValueError: If the request payload is invalid.
+ """
+ session_id = params.get("sessionId")
+ sections = params.get("sections")
+
+ if not session_id or not sections:
+ raise ValueError("invalid systemMessage.transform payload")
+
+ with self._sessions_lock:
+ session = self._sessions.get(session_id)
+ if not session:
+ raise ValueError(f"unknown session {session_id}")
+
+ return await session._handle_system_message_transform(sections)
+
# ========================================================================
# Protocol v2 backward-compatibility adapters
# ========================================================================
@@ -1571,10 +1742,14 @@ async def _handle_tool_call_request_v2(self, params: dict) -> dict:
arguments=arguments,
)
+ tp = params.get("traceparent")
+ ts = params.get("tracestate")
+
try:
- result = handler(invocation)
- if inspect.isawaitable(result):
- result = await result
+ with trace_context(tp, ts):
+ result = handler(invocation)
+ if inspect.isawaitable(result):
+ result = await result
tool_result: ToolResult = result # type: ignore[assignment]
return {
@@ -1614,6 +1789,8 @@ async def _handle_permission_request_v2(self, params: dict) -> dict:
try:
perm_request = PermissionRequest.from_dict(permission_request)
result = await session._handle_permission_request(perm_request)
+ if result.kind == "no-result":
+ raise ValueError(NO_RESULT_PERMISSION_V2_ERROR)
result_payload: dict = {"kind": result.kind}
if result.rules is not None:
result_payload["rules"] = result.rules
@@ -1624,6 +1801,14 @@ async def _handle_permission_request_v2(self, params: dict) -> dict:
if result.path is not None:
result_payload["path"] = result.path
return {"result": result_payload}
+ except ValueError as exc:
+ if str(exc) == NO_RESULT_PERMISSION_V2_ERROR:
+ raise
+ return {
+ "result": {
+ "kind": "denied-no-approval-rule-and-could-not-request-from-user",
+ }
+ }
except Exception: # pylint: disable=broad-except
return {
"result": {
diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py
index ef188b0955..14ae307d70 100644
--- a/python/copilot/generated/rpc.py
+++ b/python/copilot/generated/rpc.py
@@ -6,13 +6,14 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
- from ..jsonrpc import JsonRpcClient
+ from .._jsonrpc import JsonRpcClient
from dataclasses import dataclass
from typing import Any, TypeVar, cast
from collections.abc import Callable
from enum import Enum
+from uuid import UUID
T = TypeVar("T")
@@ -73,6 +74,11 @@ def to_enum(c: type[EnumT], x: Any) -> EnumT:
return x.value
+def from_int(x: Any) -> int:
+ assert isinstance(x, int) and not isinstance(x, bool)
+ return x
+
+
@dataclass
class PingResult:
message: str
@@ -123,6 +129,7 @@ class Billing:
"""Billing information"""
multiplier: float
+ """Billing cost multiplier relative to the base rate"""
@staticmethod
def from_dict(obj: Any) -> 'Billing':
@@ -138,9 +145,16 @@ def to_dict(self) -> dict:
@dataclass
class Limits:
+ """Token limits for prompts, outputs, and context window"""
+
max_context_window_tokens: float
+ """Maximum total context window size in tokens"""
+
max_output_tokens: float | None = None
+ """Maximum number of output/completion tokens"""
+
max_prompt_tokens: float | None = None
+ """Maximum number of prompt/input tokens"""
@staticmethod
def from_dict(obj: Any) -> 'Limits':
@@ -162,10 +176,13 @@ def to_dict(self) -> dict:
@dataclass
class Supports:
+ """Feature flags indicating what the model supports"""
+
reasoning_effort: bool | None = None
"""Whether this model supports reasoning effort configuration"""
vision: bool | None = None
+ """Whether this model supports vision/image input"""
@staticmethod
def from_dict(obj: Any) -> 'Supports':
@@ -188,7 +205,10 @@ class Capabilities:
"""Model capabilities and limits"""
limits: Limits
+ """Token limits for prompts, outputs, and context window"""
+
supports: Supports
+ """Feature flags indicating what the model supports"""
@staticmethod
def from_dict(obj: Any) -> 'Capabilities':
@@ -209,7 +229,10 @@ class Policy:
"""Policy state (if applicable)"""
state: str
+ """Current policy state for this model"""
+
terms: str
+ """Usage terms or conditions for this model"""
@staticmethod
def from_dict(obj: Any) -> 'Policy':
@@ -434,6 +457,7 @@ def to_dict(self) -> dict:
@dataclass
class SessionModelGetCurrentResult:
model_id: str | None = None
+ """Currently active model identifier"""
@staticmethod
def from_dict(obj: Any) -> 'SessionModelGetCurrentResult':
@@ -451,6 +475,7 @@ def to_dict(self) -> dict:
@dataclass
class SessionModelSwitchToResult:
model_id: str | None = None
+ """Currently active model identifier after the switch"""
@staticmethod
def from_dict(obj: Any) -> 'SessionModelSwitchToResult':
@@ -468,16 +493,23 @@ def to_dict(self) -> dict:
@dataclass
class SessionModelSwitchToParams:
model_id: str
+ """Model identifier to switch to"""
+
+ reasoning_effort: str | None = None
+ """Reasoning effort level to use for the model"""
@staticmethod
def from_dict(obj: Any) -> 'SessionModelSwitchToParams':
assert isinstance(obj, dict)
model_id = from_str(obj.get("modelId"))
- return SessionModelSwitchToParams(model_id)
+ reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort"))
+ return SessionModelSwitchToParams(model_id, reasoning_effort)
def to_dict(self) -> dict:
result: dict = {}
result["modelId"] = from_str(self.model_id)
+ if self.reasoning_effort is not None:
+ result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort)
return result
@@ -697,6 +729,7 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionFleetStartResult:
started: bool
@@ -714,6 +747,7 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionFleetStartParams:
prompt: str | None = None
@@ -733,7 +767,7 @@ def to_dict(self) -> dict:
@dataclass
-class AgentElement:
+class SessionAgentListResultAgent:
description: str
"""Description of the agent's purpose"""
@@ -744,12 +778,12 @@ class AgentElement:
"""Unique identifier of the custom agent"""
@staticmethod
- def from_dict(obj: Any) -> 'AgentElement':
+ def from_dict(obj: Any) -> 'SessionAgentListResultAgent':
assert isinstance(obj, dict)
description = from_str(obj.get("description"))
display_name = from_str(obj.get("displayName"))
name = from_str(obj.get("name"))
- return AgentElement(description, display_name, name)
+ return SessionAgentListResultAgent(description, display_name, name)
def to_dict(self) -> dict:
result: dict = {}
@@ -759,20 +793,21 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionAgentListResult:
- agents: list[AgentElement]
+ agents: list[SessionAgentListResultAgent]
"""Available custom agents"""
@staticmethod
def from_dict(obj: Any) -> 'SessionAgentListResult':
assert isinstance(obj, dict)
- agents = from_list(AgentElement.from_dict, obj.get("agents"))
+ agents = from_list(SessionAgentListResultAgent.from_dict, obj.get("agents"))
return SessionAgentListResult(agents)
def to_dict(self) -> dict:
result: dict = {}
- result["agents"] = from_list(lambda x: to_class(AgentElement, x), self.agents)
+ result["agents"] = from_list(lambda x: to_class(SessionAgentListResultAgent, x), self.agents)
return result
@@ -803,6 +838,7 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionAgentGetCurrentResult:
agent: SessionAgentGetCurrentResultAgent | None = None
@@ -849,6 +885,7 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionAgentSelectResult:
agent: SessionAgentSelectResultAgent
@@ -866,6 +903,7 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionAgentSelectParams:
name: str
@@ -883,6 +921,7 @@ def to_dict(self) -> dict:
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionAgentDeselectResult:
@staticmethod
@@ -896,264 +935,1213 @@ def to_dict(self) -> dict:
@dataclass
-class SessionCompactionCompactResult:
- messages_removed: float
- """Number of messages removed during compaction"""
+class SessionAgentReloadResultAgent:
+ description: str
+ """Description of the agent's purpose"""
- success: bool
- """Whether compaction completed successfully"""
+ display_name: str
+ """Human-readable display name"""
- tokens_removed: float
- """Number of tokens freed by compaction"""
+ name: str
+ """Unique identifier of the custom agent"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionCompactionCompactResult':
+ def from_dict(obj: Any) -> 'SessionAgentReloadResultAgent':
assert isinstance(obj, dict)
- messages_removed = from_float(obj.get("messagesRemoved"))
- success = from_bool(obj.get("success"))
- tokens_removed = from_float(obj.get("tokensRemoved"))
- return SessionCompactionCompactResult(messages_removed, success, tokens_removed)
+ description = from_str(obj.get("description"))
+ display_name = from_str(obj.get("displayName"))
+ name = from_str(obj.get("name"))
+ return SessionAgentReloadResultAgent(description, display_name, name)
def to_dict(self) -> dict:
result: dict = {}
- result["messagesRemoved"] = to_float(self.messages_removed)
- result["success"] = from_bool(self.success)
- result["tokensRemoved"] = to_float(self.tokens_removed)
+ result["description"] = from_str(self.description)
+ result["displayName"] = from_str(self.display_name)
+ result["name"] = from_str(self.name)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionToolsHandlePendingToolCallResult:
- success: bool
+class SessionAgentReloadResult:
+ agents: list[SessionAgentReloadResultAgent]
+ """Reloaded custom agents"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallResult':
+ def from_dict(obj: Any) -> 'SessionAgentReloadResult':
assert isinstance(obj, dict)
- success = from_bool(obj.get("success"))
- return SessionToolsHandlePendingToolCallResult(success)
+ agents = from_list(SessionAgentReloadResultAgent.from_dict, obj.get("agents"))
+ return SessionAgentReloadResult(agents)
def to_dict(self) -> dict:
result: dict = {}
- result["success"] = from_bool(self.success)
+ result["agents"] = from_list(lambda x: to_class(SessionAgentReloadResultAgent, x), self.agents)
return result
@dataclass
-class ResultResult:
- text_result_for_llm: str
- error: str | None = None
- result_type: str | None = None
- tool_telemetry: dict[str, Any] | None = None
+class Skill:
+ description: str
+ """Description of what the skill does"""
+
+ enabled: bool
+ """Whether the skill is currently enabled"""
+
+ name: str
+ """Unique identifier for the skill"""
+
+ source: str
+ """Source location type (e.g., project, personal, plugin)"""
+
+ user_invocable: bool
+ """Whether the skill can be invoked by the user as a slash command"""
+
+ path: str | None = None
+ """Absolute path to the skill file"""
@staticmethod
- def from_dict(obj: Any) -> 'ResultResult':
+ def from_dict(obj: Any) -> 'Skill':
assert isinstance(obj, dict)
- text_result_for_llm = from_str(obj.get("textResultForLlm"))
- error = from_union([from_str, from_none], obj.get("error"))
- result_type = from_union([from_str, from_none], obj.get("resultType"))
- tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry"))
- return ResultResult(text_result_for_llm, error, result_type, tool_telemetry)
+ description = from_str(obj.get("description"))
+ enabled = from_bool(obj.get("enabled"))
+ name = from_str(obj.get("name"))
+ source = from_str(obj.get("source"))
+ user_invocable = from_bool(obj.get("userInvocable"))
+ path = from_union([from_str, from_none], obj.get("path"))
+ return Skill(description, enabled, name, source, user_invocable, path)
def to_dict(self) -> dict:
result: dict = {}
- result["textResultForLlm"] = from_str(self.text_result_for_llm)
- if self.error is not None:
- result["error"] = from_union([from_str, from_none], self.error)
- if self.result_type is not None:
- result["resultType"] = from_union([from_str, from_none], self.result_type)
- if self.tool_telemetry is not None:
- result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry)
+ result["description"] = from_str(self.description)
+ result["enabled"] = from_bool(self.enabled)
+ result["name"] = from_str(self.name)
+ result["source"] = from_str(self.source)
+ result["userInvocable"] = from_bool(self.user_invocable)
+ if self.path is not None:
+ result["path"] = from_union([from_str, from_none], self.path)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionToolsHandlePendingToolCallParams:
- request_id: str
- error: str | None = None
- result: ResultResult | str | None = None
+class SessionSkillsListResult:
+ skills: list[Skill]
+ """Available skills"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallParams':
+ def from_dict(obj: Any) -> 'SessionSkillsListResult':
assert isinstance(obj, dict)
- request_id = from_str(obj.get("requestId"))
- error = from_union([from_str, from_none], obj.get("error"))
- result = from_union([ResultResult.from_dict, from_str, from_none], obj.get("result"))
- return SessionToolsHandlePendingToolCallParams(request_id, error, result)
+ skills = from_list(Skill.from_dict, obj.get("skills"))
+ return SessionSkillsListResult(skills)
def to_dict(self) -> dict:
result: dict = {}
- result["requestId"] = from_str(self.request_id)
- if self.error is not None:
- result["error"] = from_union([from_str, from_none], self.error)
- if self.result is not None:
- result["result"] = from_union([lambda x: to_class(ResultResult, x), from_str, from_none], self.result)
+ result["skills"] = from_list(lambda x: to_class(Skill, x), self.skills)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionPermissionsHandlePendingPermissionRequestResult:
- success: bool
-
+class SessionSkillsEnableResult:
@staticmethod
- def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestResult':
+ def from_dict(obj: Any) -> 'SessionSkillsEnableResult':
assert isinstance(obj, dict)
- success = from_bool(obj.get("success"))
- return SessionPermissionsHandlePendingPermissionRequestResult(success)
+ return SessionSkillsEnableResult()
def to_dict(self) -> dict:
result: dict = {}
- result["success"] = from_bool(self.success)
return result
-class Kind(Enum):
- APPROVED = "approved"
- DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy"
- DENIED_BY_RULES = "denied-by-rules"
- DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user"
- DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user"
-
-
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionPermissionsHandlePendingPermissionRequestParamsResult:
- kind: Kind
- rules: list[Any] | None = None
- feedback: str | None = None
- message: str | None = None
- path: str | None = None
+class SessionSkillsEnableParams:
+ name: str
+ """Name of the skill to enable"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParamsResult':
+ def from_dict(obj: Any) -> 'SessionSkillsEnableParams':
assert isinstance(obj, dict)
- kind = Kind(obj.get("kind"))
- rules = from_union([lambda x: from_list(lambda x: x, x), from_none], obj.get("rules"))
- feedback = from_union([from_str, from_none], obj.get("feedback"))
- message = from_union([from_str, from_none], obj.get("message"))
- path = from_union([from_str, from_none], obj.get("path"))
- return SessionPermissionsHandlePendingPermissionRequestParamsResult(kind, rules, feedback, message, path)
+ name = from_str(obj.get("name"))
+ return SessionSkillsEnableParams(name)
def to_dict(self) -> dict:
result: dict = {}
- result["kind"] = to_enum(Kind, self.kind)
- if self.rules is not None:
- result["rules"] = from_union([lambda x: from_list(lambda x: x, x), from_none], self.rules)
- if self.feedback is not None:
- result["feedback"] = from_union([from_str, from_none], self.feedback)
- if self.message is not None:
- result["message"] = from_union([from_str, from_none], self.message)
- if self.path is not None:
- result["path"] = from_union([from_str, from_none], self.path)
+ result["name"] = from_str(self.name)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionPermissionsHandlePendingPermissionRequestParams:
- request_id: str
- result: SessionPermissionsHandlePendingPermissionRequestParamsResult
-
+class SessionSkillsDisableResult:
@staticmethod
- def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParams':
+ def from_dict(obj: Any) -> 'SessionSkillsDisableResult':
assert isinstance(obj, dict)
- request_id = from_str(obj.get("requestId"))
- result = SessionPermissionsHandlePendingPermissionRequestParamsResult.from_dict(obj.get("result"))
- return SessionPermissionsHandlePendingPermissionRequestParams(request_id, result)
+ return SessionSkillsDisableResult()
def to_dict(self) -> dict:
result: dict = {}
- result["requestId"] = from_str(self.request_id)
- result["result"] = to_class(SessionPermissionsHandlePendingPermissionRequestParamsResult, self.result)
return result
-def ping_result_from_dict(s: Any) -> PingResult:
- return PingResult.from_dict(s)
-
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionSkillsDisableParams:
+ name: str
+ """Name of the skill to disable"""
-def ping_result_to_dict(x: PingResult) -> Any:
- return to_class(PingResult, x)
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionSkillsDisableParams':
+ assert isinstance(obj, dict)
+ name = from_str(obj.get("name"))
+ return SessionSkillsDisableParams(name)
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["name"] = from_str(self.name)
+ return result
-def ping_params_from_dict(s: Any) -> PingParams:
- return PingParams.from_dict(s)
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionSkillsReloadResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionSkillsReloadResult':
+ assert isinstance(obj, dict)
+ return SessionSkillsReloadResult()
-def ping_params_to_dict(x: PingParams) -> Any:
- return to_class(PingParams, x)
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
-def models_list_result_from_dict(s: Any) -> ModelsListResult:
- return ModelsListResult.from_dict(s)
+class ServerStatus(Enum):
+ """Connection status: connected, failed, pending, disabled, or not_configured"""
+ CONNECTED = "connected"
+ DISABLED = "disabled"
+ FAILED = "failed"
+ NOT_CONFIGURED = "not_configured"
+ PENDING = "pending"
-def models_list_result_to_dict(x: ModelsListResult) -> Any:
- return to_class(ModelsListResult, x)
+@dataclass
+class Server:
+ name: str
+ """Server name (config key)"""
-def tools_list_result_from_dict(s: Any) -> ToolsListResult:
- return ToolsListResult.from_dict(s)
+ status: ServerStatus
+ """Connection status: connected, failed, pending, disabled, or not_configured"""
+ error: str | None = None
+ """Error message if the server failed to connect"""
-def tools_list_result_to_dict(x: ToolsListResult) -> Any:
- return to_class(ToolsListResult, x)
+ source: str | None = None
+ """Configuration source: user, workspace, plugin, or builtin"""
+ @staticmethod
+ def from_dict(obj: Any) -> 'Server':
+ assert isinstance(obj, dict)
+ name = from_str(obj.get("name"))
+ status = ServerStatus(obj.get("status"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ source = from_union([from_str, from_none], obj.get("source"))
+ return Server(name, status, error, source)
-def tools_list_params_from_dict(s: Any) -> ToolsListParams:
- return ToolsListParams.from_dict(s)
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["name"] = from_str(self.name)
+ result["status"] = to_enum(ServerStatus, self.status)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ if self.source is not None:
+ result["source"] = from_union([from_str, from_none], self.source)
+ return result
-def tools_list_params_to_dict(x: ToolsListParams) -> Any:
- return to_class(ToolsListParams, x)
+@dataclass
+class SessionMCPListResult:
+ servers: list[Server]
+ """Configured MCP servers"""
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionMCPListResult':
+ assert isinstance(obj, dict)
+ servers = from_list(Server.from_dict, obj.get("servers"))
+ return SessionMCPListResult(servers)
-def account_get_quota_result_from_dict(s: Any) -> AccountGetQuotaResult:
- return AccountGetQuotaResult.from_dict(s)
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["servers"] = from_list(lambda x: to_class(Server, x), self.servers)
+ return result
-def account_get_quota_result_to_dict(x: AccountGetQuotaResult) -> Any:
- return to_class(AccountGetQuotaResult, x)
+@dataclass
+class SessionMCPEnableResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionMCPEnableResult':
+ assert isinstance(obj, dict)
+ return SessionMCPEnableResult()
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
-def session_model_get_current_result_from_dict(s: Any) -> SessionModelGetCurrentResult:
- return SessionModelGetCurrentResult.from_dict(s)
+@dataclass
+class SessionMCPEnableParams:
+ server_name: str
+ """Name of the MCP server to enable"""
-def session_model_get_current_result_to_dict(x: SessionModelGetCurrentResult) -> Any:
- return to_class(SessionModelGetCurrentResult, x)
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionMCPEnableParams':
+ assert isinstance(obj, dict)
+ server_name = from_str(obj.get("serverName"))
+ return SessionMCPEnableParams(server_name)
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["serverName"] = from_str(self.server_name)
+ return result
-def session_model_switch_to_result_from_dict(s: Any) -> SessionModelSwitchToResult:
- return SessionModelSwitchToResult.from_dict(s)
+@dataclass
+class SessionMCPDisableResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionMCPDisableResult':
+ assert isinstance(obj, dict)
+ return SessionMCPDisableResult()
-def session_model_switch_to_result_to_dict(x: SessionModelSwitchToResult) -> Any:
- return to_class(SessionModelSwitchToResult, x)
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
-def session_model_switch_to_params_from_dict(s: Any) -> SessionModelSwitchToParams:
- return SessionModelSwitchToParams.from_dict(s)
+@dataclass
+class SessionMCPDisableParams:
+ server_name: str
+ """Name of the MCP server to disable"""
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionMCPDisableParams':
+ assert isinstance(obj, dict)
+ server_name = from_str(obj.get("serverName"))
+ return SessionMCPDisableParams(server_name)
-def session_model_switch_to_params_to_dict(x: SessionModelSwitchToParams) -> Any:
- return to_class(SessionModelSwitchToParams, x)
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["serverName"] = from_str(self.server_name)
+ return result
-def session_mode_get_result_from_dict(s: Any) -> SessionModeGetResult:
- return SessionModeGetResult.from_dict(s)
+@dataclass
+class SessionMCPReloadResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionMCPReloadResult':
+ assert isinstance(obj, dict)
+ return SessionMCPReloadResult()
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
-def session_mode_get_result_to_dict(x: SessionModeGetResult) -> Any:
- return to_class(SessionModeGetResult, x)
+@dataclass
+class Plugin:
+ enabled: bool
+ """Whether the plugin is currently enabled"""
-def session_mode_set_result_from_dict(s: Any) -> SessionModeSetResult:
- return SessionModeSetResult.from_dict(s)
+ marketplace: str
+ """Marketplace the plugin came from"""
+ name: str
+ """Plugin name"""
-def session_mode_set_result_to_dict(x: SessionModeSetResult) -> Any:
- return to_class(SessionModeSetResult, x)
+ version: str | None = None
+ """Installed version"""
+ @staticmethod
+ def from_dict(obj: Any) -> 'Plugin':
+ assert isinstance(obj, dict)
+ enabled = from_bool(obj.get("enabled"))
+ marketplace = from_str(obj.get("marketplace"))
+ name = from_str(obj.get("name"))
+ version = from_union([from_str, from_none], obj.get("version"))
+ return Plugin(enabled, marketplace, name, version)
-def session_mode_set_params_from_dict(s: Any) -> SessionModeSetParams:
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["enabled"] = from_bool(self.enabled)
+ result["marketplace"] = from_str(self.marketplace)
+ result["name"] = from_str(self.name)
+ if self.version is not None:
+ result["version"] = from_union([from_str, from_none], self.version)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionPluginsListResult:
+ plugins: list[Plugin]
+ """Installed plugins"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionPluginsListResult':
+ assert isinstance(obj, dict)
+ plugins = from_list(Plugin.from_dict, obj.get("plugins"))
+ return SessionPluginsListResult(plugins)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["plugins"] = from_list(lambda x: to_class(Plugin, x), self.plugins)
+ return result
+
+
+class Source(Enum):
+ """Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)"""
+
+ PROJECT = "project"
+ USER = "user"
+
+
+class ExtensionStatus(Enum):
+ """Current status: running, disabled, failed, or starting"""
+
+ DISABLED = "disabled"
+ FAILED = "failed"
+ RUNNING = "running"
+ STARTING = "starting"
+
+
+@dataclass
+class Extension:
+ id: str
+ """Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper')"""
+
+ name: str
+ """Extension name (directory name)"""
+
+ source: Source
+ """Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)"""
+
+ status: ExtensionStatus
+ """Current status: running, disabled, failed, or starting"""
+
+ pid: int | None = None
+ """Process ID if the extension is running"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Extension':
+ assert isinstance(obj, dict)
+ id = from_str(obj.get("id"))
+ name = from_str(obj.get("name"))
+ source = Source(obj.get("source"))
+ status = ExtensionStatus(obj.get("status"))
+ pid = from_union([from_int, from_none], obj.get("pid"))
+ return Extension(id, name, source, status, pid)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = from_str(self.id)
+ result["name"] = from_str(self.name)
+ result["source"] = to_enum(Source, self.source)
+ result["status"] = to_enum(ExtensionStatus, self.status)
+ if self.pid is not None:
+ result["pid"] = from_union([from_int, from_none], self.pid)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsListResult:
+ extensions: list[Extension]
+ """Discovered extensions and their current status"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsListResult':
+ assert isinstance(obj, dict)
+ extensions = from_list(Extension.from_dict, obj.get("extensions"))
+ return SessionExtensionsListResult(extensions)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["extensions"] = from_list(lambda x: to_class(Extension, x), self.extensions)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsEnableResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsEnableResult':
+ assert isinstance(obj, dict)
+ return SessionExtensionsEnableResult()
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsEnableParams:
+ id: str
+ """Source-qualified extension ID to enable"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsEnableParams':
+ assert isinstance(obj, dict)
+ id = from_str(obj.get("id"))
+ return SessionExtensionsEnableParams(id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = from_str(self.id)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsDisableResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsDisableResult':
+ assert isinstance(obj, dict)
+ return SessionExtensionsDisableResult()
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsDisableParams:
+ id: str
+ """Source-qualified extension ID to disable"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsDisableParams':
+ assert isinstance(obj, dict)
+ id = from_str(obj.get("id"))
+ return SessionExtensionsDisableParams(id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = from_str(self.id)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsReloadResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsReloadResult':
+ assert isinstance(obj, dict)
+ return SessionExtensionsReloadResult()
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionCompactionCompactResult:
+ messages_removed: float
+ """Number of messages removed during compaction"""
+
+ success: bool
+ """Whether compaction completed successfully"""
+
+ tokens_removed: float
+ """Number of tokens freed by compaction"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionCompactionCompactResult':
+ assert isinstance(obj, dict)
+ messages_removed = from_float(obj.get("messagesRemoved"))
+ success = from_bool(obj.get("success"))
+ tokens_removed = from_float(obj.get("tokensRemoved"))
+ return SessionCompactionCompactResult(messages_removed, success, tokens_removed)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["messagesRemoved"] = to_float(self.messages_removed)
+ result["success"] = from_bool(self.success)
+ result["tokensRemoved"] = to_float(self.tokens_removed)
+ return result
+
+
+@dataclass
+class SessionToolsHandlePendingToolCallResult:
+ success: bool
+ """Whether the tool call result was handled successfully"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallResult':
+ assert isinstance(obj, dict)
+ success = from_bool(obj.get("success"))
+ return SessionToolsHandlePendingToolCallResult(success)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["success"] = from_bool(self.success)
+ return result
+
+
+@dataclass
+class ResultResult:
+ text_result_for_llm: str
+ error: str | None = None
+ result_type: str | None = None
+ tool_telemetry: dict[str, Any] | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'ResultResult':
+ assert isinstance(obj, dict)
+ text_result_for_llm = from_str(obj.get("textResultForLlm"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ result_type = from_union([from_str, from_none], obj.get("resultType"))
+ tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry"))
+ return ResultResult(text_result_for_llm, error, result_type, tool_telemetry)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["textResultForLlm"] = from_str(self.text_result_for_llm)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ if self.result_type is not None:
+ result["resultType"] = from_union([from_str, from_none], self.result_type)
+ if self.tool_telemetry is not None:
+ result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry)
+ return result
+
+
+@dataclass
+class SessionToolsHandlePendingToolCallParams:
+ request_id: str
+ error: str | None = None
+ result: ResultResult | str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallParams':
+ assert isinstance(obj, dict)
+ request_id = from_str(obj.get("requestId"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ result = from_union([ResultResult.from_dict, from_str, from_none], obj.get("result"))
+ return SessionToolsHandlePendingToolCallParams(request_id, error, result)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["requestId"] = from_str(self.request_id)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ if self.result is not None:
+ result["result"] = from_union([lambda x: to_class(ResultResult, x), from_str, from_none], self.result)
+ return result
+
+
+@dataclass
+class SessionCommandsHandlePendingCommandResult:
+ success: bool
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionCommandsHandlePendingCommandResult':
+ assert isinstance(obj, dict)
+ success = from_bool(obj.get("success"))
+ return SessionCommandsHandlePendingCommandResult(success)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["success"] = from_bool(self.success)
+ return result
+
+
+@dataclass
+class SessionCommandsHandlePendingCommandParams:
+ request_id: str
+ """Request ID from the command invocation event"""
+
+ error: str | None = None
+ """Error message if the command handler failed"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionCommandsHandlePendingCommandParams':
+ assert isinstance(obj, dict)
+ request_id = from_str(obj.get("requestId"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ return SessionCommandsHandlePendingCommandParams(request_id, error)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["requestId"] = from_str(self.request_id)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ return result
+
+
+class Action(Enum):
+ """The user's response: accept (submitted), decline (rejected), or cancel (dismissed)"""
+
+ ACCEPT = "accept"
+ CANCEL = "cancel"
+ DECLINE = "decline"
+
+
+@dataclass
+class SessionUIElicitationResult:
+ action: Action
+ """The user's response: accept (submitted), decline (rejected), or cancel (dismissed)"""
+
+ content: dict[str, float | bool | list[str] | str] | None = None
+ """The form values submitted by the user (present when action is 'accept')"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionUIElicitationResult':
+ assert isinstance(obj, dict)
+ action = Action(obj.get("action"))
+ content = from_union([lambda x: from_dict(lambda x: from_union([from_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], obj.get("content"))
+ return SessionUIElicitationResult(action, content)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["action"] = to_enum(Action, self.action)
+ if self.content is not None:
+ result["content"] = from_union([lambda x: from_dict(lambda x: from_union([to_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], self.content)
+ return result
+
+
+class Format(Enum):
+ DATE = "date"
+ DATE_TIME = "date-time"
+ EMAIL = "email"
+ URI = "uri"
+
+
+@dataclass
+class AnyOf:
+ const: str
+ title: str
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'AnyOf':
+ assert isinstance(obj, dict)
+ const = from_str(obj.get("const"))
+ title = from_str(obj.get("title"))
+ return AnyOf(const, title)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["const"] = from_str(self.const)
+ result["title"] = from_str(self.title)
+ return result
+
+
+class ItemsType(Enum):
+ STRING = "string"
+
+
+@dataclass
+class Items:
+ enum: list[str] | None = None
+ type: ItemsType | None = None
+ any_of: list[AnyOf] | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Items':
+ assert isinstance(obj, dict)
+ enum = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enum"))
+ type = from_union([ItemsType, from_none], obj.get("type"))
+ any_of = from_union([lambda x: from_list(AnyOf.from_dict, x), from_none], obj.get("anyOf"))
+ return Items(enum, type, any_of)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ if self.enum is not None:
+ result["enum"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum)
+ if self.type is not None:
+ result["type"] = from_union([lambda x: to_enum(ItemsType, x), from_none], self.type)
+ if self.any_of is not None:
+ result["anyOf"] = from_union([lambda x: from_list(lambda x: to_class(AnyOf, x), x), from_none], self.any_of)
+ return result
+
+
+@dataclass
+class OneOf:
+ const: str
+ title: str
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'OneOf':
+ assert isinstance(obj, dict)
+ const = from_str(obj.get("const"))
+ title = from_str(obj.get("title"))
+ return OneOf(const, title)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["const"] = from_str(self.const)
+ result["title"] = from_str(self.title)
+ return result
+
+
+class PropertyType(Enum):
+ ARRAY = "array"
+ BOOLEAN = "boolean"
+ INTEGER = "integer"
+ NUMBER = "number"
+ STRING = "string"
+
+
+@dataclass
+class Property:
+ type: PropertyType
+ default: float | bool | list[str] | str | None = None
+ description: str | None = None
+ enum: list[str] | None = None
+ enum_names: list[str] | None = None
+ title: str | None = None
+ one_of: list[OneOf] | None = None
+ items: Items | None = None
+ max_items: float | None = None
+ min_items: float | None = None
+ format: Format | None = None
+ max_length: float | None = None
+ min_length: float | None = None
+ maximum: float | None = None
+ minimum: float | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Property':
+ assert isinstance(obj, dict)
+ type = PropertyType(obj.get("type"))
+ default = from_union([from_float, from_bool, lambda x: from_list(from_str, x), from_str, from_none], obj.get("default"))
+ description = from_union([from_str, from_none], obj.get("description"))
+ enum = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enum"))
+ enum_names = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enumNames"))
+ title = from_union([from_str, from_none], obj.get("title"))
+ one_of = from_union([lambda x: from_list(OneOf.from_dict, x), from_none], obj.get("oneOf"))
+ items = from_union([Items.from_dict, from_none], obj.get("items"))
+ max_items = from_union([from_float, from_none], obj.get("maxItems"))
+ min_items = from_union([from_float, from_none], obj.get("minItems"))
+ format = from_union([Format, from_none], obj.get("format"))
+ max_length = from_union([from_float, from_none], obj.get("maxLength"))
+ min_length = from_union([from_float, from_none], obj.get("minLength"))
+ maximum = from_union([from_float, from_none], obj.get("maximum"))
+ minimum = from_union([from_float, from_none], obj.get("minimum"))
+ return Property(type, default, description, enum, enum_names, title, one_of, items, max_items, min_items, format, max_length, min_length, maximum, minimum)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["type"] = to_enum(PropertyType, self.type)
+ if self.default is not None:
+ result["default"] = from_union([to_float, from_bool, lambda x: from_list(from_str, x), from_str, from_none], self.default)
+ if self.description is not None:
+ result["description"] = from_union([from_str, from_none], self.description)
+ if self.enum is not None:
+ result["enum"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum)
+ if self.enum_names is not None:
+ result["enumNames"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum_names)
+ if self.title is not None:
+ result["title"] = from_union([from_str, from_none], self.title)
+ if self.one_of is not None:
+ result["oneOf"] = from_union([lambda x: from_list(lambda x: to_class(OneOf, x), x), from_none], self.one_of)
+ if self.items is not None:
+ result["items"] = from_union([lambda x: to_class(Items, x), from_none], self.items)
+ if self.max_items is not None:
+ result["maxItems"] = from_union([to_float, from_none], self.max_items)
+ if self.min_items is not None:
+ result["minItems"] = from_union([to_float, from_none], self.min_items)
+ if self.format is not None:
+ result["format"] = from_union([lambda x: to_enum(Format, x), from_none], self.format)
+ if self.max_length is not None:
+ result["maxLength"] = from_union([to_float, from_none], self.max_length)
+ if self.min_length is not None:
+ result["minLength"] = from_union([to_float, from_none], self.min_length)
+ if self.maximum is not None:
+ result["maximum"] = from_union([to_float, from_none], self.maximum)
+ if self.minimum is not None:
+ result["minimum"] = from_union([to_float, from_none], self.minimum)
+ return result
+
+
+class RequestedSchemaType(Enum):
+ OBJECT = "object"
+
+
+@dataclass
+class RequestedSchema:
+ """JSON Schema describing the form fields to present to the user"""
+
+ properties: dict[str, Property]
+ """Form field definitions, keyed by field name"""
+
+ type: RequestedSchemaType
+ """Schema type indicator (always 'object')"""
+
+ required: list[str] | None = None
+ """List of required field names"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'RequestedSchema':
+ assert isinstance(obj, dict)
+ properties = from_dict(Property.from_dict, obj.get("properties"))
+ type = RequestedSchemaType(obj.get("type"))
+ required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("required"))
+ return RequestedSchema(properties, type, required)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["properties"] = from_dict(lambda x: to_class(Property, x), self.properties)
+ result["type"] = to_enum(RequestedSchemaType, self.type)
+ if self.required is not None:
+ result["required"] = from_union([lambda x: from_list(from_str, x), from_none], self.required)
+ return result
+
+
+@dataclass
+class SessionUIElicitationParams:
+ message: str
+ """Message describing what information is needed from the user"""
+
+ requested_schema: RequestedSchema
+ """JSON Schema describing the form fields to present to the user"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionUIElicitationParams':
+ assert isinstance(obj, dict)
+ message = from_str(obj.get("message"))
+ requested_schema = RequestedSchema.from_dict(obj.get("requestedSchema"))
+ return SessionUIElicitationParams(message, requested_schema)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["message"] = from_str(self.message)
+ result["requestedSchema"] = to_class(RequestedSchema, self.requested_schema)
+ return result
+
+
+@dataclass
+class SessionPermissionsHandlePendingPermissionRequestResult:
+ success: bool
+ """Whether the permission request was handled successfully"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestResult':
+ assert isinstance(obj, dict)
+ success = from_bool(obj.get("success"))
+ return SessionPermissionsHandlePendingPermissionRequestResult(success)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["success"] = from_bool(self.success)
+ return result
+
+
+class Kind(Enum):
+ APPROVED = "approved"
+ DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy"
+ DENIED_BY_RULES = "denied-by-rules"
+ DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user"
+ DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user"
+
+
+@dataclass
+class SessionPermissionsHandlePendingPermissionRequestParamsResult:
+ kind: Kind
+ rules: list[Any] | None = None
+ feedback: str | None = None
+ message: str | None = None
+ path: str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParamsResult':
+ assert isinstance(obj, dict)
+ kind = Kind(obj.get("kind"))
+ rules = from_union([lambda x: from_list(lambda x: x, x), from_none], obj.get("rules"))
+ feedback = from_union([from_str, from_none], obj.get("feedback"))
+ message = from_union([from_str, from_none], obj.get("message"))
+ path = from_union([from_str, from_none], obj.get("path"))
+ return SessionPermissionsHandlePendingPermissionRequestParamsResult(kind, rules, feedback, message, path)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["kind"] = to_enum(Kind, self.kind)
+ if self.rules is not None:
+ result["rules"] = from_union([lambda x: from_list(lambda x: x, x), from_none], self.rules)
+ if self.feedback is not None:
+ result["feedback"] = from_union([from_str, from_none], self.feedback)
+ if self.message is not None:
+ result["message"] = from_union([from_str, from_none], self.message)
+ if self.path is not None:
+ result["path"] = from_union([from_str, from_none], self.path)
+ return result
+
+
+@dataclass
+class SessionPermissionsHandlePendingPermissionRequestParams:
+ request_id: str
+ result: SessionPermissionsHandlePendingPermissionRequestParamsResult
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParams':
+ assert isinstance(obj, dict)
+ request_id = from_str(obj.get("requestId"))
+ result = SessionPermissionsHandlePendingPermissionRequestParamsResult.from_dict(obj.get("result"))
+ return SessionPermissionsHandlePendingPermissionRequestParams(request_id, result)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["requestId"] = from_str(self.request_id)
+ result["result"] = to_class(SessionPermissionsHandlePendingPermissionRequestParamsResult, self.result)
+ return result
+
+
+@dataclass
+class SessionLogResult:
+ event_id: UUID
+ """The unique identifier of the emitted session event"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionLogResult':
+ assert isinstance(obj, dict)
+ event_id = UUID(obj.get("eventId"))
+ return SessionLogResult(event_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["eventId"] = str(self.event_id)
+ return result
+
+
+class Level(Enum):
+ """Log severity level. Determines how the message is displayed in the timeline. Defaults to
+ "info".
+ """
+ ERROR = "error"
+ INFO = "info"
+ WARNING = "warning"
+
+
+@dataclass
+class SessionLogParams:
+ message: str
+ """Human-readable message"""
+
+ ephemeral: bool | None = None
+ """When true, the message is transient and not persisted to the session event log on disk"""
+
+ level: Level | None = None
+ """Log severity level. Determines how the message is displayed in the timeline. Defaults to
+ "info".
+ """
+ url: str | None = None
+ """Optional URL the user can open in their browser for more details"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionLogParams':
+ assert isinstance(obj, dict)
+ message = from_str(obj.get("message"))
+ ephemeral = from_union([from_bool, from_none], obj.get("ephemeral"))
+ level = from_union([Level, from_none], obj.get("level"))
+ url = from_union([from_str, from_none], obj.get("url"))
+ return SessionLogParams(message, ephemeral, level, url)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["message"] = from_str(self.message)
+ if self.ephemeral is not None:
+ result["ephemeral"] = from_union([from_bool, from_none], self.ephemeral)
+ if self.level is not None:
+ result["level"] = from_union([lambda x: to_enum(Level, x), from_none], self.level)
+ if self.url is not None:
+ result["url"] = from_union([from_str, from_none], self.url)
+ return result
+
+
+@dataclass
+class SessionShellExecResult:
+ process_id: str
+ """Unique identifier for tracking streamed output"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionShellExecResult':
+ assert isinstance(obj, dict)
+ process_id = from_str(obj.get("processId"))
+ return SessionShellExecResult(process_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["processId"] = from_str(self.process_id)
+ return result
+
+
+@dataclass
+class SessionShellExecParams:
+ command: str
+ """Shell command to execute"""
+
+ cwd: str | None = None
+ """Working directory (defaults to session working directory)"""
+
+ timeout: float | None = None
+ """Timeout in milliseconds (default: 30000)"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionShellExecParams':
+ assert isinstance(obj, dict)
+ command = from_str(obj.get("command"))
+ cwd = from_union([from_str, from_none], obj.get("cwd"))
+ timeout = from_union([from_float, from_none], obj.get("timeout"))
+ return SessionShellExecParams(command, cwd, timeout)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["command"] = from_str(self.command)
+ if self.cwd is not None:
+ result["cwd"] = from_union([from_str, from_none], self.cwd)
+ if self.timeout is not None:
+ result["timeout"] = from_union([to_float, from_none], self.timeout)
+ return result
+
+
+@dataclass
+class SessionShellKillResult:
+ killed: bool
+ """Whether the signal was sent successfully"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionShellKillResult':
+ assert isinstance(obj, dict)
+ killed = from_bool(obj.get("killed"))
+ return SessionShellKillResult(killed)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["killed"] = from_bool(self.killed)
+ return result
+
+
+class Signal(Enum):
+ """Signal to send (default: SIGTERM)"""
+
+ SIGINT = "SIGINT"
+ SIGKILL = "SIGKILL"
+ SIGTERM = "SIGTERM"
+
+
+@dataclass
+class SessionShellKillParams:
+ process_id: str
+ """Process identifier returned by shell.exec"""
+
+ signal: Signal | None = None
+ """Signal to send (default: SIGTERM)"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionShellKillParams':
+ assert isinstance(obj, dict)
+ process_id = from_str(obj.get("processId"))
+ signal = from_union([Signal, from_none], obj.get("signal"))
+ return SessionShellKillParams(process_id, signal)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["processId"] = from_str(self.process_id)
+ if self.signal is not None:
+ result["signal"] = from_union([lambda x: to_enum(Signal, x), from_none], self.signal)
+ return result
+
+
+def ping_result_from_dict(s: Any) -> PingResult:
+ return PingResult.from_dict(s)
+
+
+def ping_result_to_dict(x: PingResult) -> Any:
+ return to_class(PingResult, x)
+
+
+def ping_params_from_dict(s: Any) -> PingParams:
+ return PingParams.from_dict(s)
+
+
+def ping_params_to_dict(x: PingParams) -> Any:
+ return to_class(PingParams, x)
+
+
+def models_list_result_from_dict(s: Any) -> ModelsListResult:
+ return ModelsListResult.from_dict(s)
+
+
+def models_list_result_to_dict(x: ModelsListResult) -> Any:
+ return to_class(ModelsListResult, x)
+
+
+def tools_list_result_from_dict(s: Any) -> ToolsListResult:
+ return ToolsListResult.from_dict(s)
+
+
+def tools_list_result_to_dict(x: ToolsListResult) -> Any:
+ return to_class(ToolsListResult, x)
+
+
+def tools_list_params_from_dict(s: Any) -> ToolsListParams:
+ return ToolsListParams.from_dict(s)
+
+
+def tools_list_params_to_dict(x: ToolsListParams) -> Any:
+ return to_class(ToolsListParams, x)
+
+
+def account_get_quota_result_from_dict(s: Any) -> AccountGetQuotaResult:
+ return AccountGetQuotaResult.from_dict(s)
+
+
+def account_get_quota_result_to_dict(x: AccountGetQuotaResult) -> Any:
+ return to_class(AccountGetQuotaResult, x)
+
+
+def session_model_get_current_result_from_dict(s: Any) -> SessionModelGetCurrentResult:
+ return SessionModelGetCurrentResult.from_dict(s)
+
+
+def session_model_get_current_result_to_dict(x: SessionModelGetCurrentResult) -> Any:
+ return to_class(SessionModelGetCurrentResult, x)
+
+
+def session_model_switch_to_result_from_dict(s: Any) -> SessionModelSwitchToResult:
+ return SessionModelSwitchToResult.from_dict(s)
+
+
+def session_model_switch_to_result_to_dict(x: SessionModelSwitchToResult) -> Any:
+ return to_class(SessionModelSwitchToResult, x)
+
+
+def session_model_switch_to_params_from_dict(s: Any) -> SessionModelSwitchToParams:
+ return SessionModelSwitchToParams.from_dict(s)
+
+
+def session_model_switch_to_params_to_dict(x: SessionModelSwitchToParams) -> Any:
+ return to_class(SessionModelSwitchToParams, x)
+
+
+def session_mode_get_result_from_dict(s: Any) -> SessionModeGetResult:
+ return SessionModeGetResult.from_dict(s)
+
+
+def session_mode_get_result_to_dict(x: SessionModeGetResult) -> Any:
+ return to_class(SessionModeGetResult, x)
+
+
+def session_mode_set_result_from_dict(s: Any) -> SessionModeSetResult:
+ return SessionModeSetResult.from_dict(s)
+
+
+def session_mode_set_result_to_dict(x: SessionModeSetResult) -> Any:
+ return to_class(SessionModeSetResult, x)
+
+
+def session_mode_set_params_from_dict(s: Any) -> SessionModeSetParams:
return SessionModeSetParams.from_dict(s)
@@ -1289,6 +2277,166 @@ def session_agent_deselect_result_to_dict(x: SessionAgentDeselectResult) -> Any:
return to_class(SessionAgentDeselectResult, x)
+def session_agent_reload_result_from_dict(s: Any) -> SessionAgentReloadResult:
+ return SessionAgentReloadResult.from_dict(s)
+
+
+def session_agent_reload_result_to_dict(x: SessionAgentReloadResult) -> Any:
+ return to_class(SessionAgentReloadResult, x)
+
+
+def session_skills_list_result_from_dict(s: Any) -> SessionSkillsListResult:
+ return SessionSkillsListResult.from_dict(s)
+
+
+def session_skills_list_result_to_dict(x: SessionSkillsListResult) -> Any:
+ return to_class(SessionSkillsListResult, x)
+
+
+def session_skills_enable_result_from_dict(s: Any) -> SessionSkillsEnableResult:
+ return SessionSkillsEnableResult.from_dict(s)
+
+
+def session_skills_enable_result_to_dict(x: SessionSkillsEnableResult) -> Any:
+ return to_class(SessionSkillsEnableResult, x)
+
+
+def session_skills_enable_params_from_dict(s: Any) -> SessionSkillsEnableParams:
+ return SessionSkillsEnableParams.from_dict(s)
+
+
+def session_skills_enable_params_to_dict(x: SessionSkillsEnableParams) -> Any:
+ return to_class(SessionSkillsEnableParams, x)
+
+
+def session_skills_disable_result_from_dict(s: Any) -> SessionSkillsDisableResult:
+ return SessionSkillsDisableResult.from_dict(s)
+
+
+def session_skills_disable_result_to_dict(x: SessionSkillsDisableResult) -> Any:
+ return to_class(SessionSkillsDisableResult, x)
+
+
+def session_skills_disable_params_from_dict(s: Any) -> SessionSkillsDisableParams:
+ return SessionSkillsDisableParams.from_dict(s)
+
+
+def session_skills_disable_params_to_dict(x: SessionSkillsDisableParams) -> Any:
+ return to_class(SessionSkillsDisableParams, x)
+
+
+def session_skills_reload_result_from_dict(s: Any) -> SessionSkillsReloadResult:
+ return SessionSkillsReloadResult.from_dict(s)
+
+
+def session_skills_reload_result_to_dict(x: SessionSkillsReloadResult) -> Any:
+ return to_class(SessionSkillsReloadResult, x)
+
+
+def session_mcp_list_result_from_dict(s: Any) -> SessionMCPListResult:
+ return SessionMCPListResult.from_dict(s)
+
+
+def session_mcp_list_result_to_dict(x: SessionMCPListResult) -> Any:
+ return to_class(SessionMCPListResult, x)
+
+
+def session_mcp_enable_result_from_dict(s: Any) -> SessionMCPEnableResult:
+ return SessionMCPEnableResult.from_dict(s)
+
+
+def session_mcp_enable_result_to_dict(x: SessionMCPEnableResult) -> Any:
+ return to_class(SessionMCPEnableResult, x)
+
+
+def session_mcp_enable_params_from_dict(s: Any) -> SessionMCPEnableParams:
+ return SessionMCPEnableParams.from_dict(s)
+
+
+def session_mcp_enable_params_to_dict(x: SessionMCPEnableParams) -> Any:
+ return to_class(SessionMCPEnableParams, x)
+
+
+def session_mcp_disable_result_from_dict(s: Any) -> SessionMCPDisableResult:
+ return SessionMCPDisableResult.from_dict(s)
+
+
+def session_mcp_disable_result_to_dict(x: SessionMCPDisableResult) -> Any:
+ return to_class(SessionMCPDisableResult, x)
+
+
+def session_mcp_disable_params_from_dict(s: Any) -> SessionMCPDisableParams:
+ return SessionMCPDisableParams.from_dict(s)
+
+
+def session_mcp_disable_params_to_dict(x: SessionMCPDisableParams) -> Any:
+ return to_class(SessionMCPDisableParams, x)
+
+
+def session_mcp_reload_result_from_dict(s: Any) -> SessionMCPReloadResult:
+ return SessionMCPReloadResult.from_dict(s)
+
+
+def session_mcp_reload_result_to_dict(x: SessionMCPReloadResult) -> Any:
+ return to_class(SessionMCPReloadResult, x)
+
+
+def session_plugins_list_result_from_dict(s: Any) -> SessionPluginsListResult:
+ return SessionPluginsListResult.from_dict(s)
+
+
+def session_plugins_list_result_to_dict(x: SessionPluginsListResult) -> Any:
+ return to_class(SessionPluginsListResult, x)
+
+
+def session_extensions_list_result_from_dict(s: Any) -> SessionExtensionsListResult:
+ return SessionExtensionsListResult.from_dict(s)
+
+
+def session_extensions_list_result_to_dict(x: SessionExtensionsListResult) -> Any:
+ return to_class(SessionExtensionsListResult, x)
+
+
+def session_extensions_enable_result_from_dict(s: Any) -> SessionExtensionsEnableResult:
+ return SessionExtensionsEnableResult.from_dict(s)
+
+
+def session_extensions_enable_result_to_dict(x: SessionExtensionsEnableResult) -> Any:
+ return to_class(SessionExtensionsEnableResult, x)
+
+
+def session_extensions_enable_params_from_dict(s: Any) -> SessionExtensionsEnableParams:
+ return SessionExtensionsEnableParams.from_dict(s)
+
+
+def session_extensions_enable_params_to_dict(x: SessionExtensionsEnableParams) -> Any:
+ return to_class(SessionExtensionsEnableParams, x)
+
+
+def session_extensions_disable_result_from_dict(s: Any) -> SessionExtensionsDisableResult:
+ return SessionExtensionsDisableResult.from_dict(s)
+
+
+def session_extensions_disable_result_to_dict(x: SessionExtensionsDisableResult) -> Any:
+ return to_class(SessionExtensionsDisableResult, x)
+
+
+def session_extensions_disable_params_from_dict(s: Any) -> SessionExtensionsDisableParams:
+ return SessionExtensionsDisableParams.from_dict(s)
+
+
+def session_extensions_disable_params_to_dict(x: SessionExtensionsDisableParams) -> Any:
+ return to_class(SessionExtensionsDisableParams, x)
+
+
+def session_extensions_reload_result_from_dict(s: Any) -> SessionExtensionsReloadResult:
+ return SessionExtensionsReloadResult.from_dict(s)
+
+
+def session_extensions_reload_result_to_dict(x: SessionExtensionsReloadResult) -> Any:
+ return to_class(SessionExtensionsReloadResult, x)
+
+
def session_compaction_compact_result_from_dict(s: Any) -> SessionCompactionCompactResult:
return SessionCompactionCompactResult.from_dict(s)
@@ -1313,6 +2461,38 @@ def session_tools_handle_pending_tool_call_params_to_dict(x: SessionToolsHandleP
return to_class(SessionToolsHandlePendingToolCallParams, x)
+def session_commands_handle_pending_command_result_from_dict(s: Any) -> SessionCommandsHandlePendingCommandResult:
+ return SessionCommandsHandlePendingCommandResult.from_dict(s)
+
+
+def session_commands_handle_pending_command_result_to_dict(x: SessionCommandsHandlePendingCommandResult) -> Any:
+ return to_class(SessionCommandsHandlePendingCommandResult, x)
+
+
+def session_commands_handle_pending_command_params_from_dict(s: Any) -> SessionCommandsHandlePendingCommandParams:
+ return SessionCommandsHandlePendingCommandParams.from_dict(s)
+
+
+def session_commands_handle_pending_command_params_to_dict(x: SessionCommandsHandlePendingCommandParams) -> Any:
+ return to_class(SessionCommandsHandlePendingCommandParams, x)
+
+
+def session_ui_elicitation_result_from_dict(s: Any) -> SessionUIElicitationResult:
+ return SessionUIElicitationResult.from_dict(s)
+
+
+def session_ui_elicitation_result_to_dict(x: SessionUIElicitationResult) -> Any:
+ return to_class(SessionUIElicitationResult, x)
+
+
+def session_ui_elicitation_params_from_dict(s: Any) -> SessionUIElicitationParams:
+ return SessionUIElicitationParams.from_dict(s)
+
+
+def session_ui_elicitation_params_to_dict(x: SessionUIElicitationParams) -> Any:
+ return to_class(SessionUIElicitationParams, x)
+
+
def session_permissions_handle_pending_permission_request_result_from_dict(s: Any) -> SessionPermissionsHandlePendingPermissionRequestResult:
return SessionPermissionsHandlePendingPermissionRequestResult.from_dict(s)
@@ -1329,6 +2509,54 @@ def session_permissions_handle_pending_permission_request_params_to_dict(x: Sess
return to_class(SessionPermissionsHandlePendingPermissionRequestParams, x)
+def session_log_result_from_dict(s: Any) -> SessionLogResult:
+ return SessionLogResult.from_dict(s)
+
+
+def session_log_result_to_dict(x: SessionLogResult) -> Any:
+ return to_class(SessionLogResult, x)
+
+
+def session_log_params_from_dict(s: Any) -> SessionLogParams:
+ return SessionLogParams.from_dict(s)
+
+
+def session_log_params_to_dict(x: SessionLogParams) -> Any:
+ return to_class(SessionLogParams, x)
+
+
+def session_shell_exec_result_from_dict(s: Any) -> SessionShellExecResult:
+ return SessionShellExecResult.from_dict(s)
+
+
+def session_shell_exec_result_to_dict(x: SessionShellExecResult) -> Any:
+ return to_class(SessionShellExecResult, x)
+
+
+def session_shell_exec_params_from_dict(s: Any) -> SessionShellExecParams:
+ return SessionShellExecParams.from_dict(s)
+
+
+def session_shell_exec_params_to_dict(x: SessionShellExecParams) -> Any:
+ return to_class(SessionShellExecParams, x)
+
+
+def session_shell_kill_result_from_dict(s: Any) -> SessionShellKillResult:
+ return SessionShellKillResult.from_dict(s)
+
+
+def session_shell_kill_result_to_dict(x: SessionShellKillResult) -> Any:
+ return to_class(SessionShellKillResult, x)
+
+
+def session_shell_kill_params_from_dict(s: Any) -> SessionShellKillParams:
+ return SessionShellKillParams.from_dict(s)
+
+
+def session_shell_kill_params_to_dict(x: SessionShellKillParams) -> Any:
+ return to_class(SessionShellKillParams, x)
+
+
def _timeout_kwargs(timeout: float | None) -> dict:
"""Build keyword arguments for optional timeout forwarding."""
if timeout is not None:
@@ -1438,6 +2666,7 @@ async def create_file(self, params: SessionWorkspaceCreateFileParams, *, timeout
return SessionWorkspaceCreateFileResult.from_dict(await self._client.request("session.workspace.createFile", params_dict, **_timeout_kwargs(timeout)))
+# Experimental: this API group is experimental and may change or be removed.
class FleetApi:
def __init__(self, client: "JsonRpcClient", session_id: str):
self._client = client
@@ -1449,6 +2678,7 @@ async def start(self, params: SessionFleetStartParams, *, timeout: float | None
return SessionFleetStartResult.from_dict(await self._client.request("session.fleet.start", params_dict, **_timeout_kwargs(timeout)))
+# Experimental: this API group is experimental and may change or be removed.
class AgentApi:
def __init__(self, client: "JsonRpcClient", session_id: str):
self._client = client
@@ -1468,7 +2698,90 @@ async def select(self, params: SessionAgentSelectParams, *, timeout: float | Non
async def deselect(self, *, timeout: float | None = None) -> SessionAgentDeselectResult:
return SessionAgentDeselectResult.from_dict(await self._client.request("session.agent.deselect", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+ async def reload(self, *, timeout: float | None = None) -> SessionAgentReloadResult:
+ return SessionAgentReloadResult.from_dict(await self._client.request("session.agent.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
+# Experimental: this API group is experimental and may change or be removed.
+class SkillsApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def list(self, *, timeout: float | None = None) -> SessionSkillsListResult:
+ return SessionSkillsListResult.from_dict(await self._client.request("session.skills.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+ async def enable(self, params: SessionSkillsEnableParams, *, timeout: float | None = None) -> SessionSkillsEnableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionSkillsEnableResult.from_dict(await self._client.request("session.skills.enable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def disable(self, params: SessionSkillsDisableParams, *, timeout: float | None = None) -> SessionSkillsDisableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionSkillsDisableResult.from_dict(await self._client.request("session.skills.disable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def reload(self, *, timeout: float | None = None) -> SessionSkillsReloadResult:
+ return SessionSkillsReloadResult.from_dict(await self._client.request("session.skills.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
+# Experimental: this API group is experimental and may change or be removed.
+class McpApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def list(self, *, timeout: float | None = None) -> SessionMCPListResult:
+ return SessionMCPListResult.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+ async def enable(self, params: SessionMCPEnableParams, *, timeout: float | None = None) -> SessionMCPEnableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionMCPEnableResult.from_dict(await self._client.request("session.mcp.enable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def disable(self, params: SessionMCPDisableParams, *, timeout: float | None = None) -> SessionMCPDisableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionMCPDisableResult.from_dict(await self._client.request("session.mcp.disable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def reload(self, *, timeout: float | None = None) -> SessionMCPReloadResult:
+ return SessionMCPReloadResult.from_dict(await self._client.request("session.mcp.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
+# Experimental: this API group is experimental and may change or be removed.
+class PluginsApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def list(self, *, timeout: float | None = None) -> SessionPluginsListResult:
+ return SessionPluginsListResult.from_dict(await self._client.request("session.plugins.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
+# Experimental: this API group is experimental and may change or be removed.
+class ExtensionsApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def list(self, *, timeout: float | None = None) -> SessionExtensionsListResult:
+ return SessionExtensionsListResult.from_dict(await self._client.request("session.extensions.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+ async def enable(self, params: SessionExtensionsEnableParams, *, timeout: float | None = None) -> SessionExtensionsEnableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionExtensionsEnableResult.from_dict(await self._client.request("session.extensions.enable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def disable(self, params: SessionExtensionsDisableParams, *, timeout: float | None = None) -> SessionExtensionsDisableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionExtensionsDisableResult.from_dict(await self._client.request("session.extensions.disable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def reload(self, *, timeout: float | None = None) -> SessionExtensionsReloadResult:
+ return SessionExtensionsReloadResult.from_dict(await self._client.request("session.extensions.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+# Experimental: this API group is experimental and may change or be removed.
class CompactionApi:
def __init__(self, client: "JsonRpcClient", session_id: str):
self._client = client
@@ -1489,6 +2802,28 @@ async def handle_pending_tool_call(self, params: SessionToolsHandlePendingToolCa
return SessionToolsHandlePendingToolCallResult.from_dict(await self._client.request("session.tools.handlePendingToolCall", params_dict, **_timeout_kwargs(timeout)))
+class CommandsApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def handle_pending_command(self, params: SessionCommandsHandlePendingCommandParams, *, timeout: float | None = None) -> SessionCommandsHandlePendingCommandResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionCommandsHandlePendingCommandResult.from_dict(await self._client.request("session.commands.handlePendingCommand", params_dict, **_timeout_kwargs(timeout)))
+
+
+class UiApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def elicitation(self, params: SessionUIElicitationParams, *, timeout: float | None = None) -> SessionUIElicitationResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionUIElicitationResult.from_dict(await self._client.request("session.ui.elicitation", params_dict, **_timeout_kwargs(timeout)))
+
+
class PermissionsApi:
def __init__(self, client: "JsonRpcClient", session_id: str):
self._client = client
@@ -1500,6 +2835,22 @@ async def handle_pending_permission_request(self, params: SessionPermissionsHand
return SessionPermissionsHandlePendingPermissionRequestResult.from_dict(await self._client.request("session.permissions.handlePendingPermissionRequest", params_dict, **_timeout_kwargs(timeout)))
+class ShellApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def exec(self, params: SessionShellExecParams, *, timeout: float | None = None) -> SessionShellExecResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionShellExecResult.from_dict(await self._client.request("session.shell.exec", params_dict, **_timeout_kwargs(timeout)))
+
+ async def kill(self, params: SessionShellKillParams, *, timeout: float | None = None) -> SessionShellKillResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionShellKillResult.from_dict(await self._client.request("session.shell.kill", params_dict, **_timeout_kwargs(timeout)))
+
+
class SessionRpc:
"""Typed session-scoped RPC methods."""
def __init__(self, client: "JsonRpcClient", session_id: str):
@@ -1511,7 +2862,19 @@ def __init__(self, client: "JsonRpcClient", session_id: str):
self.workspace = WorkspaceApi(client, session_id)
self.fleet = FleetApi(client, session_id)
self.agent = AgentApi(client, session_id)
+ self.skills = SkillsApi(client, session_id)
+ self.mcp = McpApi(client, session_id)
+ self.plugins = PluginsApi(client, session_id)
+ self.extensions = ExtensionsApi(client, session_id)
self.compaction = CompactionApi(client, session_id)
self.tools = ToolsApi(client, session_id)
+ self.commands = CommandsApi(client, session_id)
+ self.ui = UiApi(client, session_id)
self.permissions = PermissionsApi(client, session_id)
+ self.shell = ShellApi(client, session_id)
+
+ async def log(self, params: SessionLogParams, *, timeout: float | None = None) -> SessionLogResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionLogResult.from_dict(await self._client.request("session.log", params_dict, **_timeout_kwargs(timeout)))
diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py
index 1b442530d4..9701a4d9f3 100644
--- a/python/copilot/generated/session_events.py
+++ b/python/copilot/generated/session_events.py
@@ -122,6 +122,8 @@ class ReferenceType(Enum):
@dataclass
class End:
+ """End position of the selection"""
+
character: float
"""End character offset within the line (0-based)"""
@@ -144,6 +146,8 @@ def to_dict(self) -> dict:
@dataclass
class Start:
+ """Start position of the selection"""
+
character: float
"""Start character offset within the line (0-based)"""
@@ -169,7 +173,10 @@ class Selection:
"""Position range of the selection within the file"""
end: End
+ """End position of the selection"""
+
start: Start
+ """Start position of the selection"""
@staticmethod
def from_dict(obj: Any) -> 'Selection':
@@ -186,6 +193,7 @@ def to_dict(self) -> dict:
class AttachmentType(Enum):
+ BLOB = "blob"
DIRECTORY = "directory"
FILE = "file"
GITHUB_REFERENCE = "github_reference"
@@ -194,6 +202,18 @@ class AttachmentType(Enum):
@dataclass
class Attachment:
+ """A user message attachment — a file, directory, code selection, blob, or GitHub reference
+
+ File attachment
+
+ Directory attachment
+
+ Code selection attachment from an editor
+
+ GitHub issue, pull request, or discussion reference
+
+ Blob attachment with inline base64-encoded data
+ """
type: AttachmentType
"""Attachment type discriminator"""
@@ -206,8 +226,10 @@ class Attachment:
"""Optional line range to scope the attachment to a specific section of the file"""
path: str | None = None
- """Absolute file or directory path"""
-
+ """Absolute file path
+
+ Absolute directory path
+ """
file_path: str | None = None
"""Absolute path to the file containing the selection"""
@@ -232,6 +254,12 @@ class Attachment:
url: str | None = None
"""URL to the referenced item on GitHub"""
+ data: str | None = None
+ """Base64-encoded content"""
+
+ mime_type: str | None = None
+ """MIME type of the inline data"""
+
@staticmethod
def from_dict(obj: Any) -> 'Attachment':
assert isinstance(obj, dict)
@@ -247,7 +275,9 @@ def from_dict(obj: Any) -> 'Attachment':
state = from_union([from_str, from_none], obj.get("state"))
title = from_union([from_str, from_none], obj.get("title"))
url = from_union([from_str, from_none], obj.get("url"))
- return Attachment(type, display_name, line_range, path, file_path, selection, text, number, reference_type, state, title, url)
+ data = from_union([from_str, from_none], obj.get("data"))
+ mime_type = from_union([from_str, from_none], obj.get("mimeType"))
+ return Attachment(type, display_name, line_range, path, file_path, selection, text, number, reference_type, state, title, url, data, mime_type)
def to_dict(self) -> dict:
result: dict = {}
@@ -274,11 +304,17 @@ def to_dict(self) -> dict:
result["title"] = from_union([from_str, from_none], self.title)
if self.url is not None:
result["url"] = from_union([from_str, from_none], self.url)
+ if self.data is not None:
+ result["data"] = from_union([from_str, from_none], self.data)
+ if self.mime_type is not None:
+ result["mimeType"] = from_union([from_str, from_none], self.mime_type)
return result
@dataclass
class Agent:
+ """A background agent task"""
+
agent_id: str
"""Unique identifier of the background agent"""
@@ -307,6 +343,8 @@ def to_dict(self) -> dict:
@dataclass
class Shell:
+ """A background shell command"""
+
shell_id: str
"""Unique identifier of the background shell"""
@@ -381,6 +419,26 @@ def to_dict(self) -> dict:
return result
+@dataclass
+class DataCommand:
+ name: str
+ description: str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'DataCommand':
+ assert isinstance(obj, dict)
+ name = from_str(obj.get("name"))
+ description = from_union([from_str, from_none], obj.get("description"))
+ return DataCommand(name, description)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["name"] = from_str(self.name)
+ if self.description is not None:
+ result["description"] = from_union([from_str, from_none], self.description)
+ return result
+
+
@dataclass
class CompactionTokensUsed:
"""Token usage breakdown for the compaction LLM call"""
@@ -410,6 +468,13 @@ def to_dict(self) -> dict:
return result
+class HostType(Enum):
+ """Hosting platform type of the repository (github or ado)"""
+
+ ADO = "ado"
+ GITHUB = "github"
+
+
@dataclass
class ContextClass:
"""Working directory and git context at session start
@@ -419,31 +484,51 @@ class ContextClass:
cwd: str
"""Current working directory path"""
+ base_commit: str | None = None
+ """Base commit of current git branch at session start time"""
+
branch: str | None = None
"""Current git branch name"""
git_root: str | None = None
"""Root directory of the git repository, resolved via git rev-parse"""
+ head_commit: str | None = None
+ """Head commit of current git branch at session start time"""
+
+ host_type: HostType | None = None
+ """Hosting platform type of the repository (github or ado)"""
+
repository: str | None = None
- """Repository identifier in "owner/name" format, derived from the git remote URL"""
+ """Repository identifier derived from the git remote URL ("owner/name" for GitHub,
+ "org/project/repo" for Azure DevOps)
+ """
@staticmethod
def from_dict(obj: Any) -> 'ContextClass':
assert isinstance(obj, dict)
cwd = from_str(obj.get("cwd"))
+ base_commit = from_union([from_str, from_none], obj.get("baseCommit"))
branch = from_union([from_str, from_none], obj.get("branch"))
git_root = from_union([from_str, from_none], obj.get("gitRoot"))
+ head_commit = from_union([from_str, from_none], obj.get("headCommit"))
+ host_type = from_union([HostType, from_none], obj.get("hostType"))
repository = from_union([from_str, from_none], obj.get("repository"))
- return ContextClass(cwd, branch, git_root, repository)
+ return ContextClass(cwd, base_commit, branch, git_root, head_commit, host_type, repository)
def to_dict(self) -> dict:
result: dict = {}
result["cwd"] = from_str(self.cwd)
+ if self.base_commit is not None:
+ result["baseCommit"] = from_union([from_str, from_none], self.base_commit)
if self.branch is not None:
result["branch"] = from_union([from_str, from_none], self.branch)
if self.git_root is not None:
result["gitRoot"] = from_union([from_str, from_none], self.git_root)
+ if self.head_commit is not None:
+ result["headCommit"] = from_union([from_str, from_none], self.head_commit)
+ if self.host_type is not None:
+ result["hostType"] = from_union([lambda x: to_enum(HostType, x), from_none], self.host_type)
if self.repository is not None:
result["repository"] = from_union([from_str, from_none], self.repository)
return result
@@ -451,6 +536,8 @@ def to_dict(self) -> dict:
@dataclass
class TokenDetail:
+ """Token usage detail for a single billing category"""
+
batch_size: float
"""Number of tokens in this billing batch"""
@@ -538,6 +625,132 @@ def to_dict(self) -> dict:
return result
+class Source(Enum):
+ """Discovery source"""
+
+ PROJECT = "project"
+ USER = "user"
+
+
+class ExtensionStatus(Enum):
+ """Current status: running, disabled, failed, or starting"""
+
+ DISABLED = "disabled"
+ FAILED = "failed"
+ RUNNING = "running"
+ STARTING = "starting"
+
+
+@dataclass
+class Extension:
+ id: str
+ """Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')"""
+
+ name: str
+ """Extension name (directory name)"""
+
+ source: Source
+ """Discovery source"""
+
+ status: ExtensionStatus
+ """Current status: running, disabled, failed, or starting"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Extension':
+ assert isinstance(obj, dict)
+ id = from_str(obj.get("id"))
+ name = from_str(obj.get("name"))
+ source = Source(obj.get("source"))
+ status = ExtensionStatus(obj.get("status"))
+ return Extension(id, name, source, status)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = from_str(self.id)
+ result["name"] = from_str(self.name)
+ result["source"] = to_enum(Source, self.source)
+ result["status"] = to_enum(ExtensionStatus, self.status)
+ return result
+
+
+class KindStatus(Enum):
+ """Whether the agent completed successfully or failed"""
+
+ COMPLETED = "completed"
+ FAILED = "failed"
+
+
+class KindType(Enum):
+ AGENT_COMPLETED = "agent_completed"
+ AGENT_IDLE = "agent_idle"
+ SHELL_COMPLETED = "shell_completed"
+ SHELL_DETACHED_COMPLETED = "shell_detached_completed"
+
+
+@dataclass
+class KindClass:
+ """Structured metadata identifying what triggered this notification"""
+
+ type: KindType
+ agent_id: str | None = None
+ """Unique identifier of the background agent"""
+
+ agent_type: str | None = None
+ """Type of the agent (e.g., explore, task, general-purpose)"""
+
+ description: str | None = None
+ """Human-readable description of the agent task
+
+ Human-readable description of the command
+ """
+ prompt: str | None = None
+ """The full prompt given to the background agent"""
+
+ status: KindStatus | None = None
+ """Whether the agent completed successfully or failed"""
+
+ exit_code: float | None = None
+ """Exit code of the shell command, if available"""
+
+ shell_id: str | None = None
+ """Unique identifier of the shell session
+
+ Unique identifier of the detached shell session
+ """
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'KindClass':
+ assert isinstance(obj, dict)
+ type = KindType(obj.get("type"))
+ agent_id = from_union([from_str, from_none], obj.get("agentId"))
+ agent_type = from_union([from_str, from_none], obj.get("agentType"))
+ description = from_union([from_str, from_none], obj.get("description"))
+ prompt = from_union([from_str, from_none], obj.get("prompt"))
+ status = from_union([KindStatus, from_none], obj.get("status"))
+ exit_code = from_union([from_float, from_none], obj.get("exitCode"))
+ shell_id = from_union([from_str, from_none], obj.get("shellId"))
+ return KindClass(type, agent_id, agent_type, description, prompt, status, exit_code, shell_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["type"] = to_enum(KindType, self.type)
+ if self.agent_id is not None:
+ result["agentId"] = from_union([from_str, from_none], self.agent_id)
+ if self.agent_type is not None:
+ result["agentType"] = from_union([from_str, from_none], self.agent_type)
+ if self.description is not None:
+ result["description"] = from_union([from_str, from_none], self.description)
+ if self.prompt is not None:
+ result["prompt"] = from_union([from_str, from_none], self.prompt)
+ if self.status is not None:
+ result["status"] = from_union([lambda x: to_enum(KindStatus, x), from_none], self.status)
+ if self.exit_code is not None:
+ result["exitCode"] = from_union([to_float, from_none], self.exit_code)
+ if self.shell_id is not None:
+ result["shellId"] = from_union([from_str, from_none], self.shell_id)
+ return result
+
+
@dataclass
class Metadata:
"""Metadata about the prompt template and its construction"""
@@ -565,7 +778,11 @@ def to_dict(self) -> dict:
class Mode(Enum):
+ """Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to
+ "form" when absent.
+ """
FORM = "form"
+ URL = "url"
@dataclass
@@ -659,7 +876,7 @@ class Operation(Enum):
@dataclass
-class Command:
+class PermissionRequestCommand:
identifier: str
"""Command identifier (e.g., executable name)"""
@@ -667,11 +884,11 @@ class Command:
"""Whether this command is read-only (no side effects)"""
@staticmethod
- def from_dict(obj: Any) -> 'Command':
+ def from_dict(obj: Any) -> 'PermissionRequestCommand':
assert isinstance(obj, dict)
identifier = from_str(obj.get("identifier"))
read_only = from_bool(obj.get("readOnly"))
- return Command(identifier, read_only)
+ return PermissionRequestCommand(identifier, read_only)
def to_dict(self) -> dict:
result: dict = {}
@@ -682,6 +899,7 @@ def to_dict(self) -> dict:
class PermissionRequestKind(Enum):
CUSTOM_TOOL = "custom-tool"
+ HOOK = "hook"
MCP = "mcp"
MEMORY = "memory"
READ = "read"
@@ -709,15 +927,31 @@ def to_dict(self) -> dict:
@dataclass
class PermissionRequest:
- """Details of the permission being requested"""
-
+ """Details of the permission being requested
+
+ Shell command permission request
+
+ File write permission request
+
+ File or directory read permission request
+
+ MCP tool invocation permission request
+
+ URL access permission request
+
+ Memory storage permission request
+
+ Custom tool invocation permission request
+
+ Hook confirmation permission request
+ """
kind: PermissionRequestKind
"""Permission kind discriminator"""
can_offer_session_approval: bool | None = None
"""Whether the UI can offer session-wide approval for this command pattern"""
- commands: list[Command] | None = None
+ commands: list[PermissionRequestCommand] | None = None
"""Parsed command identifiers found in the command text"""
full_command_text: str | None = None
@@ -774,6 +1008,8 @@ class PermissionRequest:
"""Internal name of the MCP tool
Name of the custom tool
+
+ Name of the tool the hook is gating
"""
tool_title: str | None = None
"""Human-readable title of the MCP tool"""
@@ -793,12 +1029,18 @@ class PermissionRequest:
tool_description: str | None = None
"""Description of what the custom tool does"""
+ hook_message: str | None = None
+ """Optional message from the hook explaining why confirmation is needed"""
+
+ tool_args: Any = None
+ """Arguments of the tool call being gated"""
+
@staticmethod
def from_dict(obj: Any) -> 'PermissionRequest':
assert isinstance(obj, dict)
kind = PermissionRequestKind(obj.get("kind"))
can_offer_session_approval = from_union([from_bool, from_none], obj.get("canOfferSessionApproval"))
- commands = from_union([lambda x: from_list(Command.from_dict, x), from_none], obj.get("commands"))
+ commands = from_union([lambda x: from_list(PermissionRequestCommand.from_dict, x), from_none], obj.get("commands"))
full_command_text = from_union([from_str, from_none], obj.get("fullCommandText"))
has_write_file_redirection = from_union([from_bool, from_none], obj.get("hasWriteFileRedirection"))
intention = from_union([from_str, from_none], obj.get("intention"))
@@ -820,7 +1062,9 @@ def from_dict(obj: Any) -> 'PermissionRequest':
fact = from_union([from_str, from_none], obj.get("fact"))
subject = from_union([from_str, from_none], obj.get("subject"))
tool_description = from_union([from_str, from_none], obj.get("toolDescription"))
- return PermissionRequest(kind, can_offer_session_approval, commands, full_command_text, has_write_file_redirection, intention, possible_paths, possible_urls, tool_call_id, warning, diff, file_name, new_file_contents, path, args, read_only, server_name, tool_name, tool_title, url, citations, fact, subject, tool_description)
+ hook_message = from_union([from_str, from_none], obj.get("hookMessage"))
+ tool_args = obj.get("toolArgs")
+ return PermissionRequest(kind, can_offer_session_approval, commands, full_command_text, has_write_file_redirection, intention, possible_paths, possible_urls, tool_call_id, warning, diff, file_name, new_file_contents, path, args, read_only, server_name, tool_name, tool_title, url, citations, fact, subject, tool_description, hook_message, tool_args)
def to_dict(self) -> dict:
result: dict = {}
@@ -828,7 +1072,7 @@ def to_dict(self) -> dict:
if self.can_offer_session_approval is not None:
result["canOfferSessionApproval"] = from_union([from_bool, from_none], self.can_offer_session_approval)
if self.commands is not None:
- result["commands"] = from_union([lambda x: from_list(lambda x: to_class(Command, x), x), from_none], self.commands)
+ result["commands"] = from_union([lambda x: from_list(lambda x: to_class(PermissionRequestCommand, x), x), from_none], self.commands)
if self.full_command_text is not None:
result["fullCommandText"] = from_union([from_str, from_none], self.full_command_text)
if self.has_write_file_redirection is not None:
@@ -871,6 +1115,10 @@ def to_dict(self) -> dict:
result["subject"] = from_union([from_str, from_none], self.subject)
if self.tool_description is not None:
result["toolDescription"] = from_union([from_str, from_none], self.tool_description)
+ if self.hook_message is not None:
+ result["hookMessage"] = from_union([from_str, from_none], self.hook_message)
+ if self.tool_args is not None:
+ result["toolArgs"] = self.tool_args
return result
@@ -963,12 +1211,14 @@ class RequestedSchemaType(Enum):
@dataclass
class RequestedSchema:
- """JSON Schema describing the form fields to present to the user"""
+ """JSON Schema describing the form fields to present to the user (form mode only)"""
properties: dict[str, Any]
"""Form field definitions, keyed by field name"""
type: RequestedSchemaType
+ """Schema type indicator (always 'object')"""
+
required: list[str] | None = None
"""List of required field names"""
@@ -998,6 +1248,8 @@ class Theme(Enum):
@dataclass
class Icon:
+ """Icon image for a resource"""
+
src: str
"""URL or path to the icon image"""
@@ -1081,6 +1333,21 @@ class ContentType(Enum):
@dataclass
class Content:
+ """A content block within a tool result, which may be text, terminal output, image, audio,
+ or a resource
+
+ Plain text content block
+
+ Terminal/shell output content block with optional exit code and working directory
+
+ Image content block with base64-encoded data
+
+ Audio content block with base64-encoded data
+
+ Resource link content block referencing an external resource
+
+ Embedded resource content block with inline text or binary data
+ """
type: ContentType
"""Content block type discriminator"""
@@ -1236,6 +1503,52 @@ class Role(Enum):
SYSTEM = "system"
+class ServerStatus(Enum):
+ """Connection status: connected, failed, pending, disabled, or not_configured
+
+ New connection status: connected, failed, pending, disabled, or not_configured
+ """
+ CONNECTED = "connected"
+ DISABLED = "disabled"
+ FAILED = "failed"
+ NOT_CONFIGURED = "not_configured"
+ PENDING = "pending"
+
+
+@dataclass
+class Server:
+ name: str
+ """Server name (config key)"""
+
+ status: ServerStatus
+ """Connection status: connected, failed, pending, disabled, or not_configured"""
+
+ error: str | None = None
+ """Error message if the server failed to connect"""
+
+ source: str | None = None
+ """Configuration source: user, workspace, plugin, or builtin"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Server':
+ assert isinstance(obj, dict)
+ name = from_str(obj.get("name"))
+ status = ServerStatus(obj.get("status"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ source = from_union([from_str, from_none], obj.get("source"))
+ return Server(name, status, error, source)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["name"] = from_str(self.name)
+ result["status"] = to_enum(ServerStatus, self.status)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ if self.source is not None:
+ result["source"] = from_union([from_str, from_none], self.source)
+ return result
+
+
class ShutdownType(Enum):
"""Whether the session ended normally ("routine") or due to a crash/fatal error ("error")"""
@@ -1243,6 +1556,49 @@ class ShutdownType(Enum):
ROUTINE = "routine"
+@dataclass
+class Skill:
+ description: str
+ """Description of what the skill does"""
+
+ enabled: bool
+ """Whether the skill is currently enabled"""
+
+ name: str
+ """Unique identifier for the skill"""
+
+ source: str
+ """Source location type of the skill (e.g., project, personal, plugin)"""
+
+ user_invocable: bool
+ """Whether the skill can be invoked by the user as a slash command"""
+
+ path: str | None = None
+ """Absolute path to the skill file, if available"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Skill':
+ assert isinstance(obj, dict)
+ description = from_str(obj.get("description"))
+ enabled = from_bool(obj.get("enabled"))
+ name = from_str(obj.get("name"))
+ source = from_str(obj.get("source"))
+ user_invocable = from_bool(obj.get("userInvocable"))
+ path = from_union([from_str, from_none], obj.get("path"))
+ return Skill(description, enabled, name, source, user_invocable, path)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["description"] = from_str(self.description)
+ result["enabled"] = from_bool(self.enabled)
+ result["name"] = from_str(self.name)
+ result["source"] = from_str(self.source)
+ result["userInvocable"] = from_bool(self.user_invocable)
+ if self.path is not None:
+ result["path"] = from_union([from_str, from_none], self.path)
+ return result
+
+
class SourceType(Enum):
"""Origin type of the session being handed off"""
@@ -1250,6 +1606,31 @@ class SourceType(Enum):
REMOTE = "remote"
+@dataclass
+class StaticClientConfig:
+ """Static OAuth client configuration, if the server specifies one"""
+
+ client_id: str
+ """OAuth client ID for the server"""
+
+ public_client: bool | None = None
+ """Whether this is a public OAuth client"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'StaticClientConfig':
+ assert isinstance(obj, dict)
+ client_id = from_str(obj.get("clientId"))
+ public_client = from_union([from_bool, from_none], obj.get("publicClient"))
+ return StaticClientConfig(client_id, public_client)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["clientId"] = from_str(self.client_id)
+ if self.public_client is not None:
+ result["publicClient"] = from_union([from_bool, from_none], self.public_client)
+ return result
+
+
class ToolRequestType(Enum):
"""Tool call type: "function" for standard tool calls, "custom" for grammar-based tool
calls. Defaults to "function" when absent.
@@ -1260,6 +1641,8 @@ class ToolRequestType(Enum):
@dataclass
class ToolRequest:
+ """A tool invocation request from the assistant"""
+
name: str
"""Name of the tool being invoked"""
@@ -1269,6 +1652,12 @@ class ToolRequest:
arguments: Any = None
"""Arguments to pass to the tool, format depends on the tool"""
+ intention_summary: str | None = None
+ """Resolved intention summary describing what this specific call does"""
+
+ tool_title: str | None = None
+ """Human-readable display title for the tool"""
+
type: ToolRequestType | None = None
"""Tool call type: "function" for standard tool calls, "custom" for grammar-based tool
calls. Defaults to "function" when absent.
@@ -1280,8 +1669,10 @@ def from_dict(obj: Any) -> 'ToolRequest':
name = from_str(obj.get("name"))
tool_call_id = from_str(obj.get("toolCallId"))
arguments = obj.get("arguments")
+ intention_summary = from_union([from_none, from_str], obj.get("intentionSummary"))
+ tool_title = from_union([from_str, from_none], obj.get("toolTitle"))
type = from_union([ToolRequestType, from_none], obj.get("type"))
- return ToolRequest(name, tool_call_id, arguments, type)
+ return ToolRequest(name, tool_call_id, arguments, intention_summary, tool_title, type)
def to_dict(self) -> dict:
result: dict = {}
@@ -1289,6 +1680,10 @@ def to_dict(self) -> dict:
result["toolCallId"] = from_str(self.tool_call_id)
if self.arguments is not None:
result["arguments"] = self.arguments
+ if self.intention_summary is not None:
+ result["intentionSummary"] = from_union([from_none, from_str], self.intention_summary)
+ if self.tool_title is not None:
+ result["toolTitle"] = from_union([from_str, from_none], self.tool_title)
if self.type is not None:
result["type"] = from_union([lambda x: to_enum(ToolRequestType, x), from_none], self.type)
return result
@@ -1296,14 +1691,141 @@ def to_dict(self) -> dict:
@dataclass
class Data:
- """Payload indicating the agent is idle; includes any background tasks still in flight
+ """Session initialization metadata including context and configuration
+
+ Session resume metadata including current context and event count
+
+ Error details for timeline display including message and optional diagnostic information
+
+ Payload indicating the agent is idle; includes any background tasks still in flight
+
+ Session title change payload containing the new display title
+
+ Informational message for timeline display with categorization
+
+ Warning message for timeline display with categorization
+
+ Model change details including previous and new model identifiers
+
+ Agent mode change details including previous and new modes
+
+ Plan file operation details indicating what changed
+
+ Workspace file change details including path and operation type
- Empty payload; the event signals that LLM-powered conversation compaction has begun
+ Session handoff metadata including source, context, and repository information
+
+ Conversation truncation statistics including token counts and removed content metrics
+
+ Session rewind details including target event and count of removed events
+
+ Session termination metrics including usage statistics, code changes, and shutdown
+ reason
+
+ Updated working directory and git context after the change
+
+ Current context window usage statistics including token and message counts
+
+ Context window breakdown at the start of LLM-powered conversation compaction
+
+ Conversation compaction results including success status, metrics, and optional error
+ details
+
+ Task completion notification with summary from the agent
Empty payload; the event signals that the pending message queue has changed
+ Turn initialization metadata including identifier and interaction tracking
+
+ Agent intent description for current activity or plan
+
+ Assistant reasoning content for timeline display with complete thinking text
+
+ Streaming reasoning delta for incremental extended thinking updates
+
+ Streaming response progress with cumulative byte count
+
+ Assistant response containing text content, optional tool requests, and interaction
+ metadata
+
+ Streaming assistant message delta for incremental response updates
+
+ Turn completion metadata including the turn identifier
+
+ LLM API call usage metrics including tokens, costs, quotas, and billing information
+
+ Turn abort information including the reason for termination
+
+ User-initiated tool invocation request with tool name and arguments
+
+ Tool execution startup details including MCP server information when applicable
+
+ Streaming tool execution output for incremental result display
+
+ Tool execution progress notification with status message
+
+ Tool execution completion results including success status, detailed output, and error
+ information
+
+ Skill invocation details including content, allowed tools, and plugin metadata
+
+ Sub-agent startup details including parent tool call and agent information
+
+ Sub-agent completion details for successful execution
+
+ Sub-agent failure details including error message and agent information
+
+ Custom agent selection details including name and available tools
+
Empty payload; the event signals that the custom agent was deselected, returning to the
default agent
+
+ Hook invocation start details including type and input data
+
+ Hook invocation completion details including output, success status, and error
+ information
+
+ System or developer message content with role and optional template metadata
+
+ System-generated notification for runtime events like background task completion
+
+ Permission request notification requiring client approval with request details
+
+ Permission request completion notification signaling UI dismissal
+
+ User input request notification with question and optional predefined choices
+
+ User input request completion notification signaling UI dismissal
+
+ Elicitation request; may be form-based (structured input) or URL-based (browser
+ redirect)
+
+ Elicitation request completion notification signaling UI dismissal
+
+ OAuth authentication request for an MCP server
+
+ MCP OAuth request completion notification
+
+ External tool invocation request for client-side tool execution
+
+ External tool completion notification signaling UI dismissal
+
+ Queued slash command dispatch request for client execution
+
+ Registered command dispatch request routed to the owning client
+
+ Queued command completion notification signaling UI dismissal
+
+ SDK command registration change notification
+
+ Plan approval request with plan content and available user actions
+
+ Plan mode exit completion notification signaling UI dismissal
+ """
+ already_in_use: bool | None = None
+ """Whether the session was already in use by another client at start time
+
+ Whether the session was already in use by another client at resume time
"""
context: ContextClass | str | None = None
"""Working directory and git context at session start
@@ -1318,9 +1840,17 @@ class Data:
producer: str | None = None
"""Identifier of the software producing the events (e.g., "copilot-agent")"""
+ reasoning_effort: str | None = None
+ """Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high",
+ "xhigh")
+
+ Reasoning effort level after the model change, if applicable
+ """
selected_model: str | None = None
- """Model selected at session creation time, if any"""
-
+ """Model selected at session creation time, if any
+
+ Model currently selected at resume time
+ """
session_id: str | None = None
"""Unique identifier for the session
@@ -1363,6 +1893,15 @@ class Data:
status_code: int | None = None
"""HTTP status code from the upstream request, if applicable"""
+ url: str | None = None
+ """Optional URL associated with this error that the user can open in a browser
+
+ Optional URL associated with this message that the user can open in a browser
+
+ Optional URL associated with this warning that the user can open in a browser
+
+ URL to open in the user's browser (url mode only)
+ """
background_tasks: BackgroundTasks | None = None
"""Background tasks still running when the agent became idle"""
@@ -1382,6 +1921,9 @@ class Data:
previous_model: str | None = None
"""Model that was previously selected, if any"""
+ previous_reasoning_effort: str | None = None
+ """Reasoning effort level before the model change, if applicable"""
+
new_mode: str | None = None
"""Agent mode after the change (e.g., "interactive", "plan", "autopilot")"""
@@ -1401,13 +1943,18 @@ class Data:
handoff_time: datetime | None = None
"""ISO 8601 timestamp when the handoff occurred"""
+ host: str | None = None
+ """GitHub host URL for the source session (e.g., https://github.com or
+ https://tenant.ghe.com)
+ """
remote_session_id: str | None = None
"""Session ID of the remote session being handed off"""
repository: RepositoryClass | str | None = None
"""Repository context for the handed-off session
- Repository identifier in "owner/name" format, derived from the git remote URL
+ Repository identifier derived from the git remote URL ("owner/name" for GitHub,
+ "org/project/repo" for Azure DevOps)
"""
source_type: SourceType | None = None
"""Origin type of the session being handed off"""
@@ -1415,7 +1962,7 @@ class Data:
summary: str | None = None
"""Summary of the work done in the source session
- Optional summary of the completed task, provided by the agent
+ Summary of the completed task, provided by the agent
Summary of the plan that was created
"""
@@ -1452,9 +1999,23 @@ class Data:
code_changes: CodeChanges | None = None
"""Aggregate code change metrics for the session"""
+ conversation_tokens: float | None = None
+ """Non-system message token count at shutdown
+
+ Token count from non-system messages (user, assistant, tool)
+
+ Token count from non-system messages (user, assistant, tool) at compaction start
+
+ Token count from non-system messages (user, assistant, tool) after compaction
+ """
current_model: str | None = None
"""Model that was selected at the time of shutdown"""
+ current_tokens: float | None = None
+ """Total tokens in context window at shutdown
+
+ Current number of tokens in the context window
+ """
error_reason: str | None = None
"""Error description when shutdownType is "error\""""
@@ -1467,12 +2028,33 @@ class Data:
shutdown_type: ShutdownType | None = None
"""Whether the session ended normally ("routine") or due to a crash/fatal error ("error")"""
+ system_tokens: float | None = None
+ """System message token count at shutdown
+
+ Token count from system message(s)
+
+ Token count from system message(s) at compaction start
+
+ Token count from system message(s) after compaction
+ """
+ tool_definitions_tokens: float | None = None
+ """Tool definitions token count at shutdown
+
+ Token count from tool definitions
+
+ Token count from tool definitions at compaction start
+
+ Token count from tool definitions after compaction
+ """
total_api_duration_ms: float | None = None
"""Cumulative time spent in API calls during the session, in milliseconds"""
total_premium_requests: float | None = None
"""Total number of premium API requests used during the session"""
+ base_commit: str | None = None
+ """Base commit of current git branch at session start time"""
+
branch: str | None = None
"""Current git branch name"""
@@ -1482,8 +2064,14 @@ class Data:
git_root: str | None = None
"""Root directory of the git repository, resolved via git rev-parse"""
- current_tokens: float | None = None
- """Current number of tokens in the context window"""
+ head_commit: str | None = None
+ """Head commit of current git branch at session start time"""
+
+ host_type: HostType | None = None
+ """Hosting platform type of the repository (github or ado)"""
+
+ is_initial: bool | None = None
+ """Whether this is the first usage_info event emitted in this session"""
messages_length: float | None = None
"""Current number of messages in the conversation"""
@@ -1539,6 +2127,11 @@ class Data:
Request ID of the resolved elicitation request; clients should dismiss any UI for this
request
+ Unique identifier for this OAuth request; used to respond via
+ session.respondToMcpOAuth()
+
+ Request ID of the resolved OAuth request
+
Unique identifier for this request; used to respond via session.respondToExternalTool()
Request ID of the resolved external tool request; clients should dismiss any UI for this
@@ -1546,6 +2139,8 @@ class Data:
Unique identifier for this request; used to respond via session.respondToQueuedCommand()
+ Unique identifier; used to respond via session.commands.handlePendingCommand()
+
Request ID of the resolved command request; clients should dismiss any UI for this
request
@@ -1557,6 +2152,8 @@ class Data:
success: bool | None = None
"""Whether compaction completed successfully
+ Whether the tool call succeeded. False when validation failed (e.g., invalid arguments)
+
Whether the tool execution completed successfully
Whether the hook completed successfully
@@ -1583,6 +2180,8 @@ class Data:
Full content of the skill file, injected into the conversation for the model
The system or developer prompt text
+
+ The notification text, typically wrapped in XML tags
"""
interaction_id: str | None = None
"""CAPI interaction ID for correlating this user message with its turn
@@ -1709,6 +2308,12 @@ class Data:
Tool call ID of the parent tool invocation that spawned this sub-agent
+ The LLM-assigned tool call ID that triggered this request; used by remote UIs to
+ correlate responses
+
+ Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id
+ for remote UIs
+
Tool call ID assigned to this external tool invocation
"""
tool_name: str | None = None
@@ -1793,6 +2398,9 @@ class Data:
role: Role | None = None
"""Message role: "system" for system prompts, "developer" for developer-injected instructions"""
+ kind: KindClass | None = None
+ """Structured metadata identifying what triggered this notification"""
+
permission_request: PermissionRequest | None = None
"""Details of the permission being requested"""
@@ -1805,14 +2413,46 @@ class Data:
question: str | None = None
"""The question or prompt to present to the user"""
- mode: Mode | None = None
- """Elicitation mode; currently only "form" is supported. Defaults to "form" when absent."""
+ elicitation_source: str | None = None
+ """The source that initiated the request (MCP server name, or absent for agent-initiated)"""
+ mode: Mode | None = None
+ """Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to
+ "form" when absent.
+ """
requested_schema: RequestedSchema | None = None
- """JSON Schema describing the form fields to present to the user"""
+ """JSON Schema describing the form fields to present to the user (form mode only)"""
+
+ server_name: str | None = None
+ """Display name of the MCP server that requires OAuth
+
+ Name of the MCP server whose status changed
+ """
+ server_url: str | None = None
+ """URL of the MCP server that requires OAuth"""
+
+ static_client_config: StaticClientConfig | None = None
+ """Static OAuth client configuration, if the server specifies one"""
+
+ traceparent: str | None = None
+ """W3C Trace Context traceparent header for the execute_tool span"""
+
+ tracestate: str | None = None
+ """W3C Trace Context tracestate header for the execute_tool span"""
command: str | None = None
- """The slash command text to be executed (e.g., /help, /clear)"""
+ """The slash command text to be executed (e.g., /help, /clear)
+
+ The full command text (e.g., /deploy production)
+ """
+ args: str | None = None
+ """Raw argument string after the command name"""
+
+ command_name: str | None = None
+ """Command name without leading /"""
+
+ commands: list[DataCommand] | None = None
+ """Current list of registered SDK commands"""
actions: list[str] | None = None
"""Available actions the user can take (e.g., approve, edit, reject)"""
@@ -1823,12 +2463,26 @@ class Data:
recommended_action: str | None = None
"""The recommended action for the user to take"""
+ skills: list[Skill] | None = None
+ """Array of resolved skill metadata"""
+
+ servers: list[Server] | None = None
+ """Array of MCP server status summaries"""
+
+ status: ServerStatus | None = None
+ """New connection status: connected, failed, pending, disabled, or not_configured"""
+
+ extensions: list[Extension] | None = None
+ """Array of discovered extensions and their status"""
+
@staticmethod
def from_dict(obj: Any) -> 'Data':
assert isinstance(obj, dict)
+ already_in_use = from_union([from_bool, from_none], obj.get("alreadyInUse"))
context = from_union([ContextClass.from_dict, from_str, from_none], obj.get("context"))
copilot_version = from_union([from_str, from_none], obj.get("copilotVersion"))
producer = from_union([from_str, from_none], obj.get("producer"))
+ reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort"))
selected_model = from_union([from_str, from_none], obj.get("selectedModel"))
session_id = from_union([from_str, from_none], obj.get("sessionId"))
start_time = from_union([from_datetime, from_none], obj.get("startTime"))
@@ -1840,17 +2494,20 @@ def from_dict(obj: Any) -> 'Data':
provider_call_id = from_union([from_str, from_none], obj.get("providerCallId"))
stack = from_union([from_str, from_none], obj.get("stack"))
status_code = from_union([from_int, from_none], obj.get("statusCode"))
+ url = from_union([from_str, from_none], obj.get("url"))
background_tasks = from_union([BackgroundTasks.from_dict, from_none], obj.get("backgroundTasks"))
title = from_union([from_str, from_none], obj.get("title"))
info_type = from_union([from_str, from_none], obj.get("infoType"))
warning_type = from_union([from_str, from_none], obj.get("warningType"))
new_model = from_union([from_str, from_none], obj.get("newModel"))
previous_model = from_union([from_str, from_none], obj.get("previousModel"))
+ previous_reasoning_effort = from_union([from_str, from_none], obj.get("previousReasoningEffort"))
new_mode = from_union([from_str, from_none], obj.get("newMode"))
previous_mode = from_union([from_str, from_none], obj.get("previousMode"))
operation = from_union([Operation, from_none], obj.get("operation"))
path = from_union([from_str, from_none], obj.get("path"))
handoff_time = from_union([from_datetime, from_none], obj.get("handoffTime"))
+ host = from_union([from_str, from_none], obj.get("host"))
remote_session_id = from_union([from_str, from_none], obj.get("remoteSessionId"))
repository = from_union([RepositoryClass.from_dict, from_str, from_none], obj.get("repository"))
source_type = from_union([SourceType, from_none], obj.get("sourceType"))
@@ -1866,17 +2523,24 @@ def from_dict(obj: Any) -> 'Data':
events_removed = from_union([from_float, from_none], obj.get("eventsRemoved"))
up_to_event_id = from_union([from_str, from_none], obj.get("upToEventId"))
code_changes = from_union([CodeChanges.from_dict, from_none], obj.get("codeChanges"))
+ conversation_tokens = from_union([from_float, from_none], obj.get("conversationTokens"))
current_model = from_union([from_str, from_none], obj.get("currentModel"))
+ current_tokens = from_union([from_float, from_none], obj.get("currentTokens"))
error_reason = from_union([from_str, from_none], obj.get("errorReason"))
model_metrics = from_union([lambda x: from_dict(ModelMetric.from_dict, x), from_none], obj.get("modelMetrics"))
session_start_time = from_union([from_float, from_none], obj.get("sessionStartTime"))
shutdown_type = from_union([ShutdownType, from_none], obj.get("shutdownType"))
+ system_tokens = from_union([from_float, from_none], obj.get("systemTokens"))
+ tool_definitions_tokens = from_union([from_float, from_none], obj.get("toolDefinitionsTokens"))
total_api_duration_ms = from_union([from_float, from_none], obj.get("totalApiDurationMs"))
total_premium_requests = from_union([from_float, from_none], obj.get("totalPremiumRequests"))
+ base_commit = from_union([from_str, from_none], obj.get("baseCommit"))
branch = from_union([from_str, from_none], obj.get("branch"))
cwd = from_union([from_str, from_none], obj.get("cwd"))
git_root = from_union([from_str, from_none], obj.get("gitRoot"))
- current_tokens = from_union([from_float, from_none], obj.get("currentTokens"))
+ head_commit = from_union([from_str, from_none], obj.get("headCommit"))
+ host_type = from_union([HostType, from_none], obj.get("hostType"))
+ is_initial = from_union([from_bool, from_none], obj.get("isInitial"))
messages_length = from_union([from_float, from_none], obj.get("messagesLength"))
checkpoint_number = from_union([from_float, from_none], obj.get("checkpointNumber"))
checkpoint_path = from_union([from_str, from_none], obj.get("checkpointPath"))
@@ -1944,26 +2608,44 @@ def from_dict(obj: Any) -> 'Data':
output = obj.get("output")
metadata = from_union([Metadata.from_dict, from_none], obj.get("metadata"))
role = from_union([Role, from_none], obj.get("role"))
+ kind = from_union([KindClass.from_dict, from_none], obj.get("kind"))
permission_request = from_union([PermissionRequest.from_dict, from_none], obj.get("permissionRequest"))
allow_freeform = from_union([from_bool, from_none], obj.get("allowFreeform"))
choices = from_union([lambda x: from_list(from_str, x), from_none], obj.get("choices"))
question = from_union([from_str, from_none], obj.get("question"))
+ elicitation_source = from_union([from_str, from_none], obj.get("elicitationSource"))
mode = from_union([Mode, from_none], obj.get("mode"))
requested_schema = from_union([RequestedSchema.from_dict, from_none], obj.get("requestedSchema"))
+ server_name = from_union([from_str, from_none], obj.get("serverName"))
+ server_url = from_union([from_str, from_none], obj.get("serverUrl"))
+ static_client_config = from_union([StaticClientConfig.from_dict, from_none], obj.get("staticClientConfig"))
+ traceparent = from_union([from_str, from_none], obj.get("traceparent"))
+ tracestate = from_union([from_str, from_none], obj.get("tracestate"))
command = from_union([from_str, from_none], obj.get("command"))
+ args = from_union([from_str, from_none], obj.get("args"))
+ command_name = from_union([from_str, from_none], obj.get("commandName"))
+ commands = from_union([lambda x: from_list(DataCommand.from_dict, x), from_none], obj.get("commands"))
actions = from_union([lambda x: from_list(from_str, x), from_none], obj.get("actions"))
plan_content = from_union([from_str, from_none], obj.get("planContent"))
recommended_action = from_union([from_str, from_none], obj.get("recommendedAction"))
- return Data(context, copilot_version, producer, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, background_tasks, title, info_type, warning_type, new_model, previous_model, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, branch, cwd, git_root, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, permission_request, allow_freeform, choices, question, mode, requested_schema, command, actions, plan_content, recommended_action)
+ skills = from_union([lambda x: from_list(Skill.from_dict, x), from_none], obj.get("skills"))
+ servers = from_union([lambda x: from_list(Server.from_dict, x), from_none], obj.get("servers"))
+ status = from_union([ServerStatus, from_none], obj.get("status"))
+ extensions = from_union([lambda x: from_list(Extension.from_dict, x), from_none], obj.get("extensions"))
+ return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, url, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, host, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, conversation_tokens, current_model, current_tokens, error_reason, model_metrics, session_start_time, shutdown_type, system_tokens, tool_definitions_tokens, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, is_initial, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, elicitation_source, mode, requested_schema, server_name, server_url, static_client_config, traceparent, tracestate, command, args, command_name, commands, actions, plan_content, recommended_action, skills, servers, status, extensions)
def to_dict(self) -> dict:
result: dict = {}
+ if self.already_in_use is not None:
+ result["alreadyInUse"] = from_union([from_bool, from_none], self.already_in_use)
if self.context is not None:
result["context"] = from_union([lambda x: to_class(ContextClass, x), from_str, from_none], self.context)
if self.copilot_version is not None:
result["copilotVersion"] = from_union([from_str, from_none], self.copilot_version)
if self.producer is not None:
result["producer"] = from_union([from_str, from_none], self.producer)
+ if self.reasoning_effort is not None:
+ result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort)
if self.selected_model is not None:
result["selectedModel"] = from_union([from_str, from_none], self.selected_model)
if self.session_id is not None:
@@ -1986,6 +2668,8 @@ def to_dict(self) -> dict:
result["stack"] = from_union([from_str, from_none], self.stack)
if self.status_code is not None:
result["statusCode"] = from_union([from_int, from_none], self.status_code)
+ if self.url is not None:
+ result["url"] = from_union([from_str, from_none], self.url)
if self.background_tasks is not None:
result["backgroundTasks"] = from_union([lambda x: to_class(BackgroundTasks, x), from_none], self.background_tasks)
if self.title is not None:
@@ -1998,6 +2682,8 @@ def to_dict(self) -> dict:
result["newModel"] = from_union([from_str, from_none], self.new_model)
if self.previous_model is not None:
result["previousModel"] = from_union([from_str, from_none], self.previous_model)
+ if self.previous_reasoning_effort is not None:
+ result["previousReasoningEffort"] = from_union([from_str, from_none], self.previous_reasoning_effort)
if self.new_mode is not None:
result["newMode"] = from_union([from_str, from_none], self.new_mode)
if self.previous_mode is not None:
@@ -2008,6 +2694,8 @@ def to_dict(self) -> dict:
result["path"] = from_union([from_str, from_none], self.path)
if self.handoff_time is not None:
result["handoffTime"] = from_union([lambda x: x.isoformat(), from_none], self.handoff_time)
+ if self.host is not None:
+ result["host"] = from_union([from_str, from_none], self.host)
if self.remote_session_id is not None:
result["remoteSessionId"] = from_union([from_str, from_none], self.remote_session_id)
if self.repository is not None:
@@ -2038,8 +2726,12 @@ def to_dict(self) -> dict:
result["upToEventId"] = from_union([from_str, from_none], self.up_to_event_id)
if self.code_changes is not None:
result["codeChanges"] = from_union([lambda x: to_class(CodeChanges, x), from_none], self.code_changes)
+ if self.conversation_tokens is not None:
+ result["conversationTokens"] = from_union([to_float, from_none], self.conversation_tokens)
if self.current_model is not None:
result["currentModel"] = from_union([from_str, from_none], self.current_model)
+ if self.current_tokens is not None:
+ result["currentTokens"] = from_union([to_float, from_none], self.current_tokens)
if self.error_reason is not None:
result["errorReason"] = from_union([from_str, from_none], self.error_reason)
if self.model_metrics is not None:
@@ -2048,18 +2740,28 @@ def to_dict(self) -> dict:
result["sessionStartTime"] = from_union([to_float, from_none], self.session_start_time)
if self.shutdown_type is not None:
result["shutdownType"] = from_union([lambda x: to_enum(ShutdownType, x), from_none], self.shutdown_type)
+ if self.system_tokens is not None:
+ result["systemTokens"] = from_union([to_float, from_none], self.system_tokens)
+ if self.tool_definitions_tokens is not None:
+ result["toolDefinitionsTokens"] = from_union([to_float, from_none], self.tool_definitions_tokens)
if self.total_api_duration_ms is not None:
result["totalApiDurationMs"] = from_union([to_float, from_none], self.total_api_duration_ms)
if self.total_premium_requests is not None:
result["totalPremiumRequests"] = from_union([to_float, from_none], self.total_premium_requests)
+ if self.base_commit is not None:
+ result["baseCommit"] = from_union([from_str, from_none], self.base_commit)
if self.branch is not None:
result["branch"] = from_union([from_str, from_none], self.branch)
if self.cwd is not None:
result["cwd"] = from_union([from_str, from_none], self.cwd)
if self.git_root is not None:
result["gitRoot"] = from_union([from_str, from_none], self.git_root)
- if self.current_tokens is not None:
- result["currentTokens"] = from_union([to_float, from_none], self.current_tokens)
+ if self.head_commit is not None:
+ result["headCommit"] = from_union([from_str, from_none], self.head_commit)
+ if self.host_type is not None:
+ result["hostType"] = from_union([lambda x: to_enum(HostType, x), from_none], self.host_type)
+ if self.is_initial is not None:
+ result["isInitial"] = from_union([from_bool, from_none], self.is_initial)
if self.messages_length is not None:
result["messagesLength"] = from_union([to_float, from_none], self.messages_length)
if self.checkpoint_number is not None:
@@ -2194,6 +2896,8 @@ def to_dict(self) -> dict:
result["metadata"] = from_union([lambda x: to_class(Metadata, x), from_none], self.metadata)
if self.role is not None:
result["role"] = from_union([lambda x: to_enum(Role, x), from_none], self.role)
+ if self.kind is not None:
+ result["kind"] = from_union([lambda x: to_class(KindClass, x), from_none], self.kind)
if self.permission_request is not None:
result["permissionRequest"] = from_union([lambda x: to_class(PermissionRequest, x), from_none], self.permission_request)
if self.allow_freeform is not None:
@@ -2202,18 +2906,44 @@ def to_dict(self) -> dict:
result["choices"] = from_union([lambda x: from_list(from_str, x), from_none], self.choices)
if self.question is not None:
result["question"] = from_union([from_str, from_none], self.question)
+ if self.elicitation_source is not None:
+ result["elicitationSource"] = from_union([from_str, from_none], self.elicitation_source)
if self.mode is not None:
result["mode"] = from_union([lambda x: to_enum(Mode, x), from_none], self.mode)
if self.requested_schema is not None:
result["requestedSchema"] = from_union([lambda x: to_class(RequestedSchema, x), from_none], self.requested_schema)
+ if self.server_name is not None:
+ result["serverName"] = from_union([from_str, from_none], self.server_name)
+ if self.server_url is not None:
+ result["serverUrl"] = from_union([from_str, from_none], self.server_url)
+ if self.static_client_config is not None:
+ result["staticClientConfig"] = from_union([lambda x: to_class(StaticClientConfig, x), from_none], self.static_client_config)
+ if self.traceparent is not None:
+ result["traceparent"] = from_union([from_str, from_none], self.traceparent)
+ if self.tracestate is not None:
+ result["tracestate"] = from_union([from_str, from_none], self.tracestate)
if self.command is not None:
result["command"] = from_union([from_str, from_none], self.command)
+ if self.args is not None:
+ result["args"] = from_union([from_str, from_none], self.args)
+ if self.command_name is not None:
+ result["commandName"] = from_union([from_str, from_none], self.command_name)
+ if self.commands is not None:
+ result["commands"] = from_union([lambda x: from_list(lambda x: to_class(DataCommand, x), x), from_none], self.commands)
if self.actions is not None:
result["actions"] = from_union([lambda x: from_list(from_str, x), from_none], self.actions)
if self.plan_content is not None:
result["planContent"] = from_union([from_str, from_none], self.plan_content)
if self.recommended_action is not None:
result["recommendedAction"] = from_union([from_str, from_none], self.recommended_action)
+ if self.skills is not None:
+ result["skills"] = from_union([lambda x: from_list(lambda x: to_class(Skill, x), x), from_none], self.skills)
+ if self.servers is not None:
+ result["servers"] = from_union([lambda x: from_list(lambda x: to_class(Server, x), x), from_none], self.servers)
+ if self.status is not None:
+ result["status"] = from_union([lambda x: to_enum(ServerStatus, x), from_none], self.status)
+ if self.extensions is not None:
+ result["extensions"] = from_union([lambda x: from_list(lambda x: to_class(Extension, x), x), from_none], self.extensions)
return result
@@ -2228,7 +2958,9 @@ class SessionEventType(Enum):
ASSISTANT_TURN_END = "assistant.turn_end"
ASSISTANT_TURN_START = "assistant.turn_start"
ASSISTANT_USAGE = "assistant.usage"
+ COMMANDS_CHANGED = "commands.changed"
COMMAND_COMPLETED = "command.completed"
+ COMMAND_EXECUTE = "command.execute"
COMMAND_QUEUED = "command.queued"
ELICITATION_COMPLETED = "elicitation.completed"
ELICITATION_REQUESTED = "elicitation.requested"
@@ -2238,25 +2970,33 @@ class SessionEventType(Enum):
EXTERNAL_TOOL_REQUESTED = "external_tool.requested"
HOOK_END = "hook.end"
HOOK_START = "hook.start"
+ MCP_OAUTH_COMPLETED = "mcp.oauth_completed"
+ MCP_OAUTH_REQUIRED = "mcp.oauth_required"
PENDING_MESSAGES_MODIFIED = "pending_messages.modified"
PERMISSION_COMPLETED = "permission.completed"
PERMISSION_REQUESTED = "permission.requested"
+ SESSION_BACKGROUND_TASKS_CHANGED = "session.background_tasks_changed"
SESSION_COMPACTION_COMPLETE = "session.compaction_complete"
SESSION_COMPACTION_START = "session.compaction_start"
SESSION_CONTEXT_CHANGED = "session.context_changed"
SESSION_ERROR = "session.error"
+ SESSION_EXTENSIONS_LOADED = "session.extensions_loaded"
SESSION_HANDOFF = "session.handoff"
SESSION_IDLE = "session.idle"
SESSION_INFO = "session.info"
+ SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded"
+ SESSION_MCP_SERVER_STATUS_CHANGED = "session.mcp_server_status_changed"
SESSION_MODEL_CHANGE = "session.model_change"
SESSION_MODE_CHANGED = "session.mode_changed"
SESSION_PLAN_CHANGED = "session.plan_changed"
SESSION_RESUME = "session.resume"
SESSION_SHUTDOWN = "session.shutdown"
+ SESSION_SKILLS_LOADED = "session.skills_loaded"
SESSION_SNAPSHOT_REWIND = "session.snapshot_rewind"
SESSION_START = "session.start"
SESSION_TASK_COMPLETE = "session.task_complete"
SESSION_TITLE_CHANGED = "session.title_changed"
+ SESSION_TOOLS_UPDATED = "session.tools_updated"
SESSION_TRUNCATION = "session.truncation"
SESSION_USAGE_INFO = "session.usage_info"
SESSION_WARNING = "session.warning"
@@ -2268,6 +3008,7 @@ class SessionEventType(Enum):
SUBAGENT_SELECTED = "subagent.selected"
SUBAGENT_STARTED = "subagent.started"
SYSTEM_MESSAGE = "system.message"
+ SYSTEM_NOTIFICATION = "system.notification"
TOOL_EXECUTION_COMPLETE = "tool.execution_complete"
TOOL_EXECUTION_PARTIAL_RESULT = "tool.execution_partial_result"
TOOL_EXECUTION_PROGRESS = "tool.execution_progress"
@@ -2289,14 +3030,136 @@ def _missing_(cls, value: object) -> "SessionEventType":
@dataclass
class SessionEvent:
data: Data
- """Payload indicating the agent is idle; includes any background tasks still in flight
+ """Session initialization metadata including context and configuration
+
+ Session resume metadata including current context and event count
+
+ Error details for timeline display including message and optional diagnostic information
+
+ Payload indicating the agent is idle; includes any background tasks still in flight
+
+ Session title change payload containing the new display title
+
+ Informational message for timeline display with categorization
+
+ Warning message for timeline display with categorization
+
+ Model change details including previous and new model identifiers
+
+ Agent mode change details including previous and new modes
+
+ Plan file operation details indicating what changed
+
+ Workspace file change details including path and operation type
- Empty payload; the event signals that LLM-powered conversation compaction has begun
+ Session handoff metadata including source, context, and repository information
+
+ Conversation truncation statistics including token counts and removed content metrics
+
+ Session rewind details including target event and count of removed events
+
+ Session termination metrics including usage statistics, code changes, and shutdown
+ reason
+
+ Updated working directory and git context after the change
+
+ Current context window usage statistics including token and message counts
+
+ Context window breakdown at the start of LLM-powered conversation compaction
+
+ Conversation compaction results including success status, metrics, and optional error
+ details
+
+ Task completion notification with summary from the agent
Empty payload; the event signals that the pending message queue has changed
+ Turn initialization metadata including identifier and interaction tracking
+
+ Agent intent description for current activity or plan
+
+ Assistant reasoning content for timeline display with complete thinking text
+
+ Streaming reasoning delta for incremental extended thinking updates
+
+ Streaming response progress with cumulative byte count
+
+ Assistant response containing text content, optional tool requests, and interaction
+ metadata
+
+ Streaming assistant message delta for incremental response updates
+
+ Turn completion metadata including the turn identifier
+
+ LLM API call usage metrics including tokens, costs, quotas, and billing information
+
+ Turn abort information including the reason for termination
+
+ User-initiated tool invocation request with tool name and arguments
+
+ Tool execution startup details including MCP server information when applicable
+
+ Streaming tool execution output for incremental result display
+
+ Tool execution progress notification with status message
+
+ Tool execution completion results including success status, detailed output, and error
+ information
+
+ Skill invocation details including content, allowed tools, and plugin metadata
+
+ Sub-agent startup details including parent tool call and agent information
+
+ Sub-agent completion details for successful execution
+
+ Sub-agent failure details including error message and agent information
+
+ Custom agent selection details including name and available tools
+
Empty payload; the event signals that the custom agent was deselected, returning to the
default agent
+
+ Hook invocation start details including type and input data
+
+ Hook invocation completion details including output, success status, and error
+ information
+
+ System or developer message content with role and optional template metadata
+
+ System-generated notification for runtime events like background task completion
+
+ Permission request notification requiring client approval with request details
+
+ Permission request completion notification signaling UI dismissal
+
+ User input request notification with question and optional predefined choices
+
+ User input request completion notification signaling UI dismissal
+
+ Elicitation request; may be form-based (structured input) or URL-based (browser
+ redirect)
+
+ Elicitation request completion notification signaling UI dismissal
+
+ OAuth authentication request for an MCP server
+
+ MCP OAuth request completion notification
+
+ External tool invocation request for client-side tool execution
+
+ External tool completion notification signaling UI dismissal
+
+ Queued slash command dispatch request for client execution
+
+ Registered command dispatch request routed to the owning client
+
+ Queued command completion notification signaling UI dismissal
+
+ SDK command registration change notification
+
+ Plan approval request with plan content and available user actions
+
+ Plan mode exit completion notification signaling UI dismissal
"""
id: UUID
"""Unique event identifier (UUID v4), generated when the event is emitted"""
diff --git a/python/copilot/session.py b/python/copilot/session.py
index e0e72fc68d..29421724cf 100644
--- a/python/copilot/session.py
+++ b/python/copilot/session.py
@@ -9,11 +9,15 @@
import inspect
import threading
from collections.abc import Callable
-from typing import Any, cast
+from typing import Any, Literal, cast
+from ._jsonrpc import JsonRpcError, ProcessExitedError
+from ._telemetry import get_trace_context, trace_context
from .generated.rpc import (
Kind,
+ Level,
ResultResult,
+ SessionLogParams,
SessionModelSwitchToParams,
SessionPermissionsHandlePendingPermissionRequestParams,
SessionPermissionsHandlePendingPermissionRequestParamsResult,
@@ -21,11 +25,11 @@
SessionToolsHandlePendingToolCallParams,
)
from .generated.session_events import SessionEvent, SessionEventType, session_event_from_dict
-from .jsonrpc import JsonRpcError, ProcessExitedError
from .types import (
- MessageOptions,
+ Attachment,
PermissionRequest,
PermissionRequestResult,
+ SectionTransformFn,
SessionHooks,
Tool,
ToolHandler,
@@ -61,7 +65,7 @@ class CopilotSession:
... unsubscribe = session.on(lambda event: print(event.type))
...
... # Send a message
- ... await session.send({"prompt": "Hello, world!"})
+ ... await session.send("Hello, world!")
...
... # Clean up
... unsubscribe()
@@ -94,6 +98,8 @@ def __init__(self, session_id: str, client: Any, workspace_path: str | None = No
self._user_input_handler_lock = threading.Lock()
self._hooks: SessionHooks | None = None
self._hooks_lock = threading.Lock()
+ self._transform_callbacks: dict[str, SectionTransformFn] | None = None
+ self._transform_callbacks_lock = threading.Lock()
self._rpc: SessionRpc | None = None
@property
@@ -113,43 +119,57 @@ def workspace_path(self) -> str | None:
"""
return self._workspace_path
- async def send(self, options: MessageOptions) -> str:
+ async def send(
+ self,
+ prompt: str,
+ *,
+ attachments: list[Attachment] | None = None,
+ mode: Literal["enqueue", "immediate"] | None = None,
+ ) -> str:
"""
- Send a message to this session and wait for the response.
+ Send a message to this session.
The message is processed asynchronously. Subscribe to events via :meth:`on`
- to receive streaming responses and other session events.
+ to receive streaming responses and other session events. Use
+ :meth:`send_and_wait` to block until the assistant finishes processing.
Args:
- options: Message options including the prompt and optional attachments.
- Must contain a "prompt" key with the message text. Can optionally
- include "attachments" and "mode" keys.
+ prompt: The message text to send.
+ attachments: Optional file, directory, or selection attachments.
+ mode: Message delivery mode (``"enqueue"`` or ``"immediate"``).
Returns:
- The message ID of the response, which can be used to correlate events.
+ The message ID assigned by the server, which can be used to correlate events.
Raises:
Exception: If the session has been disconnected or the connection fails.
Example:
- >>> message_id = await session.send({
- ... "prompt": "Explain this code",
- ... "attachments": [{"type": "file", "path": "./src/main.py"}]
- ... })
- """
- response = await self._client.request(
- "session.send",
- {
- "sessionId": self.session_id,
- "prompt": options["prompt"],
- "attachments": options.get("attachments"),
- "mode": options.get("mode"),
- },
- )
+ >>> message_id = await session.send(
+ ... "Explain this code",
+ ... attachments=[{"type": "file", "path": "./src/main.py"}],
+ ... )
+ """
+ params: dict[str, Any] = {
+ "sessionId": self.session_id,
+ "prompt": prompt,
+ }
+ if attachments is not None:
+ params["attachments"] = attachments
+ if mode is not None:
+ params["mode"] = mode
+ params.update(get_trace_context())
+
+ response = await self._client.request("session.send", params)
return response["messageId"]
async def send_and_wait(
- self, options: MessageOptions, timeout: float | None = None
+ self,
+ prompt: str,
+ *,
+ attachments: list[Attachment] | None = None,
+ mode: Literal["enqueue", "immediate"] | None = None,
+ timeout: float = 60.0,
) -> SessionEvent | None:
"""
Send a message to this session and wait until the session becomes idle.
@@ -161,7 +181,9 @@ async def send_and_wait(
Events are still delivered to handlers registered via :meth:`on` while waiting.
Args:
- options: Message options including the prompt and optional attachments.
+ prompt: The message text to send.
+ attachments: Optional file, directory, or selection attachments.
+ mode: Message delivery mode (``"enqueue"`` or ``"immediate"``).
timeout: Timeout in seconds (default: 60). Controls how long to wait;
does not abort in-flight agent work.
@@ -173,12 +195,10 @@ async def send_and_wait(
Exception: If the session has been disconnected or the connection fails.
Example:
- >>> response = await session.send_and_wait({"prompt": "What is 2+2?"})
+ >>> response = await session.send_and_wait("What is 2+2?")
>>> if response:
... print(response.data.content)
"""
- effective_timeout = timeout if timeout is not None else 60.0
-
idle_event = asyncio.Event()
error_event: Exception | None = None
last_assistant_message: SessionEvent | None = None
@@ -197,13 +217,13 @@ def handler(event: SessionEventTypeAlias) -> None:
unsubscribe = self.on(handler)
try:
- await self.send(options)
- await asyncio.wait_for(idle_event.wait(), timeout=effective_timeout)
+ await self.send(prompt, attachments=attachments, mode=mode)
+ await asyncio.wait_for(idle_event.wait(), timeout=timeout)
if error_event:
raise error_event
return last_assistant_message
except TimeoutError:
- raise TimeoutError(f"Timeout after {effective_timeout}s waiting for session.idle")
+ raise TimeoutError(f"Timeout after {timeout}s waiting for session.idle")
finally:
unsubscribe()
@@ -228,9 +248,7 @@ def on(self, handler: Callable[[SessionEvent], None]) -> Callable[[], None]:
... print(f"Assistant: {event.data.content}")
... elif event.type == "session.error":
... print(f"Error: {event.data.message}")
- ...
>>> unsubscribe = session.on(handle_event)
- ...
>>> # Later, to stop receiving events:
>>> unsubscribe()
"""
@@ -287,9 +305,11 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None:
tool_call_id = event.data.tool_call_id or ""
arguments = event.data.arguments
+ tp = getattr(event.data, "traceparent", None)
+ ts = getattr(event.data, "tracestate", None)
asyncio.ensure_future(
self._execute_tool_and_respond(
- request_id, tool_name, tool_call_id, arguments, handler
+ request_id, tool_name, tool_call_id, arguments, handler, tp, ts
)
)
@@ -315,6 +335,8 @@ async def _execute_tool_and_respond(
tool_call_id: str,
arguments: Any,
handler: ToolHandler,
+ traceparent: str | None = None,
+ tracestate: str | None = None,
) -> None:
"""Execute a tool handler and send the result back via HandlePendingToolCall RPC."""
try:
@@ -325,9 +347,10 @@ async def _execute_tool_and_respond(
arguments=arguments,
)
- result = handler(invocation)
- if inspect.isawaitable(result):
- result = await result
+ with trace_context(traceparent, tracestate):
+ result = handler(invocation)
+ if inspect.isawaitable(result):
+ result = await result
tool_result: ToolResult
if result is None:
@@ -385,6 +408,8 @@ async def _execute_permission_and_respond(
result = await result
result = cast(PermissionRequestResult, result)
+ if result.kind == "no-result":
+ return
perm_result = SessionPermissionsHandlePendingPermissionRequestParamsResult(
kind=Kind(result.kind),
@@ -612,6 +637,62 @@ async def _handle_hooks_invoke(self, hook_type: str, input_data: Any) -> Any:
# Hook failed, return None
return None
+ def _register_transform_callbacks(
+ self, callbacks: dict[str, SectionTransformFn] | None
+ ) -> None:
+ """
+ Register transform callbacks for system message sections.
+
+ Transform callbacks allow modifying individual sections of the system
+ prompt at runtime. Each callback receives the current section content
+ and returns the transformed content.
+
+ Note:
+ This method is internal. Transform callbacks are typically registered
+ when creating a session via :meth:`CopilotClient.create_session`.
+
+ Args:
+ callbacks: A dict mapping section IDs to transform functions,
+ or None to remove all callbacks.
+ """
+ with self._transform_callbacks_lock:
+ self._transform_callbacks = callbacks
+
+ async def _handle_system_message_transform(
+ self, sections: dict[str, dict[str, str]]
+ ) -> dict[str, dict[str, dict[str, str]]]:
+ """
+ Handle a systemMessage.transform request from the runtime.
+
+ Note:
+ This method is internal and should not be called directly.
+
+ Args:
+ sections: A dict mapping section IDs to section data dicts
+ containing a ``"content"`` key.
+
+ Returns:
+ A dict with a ``"sections"`` key containing the transformed section data.
+ """
+ with self._transform_callbacks_lock:
+ callbacks = self._transform_callbacks
+
+ result: dict[str, dict[str, str]] = {}
+ for section_id, section_data in sections.items():
+ content = section_data.get("content", "")
+ callback = callbacks.get(section_id) if callbacks else None
+ if callback:
+ try:
+ transformed = callback(content)
+ if inspect.isawaitable(transformed):
+ transformed = await transformed
+ result[section_id] = {"content": str(transformed)}
+ except Exception: # pylint: disable=broad-except
+ result[section_id] = {"content": content}
+ else:
+ result[section_id] = {"content": content}
+ return {"sections": result}
+
async def get_messages(self) -> list[SessionEvent]:
"""
Retrieve all events and messages from this session's history.
@@ -706,9 +787,7 @@ async def abort(self) -> None:
>>> import asyncio
>>>
>>> # Start a long-running request
- >>> task = asyncio.create_task(
- ... session.send({"prompt": "Write a very long story..."})
- ... )
+ >>> task = asyncio.create_task(session.send("Write a very long story..."))
>>>
>>> # Abort after 5 seconds
>>> await asyncio.sleep(5)
@@ -716,7 +795,7 @@ async def abort(self) -> None:
"""
await self._client.request("session.abort", {"sessionId": self.session_id})
- async def set_model(self, model: str) -> None:
+ async def set_model(self, model: str, *, reasoning_effort: str | None = None) -> None:
"""
Change the model for this session.
@@ -725,11 +804,53 @@ async def set_model(self, model: str) -> None:
Args:
model: Model ID to switch to (e.g., "gpt-4.1", "claude-sonnet-4").
+ reasoning_effort: Optional reasoning effort level for the new model
+ (e.g., "low", "medium", "high", "xhigh").
Raises:
Exception: If the session has been destroyed or the connection fails.
Example:
>>> await session.set_model("gpt-4.1")
+ >>> await session.set_model("claude-sonnet-4.6", reasoning_effort="high")
"""
- await self.rpc.model.switch_to(SessionModelSwitchToParams(model_id=model))
+ await self.rpc.model.switch_to(
+ SessionModelSwitchToParams(
+ model_id=model,
+ reasoning_effort=reasoning_effort,
+ )
+ )
+
+ async def log(
+ self,
+ message: str,
+ *,
+ level: str | None = None,
+ ephemeral: bool | None = None,
+ ) -> None:
+ """
+ Log a message to the session timeline.
+
+ The message appears in the session event stream and is visible to SDK consumers
+ and (for non-ephemeral messages) persisted to the session event log on disk.
+
+ Args:
+ message: The human-readable message to log.
+ level: Log severity level ("info", "warning", "error"). Defaults to "info".
+ ephemeral: When True, the message is transient and not persisted to disk.
+
+ Raises:
+ Exception: If the session has been destroyed or the connection fails.
+
+ Example:
+ >>> await session.log("Processing started")
+ >>> await session.log("Something looks off", level="warning")
+ >>> await session.log("Operation failed", level="error")
+ >>> await session.log("Temporary status update", ephemeral=True)
+ """
+ params = SessionLogParams(
+ message=message,
+ level=Level(level) if level is not None else None,
+ ephemeral=ephemeral,
+ )
+ await self.rpc.log(params)
diff --git a/python/copilot/tools.py b/python/copilot/tools.py
index 573992cd52..58e58d97e2 100644
--- a/python/copilot/tools.py
+++ b/python/copilot/tools.py
@@ -26,6 +26,7 @@ def define_tool(
*,
description: str | None = None,
overrides_built_in_tool: bool = False,
+ skip_permission: bool = False,
) -> Callable[[Callable[..., Any]], Tool]: ...
@@ -37,6 +38,7 @@ def define_tool(
handler: Callable[[T, ToolInvocation], R],
params_type: type[T],
overrides_built_in_tool: bool = False,
+ skip_permission: bool = False,
) -> Tool: ...
@@ -47,6 +49,7 @@ def define_tool(
handler: Callable[[Any, ToolInvocation], Any] | None = None,
params_type: type[BaseModel] | None = None,
overrides_built_in_tool: bool = False,
+ skip_permission: bool = False,
) -> Tool | Callable[[Callable[[Any, ToolInvocation], Any]], Tool]:
"""
Define a tool with automatic JSON schema generation from Pydantic models.
@@ -79,6 +82,10 @@ def lookup_issue(params: LookupIssueParams) -> str:
handler: Optional handler function (if not using as decorator)
params_type: Optional Pydantic model type for parameters (inferred from
type hints when using as decorator)
+ overrides_built_in_tool: When True, explicitly indicates this tool is intended
+ to override a built-in tool of the same name. If not set and the
+ name clashes with a built-in tool, the runtime will return an error.
+ skip_permission: When True, the tool can execute without a permission prompt.
Returns:
A Tool instance
@@ -154,6 +161,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult:
parameters=schema,
handler=wrapped_handler,
overrides_built_in_tool=overrides_built_in_tool,
+ skip_permission=skip_permission,
)
# If handler is provided, call decorator immediately
diff --git a/python/copilot/types.py b/python/copilot/types.py
index 6c484ce409..ef9a4bce47 100644
--- a/python/copilot/types.py
+++ b/python/copilot/types.py
@@ -5,8 +5,8 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
-from dataclasses import dataclass
-from typing import Any, Literal, NotRequired, TypedDict
+from dataclasses import KW_ONLY, dataclass, field
+from typing import Any, Literal, NotRequired, Required, TypedDict
# Import generated SessionEvent types
from .generated.session_events import (
@@ -65,39 +65,103 @@ class SelectionAttachment(TypedDict):
text: NotRequired[str]
+class BlobAttachment(TypedDict):
+ """Inline base64-encoded content attachment (e.g. images)."""
+
+ type: Literal["blob"]
+ data: str
+ """Base64-encoded content"""
+ mimeType: str
+ """MIME type of the inline data"""
+ displayName: NotRequired[str]
+
+
# Attachment type - union of all attachment types
-Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment
+Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment | BlobAttachment
-# Options for creating a CopilotClient
-class CopilotClientOptions(TypedDict, total=False):
- """Options for creating a CopilotClient"""
+# Configuration for OpenTelemetry integration with the Copilot CLI.
+class TelemetryConfig(TypedDict, total=False):
+ """Configuration for OpenTelemetry integration with the Copilot CLI."""
+
+ otlp_endpoint: str
+ """OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT."""
+ file_path: str
+ """File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH."""
+ exporter_type: str
+ """Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE."""
+ source_name: str
+ """Instrumentation scope name. Sets COPILOT_OTEL_SOURCE_NAME."""
+ capture_content: bool
+ """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501
+
+
+# Configuration for CopilotClient connection modes
+
+
+@dataclass
+class SubprocessConfig:
+ """Config for spawning a local Copilot CLI subprocess.
+
+ Example:
+ >>> config = SubprocessConfig(github_token="ghp_...")
+ >>> client = CopilotClient(config)
+
+ >>> # Custom CLI path with TCP transport
+ >>> config = SubprocessConfig(
+ ... cli_path="/usr/local/bin/copilot",
+ ... use_stdio=False,
+ ... log_level="debug",
+ ... )
+ """
+
+ cli_path: str | None = None
+ """Path to the Copilot CLI executable. ``None`` uses the bundled binary."""
+
+ cli_args: list[str] = field(default_factory=list)
+ """Extra arguments passed to the CLI executable (inserted before SDK-managed args)."""
+
+ _: KW_ONLY
+
+ cwd: str | None = None
+ """Working directory for the CLI process. ``None`` uses the current directory."""
+
+ use_stdio: bool = True
+ """Use stdio transport (``True``, default) or TCP (``False``)."""
+
+ port: int = 0
+ """TCP port for the CLI server (only when ``use_stdio=False``). 0 means random."""
+
+ log_level: LogLevel = "info"
+ """Log level for the CLI process."""
+
+ env: dict[str, str] | None = None
+ """Environment variables for the CLI process. ``None`` inherits the current env."""
+
+ github_token: str | None = None
+ """GitHub token for authentication. Takes priority over other auth methods."""
+
+ use_logged_in_user: bool | None = None
+ """Use the logged-in user for authentication.
+
+ ``None`` (default) resolves to ``True`` unless ``github_token`` is set.
+ """
+
+ telemetry: TelemetryConfig | None = None
+ """OpenTelemetry configuration. Providing this enables telemetry — no separate flag needed."""
- cli_path: str # Path to the Copilot CLI executable (default: "copilot")
- # Extra arguments to pass to the CLI executable (inserted before SDK-managed args)
- cli_args: list[str]
- # Working directory for the CLI process (default: current process's cwd)
- cwd: str
- port: int # Port for the CLI server (TCP mode only, default: 0)
- use_stdio: bool # Use stdio transport instead of TCP (default: True)
- cli_url: str # URL of an existing Copilot CLI server to connect to over TCP
- # Format: "host:port" or "http://host:port" or just "port" (defaults to localhost)
- # Examples: "localhost:8080", "http://127.0.0.1:9000", "8080"
- # Mutually exclusive with cli_path, use_stdio
- log_level: LogLevel # Log level
- auto_start: bool # Auto-start the CLI server on first use (default: True)
- # Auto-restart the CLI server if it crashes (default: True)
- auto_restart: bool
- env: dict[str, str] # Environment variables for the CLI process
- # GitHub token to use for authentication.
- # When provided, the token is passed to the CLI server via environment variable.
- # This takes priority over other authentication methods.
- github_token: str
- # Whether to use the logged-in user for authentication.
- # When True, the CLI server will attempt to use stored OAuth tokens or gh CLI auth.
- # When False, only explicit tokens (github_token or environment variables) are used.
- # Default: True (but defaults to False when github_token is provided)
- use_logged_in_user: bool
+
+@dataclass
+class ExternalServerConfig:
+ """Config for connecting to an existing Copilot CLI server over TCP.
+
+ Example:
+ >>> config = ExternalServerConfig(url="localhost:3000")
+ >>> client = CopilotClient(config)
+ """
+
+ url: str
+ """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``."""
ToolResultType = Literal["success", "failure", "rejected", "denied"]
@@ -145,10 +209,56 @@ class Tool:
handler: ToolHandler
parameters: dict[str, Any] | None = None
overrides_built_in_tool: bool = False
+ skip_permission: bool = False
# System message configuration (discriminated union)
-# Use SystemMessageAppendConfig for default behavior, SystemMessageReplaceConfig for full control
+# Use SystemMessageAppendConfig for default behavior,
+# SystemMessageReplaceConfig for full control,
+# or SystemMessageCustomizeConfig for section-level overrides.
+
+# Known system prompt section identifiers for the "customize" mode.
+SystemPromptSection = Literal[
+ "identity",
+ "tone",
+ "tool_efficiency",
+ "environment_context",
+ "code_change_rules",
+ "guidelines",
+ "safety",
+ "tool_instructions",
+ "custom_instructions",
+ "last_instructions",
+]
+
+SYSTEM_PROMPT_SECTIONS: dict[SystemPromptSection, str] = {
+ "identity": "Agent identity preamble and mode statement",
+ "tone": "Response style, conciseness rules, output formatting preferences",
+ "tool_efficiency": "Tool usage patterns, parallel calling, batching guidelines",
+ "environment_context": "CWD, OS, git root, directory listing, available tools",
+ "code_change_rules": "Coding rules, linting/testing, ecosystem tools, style",
+ "guidelines": "Tips, behavioral best practices, behavioral guidelines",
+ "safety": "Environment limitations, prohibited actions, security policies",
+ "tool_instructions": "Per-tool usage instructions",
+ "custom_instructions": "Repository and organization custom instructions",
+ "last_instructions": (
+ "End-of-prompt instructions: parallel tool calling, persistence, task completion"
+ ),
+}
+
+
+SectionTransformFn = Callable[[str], str | Awaitable[str]]
+"""Transform callback: receives current section content, returns new content."""
+
+SectionOverrideAction = Literal["replace", "remove", "append", "prepend"] | SectionTransformFn
+"""Override action: a string literal for static overrides, or a callback for transforms."""
+
+
+class SectionOverride(TypedDict, total=False):
+ """Override operation for a single system prompt section."""
+
+ action: Required[SectionOverrideAction]
+ content: NotRequired[str]
class SystemMessageAppendConfig(TypedDict, total=False):
@@ -170,8 +280,21 @@ class SystemMessageReplaceConfig(TypedDict):
content: str
-# Union type - use one or the other
-SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig
+class SystemMessageCustomizeConfig(TypedDict, total=False):
+ """
+ Customize mode: Override individual sections of the system prompt.
+ Keeps the SDK-managed prompt structure while allowing targeted modifications.
+ """
+
+ mode: Required[Literal["customize"]]
+ sections: NotRequired[dict[SystemPromptSection, SectionOverride]]
+ content: NotRequired[str]
+
+
+# Union type - use one based on your needs
+SystemMessageConfig = (
+ SystemMessageAppendConfig | SystemMessageReplaceConfig | SystemMessageCustomizeConfig
+)
# Permission result types
@@ -182,6 +305,7 @@ class SystemMessageReplaceConfig(TypedDict):
"denied-by-content-exclusion-policy",
"denied-no-approval-rule-and-could-not-request-from-user",
"denied-interactively-by-user",
+ "no-result",
]
@@ -471,55 +595,6 @@ class InfiniteSessionConfig(TypedDict, total=False):
buffer_exhaustion_threshold: float
-# Configuration for creating a session
-class SessionConfig(TypedDict, total=False):
- """Configuration for creating a session"""
-
- session_id: str # Optional custom session ID
- # Client name to identify the application using the SDK.
- # Included in the User-Agent header for API requests.
- client_name: str
- model: str # Model to use for this session. Use client.list_models() to see available models.
- # Reasoning effort level for models that support it.
- # Only valid for models where capabilities.supports.reasoning_effort is True.
- reasoning_effort: ReasoningEffort
- tools: list[Tool]
- system_message: SystemMessageConfig # System message configuration
- # List of tool names to allow (takes precedence over excluded_tools)
- available_tools: list[str]
- # List of tool names to disable (ignored if available_tools is set)
- excluded_tools: list[str]
- # Handler for permission requests from the server
- on_permission_request: _PermissionHandlerFn
- # Handler for user input requests from the agent (enables ask_user tool)
- on_user_input_request: UserInputHandler
- # Hook handlers for intercepting session lifecycle events
- hooks: SessionHooks
- # Working directory for the session. Tool operations will be relative to this directory.
- working_directory: str
- # Custom provider configuration (BYOK - Bring Your Own Key)
- provider: ProviderConfig
- # Enable streaming of assistant message and reasoning chunks
- # When True, assistant.message_delta and assistant.reasoning_delta events
- # with delta_content are sent as the response is generated
- streaming: bool
- # MCP server configurations for the session
- mcp_servers: dict[str, MCPServerConfig]
- # Custom agent configurations for the session
- custom_agents: list[CustomAgentConfig]
- # Override the default configuration directory location.
- # When specified, the session will use this directory for storing config and state.
- config_dir: str
- # Directories to load skills from
- skill_directories: list[str]
- # List of skill names to disable
- disabled_skills: list[str]
- # Infinite session configuration for persistent workspaces and automatic compaction.
- # When enabled (default), sessions automatically manage context limits and persist state.
- # Set to {"enabled": False} to disable.
- infinite_sessions: InfiniteSessionConfig
-
-
# Azure-specific provider options
class AzureProviderOptions(TypedDict, total=False):
"""Azure-specific provider configuration"""
@@ -542,61 +617,6 @@ class ProviderConfig(TypedDict, total=False):
azure: AzureProviderOptions # Azure-specific options
-# Configuration for resuming a session
-class ResumeSessionConfig(TypedDict, total=False):
- """Configuration for resuming a session"""
-
- # Client name to identify the application using the SDK.
- # Included in the User-Agent header for API requests.
- client_name: str
- # Model to use for this session. Can change the model when resuming.
- model: str
- tools: list[Tool]
- system_message: SystemMessageConfig # System message configuration
- # List of tool names to allow (takes precedence over excluded_tools)
- available_tools: list[str]
- # List of tool names to disable (ignored if available_tools is set)
- excluded_tools: list[str]
- provider: ProviderConfig
- # Reasoning effort level for models that support it.
- reasoning_effort: ReasoningEffort
- on_permission_request: _PermissionHandlerFn
- # Handler for user input requestsfrom the agent (enables ask_user tool)
- on_user_input_request: UserInputHandler
- # Hook handlers for intercepting session lifecycle events
- hooks: SessionHooks
- # Working directory for the session. Tool operations will be relative to this directory.
- working_directory: str
- # Override the default configuration directory location.
- config_dir: str
- # Enable streaming of assistant message chunks
- streaming: bool
- # MCP server configurations for the session
- mcp_servers: dict[str, MCPServerConfig]
- # Custom agent configurations for the session
- custom_agents: list[CustomAgentConfig]
- # Directories to load skills from
- skill_directories: list[str]
- # List of skill names to disable
- disabled_skills: list[str]
- # Infinite session configuration for persistent workspaces and automatic compaction.
- infinite_sessions: InfiniteSessionConfig
- # When True, skips emitting the session.resume event.
- # Useful for reconnecting to a session without triggering resume-related side effects.
- disable_resume: bool
-
-
-# Options for sending a message to a session
-class MessageOptions(TypedDict):
- """Options for sending a message to a session"""
-
- prompt: str # The prompt/message to send
- # Optional file/directory attachments
- attachments: NotRequired[list[Attachment]]
- # Message processing mode
- mode: NotRequired[Literal["enqueue", "immediate"]]
-
-
# Event handler type
SessionEventHandler = Callable[[SessionEvent], None]
diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py
index cee6814f13..e82fcc0241 100644
--- a/python/e2e/test_agent_and_compact_rpc.py
+++ b/python/e2e/test_agent_and_compact_rpc.py
@@ -2,7 +2,7 @@
import pytest
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient, PermissionHandler, SubprocessConfig
from copilot.generated.rpc import SessionAgentSelectParams
from .testharness import CLI_PATH, E2ETestContext
@@ -14,28 +14,26 @@ class TestAgentSelectionRpc:
@pytest.mark.asyncio
async def test_should_list_available_custom_agents(self):
"""Test listing available custom agents via RPC."""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
session = await client.create_session(
- {
- "on_permission_request": PermissionHandler.approve_all,
- "custom_agents": [
- {
- "name": "test-agent",
- "display_name": "Test Agent",
- "description": "A test agent",
- "prompt": "You are a test agent.",
- },
- {
- "name": "another-agent",
- "display_name": "Another Agent",
- "description": "Another test agent",
- "prompt": "You are another agent.",
- },
- ],
- }
+ on_permission_request=PermissionHandler.approve_all,
+ custom_agents=[
+ {
+ "name": "test-agent",
+ "display_name": "Test Agent",
+ "description": "A test agent",
+ "prompt": "You are a test agent.",
+ },
+ {
+ "name": "another-agent",
+ "display_name": "Another Agent",
+ "description": "Another test agent",
+ "prompt": "You are another agent.",
+ },
+ ],
)
result = await session.rpc.agent.list()
@@ -54,22 +52,20 @@ async def test_should_list_available_custom_agents(self):
@pytest.mark.asyncio
async def test_should_return_null_when_no_agent_is_selected(self):
"""Test getCurrent returns null when no agent is selected."""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
session = await client.create_session(
- {
- "on_permission_request": PermissionHandler.approve_all,
- "custom_agents": [
- {
- "name": "test-agent",
- "display_name": "Test Agent",
- "description": "A test agent",
- "prompt": "You are a test agent.",
- }
- ],
- }
+ on_permission_request=PermissionHandler.approve_all,
+ custom_agents=[
+ {
+ "name": "test-agent",
+ "display_name": "Test Agent",
+ "description": "A test agent",
+ "prompt": "You are a test agent.",
+ }
+ ],
)
result = await session.rpc.agent.get_current()
@@ -83,22 +79,20 @@ async def test_should_return_null_when_no_agent_is_selected(self):
@pytest.mark.asyncio
async def test_should_select_and_get_current_agent(self):
"""Test selecting an agent and verifying getCurrent returns it."""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
session = await client.create_session(
- {
- "on_permission_request": PermissionHandler.approve_all,
- "custom_agents": [
- {
- "name": "test-agent",
- "display_name": "Test Agent",
- "description": "A test agent",
- "prompt": "You are a test agent.",
- }
- ],
- }
+ on_permission_request=PermissionHandler.approve_all,
+ custom_agents=[
+ {
+ "name": "test-agent",
+ "display_name": "Test Agent",
+ "description": "A test agent",
+ "prompt": "You are a test agent.",
+ }
+ ],
)
# Select the agent
@@ -122,22 +116,20 @@ async def test_should_select_and_get_current_agent(self):
@pytest.mark.asyncio
async def test_should_deselect_current_agent(self):
"""Test deselecting the current agent."""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
session = await client.create_session(
- {
- "on_permission_request": PermissionHandler.approve_all,
- "custom_agents": [
- {
- "name": "test-agent",
- "display_name": "Test Agent",
- "description": "A test agent",
- "prompt": "You are a test agent.",
- }
- ],
- }
+ on_permission_request=PermissionHandler.approve_all,
+ custom_agents=[
+ {
+ "name": "test-agent",
+ "display_name": "Test Agent",
+ "description": "A test agent",
+ "prompt": "You are a test agent.",
+ }
+ ],
)
# Select then deselect
@@ -155,17 +147,23 @@ async def test_should_deselect_current_agent(self):
@pytest.mark.asyncio
async def test_should_return_empty_list_when_no_custom_agents_configured(self):
- """Test listing agents returns empty when none configured."""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ """Test listing agents returns no custom agents when none configured."""
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
result = await session.rpc.agent.list()
- assert result.agents == []
+ # The CLI may return built-in/default agents even when no custom agents
+ # are configured. Verify no custom test agents appear in the list.
+ custom_names = {"test-agent", "another-agent"}
+ for agent in result.agents:
+ assert agent.name not in custom_names, (
+ f"Expected no custom agents, but found {agent.name!r}"
+ )
await session.disconnect()
await client.stop()
@@ -178,11 +176,11 @@ class TestSessionCompactionRpc:
async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext):
"""Test compacting session history via RPC."""
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
# Send a message to create some history
- await session.send_and_wait({"prompt": "What is 2+2?"})
+ await session.send_and_wait("What is 2+2?")
# Compact the session
result = await session.rpc.compaction.compact()
diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py
index bddc062df1..fc4cc60b51 100644
--- a/python/e2e/test_ask_user.py
+++ b/python/e2e/test_ask_user.py
@@ -30,19 +30,13 @@ async def on_user_input_request(request, invocation):
}
session = await ctx.client.create_session(
- {
- "on_user_input_request": on_user_input_request,
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ on_user_input_request=on_user_input_request,
)
await session.send_and_wait(
- {
- "prompt": (
- "Ask me to choose between 'Option A' and 'Option B' using the ask_user "
- "tool. Wait for my response before continuing."
- )
- }
+ "Ask me to choose between 'Option A' and 'Option B' using the ask_user "
+ "tool. Wait for my response before continuing."
)
# Should have received at least one user input request
@@ -69,19 +63,13 @@ async def on_user_input_request(request, invocation):
}
session = await ctx.client.create_session(
- {
- "on_user_input_request": on_user_input_request,
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ on_user_input_request=on_user_input_request,
)
await session.send_and_wait(
- {
- "prompt": (
- "Use the ask_user tool to ask me to pick between exactly two options: "
- "'Red' and 'Blue'. These should be provided as choices. Wait for my answer."
- )
- }
+ "Use the ask_user tool to ask me to pick between exactly two options: "
+ "'Red' and 'Blue'. These should be provided as choices. Wait for my answer."
)
# Should have received a request
@@ -110,19 +98,13 @@ async def on_user_input_request(request, invocation):
}
session = await ctx.client.create_session(
- {
- "on_user_input_request": on_user_input_request,
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ on_user_input_request=on_user_input_request,
)
response = await session.send_and_wait(
- {
- "prompt": (
- "Ask me a question using ask_user and then include my answer in your "
- "response. The question should be 'What is your favorite color?'"
- )
- }
+ "Ask me a question using ask_user and then include my answer in your "
+ "response. The question should be 'What is your favorite color?'"
)
# Should have received a request
diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py
index 1f7c76c04b..d266991f71 100644
--- a/python/e2e/test_client.py
+++ b/python/e2e/test_client.py
@@ -2,7 +2,7 @@
import pytest
-from copilot import CopilotClient, PermissionHandler, StopError
+from copilot import CopilotClient, PermissionHandler, StopError, SubprocessConfig
from .testharness import CLI_PATH
@@ -10,7 +10,7 @@
class TestClient:
@pytest.mark.asyncio
async def test_should_start_and_connect_to_server_using_stdio(self):
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
@@ -27,7 +27,7 @@ async def test_should_start_and_connect_to_server_using_stdio(self):
@pytest.mark.asyncio
async def test_should_start_and_connect_to_server_using_tcp(self):
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": False})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=False))
try:
await client.start()
@@ -46,10 +46,10 @@ async def test_should_start_and_connect_to_server_using_tcp(self):
async def test_should_raise_exception_group_on_failed_cleanup(self):
import asyncio
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
try:
- await client.create_session({"on_permission_request": PermissionHandler.approve_all})
+ await client.create_session(on_permission_request=PermissionHandler.approve_all)
# Kill the server process to force cleanup to fail
process = client._process
@@ -57,25 +57,28 @@ async def test_should_raise_exception_group_on_failed_cleanup(self):
process.kill()
await asyncio.sleep(0.1)
- with pytest.raises(ExceptionGroup) as exc_info:
+ try:
await client.stop()
- assert len(exc_info.value.exceptions) > 0
- assert isinstance(exc_info.value.exceptions[0], StopError)
- assert "Failed to disconnect session" in exc_info.value.exceptions[0].message
+ except ExceptionGroup as exc:
+ assert len(exc.exceptions) > 0
+ assert isinstance(exc.exceptions[0], StopError)
+ assert "Failed to disconnect session" in exc.exceptions[0].message
+ else:
+ assert client.get_state() == "disconnected"
finally:
await client.force_stop()
@pytest.mark.asyncio
async def test_should_force_stop_without_cleanup(self):
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
- await client.create_session({"on_permission_request": PermissionHandler.approve_all})
+ await client.create_session(on_permission_request=PermissionHandler.approve_all)
await client.force_stop()
assert client.get_state() == "disconnected"
@pytest.mark.asyncio
async def test_should_get_status_with_version_and_protocol_info(self):
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
@@ -93,7 +96,7 @@ async def test_should_get_status_with_version_and_protocol_info(self):
@pytest.mark.asyncio
async def test_should_get_auth_status(self):
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
@@ -111,7 +114,7 @@ async def test_should_get_auth_status(self):
@pytest.mark.asyncio
async def test_should_list_models_when_authenticated(self):
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
@@ -139,7 +142,7 @@ async def test_should_list_models_when_authenticated(self):
@pytest.mark.asyncio
async def test_should_cache_models_list(self):
"""Test that list_models caches results to avoid rate limiting"""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
@@ -184,11 +187,11 @@ async def test_should_cache_models_list(self):
async def test_should_report_error_with_stderr_when_cli_fails_to_start(self):
"""Test that CLI startup errors include stderr output in the error message."""
client = CopilotClient(
- {
- "cli_path": CLI_PATH,
- "cli_args": ["--nonexistent-flag-for-testing"],
- "use_stdio": True,
- }
+ SubprocessConfig(
+ cli_path=CLI_PATH,
+ cli_args=["--nonexistent-flag-for-testing"],
+ use_stdio=True,
+ )
)
try:
@@ -207,7 +210,7 @@ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self):
# Verify subsequent calls also fail (don't hang)
with pytest.raises(Exception) as exc_info2:
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
await session.send("test")
# Error message varies by platform (EINVAL on Windows, EPIPE on Linux)
diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py
index 5447b4badf..beb51e74b0 100644
--- a/python/e2e/test_compaction.py
+++ b/python/e2e/test_compaction.py
@@ -17,16 +17,14 @@ async def test_should_trigger_compaction_with_low_threshold_and_emit_events(
):
# Create session with very low compaction thresholds to trigger compaction quickly
session = await ctx.client.create_session(
- {
- "infinite_sessions": {
- "enabled": True,
- # Trigger background compaction at 0.5% context usage (~1000 tokens)
- "background_compaction_threshold": 0.005,
- # Block at 1% to ensure compaction runs
- "buffer_exhaustion_threshold": 0.01,
- },
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ infinite_sessions={
+ "enabled": True,
+ # Trigger background compaction at 0.5% context usage (~1000 tokens)
+ "background_compaction_threshold": 0.005,
+ # Block at 1% to ensure compaction runs
+ "buffer_exhaustion_threshold": 0.01,
+ },
)
compaction_start_events = []
@@ -41,13 +39,11 @@ def on_event(event):
session.on(on_event)
# Send multiple messages to fill up the context window
- await session.send_and_wait({"prompt": "Tell me a story about a dragon. Be detailed."})
+ await session.send_and_wait("Tell me a story about a dragon. Be detailed.")
await session.send_and_wait(
- {"prompt": "Continue the story with more details about the dragon's castle."}
- )
- await session.send_and_wait(
- {"prompt": "Now describe the dragon's treasure in great detail."}
+ "Continue the story with more details about the dragon's castle."
)
+ await session.send_and_wait("Now describe the dragon's treasure in great detail.")
# Should have triggered compaction at least once
assert len(compaction_start_events) >= 1, "Expected at least 1 compaction_start event"
@@ -62,7 +58,7 @@ def on_event(event):
assert last_complete.data.tokens_removed > 0, "Expected tokensRemoved > 0"
# Verify the session still works after compaction
- answer = await session.send_and_wait({"prompt": "What was the story about?"})
+ answer = await session.send_and_wait("What was the story about?")
assert answer is not None
assert answer.data.content is not None
# Should remember it was about a dragon (context preserved via summary)
@@ -72,10 +68,8 @@ async def test_should_not_emit_compaction_events_when_infinite_sessions_disabled
self, ctx: E2ETestContext
):
session = await ctx.client.create_session(
- {
- "infinite_sessions": {"enabled": False},
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ infinite_sessions={"enabled": False},
)
compaction_events = []
@@ -89,7 +83,7 @@ def on_event(event):
session.on(on_event)
- await session.send_and_wait({"prompt": "What is 2+2?"})
+ await session.send_and_wait("What is 2+2?")
# Should not have any compaction events when disabled
assert len(compaction_events) == 0, "Expected no compaction events when disabled"
diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py
index c886c6e279..2858d40f2c 100644
--- a/python/e2e/test_hooks.py
+++ b/python/e2e/test_hooks.py
@@ -24,18 +24,14 @@ async def on_pre_tool_use(input_data, invocation):
return {"permissionDecision": "allow"}
session = await ctx.client.create_session(
- {
- "hooks": {"on_pre_tool_use": on_pre_tool_use},
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ hooks={"on_pre_tool_use": on_pre_tool_use},
)
# Create a file for the model to read
write_file(ctx.work_dir, "hello.txt", "Hello from the test!")
- await session.send_and_wait(
- {"prompt": "Read the contents of hello.txt and tell me what it says"}
- )
+ await session.send_and_wait("Read the contents of hello.txt and tell me what it says")
# Should have received at least one preToolUse hook call
assert len(pre_tool_use_inputs) > 0
@@ -57,18 +53,14 @@ async def on_post_tool_use(input_data, invocation):
return None
session = await ctx.client.create_session(
- {
- "hooks": {"on_post_tool_use": on_post_tool_use},
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ hooks={"on_post_tool_use": on_post_tool_use},
)
# Create a file for the model to read
write_file(ctx.work_dir, "world.txt", "World from the test!")
- await session.send_and_wait(
- {"prompt": "Read the contents of world.txt and tell me what it says"}
- )
+ await session.send_and_wait("Read the contents of world.txt and tell me what it says")
# Should have received at least one postToolUse hook call
assert len(post_tool_use_inputs) > 0
@@ -95,18 +87,16 @@ async def on_post_tool_use(input_data, invocation):
return None
session = await ctx.client.create_session(
- {
- "hooks": {
- "on_pre_tool_use": on_pre_tool_use,
- "on_post_tool_use": on_post_tool_use,
- },
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ hooks={
+ "on_pre_tool_use": on_pre_tool_use,
+ "on_post_tool_use": on_post_tool_use,
+ },
)
write_file(ctx.work_dir, "both.txt", "Testing both hooks!")
- await session.send_and_wait({"prompt": "Read the contents of both.txt"})
+ await session.send_and_wait("Read the contents of both.txt")
# Both hooks should have been called
assert len(pre_tool_use_inputs) > 0
@@ -132,10 +122,8 @@ async def on_pre_tool_use(input_data, invocation):
return {"permissionDecision": "deny"}
session = await ctx.client.create_session(
- {
- "hooks": {"on_pre_tool_use": on_pre_tool_use},
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ hooks={"on_pre_tool_use": on_pre_tool_use},
)
# Create a file
@@ -143,7 +131,7 @@ async def on_pre_tool_use(input_data, invocation):
write_file(ctx.work_dir, "protected.txt", original_content)
response = await session.send_and_wait(
- {"prompt": "Edit protected.txt and replace 'Original' with 'Modified'"}
+ "Edit protected.txt and replace 'Original' with 'Modified'"
)
# The hook should have been called
diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py
index fd99cc2c3b..c4bd894147 100644
--- a/python/e2e/test_mcp_and_agents.py
+++ b/python/e2e/test_mcp_and_agents.py
@@ -33,13 +33,13 @@ async def test_should_accept_mcp_server_configuration_on_session_create(
}
session = await ctx.client.create_session(
- {"mcp_servers": mcp_servers, "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers
)
assert session.session_id is not None
# Simple interaction to verify session works
- message = await session.send_and_wait({"prompt": "What is 2+2?"})
+ message = await session.send_and_wait("What is 2+2?")
assert message is not None
assert "4" in message.data.content
@@ -51,10 +51,10 @@ async def test_should_accept_mcp_server_configuration_on_session_resume(
"""Test that MCP server configuration is accepted on session resume"""
# Create a session first
session1 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
session_id = session1.session_id
- await session1.send_and_wait({"prompt": "What is 1+1?"})
+ await session1.send_and_wait("What is 1+1?")
# Resume with MCP servers
mcp_servers: dict[str, MCPServerConfig] = {
@@ -68,12 +68,13 @@ async def test_should_accept_mcp_server_configuration_on_session_resume(
session2 = await ctx.client.resume_session(
session_id,
- {"mcp_servers": mcp_servers, "on_permission_request": PermissionHandler.approve_all},
+ on_permission_request=PermissionHandler.approve_all,
+ mcp_servers=mcp_servers,
)
assert session2.session_id == session_id
- message = await session2.send_and_wait({"prompt": "What is 3+3?"})
+ message = await session2.send_and_wait("What is 3+3?")
assert message is not None
assert "6" in message.data.content
@@ -95,19 +96,14 @@ async def test_should_pass_literal_env_values_to_mcp_server_subprocess(
}
session = await ctx.client.create_session(
- {
- "mcp_servers": mcp_servers,
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers
)
assert session.session_id is not None
message = await session.send_and_wait(
- {
- "prompt": "Use the env-echo/get_env tool to read the TEST_SECRET "
- "environment variable. Reply with just the value, nothing else."
- }
+ "Use the env-echo/get_env tool to read the TEST_SECRET "
+ "environment variable. Reply with just the value, nothing else."
)
assert message is not None
assert "hunter2" in message.data.content
@@ -131,13 +127,13 @@ async def test_should_accept_custom_agent_configuration_on_session_create(
]
session = await ctx.client.create_session(
- {"custom_agents": custom_agents, "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, custom_agents=custom_agents
)
assert session.session_id is not None
# Simple interaction to verify session works
- message = await session.send_and_wait({"prompt": "What is 5+5?"})
+ message = await session.send_and_wait("What is 5+5?")
assert message is not None
assert "10" in message.data.content
@@ -149,10 +145,10 @@ async def test_should_accept_custom_agent_configuration_on_session_resume(
"""Test that custom agent configuration is accepted on session resume"""
# Create a session first
session1 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
session_id = session1.session_id
- await session1.send_and_wait({"prompt": "What is 1+1?"})
+ await session1.send_and_wait("What is 1+1?")
# Resume with custom agents
custom_agents: list[CustomAgentConfig] = [
@@ -166,15 +162,13 @@ async def test_should_accept_custom_agent_configuration_on_session_resume(
session2 = await ctx.client.resume_session(
session_id,
- {
- "custom_agents": custom_agents,
- "on_permission_request": PermissionHandler.approve_all,
- },
+ on_permission_request=PermissionHandler.approve_all,
+ custom_agents=custom_agents,
)
assert session2.session_id == session_id
- message = await session2.send_and_wait({"prompt": "What is 6+6?"})
+ message = await session2.send_and_wait("What is 6+6?")
assert message is not None
assert "12" in message.data.content
@@ -203,16 +197,14 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe
]
session = await ctx.client.create_session(
- {
- "mcp_servers": mcp_servers,
- "custom_agents": custom_agents,
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ mcp_servers=mcp_servers,
+ custom_agents=custom_agents,
)
assert session.session_id is not None
- await session.send({"prompt": "What is 7+7?"})
+ await session.send("What is 7+7?")
message = await get_final_assistant_message(session)
assert "14" in message.data.content
diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py
index caf58cd554..c77ae86e16 100644
--- a/python/e2e/test_multi_client.py
+++ b/python/e2e/test_multi_client.py
@@ -15,8 +15,10 @@
from copilot import (
CopilotClient,
+ ExternalServerConfig,
PermissionHandler,
PermissionRequestResult,
+ SubprocessConfig,
ToolInvocation,
define_tool,
)
@@ -54,19 +56,19 @@ async def setup(self):
)
# Client 1 uses TCP mode so a second client can connect to the same server
- opts: dict = {
- "cli_path": self.cli_path,
- "cwd": self.work_dir,
- "env": self.get_env(),
- "use_stdio": False,
- }
- if github_token:
- opts["github_token"] = github_token
- self._client1 = CopilotClient(opts)
+ self._client1 = CopilotClient(
+ SubprocessConfig(
+ cli_path=self.cli_path,
+ cwd=self.work_dir,
+ env=self.get_env(),
+ use_stdio=False,
+ github_token=github_token,
+ )
+ )
# Trigger connection by creating and disconnecting an init session
init_session = await self._client1.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
await init_session.disconnect()
@@ -74,7 +76,7 @@ async def setup(self):
actual_port = self._client1.actual_port
assert actual_port is not None, "Client 1 should have an actual port after connecting"
- self._client2 = CopilotClient({"cli_url": f"localhost:{actual_port}"})
+ self._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}"))
async def teardown(self, test_failed: bool = False):
if self._client2:
@@ -197,24 +199,20 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str:
# Client 1 creates a session with a custom tool
session1 = await mctx.client1.create_session(
- {"on_permission_request": PermissionHandler.approve_all, "tools": [magic_number]}
+ on_permission_request=PermissionHandler.approve_all, tools=[magic_number]
)
# Client 2 resumes with NO tools — should not overwrite client 1's tools
session2 = await mctx.client2.resume_session(
- session1.session_id, {"on_permission_request": PermissionHandler.approve_all}
+ session1.session_id, on_permission_request=PermissionHandler.approve_all
)
-
- # Track events seen by each client
client1_events = []
client2_events = []
session1.on(lambda event: client1_events.append(event))
session2.on(lambda event: client2_events.append(event))
# Send a prompt that triggers the custom tool
- await session1.send(
- {"prompt": "Use the magic_number tool with seed 'hello' and tell me the result"}
- )
+ await session1.send("Use the magic_number tool with seed 'hello' and tell me the result")
response = await get_final_assistant_message(session1)
assert "MAGIC_hello_42" in (response.data.content or "")
@@ -240,17 +238,15 @@ async def test_one_client_approves_permission_and_both_see_the_result(
# Client 1 creates a session and manually approves permission requests
session1 = await mctx.client1.create_session(
- {
- "on_permission_request": lambda request, invocation: (
- permission_requests.append(request) or PermissionRequestResult(kind="approved")
- ),
- }
+ on_permission_request=lambda request, invocation: (
+ permission_requests.append(request) or PermissionRequestResult(kind="approved")
+ ),
)
# Client 2 resumes — its handler never resolves, so only client 1's approval takes effect
session2 = await mctx.client2.resume_session(
session1.session_id,
- {"on_permission_request": lambda request, invocation: asyncio.Future()},
+ on_permission_request=lambda request, invocation: asyncio.Future(),
)
client1_events = []
@@ -259,9 +255,7 @@ async def test_one_client_approves_permission_and_both_see_the_result(
session2.on(lambda event: client2_events.append(event))
# Send a prompt that triggers a write operation (requires permission)
- await session1.send(
- {"prompt": "Create a file called hello.txt containing the text 'hello world'"}
- )
+ await session1.send("Create a file called hello.txt containing the text 'hello world'")
response = await get_final_assistant_message(session1)
assert response.data.content
@@ -290,17 +284,15 @@ async def test_one_client_rejects_permission_and_both_see_the_result(
"""One client rejects a permission request and both see the result."""
# Client 1 creates a session and denies all permission requests
session1 = await mctx.client1.create_session(
- {
- "on_permission_request": lambda request, invocation: PermissionRequestResult(
- kind="denied-interactively-by-user"
- ),
- }
+ on_permission_request=lambda request, invocation: PermissionRequestResult(
+ kind="denied-interactively-by-user"
+ ),
)
# Client 2 resumes — its handler never resolves
session2 = await mctx.client2.resume_session(
session1.session_id,
- {"on_permission_request": lambda request, invocation: asyncio.Future()},
+ on_permission_request=lambda request, invocation: asyncio.Future(),
)
client1_events = []
@@ -313,7 +305,7 @@ async def test_one_client_rejects_permission_and_both_see_the_result(
with open(test_file, "w") as f:
f.write("protected content")
- await session1.send({"prompt": "Edit protected.txt and replace 'protected' with 'hacked'."})
+ await session1.send("Edit protected.txt and replace 'protected' with 'hacked'.")
await get_final_assistant_message(session1)
# Verify the file was NOT modified (permission was denied)
@@ -357,28 +349,25 @@ def currency_lookup(params: CountryCodeParams, invocation: ToolInvocation) -> st
# Client 1 creates a session with tool A
session1 = await mctx.client1.create_session(
- {"on_permission_request": PermissionHandler.approve_all, "tools": [city_lookup]}
+ on_permission_request=PermissionHandler.approve_all, tools=[city_lookup]
)
# Client 2 resumes with tool B (different tool, union should have both)
session2 = await mctx.client2.resume_session(
session1.session_id,
- {"on_permission_request": PermissionHandler.approve_all, "tools": [currency_lookup]},
+ on_permission_request=PermissionHandler.approve_all,
+ tools=[currency_lookup],
)
# Send prompts sequentially to avoid nondeterministic tool_call ordering
await session1.send(
- {"prompt": "Use the city_lookup tool with countryCode 'US' and tell me the result."}
+ "Use the city_lookup tool with countryCode 'US' and tell me the result."
)
response1 = await get_final_assistant_message(session1)
assert "CITY_FOR_US" in (response1.data.content or "")
await session1.send(
- {
- "prompt": (
- "Now use the currency_lookup tool with countryCode 'US' and tell me the result."
- )
- }
+ "Now use the currency_lookup tool with countryCode 'US' and tell me the result."
)
response2 = await get_final_assistant_message(session1)
assert "CURRENCY_FOR_US" in (response2.data.content or "")
@@ -408,30 +397,23 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str:
# Client 1 creates a session with stable_tool
session1 = await mctx.client1.create_session(
- {"on_permission_request": PermissionHandler.approve_all, "tools": [stable_tool]}
+ on_permission_request=PermissionHandler.approve_all, tools=[stable_tool]
)
# Client 2 resumes with ephemeral_tool
await mctx.client2.resume_session(
session1.session_id,
- {"on_permission_request": PermissionHandler.approve_all, "tools": [ephemeral_tool]},
+ on_permission_request=PermissionHandler.approve_all,
+ tools=[ephemeral_tool],
)
# Verify both tools work before disconnect.
# Sequential prompts avoid nondeterministic tool_call ordering.
- await session1.send(
- {
- "prompt": "Use the stable_tool with input 'test1' and tell me the result.",
- }
- )
+ await session1.send("Use the stable_tool with input 'test1' and tell me the result.")
stable_response = await get_final_assistant_message(session1)
assert "STABLE_test1" in (stable_response.data.content or "")
- await session1.send(
- {
- "prompt": "Use the ephemeral_tool with input 'test2' and tell me the result.",
- }
- )
+ await session1.send("Use the ephemeral_tool with input 'test2' and tell me the result.")
ephemeral_response = await get_final_assistant_message(session1)
assert "EPHEMERAL_test2" in (ephemeral_response.data.content or "")
@@ -443,17 +425,13 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str:
# Recreate client2 for future tests (but don't rejoin the session)
actual_port = mctx.client1.actual_port
- mctx._client2 = CopilotClient({"cli_url": f"localhost:{actual_port}"})
+ mctx._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}"))
# Now only stable_tool should be available
await session1.send(
- {
- "prompt": (
- "Use the stable_tool with input 'still_here'."
- " Also try using ephemeral_tool"
- " if it is available."
- )
- }
+ "Use the stable_tool with input 'still_here'."
+ " Also try using ephemeral_tool"
+ " if it is available."
)
after_response = await get_final_assistant_message(session1)
assert "STABLE_still_here" in (after_response.data.content or "")
diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py
index 609003e876..a673d63b52 100644
--- a/python/e2e/test_permissions.py
+++ b/python/e2e/test_permissions.py
@@ -26,13 +26,11 @@ def on_permission_request(
assert invocation["session_id"] == session.session_id
return PermissionRequestResult(kind="approved")
- session = await ctx.client.create_session({"on_permission_request": on_permission_request})
+ session = await ctx.client.create_session(on_permission_request=on_permission_request)
write_file(ctx.work_dir, "test.txt", "original content")
- await session.send_and_wait(
- {"prompt": "Edit test.txt and replace 'original' with 'modified'"}
- )
+ await session.send_and_wait("Edit test.txt and replace 'original' with 'modified'")
# Should have received at least one permission request
assert len(permission_requests) > 0
@@ -51,14 +49,12 @@ def on_permission_request(
) -> PermissionRequestResult:
return PermissionRequestResult(kind="denied-interactively-by-user")
- session = await ctx.client.create_session({"on_permission_request": on_permission_request})
+ session = await ctx.client.create_session(on_permission_request=on_permission_request)
original_content = "protected content"
write_file(ctx.work_dir, "protected.txt", original_content)
- await session.send_and_wait(
- {"prompt": "Edit protected.txt and replace 'protected' with 'hacked'."}
- )
+ await session.send_and_wait("Edit protected.txt and replace 'protected' with 'hacked'.")
# Verify the file was NOT modified
content = read_file(ctx.work_dir, "protected.txt")
@@ -74,7 +70,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies(
def deny_all(request, invocation):
return PermissionRequestResult()
- session = await ctx.client.create_session({"on_permission_request": deny_all})
+ session = await ctx.client.create_session(on_permission_request=deny_all)
denied_events = []
done_event = asyncio.Event()
@@ -94,7 +90,7 @@ def on_event(event):
session.on(on_event)
- await session.send({"prompt": "Run 'node --version'"})
+ await session.send("Run 'node --version'")
await asyncio.wait_for(done_event.wait(), timeout=60)
assert len(denied_events) > 0
@@ -106,15 +102,15 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_
):
"""Test that tool operations are denied after resume when handler explicitly denies"""
session1 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
session_id = session1.session_id
- await session1.send_and_wait({"prompt": "What is 1+1?"})
+ await session1.send_and_wait("What is 1+1?")
def deny_all(request, invocation):
return PermissionRequestResult()
- session2 = await ctx.client.resume_session(session_id, {"on_permission_request": deny_all})
+ session2 = await ctx.client.resume_session(session_id, on_permission_request=deny_all)
denied_events = []
done_event = asyncio.Event()
@@ -134,7 +130,7 @@ def on_event(event):
session2.on(on_event)
- await session2.send({"prompt": "Run 'node --version'"})
+ await session2.send("Run 'node --version'")
await asyncio.wait_for(done_event.wait(), timeout=60)
assert len(denied_events) > 0
@@ -144,10 +140,10 @@ def on_event(event):
async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETestContext):
"""Test that sessions work with approve-all permission handler"""
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
- message = await session.send_and_wait({"prompt": "What is 2+2?"})
+ message = await session.send_and_wait("What is 2+2?")
assert message is not None
assert "4" in message.data.content
@@ -166,9 +162,9 @@ async def on_permission_request(
await asyncio.sleep(0.01)
return PermissionRequestResult(kind="approved")
- session = await ctx.client.create_session({"on_permission_request": on_permission_request})
+ session = await ctx.client.create_session(on_permission_request=on_permission_request)
- await session.send_and_wait({"prompt": "Run 'echo test' and tell me what happens"})
+ await session.send_and_wait("Run 'echo test' and tell me what happens")
assert len(permission_requests) > 0
@@ -180,10 +176,10 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC
# Create initial session
session1 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
session_id = session1.session_id
- await session1.send_and_wait({"prompt": "What is 1+1?"})
+ await session1.send_and_wait("What is 1+1?")
# Resume with permission handler
def on_permission_request(
@@ -193,10 +189,10 @@ def on_permission_request(
return PermissionRequestResult(kind="approved")
session2 = await ctx.client.resume_session(
- session_id, {"on_permission_request": on_permission_request}
+ session_id, on_permission_request=on_permission_request
)
- await session2.send_and_wait({"prompt": "Run 'echo resumed' for me"})
+ await session2.send_and_wait("Run 'echo resumed' for me")
# Should have permission requests from resumed session
assert len(permission_requests) > 0
@@ -211,11 +207,9 @@ def on_permission_request(
) -> PermissionRequestResult:
raise RuntimeError("Handler error")
- session = await ctx.client.create_session({"on_permission_request": on_permission_request})
+ session = await ctx.client.create_session(on_permission_request=on_permission_request)
- message = await session.send_and_wait(
- {"prompt": "Run 'echo test'. If you can't, say 'failed'."}
- )
+ message = await session.send_and_wait("Run 'echo test'. If you can't, say 'failed'.")
# Should handle the error and deny permission
assert message is not None
@@ -238,9 +232,9 @@ def on_permission_request(
assert len(request.tool_call_id) > 0
return PermissionRequestResult(kind="approved")
- session = await ctx.client.create_session({"on_permission_request": on_permission_request})
+ session = await ctx.client.create_session(on_permission_request=on_permission_request)
- await session.send_and_wait({"prompt": "Run 'echo test'"})
+ await session.send_and_wait("Run 'echo test'")
assert received_tool_call_id
diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py
index 1b455d632f..814da067dd 100644
--- a/python/e2e/test_rpc.py
+++ b/python/e2e/test_rpc.py
@@ -2,7 +2,7 @@
import pytest
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient, PermissionHandler, SubprocessConfig
from copilot.generated.rpc import PingParams
from .testharness import CLI_PATH, E2ETestContext
@@ -14,7 +14,7 @@ class TestRpc:
@pytest.mark.asyncio
async def test_should_call_rpc_ping_with_typed_params(self):
"""Test calling rpc.ping with typed params and result"""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
@@ -30,7 +30,7 @@ async def test_should_call_rpc_ping_with_typed_params(self):
@pytest.mark.asyncio
async def test_should_call_rpc_models_list(self):
"""Test calling rpc.models.list with typed result"""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
@@ -53,7 +53,7 @@ async def test_should_call_rpc_models_list(self):
@pytest.mark.asyncio
async def test_should_call_rpc_account_get_quota(self):
"""Test calling rpc.account.getQuota when authenticated"""
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
@@ -78,7 +78,7 @@ class TestSessionRpc:
async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext):
"""Test calling session.rpc.model.getCurrent"""
session = await ctx.client.create_session(
- {"model": "claude-sonnet-4.5", "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5"
)
result = await session.rpc.model.get_current()
@@ -92,15 +92,17 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext
from copilot.generated.rpc import SessionModelSwitchToParams
session = await ctx.client.create_session(
- {"model": "claude-sonnet-4.5", "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5"
)
# Get initial model
before = await session.rpc.model.get_current()
assert before.model_id is not None
- # Switch to a different model
- result = await session.rpc.model.switch_to(SessionModelSwitchToParams(model_id="gpt-4.1"))
+ # Switch to a different model with reasoning effort
+ result = await session.rpc.model.switch_to(
+ SessionModelSwitchToParams(model_id="gpt-4.1", reasoning_effort="high")
+ )
assert result.model_id == "gpt-4.1"
# Verify the switch persisted
@@ -112,12 +114,12 @@ async def test_get_and_set_session_mode(self):
"""Test getting and setting session mode"""
from copilot.generated.rpc import Mode, SessionModeSetParams
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
# Get initial mode (default should be interactive)
@@ -148,12 +150,12 @@ async def test_read_update_and_delete_plan(self):
"""Test reading, updating, and deleting plan"""
from copilot.generated.rpc import SessionPlanUpdateParams
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
# Initially plan should not exist
@@ -191,12 +193,12 @@ async def test_create_list_and_read_workspace_files(self):
SessionWorkspaceReadFileParams,
)
- client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
await client.start()
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
# Initially no files
diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py
index 60cb7c875b..04f0b448e7 100644
--- a/python/e2e/test_session.py
+++ b/python/e2e/test_session.py
@@ -4,7 +4,7 @@
import pytest
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient, PermissionHandler, SubprocessConfig
from copilot.types import Tool, ToolResult
from .testharness import E2ETestContext, get_final_assistant_message, get_next_event_of_type
@@ -15,7 +15,7 @@
class TestSessions:
async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {"model": "fake-test-model", "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, model="fake-test-model"
)
assert session.session_id
@@ -32,16 +32,14 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext):
async def test_should_have_stateful_conversation(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
- assistant_message = await session.send_and_wait({"prompt": "What is 1+1?"})
+ assistant_message = await session.send_and_wait("What is 1+1?")
assert assistant_message is not None
assert "2" in assistant_message.data.content
- second_message = await session.send_and_wait(
- {"prompt": "Now if you double that, what do you get?"}
- )
+ second_message = await session.send_and_wait("Now if you double that, what do you get?")
assert second_message is not None
assert "4" in second_message.data.content
@@ -50,13 +48,11 @@ async def test_should_create_a_session_with_appended_systemMessage_config(
):
system_message_suffix = "End each response with the phrase 'Have a nice day!'"
session = await ctx.client.create_session(
- {
- "system_message": {"mode": "append", "content": system_message_suffix},
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ system_message={"mode": "append", "content": system_message_suffix},
)
- await session.send({"prompt": "What is your full name?"})
+ await session.send("What is your full name?")
assistant_message = await get_final_assistant_message(session)
assert "GitHub" in assistant_message.data.content
assert "Have a nice day!" in assistant_message.data.content
@@ -72,13 +68,11 @@ async def test_should_create_a_session_with_replaced_systemMessage_config(
):
test_system_message = "You are an assistant called Testy McTestface. Reply succinctly."
session = await ctx.client.create_session(
- {
- "system_message": {"mode": "replace", "content": test_system_message},
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ system_message={"mode": "replace", "content": test_system_message},
)
- await session.send({"prompt": "What is your full name?"})
+ await session.send("What is your full name?")
assistant_message = await get_final_assistant_message(session)
assert "GitHub" not in assistant_message.data.content
assert "Testy" in assistant_message.data.content
@@ -88,15 +82,40 @@ async def test_should_create_a_session_with_replaced_systemMessage_config(
system_message = _get_system_message(traffic[0])
assert system_message == test_system_message # Exact match
+ async def test_should_create_a_session_with_customized_systemMessage_config(
+ self, ctx: E2ETestContext
+ ):
+ custom_tone = "Respond in a warm, professional tone. Be thorough in explanations."
+ appended_content = "Always mention quarterly earnings."
+ session = await ctx.client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "tone": {"action": "replace", "content": custom_tone},
+ "code_change_rules": {"action": "remove"},
+ },
+ "content": appended_content,
+ },
+ )
+
+ assistant_message = await session.send_and_wait("Who are you?")
+ assert assistant_message is not None
+
+ # Validate the system message sent to the model
+ traffic = await ctx.get_exchanges()
+ system_message = _get_system_message(traffic[0])
+ assert custom_tone in system_message
+ assert appended_content in system_message
+ assert "" not in system_message
+
async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {
- "available_tools": ["view", "edit"],
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ available_tools=["view", "edit"],
)
- await session.send({"prompt": "What is 1+1?"})
+ await session.send("What is 1+1?")
await get_final_assistant_message(session)
# It only tells the model about the specified tools and no others
@@ -109,10 +128,10 @@ async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestCon
async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {"excluded_tools": ["view"], "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, excluded_tools=["view"]
)
- await session.send({"prompt": "What is 1+1?"})
+ await session.send("What is 1+1?")
await get_final_assistant_message(session)
# It has other tools, but not the one we excluded
@@ -132,9 +151,9 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont
import asyncio
s1, s2, s3 = await asyncio.gather(
- ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}),
- ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}),
- ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}),
+ ctx.client.create_session(on_permission_request=PermissionHandler.approve_all),
+ ctx.client.create_session(on_permission_request=PermissionHandler.approve_all),
+ ctx.client.create_session(on_permission_request=PermissionHandler.approve_all),
)
# All sessions should have unique IDs
@@ -157,35 +176,33 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont
async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext):
# Create initial session
session1 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
session_id = session1.session_id
- answer = await session1.send_and_wait({"prompt": "What is 1+1?"})
+ answer = await session1.send_and_wait("What is 1+1?")
assert answer is not None
assert "2" in answer.data.content
# Resume using the same client
session2 = await ctx.client.resume_session(
- session_id, {"on_permission_request": PermissionHandler.approve_all}
+ session_id, on_permission_request=PermissionHandler.approve_all
)
assert session2.session_id == session_id
answer2 = await get_final_assistant_message(session2)
assert "2" in answer2.data.content
# Can continue the conversation statefully
- answer3 = await session2.send_and_wait(
- {"prompt": "Now if you double that, what do you get?"}
- )
+ answer3 = await session2.send_and_wait("Now if you double that, what do you get?")
assert answer3 is not None
assert "4" in answer3.data.content
async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestContext):
# Create initial session
session1 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
session_id = session1.session_id
- answer = await session1.send_and_wait({"prompt": "What is 1+1?"})
+ answer = await session1.send_and_wait("What is 1+1?")
assert answer is not None
assert "2" in answer.data.content
@@ -194,17 +211,17 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont
"fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None
)
new_client = CopilotClient(
- {
- "cli_path": ctx.cli_path,
- "cwd": ctx.work_dir,
- "env": ctx.get_env(),
- "github_token": github_token,
- }
+ SubprocessConfig(
+ cli_path=ctx.cli_path,
+ cwd=ctx.work_dir,
+ env=ctx.get_env(),
+ github_token=github_token,
+ )
)
try:
session2 = await new_client.resume_session(
- session_id, {"on_permission_request": PermissionHandler.approve_all}
+ session_id, on_permission_request=PermissionHandler.approve_all
)
assert session2.session_id == session_id
@@ -214,9 +231,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont
assert "session.resume" in message_types
# Can continue the conversation statefully
- answer2 = await session2.send_and_wait(
- {"prompt": "Now if you double that, what do you get?"}
- )
+ answer2 = await session2.send_and_wait("Now if you double that, what do you get?")
assert answer2 is not None
assert "4" in answer2.data.content
finally:
@@ -225,7 +240,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont
async def test_should_throw_error_resuming_nonexistent_session(self, ctx: E2ETestContext):
with pytest.raises(Exception):
await ctx.client.resume_session(
- "non-existent-session-id", {"on_permission_request": PermissionHandler.approve_all}
+ "non-existent-session-id", on_permission_request=PermissionHandler.approve_all
)
async def test_should_list_sessions(self, ctx: E2ETestContext):
@@ -233,13 +248,13 @@ async def test_should_list_sessions(self, ctx: E2ETestContext):
# Create a couple of sessions and send messages to persist them
session1 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
- await session1.send_and_wait({"prompt": "Say hello"})
+ await session1.send_and_wait("Say hello")
session2 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
- await session2.send_and_wait({"prompt": "Say goodbye"})
+ await session2.send_and_wait("Say goodbye")
# Small delay to ensure session files are written to disk
await asyncio.sleep(0.2)
@@ -276,9 +291,9 @@ async def test_should_delete_session(self, ctx: E2ETestContext):
# Create a session and send a message to persist it
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
- await session.send_and_wait({"prompt": "Hello"})
+ await session.send_and_wait("Hello")
session_id = session.session_id
# Small delay to ensure session file is written to disk
@@ -300,7 +315,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext):
# Verify we cannot resume the deleted session
with pytest.raises(Exception):
await ctx.client.resume_session(
- session_id, {"on_permission_request": PermissionHandler.approve_all}
+ session_id, on_permission_request=PermissionHandler.approve_all
)
async def test_should_get_last_session_id(self, ctx: E2ETestContext):
@@ -308,9 +323,9 @@ async def test_should_get_last_session_id(self, ctx: E2ETestContext):
# Create a session and send a message to persist it
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
- await session.send_and_wait({"prompt": "Say hello"})
+ await session.send_and_wait("Say hello")
# Small delay to ensure session data is flushed to disk
await asyncio.sleep(0.5)
@@ -330,72 +345,64 @@ def get_secret_number_handler(invocation):
)
session = await ctx.client.create_session(
- {
- "tools": [
- Tool(
- name="get_secret_number",
- description="Gets the secret number",
- handler=get_secret_number_handler,
- parameters={
- "type": "object",
- "properties": {"key": {"type": "string", "description": "Key"}},
- "required": ["key"],
- },
- )
- ],
- "on_permission_request": PermissionHandler.approve_all,
- }
- )
-
- answer = await session.send_and_wait({"prompt": "What is the secret number for key ALPHA?"})
+ on_permission_request=PermissionHandler.approve_all,
+ tools=[
+ Tool(
+ name="get_secret_number",
+ description="Gets the secret number",
+ handler=get_secret_number_handler,
+ parameters={
+ "type": "object",
+ "properties": {"key": {"type": "string", "description": "Key"}},
+ "required": ["key"],
+ },
+ )
+ ],
+ )
+
+ answer = await session.send_and_wait("What is the secret number for key ALPHA?")
assert answer is not None
assert "54321" in answer.data.content
async def test_should_create_session_with_custom_provider(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {
- "provider": {
- "type": "openai",
- "base_url": "https://api.openai.com/v1",
- "api_key": "fake-key",
- },
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ provider={
+ "type": "openai",
+ "base_url": "https://api.openai.com/v1",
+ "api_key": "fake-key",
+ },
)
assert session.session_id
async def test_should_create_session_with_azure_provider(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {
- "provider": {
- "type": "azure",
- "base_url": "https://my-resource.openai.azure.com",
- "api_key": "fake-key",
- "azure": {
- "api_version": "2024-02-15-preview",
- },
+ on_permission_request=PermissionHandler.approve_all,
+ provider={
+ "type": "azure",
+ "base_url": "https://my-resource.openai.azure.com",
+ "api_key": "fake-key",
+ "azure": {
+ "api_version": "2024-02-15-preview",
},
- "on_permission_request": PermissionHandler.approve_all,
- }
+ },
)
assert session.session_id
async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
session_id = session.session_id
# Resume the session with a provider
session2 = await ctx.client.resume_session(
session_id,
- {
- "provider": {
- "type": "openai",
- "base_url": "https://api.openai.com/v1",
- "api_key": "fake-key",
- },
- "on_permission_request": PermissionHandler.approve_all,
+ on_permission_request=PermissionHandler.approve_all,
+ provider={
+ "type": "openai",
+ "base_url": "https://api.openai.com/v1",
+ "api_key": "fake-key",
},
)
@@ -405,7 +412,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext):
import asyncio
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
# Set up event listeners BEFORE sending to avoid race conditions
@@ -418,12 +425,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext):
# Send a message that will trigger a long-running shell command
await session.send(
- {
- "prompt": (
- "run the shell command 'sleep 100' "
- "(note this works on both bash and PowerShell)"
- )
- }
+ "run the shell command 'sleep 100' (note this works on both bash and PowerShell)"
)
# Wait for the tool to start executing
@@ -444,15 +446,27 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext):
assert len(abort_events) > 0, "Expected an abort event in messages"
# We should be able to send another message
- answer = await session.send_and_wait({"prompt": "What is 2+2?"})
+ answer = await session.send_and_wait("What is 2+2?")
assert "4" in answer.data.content
async def test_should_receive_session_events(self, ctx: E2ETestContext):
import asyncio
+ # Use on_event to capture events dispatched during session creation.
+ # session.start is emitted during the session.create RPC; if the session
+ # weren't registered in the sessions map before the RPC, it would be dropped.
+ early_events = []
+
+ def capture_early(event):
+ early_events.append(event)
+
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all,
+ on_event=capture_early,
)
+
+ assert any(e.type.value == "session.start" for e in early_events)
+
received_events = []
idle_event = asyncio.Event()
@@ -464,7 +478,7 @@ def on_event(event):
session.on(on_event)
# Send a message to trigger events
- await session.send({"prompt": "What is 100+200?"})
+ await session.send("What is 100+200?")
# Wait for session to become idle
try:
@@ -488,19 +502,108 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo
custom_config_dir = os.path.join(ctx.home_dir, "custom-config")
session = await ctx.client.create_session(
- {
- "config_dir": custom_config_dir,
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all, config_dir=custom_config_dir
)
assert session.session_id
# Session should work normally with custom config dir
- await session.send({"prompt": "What is 1+1?"})
+ await session.send("What is 1+1?")
assistant_message = await get_final_assistant_message(session)
assert "2" in assistant_message.data.content
+ async def test_session_log_emits_events_at_all_levels(self, ctx: E2ETestContext):
+ import asyncio
+
+ session = await ctx.client.create_session(
+ on_permission_request=PermissionHandler.approve_all
+ )
+
+ received_events = []
+
+ def on_event(event):
+ if event.type.value in ("session.info", "session.warning", "session.error"):
+ received_events.append(event)
+
+ session.on(on_event)
+
+ await session.log("Info message")
+ await session.log("Warning message", level="warning")
+ await session.log("Error message", level="error")
+ await session.log("Ephemeral message", ephemeral=True)
+
+ # Poll until all 4 notification events arrive
+ deadline = asyncio.get_event_loop().time() + 10
+ while len(received_events) < 4:
+ if asyncio.get_event_loop().time() > deadline:
+ pytest.fail(
+ f"Timed out waiting for 4 notification events, got {len(received_events)}"
+ )
+ await asyncio.sleep(0.1)
+
+ by_message = {e.data.message: e for e in received_events}
+
+ assert by_message["Info message"].type.value == "session.info"
+ assert by_message["Info message"].data.info_type == "notification"
+
+ assert by_message["Warning message"].type.value == "session.warning"
+ assert by_message["Warning message"].data.warning_type == "notification"
+
+ assert by_message["Error message"].type.value == "session.error"
+ assert by_message["Error message"].data.error_type == "notification"
+
+ assert by_message["Ephemeral message"].type.value == "session.info"
+ assert by_message["Ephemeral message"].data.info_type == "notification"
+
+ async def test_should_set_model_with_reasoning_effort(self, ctx: E2ETestContext):
+ """Test that setModel passes reasoningEffort and it appears in the model_change event."""
+ import asyncio
+
+ session = await ctx.client.create_session(
+ on_permission_request=PermissionHandler.approve_all
+ )
+
+ model_change_event = asyncio.get_event_loop().create_future()
+
+ def on_event(event):
+ if not model_change_event.done() and event.type.value == "session.model_change":
+ model_change_event.set_result(event)
+
+ session.on(on_event)
+
+ await session.set_model("gpt-4.1", reasoning_effort="high")
+
+ event = await asyncio.wait_for(model_change_event, timeout=30)
+ assert event.data.new_model == "gpt-4.1"
+ assert event.data.reasoning_effort == "high"
+
+ async def test_should_accept_blob_attachments(self, ctx: E2ETestContext):
+ session = await ctx.client.create_session(
+ on_permission_request=PermissionHandler.approve_all
+ )
+
+ # 1x1 transparent PNG pixel, base64-encoded
+ pixel_png = (
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAY"
+ "AAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhg"
+ "GAWjR9awAAAABJRU5ErkJggg=="
+ )
+
+ await session.send(
+ "Describe this image",
+ attachments=[
+ {
+ "type": "blob",
+ "data": pixel_png,
+ "mimeType": "image/png",
+ "displayName": "test-pixel.png",
+ },
+ ],
+ )
+
+ # Just verify send doesn't throw — blob attachment support varies by runtime
+ await session.disconnect()
+
def _get_system_message(exchange: dict) -> str:
messages = exchange.get("request", {}).get("messages", [])
diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py
index 166840e578..9b0599975f 100644
--- a/python/e2e/test_skills.py
+++ b/python/e2e/test_skills.py
@@ -56,16 +56,13 @@ async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ET
"""Test that skills are loaded and applied from skillDirectories"""
skills_dir = create_skill_dir(ctx.work_dir)
session = await ctx.client.create_session(
- {
- "skill_directories": [skills_dir],
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all, skill_directories=[skills_dir]
)
assert session.session_id is not None
# The skill instructs the model to include a marker - verify it appears
- message = await session.send_and_wait({"prompt": "Say hello briefly using the test skill."})
+ message = await session.send_and_wait("Say hello briefly using the test skill.")
assert message is not None
assert SKILL_MARKER in message.data.content
@@ -77,17 +74,15 @@ async def test_should_not_apply_skill_when_disabled_via_disabledskills(
"""Test that disabledSkills prevents skill from being applied"""
skills_dir = create_skill_dir(ctx.work_dir)
session = await ctx.client.create_session(
- {
- "skill_directories": [skills_dir],
- "disabled_skills": ["test-skill"],
- "on_permission_request": PermissionHandler.approve_all,
- }
+ on_permission_request=PermissionHandler.approve_all,
+ skill_directories=[skills_dir],
+ disabled_skills=["test-skill"],
)
assert session.session_id is not None
# The skill is disabled, so the marker should NOT appear
- message = await session.send_and_wait({"prompt": "Say hello briefly using the test skill."})
+ message = await session.send_and_wait("Say hello briefly using the test skill.")
assert message is not None
assert SKILL_MARKER not in message.data.content
@@ -105,28 +100,26 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories(
# Create a session without skills first
session1 = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
session_id = session1.session_id
# First message without skill - marker should not appear
- message1 = await session1.send_and_wait({"prompt": "Say hi."})
+ message1 = await session1.send_and_wait("Say hi.")
assert message1 is not None
assert SKILL_MARKER not in message1.data.content
# Resume with skillDirectories - skill should now be active
session2 = await ctx.client.resume_session(
session_id,
- {
- "skill_directories": [skills_dir],
- "on_permission_request": PermissionHandler.approve_all,
- },
+ on_permission_request=PermissionHandler.approve_all,
+ skill_directories=[skills_dir],
)
assert session2.session_id == session_id
# Now the skill should be applied
- message2 = await session2.send_and_wait({"prompt": "Say hello again using the test skill."})
+ message2 = await session2.send_and_wait("Say hello again using the test skill.")
assert message2 is not None
assert SKILL_MARKER in message2.data.content
diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py
index d347015a03..05e977e129 100644
--- a/python/e2e/test_streaming_fidelity.py
+++ b/python/e2e/test_streaming_fidelity.py
@@ -4,7 +4,7 @@
import pytest
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient, PermissionHandler, SubprocessConfig
from .testharness import E2ETestContext
@@ -14,13 +14,13 @@
class TestStreamingFidelity:
async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {"streaming": True, "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, streaming=True
)
events = []
session.on(lambda event: events.append(event))
- await session.send_and_wait({"prompt": "Count from 1 to 5, separated by commas."})
+ await session.send_and_wait("Count from 1 to 5, separated by commas.")
types = [e.type.value for e in events]
@@ -46,13 +46,13 @@ async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx:
async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {"streaming": False, "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, streaming=False
)
events = []
session.on(lambda event: events.append(event))
- await session.send_and_wait({"prompt": "Say 'hello world'."})
+ await session.send_and_wait("Say 'hello world'.")
delta_events = [e for e in events if e.type.value == "assistant.message_delta"]
@@ -67,9 +67,9 @@ async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E
async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
- {"streaming": False, "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, streaming=False
)
- await session.send_and_wait({"prompt": "What is 3 + 6?"})
+ await session.send_and_wait("What is 3 + 6?")
await session.disconnect()
# Resume using a new client
@@ -77,25 +77,24 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont
"fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None
)
new_client = CopilotClient(
- {
- "cli_path": ctx.cli_path,
- "cwd": ctx.work_dir,
- "env": ctx.get_env(),
- "github_token": github_token,
- }
+ SubprocessConfig(
+ cli_path=ctx.cli_path,
+ cwd=ctx.work_dir,
+ env=ctx.get_env(),
+ github_token=github_token,
+ )
)
try:
session2 = await new_client.resume_session(
session.session_id,
- {"streaming": True, "on_permission_request": PermissionHandler.approve_all},
+ on_permission_request=PermissionHandler.approve_all,
+ streaming=True,
)
events = []
session2.on(lambda event: events.append(event))
- answer = await session2.send_and_wait(
- {"prompt": "Now if you double that, what do you get?"}
- )
+ answer = await session2.send_and_wait("Now if you double that, what do you get?")
assert answer is not None
assert "18" in answer.data.content
diff --git a/python/e2e/test_system_message_transform.py b/python/e2e/test_system_message_transform.py
new file mode 100644
index 0000000000..9ae1706378
--- /dev/null
+++ b/python/e2e/test_system_message_transform.py
@@ -0,0 +1,123 @@
+"""
+Copyright (c) Microsoft Corporation.
+
+Tests for system message transform functionality
+"""
+
+import pytest
+
+from copilot import PermissionHandler
+
+from .testharness import E2ETestContext
+from .testharness.helper import write_file
+
+pytestmark = pytest.mark.asyncio(loop_scope="module")
+
+
+class TestSystemMessageTransform:
+ async def test_should_invoke_transform_callbacks_with_section_content(
+ self, ctx: E2ETestContext
+ ):
+ """Test that transform callbacks are invoked with the section content"""
+ identity_contents = []
+ tone_contents = []
+
+ async def identity_transform(content: str) -> str:
+ identity_contents.append(content)
+ return content
+
+ async def tone_transform(content: str) -> str:
+ tone_contents.append(content)
+ return content
+
+ session = await ctx.client.create_session(
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "identity": {"action": identity_transform},
+ "tone": {"action": tone_transform},
+ },
+ },
+ on_permission_request=PermissionHandler.approve_all,
+ )
+
+ write_file(ctx.work_dir, "test.txt", "Hello transform!")
+
+ await session.send_and_wait("Read the contents of test.txt and tell me what it says")
+
+ # Both transform callbacks should have been invoked
+ assert len(identity_contents) > 0
+ assert len(tone_contents) > 0
+
+ # Callbacks should have received non-empty content
+ assert all(len(c) > 0 for c in identity_contents)
+ assert all(len(c) > 0 for c in tone_contents)
+
+ await session.disconnect()
+
+ async def test_should_apply_transform_modifications_to_section_content(
+ self, ctx: E2ETestContext
+ ):
+ """Test that transform modifications are applied to the section content"""
+
+ async def identity_transform(content: str) -> str:
+ return content + "\nTRANSFORM_MARKER"
+
+ session = await ctx.client.create_session(
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "identity": {"action": identity_transform},
+ },
+ },
+ on_permission_request=PermissionHandler.approve_all,
+ )
+
+ write_file(ctx.work_dir, "hello.txt", "Hello!")
+
+ await session.send_and_wait("Read the contents of hello.txt")
+
+ # Verify the transform result was actually applied to the system message
+ traffic = await ctx.get_exchanges()
+ system_message = _get_system_message(traffic[0])
+ assert "TRANSFORM_MARKER" in system_message
+
+ await session.disconnect()
+
+ async def test_should_work_with_static_overrides_and_transforms_together(
+ self, ctx: E2ETestContext
+ ):
+ """Test that static overrides and transforms work together"""
+ identity_contents = []
+
+ async def identity_transform(content: str) -> str:
+ identity_contents.append(content)
+ return content
+
+ session = await ctx.client.create_session(
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "safety": {"action": "remove"},
+ "identity": {"action": identity_transform},
+ },
+ },
+ on_permission_request=PermissionHandler.approve_all,
+ )
+
+ write_file(ctx.work_dir, "combo.txt", "Combo test!")
+
+ await session.send_and_wait("Read the contents of combo.txt and tell me what it says")
+
+ # The transform callback should have been invoked
+ assert len(identity_contents) > 0
+
+ await session.disconnect()
+
+
+def _get_system_message(exchange: dict) -> str:
+ messages = exchange.get("request", {}).get("messages", [])
+ for msg in messages:
+ if msg.get("role") == "system":
+ return msg.get("content", "")
+ return ""
diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py
index b692e3f655..458897d497 100644
--- a/python/e2e/test_tools.py
+++ b/python/e2e/test_tools.py
@@ -24,10 +24,10 @@ async def test_invokes_built_in_tools(self, ctx: E2ETestContext):
f.write("# ELIZA, the only chatbot you'll ever need")
session = await ctx.client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
- await session.send({"prompt": "What's the first line of README.md in this directory?"})
+ await session.send("What's the first line of README.md in this directory?")
assistant_message = await get_final_assistant_message(session)
assert "ELIZA" in assistant_message.data.content
@@ -40,10 +40,10 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str:
return params.input.upper()
session = await ctx.client.create_session(
- {"tools": [encrypt_string], "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, tools=[encrypt_string]
)
- await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"})
+ await session.send("Use encrypt_string to encrypt this string: Hello")
assistant_message = await get_final_assistant_message(session)
assert "HELLO" in assistant_message.data.content
@@ -53,12 +53,10 @@ def get_user_location() -> str:
raise Exception("Melbourne")
session = await ctx.client.create_session(
- {"tools": [get_user_location], "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, tools=[get_user_location]
)
- await session.send(
- {"prompt": "What is my location? If you can't find out, just say 'unknown'."}
- )
+ await session.send("What is my location? If you can't find out, just say 'unknown'.")
answer = await get_final_assistant_message(session)
# Check the underlying traffic
@@ -118,15 +116,13 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]:
]
session = await ctx.client.create_session(
- {"tools": [db_query], "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, tools=[db_query]
)
expected_session_id = session.session_id
await session.send(
- {
- "prompt": "Perform a DB query for the 'cities' table using IDs 12 and 19, "
- "sorting ascending. Reply only with lines of the form: [cityname] [population]"
- }
+ "Perform a DB query for the 'cities' table using IDs 12 and 19, "
+ "sorting ascending. Reply only with lines of the form: [cityname] [population]"
)
assistant_message = await get_final_assistant_message(session)
@@ -138,6 +134,34 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]:
assert "135460" in response_content.replace(",", "")
assert "204356" in response_content.replace(",", "")
+ async def test_skippermission_sent_in_tool_definition(self, ctx: E2ETestContext):
+ class LookupParams(BaseModel):
+ id: str = Field(description="ID to look up")
+
+ @define_tool(
+ "safe_lookup",
+ description="A safe lookup that skips permission",
+ skip_permission=True,
+ )
+ def safe_lookup(params: LookupParams, invocation: ToolInvocation) -> str:
+ return f"RESULT: {params.id}"
+
+ did_run_permission_request = False
+
+ def tracking_handler(request, invocation):
+ nonlocal did_run_permission_request
+ did_run_permission_request = True
+ return PermissionRequestResult(kind="no-result")
+
+ session = await ctx.client.create_session(
+ on_permission_request=tracking_handler, tools=[safe_lookup]
+ )
+
+ await session.send("Use safe_lookup to look up 'test123'")
+ assistant_message = await get_final_assistant_message(session)
+ assert "RESULT: test123" in assistant_message.data.content
+ assert not did_run_permission_request
+
async def test_overrides_built_in_tool_with_custom_tool(self, ctx: E2ETestContext):
class GrepParams(BaseModel):
query: str = Field(description="Search query")
@@ -151,10 +175,10 @@ def custom_grep(params: GrepParams, invocation: ToolInvocation) -> str:
return f"CUSTOM_GREP_RESULT: {params.query}"
session = await ctx.client.create_session(
- {"tools": [custom_grep], "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, tools=[custom_grep]
)
- await session.send({"prompt": "Use grep to search for the word 'hello'"})
+ await session.send("Use grep to search for the word 'hello'")
assistant_message = await get_final_assistant_message(session)
assert "CUSTOM_GREP_RESULT" in assistant_message.data.content
@@ -173,13 +197,10 @@ def on_permission_request(request, invocation):
return PermissionRequestResult(kind="approved")
session = await ctx.client.create_session(
- {
- "tools": [encrypt_string],
- "on_permission_request": on_permission_request,
- }
+ on_permission_request=on_permission_request, tools=[encrypt_string]
)
- await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"})
+ await session.send("Use encrypt_string to encrypt this string: Hello")
assistant_message = await get_final_assistant_message(session)
assert "HELLO" in assistant_message.data.content
@@ -204,13 +225,10 @@ def on_permission_request(request, invocation):
return PermissionRequestResult(kind="denied-interactively-by-user")
session = await ctx.client.create_session(
- {
- "tools": [encrypt_string],
- "on_permission_request": on_permission_request,
- }
+ on_permission_request=on_permission_request, tools=[encrypt_string]
)
- await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"})
+ await session.send("Use encrypt_string to encrypt this string: Hello")
await get_final_assistant_message(session)
# The tool handler should NOT have been called since permission was denied
diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py
index c030889129..27dce38a11 100644
--- a/python/e2e/testharness/context.py
+++ b/python/e2e/testharness/context.py
@@ -10,7 +10,7 @@
import tempfile
from pathlib import Path
-from copilot import CopilotClient
+from copilot import CopilotClient, SubprocessConfig
from .proxy import CapiProxy
@@ -64,12 +64,12 @@ async def setup(self):
"fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None
)
self._client = CopilotClient(
- {
- "cli_path": self.cli_path,
- "cwd": self.work_dir,
- "env": self.get_env(),
- "github_token": github_token,
- }
+ SubprocessConfig(
+ cli_path=self.cli_path,
+ cwd=self.work_dir,
+ env=self.get_env(),
+ github_token=github_token,
+ )
)
async def teardown(self, test_failed: bool = False):
diff --git a/python/pyproject.toml b/python/pyproject.toml
index 741232e8a6..7c1f8bbf2e 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -40,6 +40,9 @@ dev = [
"pytest-timeout>=2.0.0",
"httpx>=0.24.0",
]
+telemetry = [
+ "opentelemetry-api>=1.0.0",
+]
# Use find with a glob so that the copilot.bin subpackage (created dynamically
# by scripts/build-wheels.mjs during publishing) is included in platform wheels.
@@ -65,6 +68,7 @@ select = [
]
[tool.ruff.format]
+docstring-code-format = true
quote-style = "double"
indent-style = "space"
diff --git a/python/samples/chat.py b/python/samples/chat.py
index eb781e4e22..ee94c21fec 100644
--- a/python/samples/chat.py
+++ b/python/samples/chat.py
@@ -9,11 +9,7 @@
async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session(
- {
- "on_permission_request": PermissionHandler.approve_all,
- }
- )
+ session = await client.create_session(on_permission_request=PermissionHandler.approve_all)
def on_event(event):
output = None
@@ -34,7 +30,7 @@ def on_event(event):
continue
print()
- reply = await session.send_and_wait({"prompt": user_input})
+ reply = await session.send_and_wait(user_input)
print(f"\nAssistant: {reply.data.content if reply else None}\n")
diff --git a/python/test_client.py b/python/test_client.py
index bcc249f302..9f8f384231 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -6,154 +6,175 @@
import pytest
-from copilot import CopilotClient, PermissionHandler, define_tool
+from copilot import (
+ CopilotClient,
+ ExternalServerConfig,
+ PermissionHandler,
+ PermissionRequestResult,
+ SubprocessConfig,
+ define_tool,
+)
+from copilot.types import ModelCapabilities, ModelInfo, ModelLimits, ModelSupports
from e2e.testharness import CLI_PATH
class TestPermissionHandlerRequired:
@pytest.mark.asyncio
async def test_create_session_raises_without_permission_handler(self):
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
await client.start()
try:
- with pytest.raises(ValueError, match="on_permission_request.*is required"):
- await client.create_session({})
+ with pytest.raises(TypeError, match="on_permission_request"):
+ await client.create_session() # type: ignore[call-arg]
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_create_session_raises_with_none_permission_handler(self):
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
+ await client.start()
+ try:
+ with pytest.raises(ValueError, match="on_permission_request handler is required"):
+ await client.create_session(on_permission_request=None) # type: ignore[arg-type]
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_v2_permission_adapter_rejects_no_result(self):
+ client = CopilotClient(SubprocessConfig(CLI_PATH))
+ await client.start()
+ try:
+ session = await client.create_session(
+ on_permission_request=lambda request, invocation: PermissionRequestResult(
+ kind="no-result"
+ )
+ )
+ with pytest.raises(ValueError, match="protocol v2 server"):
+ await client._handle_permission_request_v2(
+ {
+ "sessionId": session.session_id,
+ "permissionRequest": {"kind": "write"},
+ }
+ )
finally:
await client.force_stop()
@pytest.mark.asyncio
async def test_resume_session_raises_without_permission_handler(self):
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
await client.start()
try:
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
with pytest.raises(ValueError, match="on_permission_request.*is required"):
- await client.resume_session(session.session_id, {})
+ await client.resume_session(session.session_id, on_permission_request=None)
finally:
await client.force_stop()
class TestURLParsing:
def test_parse_port_only_url(self):
- client = CopilotClient({"cli_url": "8080", "log_level": "error"})
+ client = CopilotClient(ExternalServerConfig(url="8080"))
assert client._actual_port == 8080
assert client._actual_host == "localhost"
assert client._is_external_server
def test_parse_host_port_url(self):
- client = CopilotClient({"cli_url": "127.0.0.1:9000", "log_level": "error"})
+ client = CopilotClient(ExternalServerConfig(url="127.0.0.1:9000"))
assert client._actual_port == 9000
assert client._actual_host == "127.0.0.1"
assert client._is_external_server
def test_parse_http_url(self):
- client = CopilotClient({"cli_url": "http://localhost:7000", "log_level": "error"})
+ client = CopilotClient(ExternalServerConfig(url="http://localhost:7000"))
assert client._actual_port == 7000
assert client._actual_host == "localhost"
assert client._is_external_server
def test_parse_https_url(self):
- client = CopilotClient({"cli_url": "https://example.com:443", "log_level": "error"})
+ client = CopilotClient(ExternalServerConfig(url="https://example.com:443"))
assert client._actual_port == 443
assert client._actual_host == "example.com"
assert client._is_external_server
def test_invalid_url_format(self):
with pytest.raises(ValueError, match="Invalid cli_url format"):
- CopilotClient({"cli_url": "invalid-url", "log_level": "error"})
+ CopilotClient(ExternalServerConfig(url="invalid-url"))
def test_invalid_port_too_high(self):
with pytest.raises(ValueError, match="Invalid port in cli_url"):
- CopilotClient({"cli_url": "localhost:99999", "log_level": "error"})
+ CopilotClient(ExternalServerConfig(url="localhost:99999"))
def test_invalid_port_zero(self):
with pytest.raises(ValueError, match="Invalid port in cli_url"):
- CopilotClient({"cli_url": "localhost:0", "log_level": "error"})
+ CopilotClient(ExternalServerConfig(url="localhost:0"))
def test_invalid_port_negative(self):
with pytest.raises(ValueError, match="Invalid port in cli_url"):
- CopilotClient({"cli_url": "localhost:-1", "log_level": "error"})
-
- def test_cli_url_with_use_stdio(self):
- with pytest.raises(ValueError, match="cli_url is mutually exclusive"):
- CopilotClient({"cli_url": "localhost:8080", "use_stdio": True, "log_level": "error"})
-
- def test_cli_url_with_cli_path(self):
- with pytest.raises(ValueError, match="cli_url is mutually exclusive"):
- CopilotClient(
- {"cli_url": "localhost:8080", "cli_path": "/path/to/cli", "log_level": "error"}
- )
-
- def test_use_stdio_false_when_cli_url(self):
- client = CopilotClient({"cli_url": "8080", "log_level": "error"})
- assert not client.options["use_stdio"]
+ CopilotClient(ExternalServerConfig(url="localhost:-1"))
def test_is_external_server_true(self):
- client = CopilotClient({"cli_url": "localhost:8080", "log_level": "error"})
+ client = CopilotClient(ExternalServerConfig(url="localhost:8080"))
assert client._is_external_server
class TestAuthOptions:
def test_accepts_github_token(self):
client = CopilotClient(
- {"cli_path": CLI_PATH, "github_token": "gho_test_token", "log_level": "error"}
+ SubprocessConfig(
+ cli_path=CLI_PATH,
+ github_token="gho_test_token",
+ log_level="error",
+ )
)
- assert client.options.get("github_token") == "gho_test_token"
+ assert isinstance(client._config, SubprocessConfig)
+ assert client._config.github_token == "gho_test_token"
def test_default_use_logged_in_user_true_without_token(self):
- client = CopilotClient({"cli_path": CLI_PATH, "log_level": "error"})
- assert client.options.get("use_logged_in_user") is True
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, log_level="error"))
+ assert isinstance(client._config, SubprocessConfig)
+ assert client._config.use_logged_in_user is True
def test_default_use_logged_in_user_false_with_token(self):
client = CopilotClient(
- {"cli_path": CLI_PATH, "github_token": "gho_test_token", "log_level": "error"}
+ SubprocessConfig(
+ cli_path=CLI_PATH,
+ github_token="gho_test_token",
+ log_level="error",
+ )
)
- assert client.options.get("use_logged_in_user") is False
+ assert isinstance(client._config, SubprocessConfig)
+ assert client._config.use_logged_in_user is False
def test_explicit_use_logged_in_user_true_with_token(self):
client = CopilotClient(
- {
- "cli_path": CLI_PATH,
- "github_token": "gho_test_token",
- "use_logged_in_user": True,
- "log_level": "error",
- }
+ SubprocessConfig(
+ cli_path=CLI_PATH,
+ github_token="gho_test_token",
+ use_logged_in_user=True,
+ log_level="error",
+ )
)
- assert client.options.get("use_logged_in_user") is True
+ assert isinstance(client._config, SubprocessConfig)
+ assert client._config.use_logged_in_user is True
def test_explicit_use_logged_in_user_false_without_token(self):
client = CopilotClient(
- {"cli_path": CLI_PATH, "use_logged_in_user": False, "log_level": "error"}
- )
- assert client.options.get("use_logged_in_user") is False
-
- def test_github_token_with_cli_url_raises(self):
- with pytest.raises(
- ValueError, match="github_token and use_logged_in_user cannot be used with cli_url"
- ):
- CopilotClient(
- {
- "cli_url": "localhost:8080",
- "github_token": "gho_test_token",
- "log_level": "error",
- }
- )
-
- def test_use_logged_in_user_with_cli_url_raises(self):
- with pytest.raises(
- ValueError, match="github_token and use_logged_in_user cannot be used with cli_url"
- ):
- CopilotClient(
- {"cli_url": "localhost:8080", "use_logged_in_user": False, "log_level": "error"}
+ SubprocessConfig(
+ cli_path=CLI_PATH,
+ use_logged_in_user=False,
+ log_level="error",
)
+ )
+ assert isinstance(client._config, SubprocessConfig)
+ assert client._config.use_logged_in_user is False
class TestOverridesBuiltInTool:
@pytest.mark.asyncio
async def test_overrides_built_in_tool_sent_in_tool_definition(self):
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
await client.start()
try:
@@ -171,7 +192,7 @@ def grep(params) -> str:
return "ok"
await client.create_session(
- {"tools": [grep], "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, tools=[grep]
)
tool_defs = captured["session.create"]["tools"]
assert len(tool_defs) == 1
@@ -182,12 +203,12 @@ def grep(params) -> str:
@pytest.mark.asyncio
async def test_resume_session_sends_overrides_built_in_tool(self):
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
await client.start()
try:
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
captured = {}
@@ -205,7 +226,8 @@ def grep(params) -> str:
await client.resume_session(
session.session_id,
- {"tools": [grep], "on_permission_request": PermissionHandler.approve_all},
+ on_permission_request=PermissionHandler.approve_all,
+ tools=[grep],
)
tool_defs = captured["session.resume"]["tools"]
assert len(tool_defs) == 1
@@ -214,10 +236,132 @@ def grep(params) -> str:
await client.force_stop()
+class TestOnListModels:
+ @pytest.mark.asyncio
+ async def test_list_models_with_custom_handler(self):
+ """Test that on_list_models handler is called instead of RPC"""
+ custom_models = [
+ ModelInfo(
+ id="my-custom-model",
+ name="My Custom Model",
+ capabilities=ModelCapabilities(
+ supports=ModelSupports(vision=False, reasoning_effort=False),
+ limits=ModelLimits(max_context_window_tokens=128000),
+ ),
+ )
+ ]
+
+ handler_calls = []
+
+ def handler():
+ handler_calls.append(1)
+ return custom_models
+
+ client = CopilotClient(
+ SubprocessConfig(cli_path=CLI_PATH),
+ on_list_models=handler,
+ )
+ await client.start()
+ try:
+ models = await client.list_models()
+ assert len(handler_calls) == 1
+ assert models == custom_models
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_list_models_handler_caches_results(self):
+ """Test that on_list_models results are cached"""
+ custom_models = [
+ ModelInfo(
+ id="cached-model",
+ name="Cached Model",
+ capabilities=ModelCapabilities(
+ supports=ModelSupports(vision=False, reasoning_effort=False),
+ limits=ModelLimits(max_context_window_tokens=128000),
+ ),
+ )
+ ]
+
+ handler_calls = []
+
+ def handler():
+ handler_calls.append(1)
+ return custom_models
+
+ client = CopilotClient(
+ SubprocessConfig(cli_path=CLI_PATH),
+ on_list_models=handler,
+ )
+ await client.start()
+ try:
+ await client.list_models()
+ await client.list_models()
+ assert len(handler_calls) == 1 # Only called once due to caching
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_list_models_async_handler(self):
+ """Test that async on_list_models handler works"""
+ custom_models = [
+ ModelInfo(
+ id="async-model",
+ name="Async Model",
+ capabilities=ModelCapabilities(
+ supports=ModelSupports(vision=False, reasoning_effort=False),
+ limits=ModelLimits(max_context_window_tokens=128000),
+ ),
+ )
+ ]
+
+ async def handler():
+ return custom_models
+
+ client = CopilotClient(
+ SubprocessConfig(cli_path=CLI_PATH),
+ on_list_models=handler,
+ )
+ await client.start()
+ try:
+ models = await client.list_models()
+ assert models == custom_models
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_list_models_handler_without_start(self):
+ """Test that on_list_models works without starting the CLI connection"""
+ custom_models = [
+ ModelInfo(
+ id="no-start-model",
+ name="No Start Model",
+ capabilities=ModelCapabilities(
+ supports=ModelSupports(vision=False, reasoning_effort=False),
+ limits=ModelLimits(max_context_window_tokens=128000),
+ ),
+ )
+ ]
+
+ handler_calls = []
+
+ def handler():
+ handler_calls.append(1)
+ return custom_models
+
+ client = CopilotClient(
+ SubprocessConfig(cli_path=CLI_PATH),
+ on_list_models=handler,
+ )
+ models = await client.list_models()
+ assert len(handler_calls) == 1
+ assert models == custom_models
+
+
class TestSessionConfigForwarding:
@pytest.mark.asyncio
async def test_create_session_forwards_client_name(self):
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
await client.start()
try:
@@ -230,7 +374,7 @@ async def mock_request(method, params):
client._client.request = mock_request
await client.create_session(
- {"client_name": "my-app", "on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all, client_name="my-app"
)
assert captured["session.create"]["clientName"] == "my-app"
finally:
@@ -238,12 +382,12 @@ async def mock_request(method, params):
@pytest.mark.asyncio
async def test_resume_session_forwards_client_name(self):
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
await client.start()
try:
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
captured = {}
@@ -259,20 +403,74 @@ async def mock_request(method, params):
client._client.request = mock_request
await client.resume_session(
session.session_id,
- {"client_name": "my-app", "on_permission_request": PermissionHandler.approve_all},
+ on_permission_request=PermissionHandler.approve_all,
+ client_name="my-app",
)
assert captured["session.resume"]["clientName"] == "my-app"
finally:
await client.force_stop()
+ @pytest.mark.asyncio
+ async def test_create_session_forwards_agent(self):
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
+ await client.start()
+
+ try:
+ captured = {}
+ original_request = client._client.request
+
+ async def mock_request(method, params):
+ captured[method] = params
+ return await original_request(method, params)
+
+ client._client.request = mock_request
+ await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ agent="test-agent",
+ custom_agents=[{"name": "test-agent", "prompt": "You are a test agent."}],
+ )
+ assert captured["session.create"]["agent"] == "test-agent"
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_resume_session_forwards_agent(self):
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
+ await client.start()
+
+ try:
+ session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all
+ )
+
+ captured = {}
+ original_request = client._client.request
+
+ async def mock_request(method, params):
+ captured[method] = params
+ if method == "session.resume":
+ return {"sessionId": session.session_id}
+ return await original_request(method, params)
+
+ client._client.request = mock_request
+ await client.resume_session(
+ session.session_id,
+ on_permission_request=PermissionHandler.approve_all,
+ agent="test-agent",
+ custom_agents=[{"name": "test-agent", "prompt": "You are a test agent."}],
+ )
+ assert captured["session.resume"]["agent"] == "test-agent"
+ finally:
+ await client.force_stop()
+
@pytest.mark.asyncio
async def test_set_model_sends_correct_rpc(self):
- client = CopilotClient({"cli_path": CLI_PATH})
+ client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))
await client.start()
try:
session = await client.create_session(
- {"on_permission_request": PermissionHandler.approve_all}
+ on_permission_request=PermissionHandler.approve_all
)
captured = {}
diff --git a/python/test_jsonrpc.py b/python/test_jsonrpc.py
index 2533fc8a79..c0ab2c6f42 100644
--- a/python/test_jsonrpc.py
+++ b/python/test_jsonrpc.py
@@ -7,10 +7,13 @@
import io
import json
+import os
+import threading
+import time
import pytest
-from copilot.jsonrpc import JsonRpcClient
+from copilot._jsonrpc import JsonRpcClient
class MockProcess:
@@ -265,3 +268,62 @@ def test_read_message_multiple_messages_in_sequence(self):
result2 = client._read_message()
assert result2 == message2
+
+
+class ClosingStream:
+ """Stream that immediately returns empty bytes (simulates process death / EOF)."""
+
+ def readline(self):
+ return b""
+
+ def read(self, n: int) -> bytes:
+ return b""
+
+
+class TestOnClose:
+ """Tests for the on_close callback when the read loop exits unexpectedly."""
+
+ def test_on_close_called_on_unexpected_exit(self):
+ """on_close fires when the stream closes while client is still running."""
+ import asyncio
+
+ process = MockProcess()
+ process.stdout = ClosingStream()
+
+ client = JsonRpcClient(process)
+
+ called = threading.Event()
+ client.on_close = lambda: called.set()
+
+ loop = asyncio.new_event_loop()
+ try:
+ client.start(loop=loop)
+ assert called.wait(timeout=2), "on_close was not called within 2 seconds"
+ finally:
+ loop.close()
+
+ def test_on_close_not_called_on_intentional_stop(self):
+ """on_close should not fire when stop() is called intentionally."""
+ import asyncio
+
+ r_fd, w_fd = os.pipe()
+ process = MockProcess()
+ process.stdout = os.fdopen(r_fd, "rb")
+
+ client = JsonRpcClient(process)
+
+ called = threading.Event()
+ client.on_close = lambda: called.set()
+
+ loop = asyncio.new_event_loop()
+ try:
+ client.start(loop=loop)
+
+ # Intentional stop sets _running = False before the thread sees EOF
+ loop.run_until_complete(client.stop())
+ os.close(w_fd)
+
+ time.sleep(0.5)
+ assert not called.is_set(), "on_close should not be called on intentional stop"
+ finally:
+ loop.close()
diff --git a/python/test_telemetry.py b/python/test_telemetry.py
new file mode 100644
index 0000000000..aec38f8168
--- /dev/null
+++ b/python/test_telemetry.py
@@ -0,0 +1,128 @@
+"""Tests for OpenTelemetry telemetry helpers."""
+
+from __future__ import annotations
+
+from unittest.mock import patch
+
+from copilot._telemetry import get_trace_context, trace_context
+from copilot.types import SubprocessConfig, TelemetryConfig
+
+
+class TestGetTraceContext:
+ def test_returns_empty_dict_when_otel_not_installed(self):
+ """get_trace_context() returns {} when opentelemetry is not importable."""
+ real_import = __import__
+
+ def _block_otel(name: str, *args, **kwargs):
+ if name.startswith("opentelemetry"):
+ raise ImportError("mocked")
+ return real_import(name, *args, **kwargs)
+
+ with patch("builtins.__import__", side_effect=_block_otel):
+ result = get_trace_context()
+
+ assert result == {}
+
+ def test_returns_dict_type(self):
+ """get_trace_context() always returns a dict."""
+ result = get_trace_context()
+ assert isinstance(result, dict)
+
+
+class TestTraceContext:
+ def test_yields_without_error_when_no_traceparent(self):
+ """trace_context() with no traceparent should yield without error."""
+ with trace_context(None, None):
+ pass # should not raise
+
+ def test_yields_without_error_when_otel_not_installed(self):
+ """trace_context() should gracefully yield even if opentelemetry is missing."""
+ real_import = __import__
+
+ def _block_otel(name: str, *args, **kwargs):
+ if name.startswith("opentelemetry"):
+ raise ImportError("mocked")
+ return real_import(name, *args, **kwargs)
+
+ with patch("builtins.__import__", side_effect=_block_otel):
+ with trace_context("00-abc-def-01", None):
+ pass # should not raise
+
+ def test_yields_without_error_with_traceparent(self):
+ """trace_context() with a traceparent value should yield without error."""
+ tp = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
+ with trace_context(tp, None):
+ pass # should not raise
+
+ def test_yields_without_error_with_tracestate(self):
+ """trace_context() with both traceparent and tracestate should yield without error."""
+ tp = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
+ with trace_context(tp, "congo=t61rcWkgMzE"):
+ pass # should not raise
+
+
+class TestTelemetryConfig:
+ def test_telemetry_config_type(self):
+ """TelemetryConfig can be constructed as a TypedDict."""
+ config: TelemetryConfig = {
+ "otlp_endpoint": "http://localhost:4318",
+ "exporter_type": "otlp-http",
+ "source_name": "my-app",
+ "capture_content": True,
+ }
+ assert config["otlp_endpoint"] == "http://localhost:4318"
+ assert config["capture_content"] is True
+
+ def test_telemetry_config_in_subprocess_config(self):
+ """TelemetryConfig can be used in SubprocessConfig."""
+ config = SubprocessConfig(
+ telemetry={
+ "otlp_endpoint": "http://localhost:4318",
+ "exporter_type": "otlp-http",
+ }
+ )
+ assert config.telemetry is not None
+ assert config.telemetry["otlp_endpoint"] == "http://localhost:4318"
+
+ def test_telemetry_env_var_mapping(self):
+ """TelemetryConfig fields map to expected environment variable names."""
+ config: TelemetryConfig = {
+ "otlp_endpoint": "http://localhost:4318",
+ "file_path": "/tmp/traces.jsonl",
+ "exporter_type": "file",
+ "source_name": "test-app",
+ "capture_content": True,
+ }
+
+ env: dict[str, str] = {}
+ env["COPILOT_OTEL_ENABLED"] = "true"
+ if "otlp_endpoint" in config:
+ env["OTEL_EXPORTER_OTLP_ENDPOINT"] = config["otlp_endpoint"]
+ if "file_path" in config:
+ env["COPILOT_OTEL_FILE_EXPORTER_PATH"] = config["file_path"]
+ if "exporter_type" in config:
+ env["COPILOT_OTEL_EXPORTER_TYPE"] = config["exporter_type"]
+ if "source_name" in config:
+ env["COPILOT_OTEL_SOURCE_NAME"] = config["source_name"]
+ if "capture_content" in config:
+ env["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = str(
+ config["capture_content"]
+ ).lower()
+
+ assert env["COPILOT_OTEL_ENABLED"] == "true"
+ assert env["OTEL_EXPORTER_OTLP_ENDPOINT"] == "http://localhost:4318"
+ assert env["COPILOT_OTEL_FILE_EXPORTER_PATH"] == "/tmp/traces.jsonl"
+ assert env["COPILOT_OTEL_EXPORTER_TYPE"] == "file"
+ assert env["COPILOT_OTEL_SOURCE_NAME"] == "test-app"
+ assert env["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] == "true"
+
+ def test_capture_content_false_maps_to_lowercase(self):
+ """capture_content=False should map to 'false' string."""
+ config: TelemetryConfig = {"capture_content": False}
+ value = str(config["capture_content"]).lower()
+ assert value == "false"
+
+ def test_empty_telemetry_config(self):
+ """An empty TelemetryConfig is valid since total=False."""
+ config: TelemetryConfig = {}
+ assert len(config) == 0
diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts
index 463d856c89..a48ed47b65 100644
--- a/scripts/codegen/csharp.ts
+++ b/scripts/codegen/csharp.ts
@@ -16,6 +16,7 @@ import {
getApiSchemaPath,
writeGeneratedFile,
isRpcMethod,
+ isNodeFullyExperimental,
EXCLUDED_EVENT_TYPES,
REPO_ROOT,
type ApiSchema,
@@ -24,11 +25,83 @@ import {
const execFileAsync = promisify(execFile);
+// ── C# type rename overrides ────────────────────────────────────────────────
+// Map generated class names to shorter public-facing names.
+// Applied to base classes AND their derived variants (e.g., FooBar → Bar, FooBazShell → BarShell).
+const TYPE_RENAMES: Record = {
+ PermissionRequestedDataPermissionRequest: "PermissionRequest",
+};
+
+/** Apply rename to a generated class name, checking both exact match and prefix replacement for derived types. */
+function applyTypeRename(className: string): string {
+ if (TYPE_RENAMES[className]) return TYPE_RENAMES[className];
+ for (const [from, to] of Object.entries(TYPE_RENAMES)) {
+ if (className.startsWith(from)) {
+ return to + className.slice(from.length);
+ }
+ }
+ return className;
+}
+
// ── C# utilities ────────────────────────────────────────────────────────────
+function escapeXml(text: string): string {
+ return text.replace(/&/g, "&").replace(//g, ">");
+}
+
+/** Ensures text ends with sentence-ending punctuation. */
+function ensureTrailingPunctuation(text: string): string {
+ const trimmed = text.trimEnd();
+ if (/[.!?]$/.test(trimmed)) return trimmed;
+ return `${trimmed}.`;
+}
+
+function xmlDocComment(description: string | undefined, indent: string): string[] {
+ if (!description) return [];
+ const escaped = ensureTrailingPunctuation(escapeXml(description.trim()));
+ const lines = escaped.split(/\r?\n/);
+ if (lines.length === 1) {
+ return [`${indent}/// ${lines[0]}
`];
+ }
+ return [
+ `${indent}/// `,
+ ...lines.map((l) => `${indent}/// ${l}`),
+ `${indent}///
`,
+ ];
+}
+
+/** Like xmlDocComment but skips XML escaping — use only for codegen-controlled strings that already contain valid XML tags. */
+function rawXmlDocSummary(text: string, indent: string): string[] {
+ const line = ensureTrailingPunctuation(text.trim());
+ return [`${indent}/// ${line}
`];
+}
+
+/** Emits a summary (from description or fallback) and, when a real description exists, a remarks line with the fallback. */
+function xmlDocCommentWithFallback(description: string | undefined, fallback: string, indent: string): string[] {
+ if (description) {
+ return [
+ ...xmlDocComment(description, indent),
+ `${indent}/// ${ensureTrailingPunctuation(fallback)}`,
+ ];
+ }
+ return rawXmlDocSummary(fallback, indent);
+}
+
+/** Emits a summary from the schema description, or a fallback naming the property by its JSON key. */
+function xmlDocPropertyComment(description: string | undefined, jsonPropName: string, indent: string): string[] {
+ if (description) return xmlDocComment(description, indent);
+ return rawXmlDocSummary(`Gets or sets the ${escapeXml(jsonPropName)} value.`, indent);
+}
+
+/** Emits a summary from the schema description, or a generic fallback. */
+function xmlDocEnumComment(description: string | undefined, indent: string): string[] {
+ if (description) return xmlDocComment(description, indent);
+ return rawXmlDocSummary(`Defines the allowed values.`, indent);
+}
+
function toPascalCase(name: string): string {
- if (name.includes("_")) {
- return name.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
+ if (name.includes("_") || name.includes("-")) {
+ return name.split(/[-_]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
}
return name.charAt(0).toUpperCase() + name.slice(1);
}
@@ -121,11 +194,12 @@ interface EventVariant {
className: string;
dataClassName: string;
dataSchema: JSONSchema7;
+ dataDescription?: string;
}
let generatedEnums = new Map();
-function getOrCreateEnum(parentClassName: string, propName: string, values: string[], enumOutput: string[]): string {
+function getOrCreateEnum(parentClassName: string, propName: string, values: string[], enumOutput: string[], description?: string): string {
const valuesKey = [...values].sort().join("|");
for (const [, existing] of generatedEnums) {
if ([...existing.values].sort().join("|") === valuesKey) return existing.enumName;
@@ -133,8 +207,11 @@ function getOrCreateEnum(parentClassName: string, propName: string, values: stri
const enumName = `${parentClassName}${propName}`;
generatedEnums.set(enumName, { enumName, values });
- const lines = [`[JsonConverter(typeof(JsonStringEnumConverter<${enumName}>))]`, `public enum ${enumName}`, `{`];
+ const lines: string[] = [];
+ lines.push(...xmlDocEnumComment(description, ""));
+ lines.push(`[JsonConverter(typeof(JsonStringEnumConverter<${enumName}>))]`, `public enum ${enumName}`, `{`);
for (const value of values) {
+ lines.push(` /// The ${escapeXml(value)} variant.
`);
lines.push(` [JsonStringEnumMemberName("${value}")]`, ` ${toPascalCaseEnumMember(value)},`);
}
lines.push(`}`, "");
@@ -153,11 +230,13 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] {
const typeName = typeSchema?.const as string;
if (!typeName) throw new Error("Variant must have type.const");
const baseName = typeToClassName(typeName);
+ const dataSchema = variant.properties.data as JSONSchema7;
return {
typeName,
className: `${baseName}Event`,
dataClassName: `${baseName}Data`,
- dataSchema: variant.properties.data as JSONSchema7,
+ dataSchema,
+ dataDescription: dataSchema?.description,
};
})
.filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName));
@@ -204,30 +283,34 @@ function generatePolymorphicClasses(
variants: JSONSchema7[],
knownTypes: Map,
nestedClasses: Map,
- enumOutput: string[]
+ enumOutput: string[],
+ description?: string
): string {
const lines: string[] = [];
const discriminatorInfo = findDiscriminator(variants)!;
+ const renamedBase = applyTypeRename(baseClassName);
+ lines.push(...xmlDocCommentWithFallback(description, `Polymorphic base type discriminated by ${escapeXml(discriminatorProperty)}.`, ""));
lines.push(`[JsonPolymorphic(`);
lines.push(` TypeDiscriminatorPropertyName = "${discriminatorProperty}",`);
lines.push(` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]`);
for (const [constValue] of discriminatorInfo.mapping) {
- const derivedClassName = `${baseClassName}${toPascalCase(constValue)}`;
+ const derivedClassName = applyTypeRename(`${baseClassName}${toPascalCase(constValue)}`);
lines.push(`[JsonDerivedType(typeof(${derivedClassName}), "${constValue}")]`);
}
- lines.push(`public partial class ${baseClassName}`);
+ lines.push(`public partial class ${renamedBase}`);
lines.push(`{`);
+ lines.push(` /// The type discriminator.
`);
lines.push(` [JsonPropertyName("${discriminatorProperty}")]`);
lines.push(` public virtual string ${toPascalCase(discriminatorProperty)} { get; set; } = string.Empty;`);
lines.push(`}`);
lines.push("");
for (const [constValue, variant] of discriminatorInfo.mapping) {
- const derivedClassName = `${baseClassName}${toPascalCase(constValue)}`;
- const derivedCode = generateDerivedClass(derivedClassName, baseClassName, discriminatorProperty, constValue, variant, knownTypes, nestedClasses, enumOutput);
+ const derivedClassName = applyTypeRename(`${baseClassName}${toPascalCase(constValue)}`);
+ const derivedCode = generateDerivedClass(derivedClassName, renamedBase, discriminatorProperty, constValue, variant, knownTypes, nestedClasses, enumOutput);
nestedClasses.set(derivedClassName, derivedCode);
}
@@ -250,8 +333,10 @@ function generateDerivedClass(
const lines: string[] = [];
const required = new Set(schema.required || []);
+ lines.push(...xmlDocCommentWithFallback(schema.description, `The ${escapeXml(discriminatorValue)} variant of .`, ""));
lines.push(`public partial class ${className} : ${baseClassName}`);
lines.push(`{`);
+ lines.push(` /// `);
lines.push(` [JsonIgnore]`);
lines.push(` public override string ${toPascalCase(discriminatorProperty)} => "${discriminatorValue}";`);
lines.push("");
@@ -265,6 +350,7 @@ function generateDerivedClass(
const csharpName = toPascalCase(propName);
const csharpType = resolveSessionPropertyType(propSchema as JSONSchema7, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput);
+ lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " "));
if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`);
lines.push(` [JsonPropertyName("${propName}")]`);
const reqMod = isReq && !csharpType.endsWith("?") ? "required " : "";
@@ -285,7 +371,9 @@ function generateNestedClass(
enumOutput: string[]
): string {
const required = new Set(schema.required || []);
- const lines = [`public partial class ${className}`, `{`];
+ const lines: string[] = [];
+ lines.push(...xmlDocCommentWithFallback(schema.description, `Nested data type for ${className}.`, ""));
+ lines.push(`public partial class ${className}`, `{`);
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
if (typeof propSchema !== "object") continue;
@@ -294,6 +382,7 @@ function generateNestedClass(
const csharpName = toPascalCase(propName);
const csharpType = resolveSessionPropertyType(prop, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput);
+ lines.push(...xmlDocPropertyComment(prop.description, propName, " "));
if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`);
lines.push(` [JsonPropertyName("${propName}")]`);
const reqMod = isReq && !csharpType.endsWith("?") ? "required " : "";
@@ -319,10 +408,22 @@ function resolveSessionPropertyType(
if (nonNull.length === 1) {
return resolveSessionPropertyType(nonNull[0] as JSONSchema7, parentClassName, propName, isRequired && !hasNull, knownTypes, nestedClasses, enumOutput);
}
+ // Discriminated union: anyOf with multiple object variants sharing a const discriminator
+ if (nonNull.length > 1) {
+ const variants = nonNull as JSONSchema7[];
+ const discriminatorInfo = findDiscriminator(variants);
+ if (discriminatorInfo) {
+ const baseClassName = `${parentClassName}${propName}`;
+ const renamedBase = applyTypeRename(baseClassName);
+ const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput, propSchema.description);
+ nestedClasses.set(renamedBase, polymorphicCode);
+ return isRequired && !hasNull ? renamedBase : `${renamedBase}?`;
+ }
+ }
return hasNull || !isRequired ? "object?" : "object";
}
if (propSchema.enum && Array.isArray(propSchema.enum)) {
- const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput);
+ const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description);
return isRequired ? enumName : `${enumName}?`;
}
if (propSchema.type === "object" && propSchema.properties) {
@@ -338,9 +439,10 @@ function resolveSessionPropertyType(
const discriminatorInfo = findDiscriminator(variants);
if (discriminatorInfo) {
const baseClassName = `${parentClassName}${propName}Item`;
- const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput);
- nestedClasses.set(baseClassName, polymorphicCode);
- return isRequired ? `${baseClassName}[]` : `${baseClassName}[]?`;
+ const renamedBase = applyTypeRename(baseClassName);
+ const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput, items.description);
+ nestedClasses.set(renamedBase, polymorphicCode);
+ return isRequired ? `${renamedBase}[]` : `${renamedBase}[]?`;
}
}
if (items.type === "object" && items.properties) {
@@ -349,7 +451,7 @@ function resolveSessionPropertyType(
return isRequired ? `${itemClassName}[]` : `${itemClassName}[]?`;
}
if (items.enum && Array.isArray(items.enum)) {
- const enumName = getOrCreateEnum(parentClassName, `${propName}Item`, items.enum as string[], enumOutput);
+ const enumName = getOrCreateEnum(parentClassName, `${propName}Item`, items.enum as string[], enumOutput, items.description);
return isRequired ? `${enumName}[]` : `${enumName}[]?`;
}
const itemType = schemaTypeToCSharp(items, true, knownTypes);
@@ -362,7 +464,13 @@ function generateDataClass(variant: EventVariant, knownTypes: Map.`, ""));
+ }
+ lines.push(`public partial class ${variant.dataClassName}`, `{`);
for (const [propName, propSchema] of Object.entries(variant.dataSchema.properties)) {
if (typeof propSchema !== "object") continue;
@@ -370,6 +478,7 @@ function generateDataClass(variant: EventVariant, knownTypes: Map();
const enumOutput: string[] = [];
+ // Extract descriptions for base class properties from the first variant
+ const firstVariant = (schema.definitions?.SessionEvent as JSONSchema7)?.anyOf?.[0];
+ const baseProps = typeof firstVariant === "object" && firstVariant?.properties ? firstVariant.properties : {};
+ const baseDesc = (name: string) => {
+ const prop = baseProps[name];
+ return typeof prop === "object" ? (prop as JSONSchema7).description : undefined;
+ };
+
const lines: string[] = [];
lines.push(`${COPYRIGHT}
// AUTO-GENERATED FILE - DO NOT EDIT
// Generated from: session-events.schema.json
-// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings.
-#pragma warning disable CS1591
-
+using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -404,25 +519,44 @@ namespace GitHub.Copilot.SDK;
// Base class with XML doc
lines.push(`///