| name | copilot-python-sdk |
| description | Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC |
| author | Tim Sonner |
| license | MIT |
| compatibility | opencode |
| metadata | {"audience":"developers","workflow":"sdk-usage","language":"python","type":"sdk"} |
Copilot Python SDK
Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC.
Note: This SDK is in technical preview and may change in breaking ways.
Installation
pip install -e ".[telemetry,dev]"
uv pip install -e ".[telemetry,dev]"
Run the Sample
Try the interactive chat sample (from the repo root):
cd python/samples
python chat.py
Quick Start
import asyncio
from copilot import CopilotClient
from copilot.session import PermissionHandler
async def main():
async with CopilotClient() as client:
async with await client.create_session(model="gpt-5") as session:
done = asyncio.Event()
def on_event(event):
if event.type.value == "assistant.message":
print(event.data.content)
elif event.type.value == "session.idle":
done.set()
session.on(on_event)
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:
import asyncio
from copilot import CopilotClient
from copilot.session import PermissionHandler
async def main():
client = CopilotClient()
await client.start()
session = await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
)
done = asyncio.Event()
def on_event(event):
if event.type.value == "assistant.message":
print(event.data.content)
elif event.type.value == "session.idle":
done.set()
session.on(on_event)
await session.send("What is 2+2?")
await done.wait()
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_messages()
- ✅ Type hints throughout
- ✅ Async/await native
- ✅ Async context manager support for automatic resource cleanup
API Reference
CopilotClient
from copilot import CopilotClient, SubprocessConfig
from copilot.session import PermissionHandler
async with CopilotClient() as client:
async with await client.create_session(model="gpt-5") as session:
def on_event(event):
print(f"Event: {event.type}")
session.on(on_event)
await session.send("Hello!")
Note: For manual lifecycle management, see Manual Resource Management above.
from copilot import CopilotClient, ExternalServerConfig
client = CopilotClient(ExternalServerConfig(url="localhost:3000"))
CopilotClient Constructor:
CopilotClient(
config=None,
*,
auto_start=True,
on_list_models=None,
)
SubprocessConfig — spawn a local CLI process:
cli_path (str | None): Path to CLI executable (default: COPILOT_CLI_PATH env var, or bundled binary)
cli_args (list[str]): Extra arguments for the CLI executable
cwd (str | None): Working directory for CLI process (default: current dir)
use_stdio (bool): Use stdio transport instead of TCP (default: True)
port (int): Server port for TCP mode (default: 0 for random)
log_level (str): Log level (default: "info")
env (dict | None): Environment variables for the CLI process
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 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").
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
system_message (SystemMessageConfig): System message configuration
streaming (bool): Enable streaming delta events
provider (ProviderConfig): Custom API provider configuration (BYOK). See Custom Providers section.
infinite_sessions (InfiniteSessionConfig): Automatic context compaction configuration
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 section.
on_user_input_request (callable): Handler for user input requests from the agent (enables ask_user tool). See User Input Requests section.
hooks (SessionHooks): Hook handlers for session lifecycle events. See Session Hooks section.
Session Lifecycle Methods:
session_id = await client.get_foreground_session_id()
await client.set_foreground_session_id("session-123")
def on_lifecycle(event):
print(f"{event.type}: {event.sessionId}")
unsubscribe = client.on(on_lifecycle)
unsubscribe = client.on("session.foreground", lambda e: print(f"Foreground: {e.sessionId}"))
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:
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:
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.
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.
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:
Skipping Permission Prompts
Set skip_permission=True on a tool definition to allow it to execute without triggering a permission prompt:
@define_tool(name="safe_lookup", description="A read-only lookup that needs no confirmation", skip_permission=True)
async def safe_lookup(params: LookupParams) -> str:
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:
await session.send(
"What's in this image?",
attachments=[
{
"type": "file",
"path": "/path/to/image.jpg",
}
],
)
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:
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:
import asyncio
from copilot import CopilotClient
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:
done = asyncio.Event()
def on_event(event):
match event.type.value:
case "assistant.message_delta":
delta = event.data.delta_content or ""
print(delta, end="", flush=True)
case "assistant.reasoning_delta":
delta = event.data.delta_content or ""
print(delta, end="", flush=True)
case "assistant.message":
print("\n--- Final message ---")
print(event.data.content)
case "assistant.reasoning":
print("--- Reasoning ---")
print(event.data.content)
case "session.idle":
done.set()
session.on(on_event)
await session.send("Tell me a short story")
await done.wait()
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.
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
) as session:
print(session.workspace_path)
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
infinite_sessions={
"enabled": True,
"background_compaction_threshold": 0.80,
"buffer_exhaustion_threshold": 0.95,
},
) as session:
...
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:
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="deepseek-coder-v2:16b",
provider={
"type": "openai",
"base_url": "http://localhost:11434/v1",
},
) as session:
await session.send("Hello!")
Example with custom OpenAI-compatible API:
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:
import os
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-4",
provider={
"type": "azure",
"base_url": "https://my-resource.openai.azure.com",
"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.
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:
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):
from copilot.session import PermissionRequestResult
from copilot.generated.session_events import PermissionRequest
def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
if request.kind.value == "shell":
return PermissionRequestResult(kind="denied-interactively-by-user")
return PermissionRequestResult(kind="approved")
session = await client.create_session(
on_permission_request=on_permission_request,
model="gpt-5",
)
Async handlers are also supported:
async def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
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:
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 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:
async def handle_user_input(request, invocation):
print(f"Agent asks: {request['question']}")
if request.get("choices"):
print(f"Choices: {', '.join(request['choices'])}")
return {
"answer": "User's answer here",
"wasFreeform": True,
}
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:
async def on_pre_tool_use(input, invocation):
print(f"About to run tool: {input['toolName']}")
return {
"permissionDecision": "allow",
"modifiedArgs": input.get("toolArgs"),
"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_user_prompt_submitted(input, invocation):
print(f"User prompt: {input['prompt']}")
return {
"modifiedPrompt": input["prompt"],
}
async def on_session_start(input, invocation):
print(f"Session started from: {input['source']}")
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",
}
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_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 execution. Can modify results or add context.
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.
Requirements
- Python 3.11+
- GitHub Copilot CLI installed and accessible