# Copilot Python SDK Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC. ## Installation ```bash pip install github-copilot-sdk ``` To include OpenTelemetry support: ```bash pip install "github-copilot-sdk[telemetry]" ``` ## Run the Sample Try the interactive chat sample (from the repo root): ```bash cd python/samples python chat.py ``` ## Quick Start ```python import asyncio from copilot import CopilotClient from copilot.session_events import AssistantMessageData, SessionIdleData from copilot.session import PermissionHandler async def main(): # Client automatically starts on enter and cleans up on exit async with CopilotClient() as client: # Create a session with automatic cleanup async with await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", ) as session: # Wait for response using session.idle event done = asyncio.Event() def on_event(event): match event.data: case AssistantMessageData() as data: print(data.content) case SessionIdleData(): done.set() session.on(on_event) # Send a message and wait for completion await session.send("What is 2+2?") await done.wait() asyncio.run(main()) ``` ### Manual Resource Management If you need more control over the lifecycle, you can call `start()`, `stop()`, and `disconnect()` manually: ```python import asyncio from copilot import CopilotClient from copilot.session_events import AssistantMessageData, SessionIdleData from copilot.session import PermissionHandler async def main(): client = CopilotClient() await client.start() # Create a session (on_permission_request is optional; approve_all allows every tool) session = await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", ) done = asyncio.Event() def on_event(event): match event.data: case AssistantMessageData() as data: print(data.content) case SessionIdleData(): done.set() session.on(on_event) await session.send("What is 2+2?") await done.wait() # Clean up manually await session.disconnect() await client.stop() asyncio.run(main()) ``` ## Features - ✅ Full JSON-RPC protocol support - ✅ stdio and TCP transports - ✅ Real-time streaming events - ✅ Session history with `get_events()` - ✅ Type hints throughout - ✅ Async/await native - ✅ Async context manager support for automatic resource cleanup ## API Reference ### CopilotClient ```python from copilot import CopilotClient from copilot.session import PermissionHandler async with CopilotClient() as client: async with await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", ) as session: def on_event(event): print(f"Event: {event.type}") session.on(on_event) await session.send("Hello!") # ... wait for events ... ``` > **Note:** For manual lifecycle management, see [Manual Resource Management](#manual-resource-management) above. ```python from copilot import CopilotClient, RuntimeConnection # Connect to an existing CLI server client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:3000")) ``` **CopilotClient Constructor:** ```python CopilotClient() # spawn the bundled runtime with defaults CopilotClient(connection=..., log_level="debug", github_token=..., ...) ``` All options are kw-only parameters: - `connection` (RuntimeConnection | None): How to reach the runtime. Use `RuntimeConnection.for_stdio(...)`, `RuntimeConnection.for_tcp(...)`, or `RuntimeConnection.for_uri(...)`. Defaults to a stdio connection with the bundled binary. - `working_directory` (str | None): Working directory for the CLI process (default: current dir). - `log_level` (str): Log level (default: "info"). - `env` (dict | None): Environment variables for the CLI process. - `github_token` (str | None): GitHub token for authentication. When provided, takes priority over other auth methods. - `base_directory` (str | None): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When `None`, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using a `UriRuntimeConnection`. - `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). - `telemetry` (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. - `session_fs` (dict | None): Connection-level session filesystem provider configuration. - `session_idle_timeout_seconds` (int | None): Server-wide session idle timeout in seconds. Set to `None` or `0` to disable. - `enable_remote_sessions` (bool): Enable remote/cloud session support (default: False). - `on_list_models` (callable | None): Custom handler for `list_models()`. When provided, the handler is called instead of querying the runtime. - `mode` (str): Client mode (default: `"copilot-cli"`). **RuntimeConnection variants:** - `RuntimeConnection.for_stdio(path=None, args=None)` — spawn a local CLI process and talk over stdio. - `RuntimeConnection.for_tcp(port=0, connection_token=None, path=None, args=None)` — spawn a local CLI in TCP mode. - `RuntimeConnection.for_uri(url, connection_token=None)` — connect to an existing CLI server (e.g. `"localhost:8080"`). **`CopilotClient.create_session()`:** These are passed as keyword arguments to `create_session()`: - `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** - `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. Tools with `handler=None` are declaration-only and must be resolved via pending tool-call RPCs. - `system_message` (SystemMessageConfig): System message configuration - `streaming` (bool): Enable streaming delta events - `provider` (ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. - `infinite_sessions` (InfiniteSessionConfig): Automatic context compaction configuration - `on_permission_request` (callable): Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `hooks` (SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. **Session Lifecycle Methods:** ```python # Get the session currently displayed in TUI (TUI+server mode only) session_id = await client.get_foreground_session_id() # Request TUI to display a specific session (TUI+server mode only) await client.set_foreground_session_id("session-123") # Subscribe to all lifecycle events def on_lifecycle(event): print(f"{event.type}: {event.session_id}") unsubscribe = client.on_lifecycle(on_lifecycle) # Subscribe to specific event type unsubscribe = client.on_lifecycle("session.foreground", lambda e: print(f"Foreground: {e.session_id}")) # Later, to stop receiving events: unsubscribe() ``` **Lifecycle Event Types:** - `session.created` - A new session was created - `session.deleted` - A session was deleted - `session.updated` - A session was updated - `session.foreground` - A session became the foreground session in TUI - `session.background` - A session is no longer the foreground session ### 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 class LookupIssueParams(BaseModel): id: str = Field(description="Issue identifier") @define_tool(description="Fetch issue details from our tracker") async def lookup_issue(params: LookupIssueParams) -> str: issue = await fetch_issue(params.id) return issue.summary async with await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", tools=[lookup_issue], ) as session: ... ``` > **Note:** When using `from __future__ import annotations`, define Pydantic models at module level (not inside functions). **Low-level API (without Pydantic):** For users who prefer manual schema definition: ```python from copilot import CopilotClient from copilot.tools import Tool, ToolInvocation, ToolResult from copilot.session import PermissionHandler async def lookup_issue(invocation: ToolInvocation) -> ToolResult: issue_id = invocation.arguments["id"] issue = await fetch_issue(issue_id) return ToolResult( text_result_for_llm=issue.summary, result_type="success", session_log=f"Fetched issue {issue_id}", ) async with 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", parameters={ "type": "object", "properties": { "id": {"type": "string", "description": "Issue identifier"}, }, "required": ["id"], }, handler=lookup_issue, ) ], ) as session: ... ``` The SDK automatically handles `tool.call`, executes your handler (sync or async), and responds with the final result when the tool completes. If a tool has no handler, it is exposed as a declaration only; observe `external_tool.requested` events and resolve the call with the pending tool RPC. You can also create a declaration-only tool with generated Pydantic parameters: ```python tool = define_tool( "lookup_issue", description="Fetch issue details from our tracker", params_type=LookupIssueParams, ) ``` #### Overriding Built-in Tools If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the SDK will throw an error unless you explicitly opt in by setting `overrides_built_in_tool=True`. This flag signals that you intend to replace the built-in tool with your custom implementation. ```python class EditFileParams(BaseModel): path: str = Field(description="File path") content: str = Field(description="New file content") @define_tool(name="edit_file", description="Custom file editor with project-specific validation", overrides_built_in_tool=True) 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, or by passing base64-encoded data directly using a blob attachment: ```python # 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("What does the most recent jpg in this directory portray?") ``` ## Streaming Enable streaming to receive assistant response chunks as they're generated: ```python import asyncio from copilot import CopilotClient from copilot.session_events import ( AssistantMessageData, AssistantMessageDeltaData, AssistantReasoningData, AssistantReasoningDeltaData, SessionIdleData, ) from copilot.session import PermissionHandler async def main(): async with CopilotClient() as client: async with await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", streaming=True, ) as session: # Use asyncio.Event to wait for completion done = asyncio.Event() def on_event(event): match event.data: case AssistantMessageDeltaData() as data: # Streaming message chunk - print incrementally delta = data.delta_content or "" print(delta, end="", flush=True) case AssistantReasoningDeltaData() as data: # Streaming reasoning chunk (if model supports reasoning) delta = data.delta_content or "" print(delta, end="", flush=True) case AssistantMessageData() as data: # Final message - complete content print("\n--- Final message ---") print(data.content) case AssistantReasoningData() as data: # Final reasoning content (if model supports reasoning) print("--- Reasoning ---") print(data.content) case SessionIdleData(): # Session finished processing done.set() session.on(on_event) await session.send("Tell me a short story") await done.wait() # Wait for streaming to complete asyncio.run(main()) ``` When `streaming=True`: - `assistant.message_delta` events are sent with `delta_content` containing incremental text - `assistant.reasoning_delta` events are sent with `delta_content` for reasoning/chain-of-thought (model-dependent) - Accumulate `delta_content` values to build the full response progressively - The final `assistant.message` and `assistant.reasoning` events contain the complete content Note: `assistant.message` and `assistant.reasoning` (final events) are always sent regardless of streaming setting. ## Infinite Sessions By default, sessions use **infinite sessions** which automatically manage context window limits through background compaction and persist state to a workspace directory. ```python # Default: infinite sessions enabled with default thresholds async with await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", ) as session: # Access the workspace path for checkpoints and files print(session.workspace_path) # => ~/.copilot/session-state/{session_id}/ # Custom thresholds async with 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 }, ) as session: ... # Disable infinite sessions async with await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", infinite_sessions={"enabled": False}, ) as session: ... ``` When enabled, sessions emit compaction events: - `session.compaction_start` - Background compaction started - `session.compaction_complete` - Compaction finished (includes token counts) ## Custom Providers The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own Key), including local providers like Ollama. When using a custom provider, you must specify the `model` explicitly. **ProviderConfig fields:** - `type` (str): Provider type - `"openai"`, `"azure"`, or `"anthropic"` (default: `"openai"`) - `base_url` (str): API endpoint URL (required) - `api_key` (str): API key (optional for local providers like Ollama) - `bearer_token` (str): Bearer token for authentication (takes precedence over `api_key`) - `wire_api` (str): API format for OpenAI/Azure - `"completions"` or `"responses"` (default: `"completions"`) - `azure` (dict): Azure-specific options with `api_version` (default: `"2024-10-21"`) **Example with Ollama:** ```python async with await client.create_session( on_permission_request=PermissionHandler.approve_all, model="deepseek-coder-v2:16b", # Required when using custom provider provider={ "type": "openai", "base_url": "http://localhost:11434/v1", # Ollama endpoint # api_key not required for Ollama }, ) as session: await session.send("Hello!") ``` **Example with custom OpenAI-compatible API:** ```python import os async with 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"], }, ) as session: ... ``` **Example with Azure OpenAI:** ```python import os async with 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"], "azure": { "api_version": "2024-10-21", }, }, ) as session: ... ``` > **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 client = CopilotClient( 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 "github-copilot-sdk[telemetry]"` (provides `opentelemetry-api`) ## Permission Handling An `on_permission_request` handler is optional when you create or resume a session. When provided, it is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and returns a decision. When omitted, permission requests are emitted as events and left pending for the consumer to resolve with the pending permission RPC. ### Approve All (simplest) Use the built-in `PermissionHandler.approve_all` helper to allow every tool call without any checks: ```python from copilot import CopilotClient from copilot.session import PermissionHandler session = await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", ) ``` ### Custom Permission Handler Provide your own function to inspect each request and apply custom logic (sync or async): ```python from copilot import PermissionRequest, PermissionRequestResult from copilot.rpc import ( PermissionDecisionApproveOnce, PermissionDecisionReject, ) from copilot.session_events import PermissionRequestShell def on_permission_request( request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: # ``PermissionRequest`` is a discriminated union — pattern-match on # the variant class to access the per-kind fields. match request: case PermissionRequestShell(full_command_text=cmd): # Deny shell commands return PermissionDecisionReject(feedback=f"Shell denied: {cmd}") case _: return PermissionDecisionApproveOnce() session = await client.create_session( on_permission_request=on_permission_request, model="gpt-5", ) ``` 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 PermissionDecisionApproveOnce() ``` ### Permission Result Kinds The handler returns a ``PermissionRequestResult``, which is an alias for ``PermissionDecision | PermissionNoResult`` (the generated wire-level union of every decision variant, plus a small sentinel for v1 servers). Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. | Variant | Meaning | | --------------------------------------------- | ------------------------------------------------------------------------------------------- | | `PermissionDecisionApproveOnce()` | Allow this single request | | `PermissionDecisionReject(feedback="…")` | Deny the request (optional feedback string forwarded to the LLM) | | `PermissionDecisionUserNotAvailable()` | Deny the request because no user is available to confirm it (the default) | | `PermissionNoResult()` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | Several richer variants (``PermissionDecisionApproveForSession``, ``PermissionDecisionApproveForLocation``, ``PermissionDecisionApprovePermanently``, …) are available for granting longer-lived approvals; see the generated ``copilot.rpc`` module for the full list. ### Resuming Sessions You may pass `on_permission_request` when resuming a session too: ```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: ```python async def handle_user_input(request, invocation): # request["question"] - The question to ask # request.get("choices") - Optional list of choices for multiple choice # request.get("allowFreeform", True) - Whether freeform input is allowed print(f"Agent asks: {request['question']}") if request.get("choices"): print(f"Choices: {', '.join(request['choices'])}") # Return the user's response return { "answer": "User's answer here", "wasFreeform": True, # Whether the answer was freeform (not from choices) } async with await client.create_session( on_permission_request=PermissionHandler.approve_all, model="gpt-5", on_user_input_request=handle_user_input, ) as session: ... ``` ## Session Hooks Hook into session lifecycle events by providing handlers in the `hooks` configuration: ```python async def on_pre_tool_use(input, invocation): print(f"About to run tool: {input['toolName']}") # Return permission decision and optionally modify args return { "permissionDecision": "allow", # "allow", "deny", or "ask" "modifiedArgs": input.get("toolArgs"), # Optionally modify tool arguments "additionalContext": "Extra context for the model", } async def on_post_tool_use(input, invocation): print(f"Tool {input['toolName']} completed") return { "additionalContext": "Post-execution notes", } async def on_post_tool_use_failure(input, invocation): # Fires when a tool's result was a failure. `on_post_tool_use` only fires # on success, so register this handler to observe failed tool calls. The # CLI extracts the failure message and passes it as the `error` field. print(f"Tool {input['toolName']} failed: {input['error']}") return { "additionalContext": f"Retry guidance for {input['toolName']}", } async def on_user_prompt_submitted(input, invocation): print(f"User prompt: {input['prompt']}") return { "modifiedPrompt": input["prompt"], # Optionally modify the prompt } async def on_session_start(input, invocation): print(f"Session started from: {input['source']}") # "startup", "resume", "new" return { "additionalContext": "Session initialization context", } async def on_session_end(input, invocation): print(f"Session ended: {input['reason']}") async def on_error_occurred(input, invocation): print(f"Error in {input['errorContext']}: {input['error']}") return { "errorHandling": "retry", # "retry", "skip", or "abort" } async with 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_post_tool_use_failure": on_post_tool_use_failure, "on_user_prompt_submitted": on_user_prompt_submitted, "on_session_start": on_session_start, "on_session_end": on_session_end, "on_error_occurred": on_error_occurred, }, ) as session: ... ``` **Available hooks:** - `on_pre_tool_use` - Intercept tool calls before execution. Can allow/deny or modify arguments. - `on_post_tool_use` - Process tool results after successful execution. Can modify results or add context. - `on_post_tool_use_failure` - Observe failed tool executions and inject extra context to guide the model's next step. - `on_user_prompt_submitted` - Intercept user prompts. Can modify the prompt before processing. - `on_session_start` - Run logic when a session starts or resumes. - `on_session_end` - Cleanup or logging when session ends. - `on_error_occurred` - Handle errors with retry/skip/abort strategies. ## Commands Register slash commands that users can invoke from the CLI TUI. When the user types `/commandName`, the SDK dispatches the event to your handler. ```python from copilot.session import CommandDefinition, CommandContext, PermissionHandler async def handle_deploy(ctx: CommandContext) -> None: print(f"Deploying with args: {ctx.args}") # ctx.session_id — the session where the command was invoked # ctx.command — full command text (e.g. "/deploy production") # ctx.command_name — command name without leading / (e.g. "deploy") # ctx.args — raw argument string (e.g. "production") async with await client.create_session( on_permission_request=PermissionHandler.approve_all, commands=[ CommandDefinition( name="deploy", description="Deploy the app", handler=handle_deploy, ), CommandDefinition( name="rollback", description="Rollback to previous version", handler=lambda ctx: print("Rolling back..."), ), ], ) as session: ... ``` Commands can also be provided when resuming a session via `resume_session(commands=[...])`. ## UI Elicitation The `session.ui` API provides convenience methods for asking the user questions through interactive dialogs. These methods are only available when the CLI host supports elicitation — check `session.capabilities` before calling. ### Capability Check ```python ui_caps = session.capabilities.get("ui", {}) if ui_caps.get("elicitation"): # Safe to call session.ui methods ... ``` ### Confirm Shows a yes/no confirmation dialog: ```python ok = await session.ui.confirm("Deploy to production?") if ok: print("Deploying...") ``` ### Select Shows a selection dialog with a list of options: ```python env = await session.ui.select("Choose environment:", ["staging", "production", "dev"]) if env: print(f"Selected: {env}") ``` ### Input Shows a text input dialog with optional constraints: ```python name = await session.ui.input("Enter your name:") # With options email = await session.ui.input("Enter email:", { "title": "Email Address", "description": "We'll use this for notifications", "format": "email", }) ``` ### Custom Elicitation For full control, use the `elicitation()` method with a custom JSON schema: ```python result = await session.ui.elicitation({ "message": "Configure deployment", "requestedSchema": { "type": "object", "properties": { "region": {"type": "string", "enum": ["us-east-1", "eu-west-1"]}, "replicas": {"type": "number", "minimum": 1, "maximum": 10}, }, "required": ["region"], }, }) if result["action"] == "accept": region = result["content"]["region"] replicas = result["content"].get("replicas", 1) ``` ## Elicitation Request Handler When the server (or an MCP tool) needs to ask the end-user a question, it sends an `elicitation.requested` event. Provide an `on_elicitation_request` handler to respond: ```python from copilot.session import ElicitationContext, ElicitationResult, PermissionHandler async def handle_elicitation( context: ElicitationContext, ) -> ElicitationResult: # context["session_id"] — the session ID # context["message"] — what the server is asking # context.get("requestedSchema") — optional JSON schema for form fields # context.get("mode") — "form" or "url" print(f"Server asks: {context['message']}") # Return the user's response return { "action": "accept", # or "decline" or "cancel" "content": {"answer": "yes"}, } async with await client.create_session( on_permission_request=PermissionHandler.approve_all, on_elicitation_request=handle_elicitation, ) as session: ... ``` When `on_elicitation_request` is provided, the SDK automatically: - Sends `requestElicitation: true` to the server during session creation/resumption - Reports the `elicitation` capability on the session - Dispatches `elicitation.requested` events to your handler - Auto-cancels if your handler throws an error (so the server doesn't hang) ## Requirements - Python 3.11+ - GitHub Copilot CLI installed and accessible