copilot-sdk
Use this skill whenever the user wants to build Python applications or scripts using the GitHub Copilot SDK. Triggers include: building Copilot-powered agents or tools in Python, integrating GitHub Co
SKILL.mdskills/copilot-sdk/SKILL.md
GitHubGitHub Copilot Python SDK
Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC.
Requirements: Python 3.11+, GitHub Copilot CLI installed and accessible.
Installation
pip install -e ".[telemetry,dev]"
# or
uv pip install -e ".[telemetry,dev]"
Core Pattern: CopilotClient + Session
Always prefer async context managers — they handle startup and cleanup automatically.
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",
) 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("Hello!")
await done.wait()
asyncio.run(main())
Key rule: on_permission_request is required in create_session() and resume_session(). Use PermissionHandler.approve_all for dev/trusted environments; write a custom handler for production.
create_session() Parameters
| Parameter | Type | Notes |
|---|---|---|
model |
str | Required with custom providers. e.g. "gpt-5", "claude-sonnet-4.5" |
on_permission_request |
callable | Required. Sync or async. |
on_user_input_request |
callable | Enables ask_user tool |
tools |
list | Custom tool definitions |
system_message |
SystemMessageConfig | System prompt config |
streaming |
bool | Enable delta streaming events |
provider |
ProviderConfig | BYOK / custom LLM provider |
infinite_sessions |
InfiniteSessionConfig | Context compaction config |
hooks |
SessionHooks | Lifecycle hook handlers |
commands |
list[CommandDefinition] | Slash commands for TUI |
session_id |
str | Custom session ID |
Defining Custom Tools
With Pydantic (recommended)
from pydantic import BaseModel, Field
from copilot import define_tool
class SearchParams(BaseModel):
query: str = Field(description="Search query string")
limit: int = Field(default=10, description="Max results")
@define_tool(description="Search the internal knowledge base")
async def search_kb(params: SearchParams) -> str:
results = await db.search(params.query, limit=params.limit)
return "\n".join(results)
# Pass to create_session:
# tools=[search_kb]
Note: When using from __future__ import annotations, define Pydantic models at module level.
Override Built-in Tools
@define_tool(name="edit_file", description="Custom editor", overrides_built_in_tool=True)
async def edit_file(params: EditFileParams) -> str: ...
Skip Permission Prompt
@define_tool(name="safe_read", description="Read-only lookup", skip_permission=True)
async def safe_read(params: ReadParams) -> str: ...
Permission Handling
from copilot.session import PermissionRequestResult
from copilot.generated.session_events import PermissionRequest
def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
# request.kind.value: "shell" | "write" | "read" | "mcp" | "custom-tool" | "url" | "memory" | "hook"
# request.tool_name, request.file_name, request.full_command_text
if request.kind.value == "shell":
return PermissionRequestResult(kind="denied-interactively-by-user")
return PermissionRequestResult(kind="approved")
Permission result kinds: "approved", "denied-interactively-by-user", "denied-by-rules", "denied-by-content-exclusion-policy", "no-result".
Streaming
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":
print(event.data.delta_content or "", end="", flush=True)
case "assistant.message":
print() # newline after streaming ends
case "session.idle":
done.set()
session.on(on_event)
await session.send("Tell me a story")
await done.wait()
Custom Providers (BYOK)
When using a custom provider, model is required.
# Ollama (local)
provider={"type": "openai", "base_url": "http://localhost:11434/v1"}
# Custom OpenAI-compatible
provider={"type": "openai", "base_url": "https://my-api.example.com/v1", "api_key": os.environ["MY_KEY"]}
# Azure OpenAI — MUST use type "azure", NOT "openai"
provider={
"type": "azure",
"base_url": "https://my-resource.openai.azure.com", # host only, no path
"api_key": os.environ["AZURE_KEY"],
"azure": {"api_version": "2024-10-21"},
}
Azure pitfall: Use
"type": "azure"for*.openai.azure.comendpoints. Using"openai"will fail.
Session Hooks
async def on_pre_tool_use(input, invocation):
# input["toolName"], input["toolArgs"]
return {"permissionDecision": "allow", "modifiedArgs": input.get("toolArgs")}
async def on_error_occurred(input, invocation):
# input["error"], input["errorContext"]
return {"errorHandling": "retry"} # "retry" | "skip" | "abort"
# In create_session:
hooks={
"on_pre_tool_use": on_pre_tool_use,
"on_post_tool_use": ...,
"on_user_prompt_submitted": ...,
"on_session_start": ...,
"on_session_end": ...,
"on_error_occurred": on_error_occurred,
}
Image Attachments
# From file path
await session.send("Describe this image", attachments=[{"type": "file", "path": "/path/to/image.jpg"}])
# From base64
await session.send("Describe this", attachments=[{"type": "blob", "data": b64_data, "mimeType": "image/png"}])
Infinite Sessions (Context Compaction)
# Default: enabled. Customize thresholds:
infinite_sessions={
"enabled": True,
"background_compaction_threshold": 0.80,
"buffer_exhaustion_threshold": 0.95,
}
# Disable:
infinite_sessions={"enabled": False}
Emits session.compaction_start and session.compaction_complete events.
UI Elicitation
Only available when session.capabilities["ui"]["elicitation"] is truthy.
ui_caps = session.capabilities.get("ui", {})
if ui_caps.get("elicitation"):
ok = await session.ui.confirm("Deploy to production?")
env = await session.ui.select("Choose env:", ["staging", "production"])
name = await session.ui.input("Enter name:", {"format": "email"})
Telemetry (OpenTelemetry)
from copilot import CopilotClient, SubprocessConfig
client = CopilotClient(SubprocessConfig(
telemetry={"otlp_endpoint": "http://localhost:4318"}
))
Install extras: pip install copilot-sdk[telemetry]
Manual Lifecycle Management
Use only when you need fine-grained control:
client = CopilotClient()
await client.start()
session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5")
# ... use session ...
await session.disconnect()
await client.stop()
SubprocessConfig Options
from copilot import CopilotClient, SubprocessConfig
client = CopilotClient(SubprocessConfig(
cli_path=None, # path to CLI binary or COPILOT_CLI_PATH env var
github_token="ghp_...", # takes priority over other auth methods
use_stdio=True, # stdio vs TCP transport
log_level="info",
env={"MY_VAR": "value"},
))
Common Event Types
| Event | When |
|---|---|
assistant.message |
Full response ready |
assistant.message_delta |
Streaming chunk (requires streaming=True) |
assistant.reasoning / _delta |
Chain-of-thought (model-dependent) |
session.idle |
Session done processing — use to signal completion |
session.compaction_start/complete |
Context compaction lifecycle |
session.created/deleted/updated |
Lifecycle (client-level) |
For detailed API reference, see the full README: https://github.com/github/copilot-sdk/blob/main/python/README.md