{"kind":"AgentDefinition","metadata":{"namespace":"community","name":"copilot-sdk-python","version":"0.1.0"},"spec":{"agents_md":"---\napplyTo: \"**.py, pyproject.toml, setup.py\"\ndescription: \"This file provides guidance on building Python applications using GitHub Copilot SDK.\"\nname: \"GitHub Copilot SDK Python Instructions\"\n---\n\n## Core Principles\n\n- The SDK is in technical preview and may have breaking changes\n- Requires Python 3.9 or later\n- Requires GitHub Copilot CLI installed and in PATH\n- Uses async/await patterns throughout (asyncio)\n- Supports both async context managers and manual lifecycle management\n- Type hints provided for better IDE support\n\n## Installation\n\nAlways install via pip:\n\n```bash\npip install github-copilot-sdk\n# or with poetry\npoetry add github-copilot-sdk\n# or with uv\nuv add github-copilot-sdk\n```\n\n## Client Initialization\n\n### Basic Client Setup\n\n```python\nfrom copilot import CopilotClient, PermissionHandler\nimport asyncio\n\nasync def main():\n    async with CopilotClient() as client:\n        # Use client...\n        pass\n\nasyncio.run(main())\n```\n\n### Client Configuration Options\n\nWhen creating a CopilotClient, use a dict with these keys:\n\n- `cli_path` - Path to CLI executable (default: \"copilot\" from PATH or COPILOT_CLI_PATH env var)\n- `cli_url` - URL of existing CLI server (e.g., \"localhost:8080\"). When provided, client won't spawn a process\n- `port` - Server port (default: 0 for random)\n- `use_stdio` - Use stdio transport instead of TCP (default: True)\n- `log_level` - Log level (default: \"info\")\n- `auto_start` - Auto-start server (default: True)\n- `auto_restart` - Auto-restart on crash (default: True)\n- `cwd` - Working directory for the CLI process (default: os.getcwd())\n- `env` - Environment variables for the CLI process (dict)\n\n### Manual Server Control\n\nFor explicit control:\n\n```python\nfrom copilot import CopilotClient\nimport asyncio\n\nasync def main():\n    client = CopilotClient({\"auto_start\": False})\n    await client.start()\n    # Use client...\n    await client.stop()\n\nasyncio.run(main())\n```\n\nUse `force_stop()` when `stop()` takes too long.\n\n## Session Management\n\n### Creating Sessions\n\nUse a dict for SessionConfig:\n\n```python\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"model\": \"gpt-5\",\n    \"streaming\": True,\n    \"tools\": [...],\n    \"system_message\": { ... },\n    \"available_tools\": [\"tool1\", \"tool2\"],\n    \"excluded_tools\": [\"tool3\"],\n    \"provider\": { ... }\n})\n```\n\n### Session Config Options\n\n- `session_id` - Custom session ID (str)\n- `model` - Model name (\"gpt-5\", \"claude-sonnet-4.5\", etc.)\n- `tools` - Custom tools exposed to the CLI (list[Tool])\n- `system_message` - System message customization (dict)\n- `available_tools` - Allowlist of tool names (list[str])\n- `excluded_tools` - Blocklist of tool names (list[str])\n- `provider` - Custom API provider configuration (BYOK) (ProviderConfig)\n- `streaming` - Enable streaming response chunks (bool)\n- `mcp_servers` - MCP server configurations (list)\n- `custom_agents` - Custom agent configurations (list)\n- `config_dir` - Config directory override (str)\n- `skill_directories` - Skill directories (list[str])\n- `disabled_skills` - Disabled skills (list[str])\n- `on_permission_request` - Permission request handler (callable)\n\n### Resuming Sessions\n\n```python\nsession = await client.resume_session(\"session-id\", {\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"tools\": [my_new_tool]\n})\n```\n\n### Session Operations\n\n- `session.session_id` - Get session identifier (str)\n- `await session.send({\"prompt\": \"...\", \"attachments\": [...]})` - Send message, returns str (message ID)\n- `await session.send_and_wait({\"prompt\": \"...\"}, timeout=60.0)` - Send and wait for idle, returns SessionEvent | None\n- `await session.abort()` - Abort current processing\n- `await session.get_messages()` - Get all events/messages, returns list[SessionEvent]\n- `await session.destroy()` - Clean up session\n\n## Event Handling\n\n### Event Subscription Pattern\n\nALWAYS use asyncio events or futures for waiting on session events:\n\n```python\nimport asyncio\n\ndone = asyncio.Event()\n\ndef handler(event):\n    if event.type == \"assistant.message\":\n        print(event.data.content)\n    elif event.type == \"session.idle\":\n        done.set()\n\nsession.on(handler)\nawait session.send({\"prompt\": \"...\"})\nawait done.wait()\n```\n\n### Unsubscribing from Events\n\nThe `on()` method returns a function that unsubscribes:\n\n```python\nunsubscribe = session.on(lambda event: print(event.type))\n# Later...\nunsubscribe()\n```\n\n### Event Types\n\nUse attribute access for event type checking:\n\n```python\ndef handler(event):\n    if event.type == \"user.message\":\n        # Handle user message\n        pass\n    elif event.type == \"assistant.message\":\n        print(event.data.content)\n    elif event.type == \"tool.executionStart\":\n        # Tool execution started\n        pass\n    elif event.type == \"tool.executionComplete\":\n        # Tool execution completed\n        pass\n    elif event.type == \"session.start\":\n        # Session started\n        pass\n    elif event.type == \"session.idle\":\n        # Session is idle (processing complete)\n        pass\n    elif event.type == \"session.error\":\n        print(f\"Error: {event.data.message}\")\n\nsession.on(handler)\n```\n\n## Streaming Responses\n\n### Enabling Streaming\n\nSet `streaming: True` in SessionConfig:\n\n```python\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"model\": \"gpt-5\",\n    \"streaming\": True\n})\n```\n\n### Handling Streaming Events\n\nHandle both delta events (incremental) and final events:\n\n```python\nimport asyncio\n\ndone = asyncio.Event()\n\ndef handler(event):\n    if event.type == \"assistant.message.delta\":\n        # Incremental text chunk\n        print(event.data.delta_content, end=\"\", flush=True)\n    elif event.type == \"assistant.reasoning.delta\":\n        # Incremental reasoning chunk (model-dependent)\n        print(event.data.delta_content, end=\"\", flush=True)\n    elif event.type == \"assistant.message\":\n        # Final complete message\n        print(\"\\n--- Final ---\")\n        print(event.data.content)\n    elif event.type == \"assistant.reasoning\":\n        # Final reasoning content\n        print(\"--- Reasoning ---\")\n        print(event.data.content)\n    elif event.type == \"session.idle\":\n        done.set()\n\nsession.on(handler)\nawait session.send({\"prompt\": \"Tell me a story\"})\nawait done.wait()\n```\n\nNote: Final events (`assistant.message`, `assistant.reasoning`) are ALWAYS sent regardless of streaming setting.\n\n## Custom Tools\n\n### Defining Tools with define_tool\n\nUse `define_tool` for tool definitions:\n\n```python\nfrom copilot import define_tool\n\nasync def fetch_issue(issue_id: str):\n    # Fetch issue from tracker\n    return {\"id\": issue_id, \"status\": \"open\"}\n\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"model\": \"gpt-5\",\n    \"tools\": [\n        define_tool(\n            name=\"lookup_issue\",\n            description=\"Fetch issue details from tracker\",\n            parameters={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"id\": {\"type\": \"string\", \"description\": \"Issue ID\"}\n                },\n                \"required\": [\"id\"]\n            },\n            handler=lambda args, inv: fetch_issue(args[\"id\"])\n        )\n    ]\n})\n```\n\n### Using Pydantic for Parameters\n\nThe SDK works well with Pydantic models:\n\n```python\nfrom pydantic import BaseModel, Field\n\nclass WeatherArgs(BaseModel):\n    location: str = Field(description=\"City name\")\n    units: str = Field(default=\"fahrenheit\", description=\"Temperature units\")\n\nasync def get_weather(args: WeatherArgs, inv):\n    return {\"temperature\": 72, \"units\": args.units}\n\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"tools\": [\n        define_tool(\n            name=\"get_weather\",\n            description=\"Get weather for a location\",\n            parameters=WeatherArgs.model_json_schema(),\n            handler=lambda args, inv: get_weather(WeatherArgs(**args), inv)\n        )\n    ]\n})\n```\n\n### Tool Return Types\n\n- Return any JSON-serializable value (automatically wrapped)\n- Or return a ToolResult dict for full control:\n\n```python\n{\n    \"text_result_for_llm\": str,  # Result shown to LLM\n    \"result_type\": \"success\" | \"failure\",\n    \"error\": str,  # Optional: Internal error (not shown to LLM)\n    \"tool_telemetry\": dict  # Optional: Telemetry data\n}\n```\n\n### Tool Handler Signature\n\nTool handlers receive two arguments:\n\n- `args` (dict) - The tool arguments passed by the LLM\n- `invocation` (ToolInvocation) - Metadata about the invocation\n  - `invocation.session_id` - Session ID\n  - `invocation.tool_call_id` - Tool call ID\n  - `invocation.tool_name` - Tool name\n  - `invocation.arguments` - Same as args parameter\n\n### Tool Execution Flow\n\nWhen Copilot invokes a tool, the client automatically:\n\n1. Runs your handler function\n2. Serializes the return value\n3. Responds to the CLI\n\n## System Message Customization\n\n### Append Mode (Default - Preserves Guardrails)\n\n```python\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"model\": \"gpt-5\",\n    \"system_message\": {\n        \"mode\": \"append\",\n        \"content\": \"\"\"\n\u003cworkflow_rules\u003e\n- Always check for security vulnerabilities\n- Suggest performance improvements when applicable\n\u003c/workflow_rules\u003e\n\"\"\"\n    }\n})\n```\n\n### Replace Mode (Full Control - Removes Guardrails)\n\n```python\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"model\": \"gpt-5\",\n    \"system_message\": {\n        \"mode\": \"replace\",\n        \"content\": \"You are a helpful assistant.\"\n    }\n})\n```\n\n## File Attachments\n\nAttach files to messages:\n\n```python\nawait session.send({\n    \"prompt\": \"Analyze this file\",\n    \"attachments\": [\n        {\n            \"type\": \"file\",\n            \"path\": \"/path/to/file.py\",\n            \"display_name\": \"My File\"\n        }\n    ]\n})\n```\n\n## Message Delivery Modes\n\nUse the `mode` key in message options:\n\n- `\"enqueue\"` - Queue message for processing\n- `\"immediate\"` - Process message immediately\n\n```python\nawait session.send({\n    \"prompt\": \"...\",\n    \"mode\": \"enqueue\"\n})\n```\n\n## Multiple Sessions\n\nSessions are independent and can run concurrently:\n\n```python\nsession1 = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"model\": \"gpt-5\",\n})\nsession2 = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"model\": \"claude-sonnet-4.5\",\n})\n\nawait asyncio.gather(\n    session1.send({\"prompt\": \"Hello from session 1\"}),\n    session2.send({\"prompt\": \"Hello from session 2\"})\n)\n```\n\n## Bring Your Own Key (BYOK)\n\nUse custom API providers via `provider`:\n\n```python\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"provider\": {\n        \"type\": \"openai\",\n        \"base_url\": \"https://api.openai.com/v1\",\n        \"api_key\": \"your-api-key\"\n    }\n})\n```\n\n## Session Lifecycle Management\n\n### Listing Sessions\n\n```python\nsessions = await client.list_sessions()\nfor metadata in sessions:\n    print(f\"{metadata.session_id}: {metadata.summary}\")\n```\n\n### Deleting Sessions\n\n```python\nawait client.delete_session(session_id)\n```\n\n### Getting Last Session ID\n\n```python\nlast_id = await client.get_last_session_id()\nif last_id:\n    session = await client.resume_session(last_id, on_permission_request=PermissionHandler.approve_all)\n```\n\n### Checking Connection State\n\n```python\nstate = client.get_state()\n# Returns: \"disconnected\" | \"connecting\" | \"connected\" | \"error\"\n```\n\n## Error Handling\n\n### Standard Exception Handling\n\n```python\ntry:\n    session = await client.create_session(on_permission_request=PermissionHandler.approve_all)\n    await session.send({\"prompt\": \"Hello\"})\nexcept Exception as e:\n    print(f\"Error: {e}\")\n```\n\n### Session Error Events\n\nMonitor `session.error` event type for runtime errors:\n\n```python\ndef handler(event):\n    if event.type == \"session.error\":\n        print(f\"Session Error: {event.data.message}\")\n\nsession.on(handler)\n```\n\n## Connectivity Testing\n\nUse ping to verify server connectivity:\n\n```python\nresponse = await client.ping(\"health check\")\nprint(f\"Server responded at {response['timestamp']}\")\n```\n\n## Resource Cleanup\n\n### Automatic Cleanup with Context Managers\n\nALWAYS use async context managers for automatic cleanup:\n\n```python\nasync with CopilotClient() as client:\n    async with await client.create_session(on_permission_request=PermissionHandler.approve_all) as session:\n        # Use session...\n        await session.send({\"prompt\": \"Hello\"})\n    # Session automatically destroyed\n# Client automatically stopped\n```\n\n### Manual Cleanup with Try-Finally\n\n```python\nclient = CopilotClient()\ntry:\n    await client.start()\n    session = await client.create_session(on_permission_request=PermissionHandler.approve_all)\n    try:\n        # Use session...\n        pass\n    finally:\n        await session.destroy()\nfinally:\n    await client.stop()\n```\n\n## Best Practices\n\n1. **Always use async context managers** (`async with`) for automatic cleanup\n2. **Use asyncio.Event or asyncio.Future** to wait for session.idle event\n3. **Handle session.error** events for robust error handling\n4. **Use if/elif chains** for event type checking\n5. **Enable streaming** for better UX in interactive scenarios\n6. **Use define_tool** for tool definitions\n7. **Use Pydantic models** for type-safe parameter validation\n8. **Dispose event subscriptions** when no longer needed\n9. **Use system_message with mode: \"append\"** to preserve safety guardrails\n10. **Handle both delta and final events** when streaming is enabled\n11. **Use type hints** for better IDE support and code clarity\n\n## Common Patterns\n\n### Simple Query-Response\n\n```python\nfrom copilot import CopilotClient, PermissionHandler\nimport asyncio\n\nasync def main():\n    async with CopilotClient() as client:\n        async with await client.create_session({\n            \"on_permission_request\": PermissionHandler.approve_all,\n            \"model\": \"gpt-5\",\n        }) as session:\n            done = asyncio.Event()\n\n            def handler(event):\n                if event.type == \"assistant.message\":\n                    print(event.data.content)\n                elif event.type == \"session.idle\":\n                    done.set()\n\n            session.on(handler)\n            await session.send({\"prompt\": \"What is 2+2?\"})\n            await done.wait()\n\nasyncio.run(main())\n```\n\n### Multi-Turn Conversation\n\n```python\nasync def send_and_wait(session, prompt: str):\n    done = asyncio.Event()\n    result = []\n\n    def handler(event):\n        if event.type == \"assistant.message\":\n            result.append(event.data.content)\n            print(event.data.content)\n        elif event.type == \"session.idle\":\n            done.set()\n        elif event.type == \"session.error\":\n            result.append(None)\n            done.set()\n\n    unsubscribe = session.on(handler)\n    await session.send({\"prompt\": prompt})\n    await done.wait()\n    unsubscribe()\n\n    return result[0] if result else None\n\nasync with await client.create_session(on_permission_request=PermissionHandler.approve_all) as session:\n    await send_and_wait(session, \"What is the capital of France?\")\n    await send_and_wait(session, \"What is its population?\")\n```\n\n### SendAndWait Helper\n\n```python\n# Use built-in send_and_wait for simpler synchronous interaction\nasync with await client.create_session(on_permission_request=PermissionHandler.approve_all) as session:\n    response = await session.send_and_wait(\n        {\"prompt\": \"What is 2+2?\"},\n        timeout=60.0\n    )\n\n    if response and response.type == \"assistant.message\":\n        print(response.data.content)\n```\n\n### Tool with Dataclass Return Type\n\n```python\nfrom dataclasses import dataclass, asdict\nfrom copilot import define_tool\n\n@dataclass\nclass UserInfo:\n    id: str\n    name: str\n    email: str\n    role: str\n\nasync def get_user(args, inv) -\u003e dict:\n    user = UserInfo(\n        id=args[\"user_id\"],\n        name=\"John Doe\",\n        email=\"john@example.com\",\n        role=\"Developer\"\n    )\n    return asdict(user)\n\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"tools\": [\n        define_tool(\n            name=\"get_user\",\n            description=\"Retrieve user information\",\n            parameters={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"user_id\": {\"type\": \"string\", \"description\": \"User ID\"}\n                },\n                \"required\": [\"user_id\"]\n            },\n            handler=get_user\n        )\n    ]\n})\n```\n\n### Streaming with Progress\n\n```python\nimport asyncio\n\ncurrent_message = []\ndone = asyncio.Event()\n\ndef handler(event):\n    if event.type == \"assistant.message.delta\":\n        current_message.append(event.data.delta_content)\n        print(event.data.delta_content, end=\"\", flush=True)\n    elif event.type == \"assistant.message\":\n        print(f\"\\n\\n=== Complete ===\")\n        print(f\"Total length: {len(event.data.content)} chars\")\n    elif event.type == \"session.idle\":\n        done.set()\n\nunsubscribe = session.on(handler)\nawait session.send({\"prompt\": \"Write a long story\"})\nawait done.wait()\nunsubscribe()\n```\n\n### Error Recovery\n\n```python\ndef handler(event):\n    if event.type == \"session.error\":\n        print(f\"Session error: {event.data.message}\")\n        # Optionally retry or handle error\n\nsession.on(handler)\n\ntry:\n    await session.send({\"prompt\": \"risky operation\"})\nexcept Exception as e:\n    # Handle send errors\n    print(f\"Failed to send: {e}\")\n```\n\n### Using TypedDict for Type Safety\n\n```python\nfrom typing import TypedDict, List\n\nclass MessageOptions(TypedDict, total=False):\n    prompt: str\n    attachments: List[dict]\n    mode: str\n\nclass SessionConfig(TypedDict, total=False):\n    model: str\n    streaming: bool\n    tools: List\n\n# Usage with type hints\noptions: MessageOptions = {\n    \"prompt\": \"Hello\",\n    \"mode\": \"enqueue\"\n}\nawait session.send(options)\n\nconfig: SessionConfig = {\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"model\": \"gpt-5\",\n    \"streaming\": True\n}\nsession = await client.create_session(config)\n```\n\n### Async Generator for Streaming\n\n```python\nfrom typing import AsyncGenerator\n\nasync def stream_response(session, prompt: str) -\u003e AsyncGenerator[str, None]:\n    \"\"\"Stream response chunks as an async generator.\"\"\"\n    queue = asyncio.Queue()\n    done = asyncio.Event()\n\n    def handler(event):\n        if event.type == \"assistant.message.delta\":\n            queue.put_nowait(event.data.delta_content)\n        elif event.type == \"session.idle\":\n            done.set()\n\n    unsubscribe = session.on(handler)\n    await session.send({\"prompt\": prompt})\n\n    while not done.is_set():\n        try:\n            chunk = await asyncio.wait_for(queue.get(), timeout=0.1)\n            yield chunk\n        except asyncio.TimeoutError:\n            continue\n\n    # Drain remaining items\n    while not queue.empty():\n        yield queue.get_nowait()\n\n    unsubscribe()\n\n# Usage\nasync for chunk in stream_response(session, \"Tell me a story\"):\n    print(chunk, end=\"\", flush=True)\n```\n\n### Decorator Pattern for Tools\n\n```python\nfrom typing import Callable, Any\nfrom copilot import define_tool\n\ndef copilot_tool(\n    name: str,\n    description: str,\n    parameters: dict\n) -\u003e Callable:\n    \"\"\"Decorator to convert a function into a Copilot tool.\"\"\"\n    def decorator(func: Callable) -\u003e Any:\n        return define_tool(\n            name=name,\n            description=description,\n            parameters=parameters,\n            handler=lambda args, inv: func(**args)\n        )\n    return decorator\n\n@copilot_tool(\n    name=\"calculate\",\n    description=\"Perform a calculation\",\n    parameters={\n        \"type\": \"object\",\n        \"properties\": {\n            \"expression\": {\"type\": \"string\", \"description\": \"Math expression\"}\n        },\n        \"required\": [\"expression\"]\n    }\n)\ndef calculate(expression: str) -\u003e float:\n    return eval(expression)\n\nsession = await client.create_session({\n    \"on_permission_request\": PermissionHandler.approve_all,\n    \"tools\": [calculate]})\n```\n\n## Python-Specific Features\n\n### Async Context Manager Protocol\n\nThe SDK implements `__aenter__` and `__aexit__`:\n\n```python\nclass CopilotClient:\n    async def __aenter__(self):\n        await self.start()\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        await self.stop()\n        return False\n\nclass CopilotSession:\n    async def __aenter__(self):\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        await self.destroy()\n        return False\n```\n\n### Dataclass Support\n\nEvent data is available as attributes:\n\n```python\ndef handler(event):\n    # Access event attributes directly\n    print(event.type)\n    print(event.data.content)  # For assistant.message\n    print(event.data.delta_content)  # For assistant.message.delta\n```\n","description":"This file provides guidance on building Python applications using GitHub Copilot SDK.","import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/blob/541b7819d8c3545c6df122491af4fa1eae415779/instructions/copilot-sdk-python.instructions.md"},"manifest":{}},"content_hash":[199,6,234,24,98,62,209,9,130,49,218,57,68,82,113,122,16,108,118,91,0,230,194,13,229,146,24,66,182,67,232,229],"trust_level":"unsigned","yanked":false}
