Python: Migrate GitHub Copilot package to SDK 0.2.x (#5107)

* Python: Migrate GitHub Copilot package to SDK 0.2.x

Replace all imports from the non-existent copilot.types module with
correct SDK 0.2.x module paths (copilot.session, copilot.client,
copilot.tools, copilot.generated.session_events). Fix PermissionRequest
attribute access from dict-style .get() to dataclass attribute access.
Add OTel telemetry support to Copilot samples via configure_otel_providers
and document new telemetry environment variables in samples README.

* Python: Fix remaining copilot.types import in sample validation script

* Python: Include model in default_options for telemetry span attributes

* Python: Address review feedback on log_level and session kwargs typing

* Python: Scope PR to SDK 0.2.x migration only, remove net-new OTel features

- Remove RawGitHubCopilotAgent split and AgentTelemetryLayer inheritance
- Remove TelemetryConfig plumbing and OTLP/file telemetry settings
- Remove configure_otel_providers() calls from samples
- Remove telemetry env var rows from samples README
- Retain only: import path fixes, PermissionRequest attribute access fix,
  log_level default fix, session kwargs typed fix, dependency pin

* Python: Update tests for SDK 0.2.x API changes

- SubprocessConfig replaces CopilotClientOptions dict
- create_session and resume_session now use keyword args
- send and send_and_wait take plain string prompt instead of MessageOptions
- on_permission_request is always required; deny-all fallback replaces omission

* Python: Pin github-copilot-sdk to >=0.2.0,<=0.2.0

Tighten the upper bound from <0.3.0 to <=0.2.0 to avoid pulling in 0.2.1+
which has breaking API changes relative to 0.2.0. The lower bound stays at
>=0.2.0 since this migration requires the 0.2.x import paths; 0.1.x would
fail at import time.

* Python: Pin github-copilot-sdk to >=0.2.1,<=0.2.1

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
This commit is contained in:
Dineshsuriya D
2026-04-10 06:37:14 +05:30
committed by GitHub
Unverified
parent 790a759dbf
commit d4036c5aef
12 changed files with 112 additions and 111 deletions
@@ -7,7 +7,7 @@ import contextlib
import logging
import sys
from collections.abc import AsyncIterable, Awaitable, Callable, MutableMapping, Sequence
from typing import Any, ClassVar, Generic, Literal, TypedDict, cast, overload
from typing import Any, ClassVar, Generic, Literal, TypedDict, overload
from agent_framework import (
AgentMiddlewareTypes,
@@ -29,20 +29,11 @@ from agent_framework._types import AgentRunInputs, normalize_tools
from agent_framework.exceptions import AgentException
try:
from copilot import CopilotClient, CopilotSession
from copilot import CopilotClient, CopilotSession, SubprocessConfig
from copilot.generated.session_events import PermissionRequest, SessionEvent, SessionEventType
from copilot.types import (
CopilotClientOptions,
MCPServerConfig,
MessageOptions,
PermissionRequestResult,
ResumeSessionConfig,
SessionConfig,
SystemMessageConfig,
ToolInvocation,
ToolResult,
)
from copilot.types import Tool as CopilotTool
from copilot.session import MCPServerConfig, PermissionRequestResult, SystemMessageConfig
from copilot.tools import Tool as CopilotTool
from copilot.tools import ToolInvocation, ToolResult
except ImportError as _copilot_import_error:
raise ImportError(
"GitHubCopilotAgent requires the 'github-copilot-sdk' package, which is only available on Python 3.11+. "
@@ -64,6 +55,14 @@ PermissionHandlerType = Callable[[PermissionRequest, dict[str, str]], Permission
logger = logging.getLogger("agent_framework.github_copilot")
def _deny_all_permissions(
_request: PermissionRequest,
_invocation: dict[str, str],
) -> PermissionRequestResult:
"""Default permission handler that denies all requests."""
return PermissionRequestResult()
class GitHubCopilotSettings(TypedDict, total=False):
"""GitHub Copilot model settings.
@@ -274,16 +273,13 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
return
if self._client is None:
client_options: CopilotClientOptions = {}
cli_path = self._settings.get("cli_path")
if cli_path:
client_options["cli_path"] = cli_path
cli_path = self._settings.get("cli_path") or None
log_level = self._settings.get("log_level") or None
log_level = self._settings.get("log_level")
subprocess_kwargs: dict[str, Any] = {"cli_path": cli_path}
if log_level:
client_options["log_level"] = log_level # type: ignore[typeddict-item]
self._client = CopilotClient(client_options if client_options else None)
subprocess_kwargs["log_level"] = log_level
self._client = CopilotClient(SubprocessConfig(**subprocess_kwargs))
try:
await self._client.start()
@@ -407,10 +403,9 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
prompt = "\n".join([message.text for message in context_messages])
if session_context.instructions:
prompt = "\n".join(session_context.instructions) + "\n" + prompt
message_options = cast(MessageOptions, {"prompt": prompt})
try:
response_event = await copilot_session.send_and_wait(message_options, timeout=timeout)
response_event = await copilot_session.send_and_wait(prompt, timeout=timeout)
except Exception as ex:
raise AgentException(f"GitHub Copilot request failed: {ex}") from ex
@@ -489,7 +484,6 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
prompt = "\n".join([message.text for message in context_messages])
if session_context.instructions:
prompt = "\n".join(session_context.instructions) + "\n" + prompt
message_options = cast(MessageOptions, {"prompt": prompt})
queue: asyncio.Queue[AgentResponseUpdate | Exception | None] = asyncio.Queue()
@@ -550,7 +544,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
unsubscribe = copilot_session.on(event_handler)
try:
await copilot_session.send(message_options)
await copilot_session.send(prompt)
while (item := await queue.get()) is not None:
if isinstance(item, Exception):
@@ -730,43 +724,35 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
raise RuntimeError("GitHub Copilot client not initialized. Call start() first.")
opts = runtime_options or {}
config: SessionConfig = {"streaming": streaming}
model = opts.get("model") or self._settings.get("model") or None
system_message = opts.get("system_message") or self._default_options.get("system_message") or None
permission_handler: PermissionHandlerType = (
opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions
)
mcp_servers = opts.get("mcp_servers") or self._mcp_servers or None
tools = self._prepare_tools(self._tools) if self._tools else None
model = opts.get("model") or self._settings.get("model")
if model:
config["model"] = model # type: ignore[typeddict-item]
system_message = opts.get("system_message") or self._default_options.get("system_message")
if system_message:
config["system_message"] = system_message
if self._tools:
config["tools"] = self._prepare_tools(self._tools)
permission_handler = opts.get("on_permission_request") or self._permission_handler
if permission_handler:
config["on_permission_request"] = permission_handler
mcp_servers = opts.get("mcp_servers") or self._mcp_servers
if mcp_servers:
config["mcp_servers"] = mcp_servers
return await self._client.create_session(config)
return await self._client.create_session(
on_permission_request=permission_handler,
streaming=streaming,
model=model or None,
system_message=system_message or None,
tools=tools or None,
mcp_servers=mcp_servers or None,
)
async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSession:
"""Resume an existing Copilot session by ID."""
if not self._client:
raise RuntimeError("GitHub Copilot client not initialized. Call start() first.")
config: ResumeSessionConfig = {"streaming": streaming}
permission_handler: PermissionHandlerType = self._permission_handler or _deny_all_permissions
tools = self._prepare_tools(self._tools) if self._tools else None
if self._tools:
config["tools"] = self._prepare_tools(self._tools)
if self._permission_handler:
config["on_permission_request"] = self._permission_handler
if self._mcp_servers:
config["mcp_servers"] = self._mcp_servers
return await self._client.resume_session(session_id, config)
return await self._client.resume_session(
session_id,
on_permission_request=permission_handler,
streaming=streaming,
tools=tools or None,
mcp_servers=self._mcp_servers or None,
)
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0,<2",
"github-copilot-sdk>=0.1.31,<0.1.33; python_version >= '3.11'",
"github-copilot-sdk>=0.2.1,<=0.2.1; python_version >= '3.11'",
]
[tool.uv]
@@ -102,3 +102,4 @@ interpreter = "posix"
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
build-backend = "flit_core.buildapi"
@@ -23,7 +23,7 @@ from agent_framework import (
)
from agent_framework.exceptions import AgentException
from copilot.generated.session_events import Data, ErrorClass, Result, SessionEvent, SessionEventType
from copilot.types import ToolInvocation, ToolResult
from copilot.tools import ToolInvocation, ToolResult
from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions
@@ -268,8 +268,8 @@ class TestGitHubCopilotAgentLifecycle:
await agent.start()
call_args = MockClient.call_args[0][0]
assert call_args["cli_path"] == "/custom/path"
assert call_args["log_level"] == "debug"
assert call_args.cli_path == "/custom/path"
assert call_args.log_level == "debug"
class TestGitHubCopilotAgentRun:
@@ -855,7 +855,13 @@ class TestGitHubCopilotAgentSessionManagement:
await agent.run("World", session=session)
mock_client.create_session.assert_called_once()
mock_client.resume_session.assert_called_once_with(mock_session.session_id, unittest.mock.ANY)
mock_client.resume_session.assert_called_once_with(
mock_session.session_id,
on_permission_request=unittest.mock.ANY,
streaming=unittest.mock.ANY,
tools=unittest.mock.ANY,
mcp_servers=unittest.mock.ANY,
)
async def test_session_config_includes_model(
self,
@@ -871,7 +877,7 @@ class TestGitHubCopilotAgentSessionManagement:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
assert config["model"] == "claude-sonnet-4"
async def test_session_config_includes_instructions(
@@ -889,7 +895,7 @@ class TestGitHubCopilotAgentSessionManagement:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
assert config["system_message"]["mode"] == "append"
assert config["system_message"]["content"] == "You are a helpful assistant."
@@ -914,7 +920,7 @@ class TestGitHubCopilotAgentSessionManagement:
)
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
assert config["system_message"]["mode"] == "replace"
assert config["system_message"]["content"] == "Runtime instructions"
@@ -930,7 +936,7 @@ class TestGitHubCopilotAgentSessionManagement:
await agent._get_or_create_session(AgentSession(), streaming=True) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
assert config["streaming"] is True
async def test_resume_session_with_existing_service_session_id(
@@ -958,7 +964,8 @@ class TestGitHubCopilotAgentSessionManagement:
mock_session: MagicMock,
) -> None:
"""Test that resumed session config includes tools and permission handler."""
from copilot.types import PermissionRequest, PermissionRequestResult
from copilot.generated.session_events import PermissionRequest
from copilot.session import PermissionRequestResult
def my_handler(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
return PermissionRequestResult(kind="approved")
@@ -981,7 +988,7 @@ class TestGitHubCopilotAgentSessionManagement:
mock_client.resume_session.assert_called_once()
call_args = mock_client.resume_session.call_args
config = call_args[0][1]
config = call_args.kwargs
assert "tools" in config
assert "on_permission_request" in config
@@ -995,7 +1002,7 @@ class TestGitHubCopilotAgentMCPServers:
mock_session: MagicMock,
) -> None:
"""Test that mcp_servers are passed through to create_session config."""
from copilot.types import MCPServerConfig
from copilot.session import MCPServerConfig
mcp_servers: dict[str, MCPServerConfig] = {
"filesystem": {
@@ -1020,7 +1027,7 @@ class TestGitHubCopilotAgentMCPServers:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
assert "mcp_servers" in config
assert "filesystem" in config["mcp_servers"]
assert "remote" in config["mcp_servers"]
@@ -1033,7 +1040,7 @@ class TestGitHubCopilotAgentMCPServers:
mock_session: MagicMock,
) -> None:
"""Test that mcp_servers are passed through to resume_session config."""
from copilot.types import MCPServerConfig
from copilot.session import MCPServerConfig
mcp_servers: dict[str, MCPServerConfig] = {
"test-server": {
@@ -1057,7 +1064,7 @@ class TestGitHubCopilotAgentMCPServers:
mock_client.resume_session.assert_called_once()
call_args = mock_client.resume_session.call_args
config = call_args[0][1]
config = call_args.kwargs
assert "mcp_servers" in config
assert "test-server" in config["mcp_servers"]
@@ -1073,8 +1080,8 @@ class TestGitHubCopilotAgentMCPServers:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
assert "mcp_servers" not in config
config = call_args.kwargs
assert config["mcp_servers"] is None
class TestGitHubCopilotAgentToolConversion:
@@ -1097,7 +1104,7 @@ class TestGitHubCopilotAgentToolConversion:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
assert "tools" in config
assert len(config["tools"]) == 1
assert config["tools"][0].name == "my_tool"
@@ -1120,7 +1127,7 @@ class TestGitHubCopilotAgentToolConversion:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
copilot_tool = config["tools"][0]
result = await copilot_tool.handler(ToolInvocation(arguments={"arg": "test"}))
@@ -1146,7 +1153,7 @@ class TestGitHubCopilotAgentToolConversion:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
copilot_tool = config["tools"][0]
result = await copilot_tool.handler(ToolInvocation(arguments={"arg": "test"}))
@@ -1173,7 +1180,7 @@ class TestGitHubCopilotAgentToolConversion:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
copilot_tool = config["tools"][0]
with pytest.raises((TypeError, AttributeError)):
@@ -1196,7 +1203,7 @@ class TestGitHubCopilotAgentToolConversion:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
copilot_tool = config["tools"][0]
result = await copilot_tool.handler(ToolInvocation(arguments={}))
@@ -1210,7 +1217,7 @@ class TestGitHubCopilotAgentToolConversion:
mock_client: MagicMock,
) -> None:
"""Test that CopilotTool instances are passed through as-is."""
from copilot.types import Tool as CopilotTool
from copilot.tools import Tool as CopilotTool
async def tool_handler(invocation: Any) -> Any:
return {"text_result_for_llm": "result", "result_type": "success"}
@@ -1234,7 +1241,7 @@ class TestGitHubCopilotAgentToolConversion:
) -> None:
"""Test that mixed tool types are handled correctly."""
from agent_framework import tool
from copilot.types import Tool as CopilotTool
from copilot.tools import Tool as CopilotTool
@tool(approval_mode="never_require")
def my_function(arg: str) -> str:
@@ -1317,10 +1324,11 @@ class TestGitHubCopilotAgentPermissions:
def test_permission_handler_set_when_provided(self) -> None:
"""Test that a handler is set when on_permission_request is provided."""
from copilot.types import PermissionRequest, PermissionRequestResult
from copilot.generated.session_events import PermissionRequest
from copilot.session import PermissionRequestResult
def approve_shell(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
if request.get("kind") == "shell":
if request.kind == "shell":
return PermissionRequestResult(kind="approved")
return PermissionRequestResult(kind="denied-interactively-by-user")
@@ -1335,10 +1343,11 @@ class TestGitHubCopilotAgentPermissions:
mock_session: MagicMock,
) -> None:
"""Test that session config includes permission handler when provided."""
from copilot.types import PermissionRequest, PermissionRequestResult
from copilot.generated.session_events import PermissionRequest
from copilot.session import PermissionRequestResult
def approve_shell_read(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
if request.get("kind") in ("shell", "read"):
if request.kind in ("shell", "read"):
return PermissionRequestResult(kind="approved")
return PermissionRequestResult(kind="denied-interactively-by-user")
@@ -1351,24 +1360,29 @@ class TestGitHubCopilotAgentPermissions:
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
config = call_args.kwargs
assert "on_permission_request" in config
assert config["on_permission_request"] is not None
async def test_session_config_excludes_permission_handler_when_not_set(
async def test_session_config_uses_deny_all_when_no_permission_handler_set(
self,
mock_client: MagicMock,
mock_session: MagicMock,
) -> None:
"""Test that session config does not include permission handler when not set."""
"""Test that session config uses deny-all handler when no permission handler is set.
In SDK 0.2.x, on_permission_request is required by create_session, so the agent
always falls back to _deny_all_permissions when no handler is provided.
"""
agent = GitHubCopilotAgent(client=mock_client)
await agent.start()
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
assert "on_permission_request" not in config
config = call_args.kwargs
assert "on_permission_request" in config
assert config["on_permission_request"] is not None
class SpyContextProvider(ContextProvider):
@@ -1454,7 +1468,7 @@ class TestGitHubCopilotAgentContextProviders:
session = agent.create_session()
await agent.run("Hello", session=session)
sent_prompt = mock_session.send_and_wait.call_args[0][0]["prompt"]
sent_prompt = mock_session.send_and_wait.call_args[0][0]
assert "Injected by spy provider" in sent_prompt
async def test_after_run_receives_response(
@@ -1547,7 +1561,7 @@ class TestGitHubCopilotAgentContextProviders:
async for _ in agent.run("Hello", stream=True, session=session):
pass
sent_prompt = mock_session.send.call_args[0][0]["prompt"]
sent_prompt = mock_session.send.call_args[0][0]
assert "Injected by spy provider" in sent_prompt
async def test_context_preserved_across_runs(
@@ -1608,7 +1622,7 @@ class TestGitHubCopilotAgentContextProviders:
session = agent.create_session()
await agent.run("Hello", session=session)
sent_prompt = mock_session.send_and_wait.call_args[0][0]["prompt"]
sent_prompt = mock_session.send_and_wait.call_args[0][0]
assert "History message" in sent_prompt
assert "Hello" in sent_prompt
@@ -1659,7 +1673,7 @@ class TestGitHubCopilotAgentContextProviders:
async for _ in agent.run("Hello", stream=True, session=session):
pass
sent_prompt = mock_session.send.call_args[0][0]["prompt"]
sent_prompt = mock_session.send.call_args[0][0]
assert "History message" in sent_prompt
assert "Hello" in sent_prompt