From db3f9b3faceaecb95293dcc3fc706fc0d8b56f98 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Tue, 5 May 2026 15:20:45 -0700 Subject: [PATCH 1/4] Upgrade github-copilot-sdk to v1.0.0b1 and implement new features - Bump github-copilot-sdk dependency from 0.2.1 to 1.0.0b1 - Fix breaking type renames: ErrorClass -> ToolExecutionCompleteError, Result -> ToolExecutionCompleteResult - Add instruction_directories support in GitHubCopilotOptions (session-level) - Add copilot_home support in GitHubCopilotSettings (client-level) - Add sample: github_copilot_with_instruction_directories.py - Update README with new env var and sample entry - Add 8 new unit tests covering the new features (103 total, 96% coverage) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_github_copilot/_agent.py | 21 +++ python/packages/github_copilot/pyproject.toml | 2 +- .../tests/test_github_copilot_agent.py | 130 ++++++++++++++++- .../providers/github_copilot/README.md | 2 + ...ub_copilot_with_instruction_directories.py | 137 ++++++++++++++++++ .../security/email_security_example.py | 4 +- .../hyperlight_codeact/call_server.py | 3 +- python/uv.lock | 16 +- 8 files changed, 296 insertions(+), 19 deletions(-) create mode 100644 python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 171b0d7f5d..d343527a29 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -140,12 +140,18 @@ class GitHubCopilotSettings(TypedDict, total=False): Can be set via environment variable GITHUB_COPILOT_TIMEOUT. log_level: CLI log level. Can be set via environment variable GITHUB_COPILOT_LOG_LEVEL. + copilot_home: Directory where the CLI stores session state, configuration, + and other persistent data. Can be set via environment variable + GITHUB_COPILOT_COPILOT_HOME. Defaults to ~/.copilot when not set. + Only applicable when the SDK spawns the CLI process (ignored when + connecting to an external server via a pre-configured client). """ cli_path: str | None model: str | None timeout: float | None log_level: str | None + copilot_home: str | None class GitHubCopilotOptions(TypedDict, total=False): @@ -187,6 +193,12 @@ class GitHubCopilotOptions(TypedDict, total=False): instead of the default GitHub Copilot backend. """ + instruction_directories: list[str] + """Additional directories to search for custom instruction files. + Lets applications point the CLI at project-specific or team-shared instruction + files beyond the default locations. + """ + on_function_approval: FunctionApprovalCallback """Approval callback for ``FunctionTool`` instances declared with ``approval_mode="always_require"``. The callback is awaited (sync or async) @@ -300,7 +312,9 @@ def __init__( on_permission_request: PermissionHandlerType | None = opts.pop("on_permission_request", None) mcp_servers: dict[str, MCPServerConfig] | None = opts.pop("mcp_servers", None) provider: ProviderConfig | None = opts.pop("provider", None) + instruction_directories: list[str] | None = opts.pop("instruction_directories", None) on_function_approval: FunctionApprovalCallback | None = opts.pop("on_function_approval", None) + copilot_home = opts.pop("copilot_home", None) self._settings = load_settings( GitHubCopilotSettings, @@ -309,6 +323,7 @@ def __init__( model=model, timeout=timeout, log_level=log_level, + copilot_home=copilot_home, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) @@ -318,6 +333,7 @@ def __init__( self._function_approval_handler: FunctionApprovalCallback | None = on_function_approval self._mcp_servers = mcp_servers self._provider = provider + self._instruction_directories = instruction_directories self._default_options = opts self._started = False @@ -346,10 +362,13 @@ async def start(self) -> None: if self._client is None: cli_path = self._settings.get("cli_path") or None log_level = self._settings.get("log_level") or None + copilot_home = self._settings.get("copilot_home") or None subprocess_kwargs: dict[str, Any] = {"cli_path": cli_path} if log_level: subprocess_kwargs["log_level"] = log_level + if copilot_home: + subprocess_kwargs["copilot_home"] = copilot_home self._client = CopilotClient(SubprocessConfig(**subprocess_kwargs)) try: @@ -868,6 +887,7 @@ async def _create_session( ) mcp_servers = opts.get("mcp_servers") or self._mcp_servers or None provider = opts.get("provider") or self._provider or None + instruction_directories = opts.get("instruction_directories") or self._instruction_directories or None tools = self._prepare_tools(self._tools) if self._tools else None return await self._client.create_session( @@ -878,6 +898,7 @@ async def _create_session( tools=tools or None, mcp_servers=mcp_servers or None, provider=provider or None, + instruction_directories=instruction_directories or None, ) async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSession: diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 7b9afaccac..3e6f4f75b2 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.2.2,<2", - "github-copilot-sdk>=0.2.1,<=0.2.1; python_version >= '3.11'", + "github-copilot-sdk>=1.0.0b1,<=1.0.0b1; python_version >= '3.11'", ] [tool.uv] diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index 099c5f643b..f757545f11 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -22,7 +22,13 @@ Message, ) from agent_framework.exceptions import AgentException -from copilot.generated.session_events import Data, ErrorClass, Result, SessionEvent, SessionEventType +from copilot.generated.session_events import ( + Data, + SessionEvent, + SessionEventType, + ToolExecutionCompleteError, + ToolExecutionCompleteResult, +) from copilot.tools import ToolInvocation, ToolResult from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions @@ -212,6 +218,18 @@ def test_default_options_returns_independent_copy(self) -> None: opts["model"] = "mutated" assert agent._settings.get("model") == "gpt-5.1-mini" + def test_init_stores_instruction_directories(self) -> None: + """Test that instruction_directories are stored on the agent instance.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + default_options={"instruction_directories": ["/my/instructions"]} + ) + assert agent._instruction_directories == ["/my/instructions"] # type: ignore + + def test_init_without_instruction_directories(self) -> None: + """Test that instruction_directories default to None when not provided.""" + agent = GitHubCopilotAgent() + assert agent._instruction_directories is None # type: ignore + class TestGitHubCopilotAgentLifecycle: """Test cases for agent lifecycle management.""" @@ -294,6 +312,50 @@ async def test_start_creates_client_with_options(self) -> None: assert call_args.cli_path == "/custom/path" assert call_args.log_level == "debug" + async def test_start_passes_copilot_home_to_subprocess_config(self) -> None: + """Test that copilot_home is passed through to SubprocessConfig.""" + with patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient: + mock_client = MagicMock() + mock_client.start = AsyncMock() + MockClient.return_value = mock_client + + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + default_options={"copilot_home": "/custom/copilot/home"} + ) + await agent.start() + + call_args = MockClient.call_args[0][0] + assert call_args.copilot_home == "/custom/copilot/home" + + async def test_start_copilot_home_not_set_when_unspecified(self) -> None: + """Test that copilot_home is not included in SubprocessConfig when not specified.""" + with patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient: + mock_client = MagicMock() + mock_client.start = AsyncMock() + MockClient.return_value = mock_client + + agent = GitHubCopilotAgent() + await agent.start() + + call_args = MockClient.call_args[0][0] + assert call_args.copilot_home is None + + async def test_start_copilot_home_from_env_variable(self) -> None: + """Test that copilot_home can be set via GITHUB_COPILOT_COPILOT_HOME env variable.""" + with ( + patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient, + patch.dict("os.environ", {"GITHUB_COPILOT_COPILOT_HOME": "/env/copilot/home"}), + ): + mock_client = MagicMock() + mock_client.start = AsyncMock() + MockClient.return_value = mock_client + + agent = GitHubCopilotAgent() + await agent.start() + + call_args = MockClient.call_args[0][0] + assert call_args.copilot_home == "/env/copilot/home" + class TestGitHubCopilotAgentRun: """Test cases for run method.""" @@ -537,7 +599,7 @@ async def test_run_streaming_tool_execution_complete( """Test that TOOL_EXECUTION_COMPLETE events produce function_result content.""" tool_event_data = MagicMock() tool_event_data.tool_call_id = "call_abc123" - tool_event_data.result = Result(content="Sunny, 72°F") + tool_event_data.result = ToolExecutionCompleteResult(content="Sunny, 72°F") tool_event_data.success = True tool_event_data.error = None @@ -652,9 +714,9 @@ async def test_run_streaming_tool_execution_failure( """Test that a failed tool result surfaces the error as exception.""" tool_event_data = MagicMock() tool_event_data.tool_call_id = "call_fail" - tool_event_data.result = Result(content="Error: connection timeout") + tool_event_data.result = ToolExecutionCompleteResult(content="Error: connection timeout") tool_event_data.success = False - tool_event_data.error = ErrorClass(message="connection timeout") + tool_event_data.error = ToolExecutionCompleteError(message="connection timeout") tool_event = SessionEvent( data=tool_event_data, @@ -691,7 +753,7 @@ async def test_run_streaming_tool_execution_failure_string_error( """Test that a failed tool result with a string error is surfaced.""" tool_event_data = MagicMock() tool_event_data.tool_call_id = "call_fail2" - tool_event_data.result = Result(content="") + tool_event_data.result = ToolExecutionCompleteResult(content="") tool_event_data.success = False tool_event_data.error = "something went wrong" @@ -729,7 +791,7 @@ async def test_run_streaming_tool_execution_success_with_error_field( """Test that a successful tool result with error field does not propagate exception.""" tool_event_data = MagicMock() tool_event_data.tool_call_id = "call_ok" - tool_event_data.result = Result(content="partial result") + tool_event_data.result = ToolExecutionCompleteResult(content="partial result") tool_event_data.success = True tool_event_data.error = "some warning" @@ -817,7 +879,7 @@ async def test_run_streaming_tool_call_and_result_sequence( # Tool result event result_data = MagicMock() result_data.tool_call_id = "call_001" - result_data.result = Result(content="72°F and sunny") + result_data.result = ToolExecutionCompleteResult(content="72°F and sunny") result_data.success = True result_data.error = None tool_result_event = SessionEvent( @@ -1016,6 +1078,60 @@ def my_tool(arg: str) -> str: assert "tools" in config assert "on_permission_request" in config + async def test_instruction_directories_passed_to_create_session( + self, + mock_client: MagicMock, + mock_session: MagicMock, + ) -> None: + """Test that instruction_directories are passed through to create_session.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + client=mock_client, + default_options={"instruction_directories": ["/path/to/instructions", "/other/path"]}, + ) + await agent.start() + + await agent._get_or_create_session(AgentSession()) # type: ignore + + call_args = mock_client.create_session.call_args + config = call_args.kwargs + assert config["instruction_directories"] == ["/path/to/instructions", "/other/path"] + + async def test_instruction_directories_runtime_override( + self, + mock_client: MagicMock, + mock_session: MagicMock, + ) -> None: + """Test that runtime instruction_directories take precedence over defaults.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + client=mock_client, + default_options={"instruction_directories": ["/default/path"]}, + ) + await agent.start() + + runtime_options: GitHubCopilotOptions = { + "instruction_directories": ["/runtime/path"] + } + await agent._get_or_create_session(AgentSession(), runtime_options=runtime_options) # type: ignore + + call_args = mock_client.create_session.call_args + config = call_args.kwargs + assert config["instruction_directories"] == ["/runtime/path"] + + async def test_instruction_directories_none_when_not_specified( + self, + mock_client: MagicMock, + mock_session: MagicMock, + ) -> None: + """Test that instruction_directories is None when not specified.""" + 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.kwargs + assert config["instruction_directories"] is None + class TestGitHubCopilotAgentMCPServers: """Test cases for MCP server configuration.""" diff --git a/python/samples/02-agents/providers/github_copilot/README.md b/python/samples/02-agents/providers/github_copilot/README.md index 5f8ee344d6..c3132ed1e9 100644 --- a/python/samples/02-agents/providers/github_copilot/README.md +++ b/python/samples/02-agents/providers/github_copilot/README.md @@ -23,6 +23,7 @@ The following environment variables can be configured: | `GITHUB_COPILOT_MODEL` | Model to use (e.g., "gpt-5", "claude-sonnet-4") | Server default | | `GITHUB_COPILOT_TIMEOUT` | Request timeout in seconds | `60` | | `GITHUB_COPILOT_LOG_LEVEL` | CLI log level | `info` | +| `GITHUB_COPILOT_COPILOT_HOME` | Directory for CLI session state and config | `~/.copilot` | ## Observability @@ -50,4 +51,5 @@ See the [observability samples](../../../02-agents/observability/) for full exam | [`github_copilot_with_file_operations.py`](github_copilot_with_file_operations.py) | Shows how to enable file read and write permissions. Demonstrates reading file contents and creating new files. | | [`github_copilot_with_url.py`](github_copilot_with_url.py) | Shows how to enable URL fetching permissions. Demonstrates fetching and processing web content. | | [`github_copilot_with_mcp.py`](github_copilot_with_mcp.py) | Shows how to configure MCP (Model Context Protocol) servers, including local (stdio) and remote (HTTP) servers. | +| [`github_copilot_with_instruction_directories.py`](github_copilot_with_instruction_directories.py) | Shows how to configure custom instruction directories for project-specific or team-shared guidelines. | | [`github_copilot_with_multiple_permissions.py`](github_copilot_with_multiple_permissions.py) | Shows how to combine multiple permission types for complex tasks that require shell, read, and write access. | diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py new file mode 100644 index 0000000000..e30f114f7d --- /dev/null +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py @@ -0,0 +1,137 @@ +# Copyright (c) Microsoft. All rights reserved. + +""" +GitHub Copilot Agent with Instruction Directories + +This sample demonstrates how to configure custom instruction directories with +GitHubCopilotAgent. Instruction directories let the CLI load project-specific +or team-shared instruction files that shape the agent's behavior beyond the +default system message. + +Use cases: +- Point the agent at a team-shared set of coding conventions. +- Load project-specific guidelines from a local `.copilot/instructions/` folder. +- Override or augment default instructions per session at runtime. + +Environment variables (optional): +- GITHUB_COPILOT_CLI_PATH - Path to the Copilot CLI executable +- GITHUB_COPILOT_MODEL - Model to use (e.g., "gpt-5", "claude-sonnet-4") +""" + +import asyncio +from pathlib import Path + +from agent_framework.github import GitHubCopilotAgent +from copilot.generated.session_events import PermissionRequest +from copilot.session import PermissionRequestResult +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + + +def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: + """Permission handler that prompts the user for approval.""" + print(f"\n[Permission Request: {request.kind}]") + + response = input("Approve? (y/n): ").strip().lower() + if response in ("y", "yes"): + return PermissionRequestResult(kind="approved") + return PermissionRequestResult(kind="denied-interactively-by-user") + + +async def default_instructions_example() -> None: + """Example of pointing the agent at project-specific instruction directories.""" + print("=== Instruction Directories (Default) ===\n") + + # 1. Define instruction directories. + # These paths contain custom instruction files the CLI will load + # alongside its built-in instructions. + project_root = Path.cwd() + instruction_dirs = [ + str(project_root / ".copilot" / "instructions"), + str(project_root / "docs" / "agent-guidelines"), + ] + + # 2. Create the agent with instruction directories in default_options. + # These directories apply to every session created by this agent. + agent = GitHubCopilotAgent( + instructions="You are a helpful coding assistant.", + default_options={ + "on_permission_request": prompt_permission, + "instruction_directories": instruction_dirs, + }, + ) + + # 3. Run the agent — instruction files from those directories are loaded + # automatically by the CLI when the session starts. + async with agent: + query = "Summarize the coding conventions I should follow in this project." + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + + +async def runtime_override_example() -> None: + """Example of overriding instruction directories at runtime.""" + print("=== Instruction Directories (Runtime Override) ===\n") + + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant.", + default_options={ + "on_permission_request": prompt_permission, + "instruction_directories": ["/team/shared/instructions"], + }, + ) + + async with agent: + # First call uses the default instruction directories + query = "What instructions are you following?" + print(f"User: {query}") + result1 = await agent.run(query) + print(f"Agent: {result1}\n") + + # Second call overrides with different instruction directories at runtime. + # Runtime options take precedence over the defaults for that session. + print("Overriding with project-specific instructions...\n") + query2 = "Now what instructions are you following?" + print(f"User: {query2}") + result2 = await agent.run( + query2, + options={ + "instruction_directories": ["/project/specific/instructions"], + }, + ) + print(f"Agent: {result2}\n") + + +async def main() -> None: + print("=== GitHub Copilot Agent with Instruction Directories ===\n") + + await default_instructions_example() + await runtime_override_example() + + +if __name__ == "__main__": + asyncio.run(main()) + +""" +Sample output: + +=== GitHub Copilot Agent with Instruction Directories === + +=== Instruction Directories (Default) === + +User: Summarize the coding conventions I should follow in this project. +Agent: Based on the project instructions, you should follow these conventions... + +=== Instruction Directories (Runtime Override) === + +User: What instructions are you following? +Agent: I'm following the team-shared coding guidelines which include... + +Overriding with project-specific instructions... + +User: Now what instructions are you following? +Agent: I'm now following the project-specific instructions which include... +""" diff --git a/python/samples/02-agents/security/email_security_example.py b/python/samples/02-agents/security/email_security_example.py index b8cd0a36d1..97cd20c158 100644 --- a/python/samples/02-agents/security/email_security_example.py +++ b/python/samples/02-agents/security/email_security_example.py @@ -284,7 +284,9 @@ async def run_scenarios(agent, config): # attempt to call send_email, so the policy enforcer would never trigger. session = agent.create_session() - response = await agent.run("Please fetch my recent emails and give me a brief summary of each one.", session=session) + response = await agent.run( + "Please fetch my recent emails and give me a brief summary of each one.", session=session + ) print(f"\n📋 Agent Response:\n{'-' * 40}") print(response.text) diff --git a/python/samples/04-hosting/container/hyperlight_codeact/call_server.py b/python/samples/04-hosting/container/hyperlight_codeact/call_server.py index e57da7c086..90ea113b1f 100644 --- a/python/samples/04-hosting/container/hyperlight_codeact/call_server.py +++ b/python/samples/04-hosting/container/hyperlight_codeact/call_server.py @@ -20,8 +20,7 @@ # https://.services.ai.azure.com/api/projects//agents/ ENDPOINT = os.environ.get( "FOUNDRY_AGENT_ENDPOINT", - "https://.services.ai.azure.com" - "/api/projects//agents/", + "https://.services.ai.azure.com/api/projects//agents/", ) SCOPE = "https://ai.azure.com/.default" PROMPT = ( diff --git a/python/uv.lock b/python/uv.lock index b2b5106c48..53b9062138 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -598,7 +598,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.2.1,<=0.2.1" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b1,>=1.0.0b1" }, ] [[package]] @@ -2514,19 +2514,19 @@ wheels = [ [[package]] name = "github-copilot-sdk" -version = "0.2.1" +version = "1.0.0b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "python-dateutil", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/67/41/76a9d50d7600bf8d26c659dc113be62e4e56e00a5cbfd544e1b5b200f45c/github_copilot_sdk-0.2.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:c0823150f3b73431f04caee43d1dbafac22ae7e8bd1fc83727ee8363089ee038", size = 61076141, upload-time = "2026-04-03T20:18:22.062Z" }, - { url = "https://files.pythonhosted.org/packages/04/04/d2e8bf4587c4da270ccb9cbd5ab8a2c4b41217c2bf04a43904be8a27ae20/github_copilot_sdk-0.2.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ef7ff68eb8960515e1a2e199ac0ffb9a17cd3325266461e6edd7290e43dcf012", size = 57838464, upload-time = "2026-04-03T20:18:26.042Z" }, - { url = "https://files.pythonhosted.org/packages/78/8b/cc8ee46724bd9fdfd6afe855a043c8403ed6884c5f3a55a9737780810396/github_copilot_sdk-0.2.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:890f7124e3b147532a1ac6c8d5f66421ea37757b2b9990d7967f3f147a2f533a", size = 63940155, upload-time = "2026-04-03T20:18:30.297Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ee/facf04e22e42d4bdd4fe3d356f3a51180a6ea769ae2ac306d0897f9bf9d9/github_copilot_sdk-0.2.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6502be0b9ececacbda671835e5f61c7aaa906c6b8657ee252cad6cc8335cac8e", size = 62130538, upload-time = "2026-04-03T20:18:34.061Z" }, - { url = "https://files.pythonhosted.org/packages/3f/1c/8b105f14bf61d1d304a00ac29460cb0d4e7406ceb89907d5a7b41a72fe85/github_copilot_sdk-0.2.1-py3-none-win_amd64.whl", hash = "sha256:8275ca8e387e6b29bc5155a3c02a0eb3d035c6bc7b1896253eb0d469f2385790", size = 56547331, upload-time = "2026-04-03T20:18:37.859Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c1/0ce319d2f618e9bc89f275e60b1920f4587eb0218bba6cbb84283dc7a7f3/github_copilot_sdk-0.2.1-py3-none-win_arm64.whl", hash = "sha256:1f9b59b7c41f31be416bf20818f58e25b6adc76f6d17357653fde6fbab662606", size = 54499549, upload-time = "2026-04-03T20:18:41.77Z" }, + { url = "https://files.pythonhosted.org/packages/78/8d/d3a67c2b0625bd7e676e88243e83d4a3e946079361cc7343ba720887aedd/github_copilot_sdk-1.0.0b1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:64518586314c40ed56753f638b5a6ea61c5178c338ca74246577ff5703f27772", size = 59211810, upload-time = "2026-05-04T12:53:03.519Z" }, + { url = "https://files.pythonhosted.org/packages/54/b0/eb0402143756b97025abf632e06a54b4c22490be453c25875fffb9f6e757/github_copilot_sdk-1.0.0b1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34ee3de569b02c5ef70da8a8c98438f7436dd35740c2f0f8f1b6c2d67dacb6cb", size = 55957273, upload-time = "2026-05-04T12:53:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/db/8e/0dfd7cdf3216381af41e9f305e7c24e4aad5047cd187458400ddea901709/github_copilot_sdk-1.0.0b1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:bd724e12a3bdcb06d3279782e7fc6476543eb465e1461f7435f40704b082fe0a", size = 62044108, upload-time = "2026-05-04T12:53:13.388Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/484ef06aea1f27bb04aedc538115dae93c50b989b5df1a8b0d974f40b70e/github_copilot_sdk-1.0.0b1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:d16e917ebce34c4b615f409d9c894575130727a87a8aed25cc3fd550d547c523", size = 60273559, upload-time = "2026-05-04T12:53:19.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5f/f7d3296221dd600d3dee73c3560bce8cd43e341be58bbbd751030a8e21c3/github_copilot_sdk-1.0.0b1-py3-none-win_amd64.whl", hash = "sha256:e32327ca871597b408379db43d9737789d15185bf6ded034660c396a74141723", size = 54673005, upload-time = "2026-05-04T12:53:24.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/fa/d46acb18d8cd3d99fba3040e5a7467d0f264bf0dfe82087e4addfd0f1834/github_copilot_sdk-1.0.0b1-py3-none-win_arm64.whl", hash = "sha256:e2919074ffcbbf4308f6095bf5b50def6d0d5b154af72c869a3d268cfe59a93a", size = 52475405, upload-time = "2026-05-04T12:53:29.422Z" }, ] [[package]] From aaf29134aea5b943705489c34bb5fed32a596aac Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Tue, 5 May 2026 15:39:51 -0700 Subject: [PATCH 2/4] mypy fix --- .../agent_framework_github_copilot/_agent.py | 23 +++++++++++-------- .../tests/test_github_copilot_agent.py | 23 ++++++++++++++++--- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index d343527a29..56341cd782 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -542,13 +542,14 @@ async def _run_impl( # send_and_wait returns only the final ASSISTANT_MESSAGE event; # other events (deltas, tool calls) are handled internally by the SDK. if response_event and response_event.type == SessionEventType.ASSISTANT_MESSAGE: - message_id = response_event.data.message_id + data: Any = response_event.data + message_id = data.message_id - if response_event.data.content: + if data.content: response_messages.append( Message( role="assistant", - contents=[Content.from_text(response_event.data.content)], + contents=[Content.from_text(data.content)], message_id=message_id, raw_representation=response_event, ) @@ -622,12 +623,13 @@ async def _stream_updates( def event_handler(event: SessionEvent) -> None: if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: - if event.data.delta_content: + data: Any = event.data + if data.delta_content: update = AgentResponseUpdate( role="assistant", - contents=[Content.from_text(event.data.delta_content)], - response_id=event.data.message_id, - message_id=event.data.message_id, + contents=[Content.from_text(data.delta_content)], + response_id=data.message_id, + message_id=data.message_id, raw_representation=event, ) queue.put_nowait(update) @@ -671,7 +673,8 @@ def event_handler(event: SessionEvent) -> None: elif event.type == SessionEventType.SESSION_IDLE: queue.put_nowait(None) elif event.type == SessionEventType.SESSION_ERROR: - error_msg = event.data.message or "Unknown error" + error_data: Any = event.data + error_msg = error_data.message or "Unknown error" queue.put_nowait(AgentException(f"GitHub Copilot session error: {error_msg}")) unsubscribe = copilot_session.on(event_handler) @@ -887,7 +890,7 @@ async def _create_session( ) mcp_servers = opts.get("mcp_servers") or self._mcp_servers or None provider = opts.get("provider") or self._provider or None - instruction_directories = opts.get("instruction_directories") or self._instruction_directories or None + instruction_directories = opts.get("instruction_directories", self._instruction_directories) tools = self._prepare_tools(self._tools) if self._tools else None return await self._client.create_session( @@ -898,7 +901,7 @@ async def _create_session( tools=tools or None, mcp_servers=mcp_servers or None, provider=provider or None, - instruction_directories=instruction_directories or None, + instruction_directories=instruction_directories, ) async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSession: diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index f757545f11..65c2ef7d48 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -1108,9 +1108,7 @@ async def test_instruction_directories_runtime_override( ) await agent.start() - runtime_options: GitHubCopilotOptions = { - "instruction_directories": ["/runtime/path"] - } + runtime_options: GitHubCopilotOptions = {"instruction_directories": ["/runtime/path"]} await agent._get_or_create_session(AgentSession(), runtime_options=runtime_options) # type: ignore call_args = mock_client.create_session.call_args @@ -1132,6 +1130,25 @@ async def test_instruction_directories_none_when_not_specified( config = call_args.kwargs assert config["instruction_directories"] is None + async def test_instruction_directories_empty_list_clears_defaults( + self, + mock_client: MagicMock, + mock_session: MagicMock, + ) -> None: + """Test that an explicit empty list at runtime clears the agent-level defaults.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + client=mock_client, + default_options={"instruction_directories": ["/default/path"]}, + ) + await agent.start() + + runtime_options: GitHubCopilotOptions = {"instruction_directories": []} + await agent._get_or_create_session(AgentSession(), runtime_options=runtime_options) # type: ignore + + call_args = mock_client.create_session.call_args + config = call_args.kwargs + assert config["instruction_directories"] == [] + class TestGitHubCopilotAgentMCPServers: """Test cases for MCP server configuration.""" From 91043076d27ce88d07845a1ed3ee8dab71b95099 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Tue, 5 May 2026 17:26:39 -0700 Subject: [PATCH 3/4] small fix --- .../github_copilot/agent_framework_github_copilot/_agent.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 56341cd782..cee60dea7f 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -174,6 +174,12 @@ class GitHubCopilotOptions(TypedDict, total=False): log_level: str """CLI log level. Defaults to GITHUB_COPILOT_LOG_LEVEL environment variable.""" + copilot_home: str + """Directory where the CLI stores session state, configuration, and other + persistent data. Defaults to ~/.copilot when not set. Only applicable when + the SDK spawns the CLI process (ignored when connecting to an external server + via a pre-configured client).""" + on_permission_request: PermissionHandlerType """Permission request handler. Called when Copilot requests permission to perform an action (shell, read, write, etc.). From d58a8e7c6a1a390f9bccbba05a05dea5982fb3f9 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Thu, 7 May 2026 12:52:43 -0700 Subject: [PATCH 4/4] Address PR feedback: fix resume path, remove copilot_home from Options, bump to beta.2 - Forward runtime_options through _resume_session (fixes silent drop of instruction_directories/model/etc on resumed sessions) - Remove copilot_home from GitHubCopilotOptions (client-level setting only consumed at startup, not per-call) - Bump github-copilot-sdk from 1.0.0b1 to 1.0.0b2 - Add test for instruction_directories override on resumed sessions - Update existing resume test to match new _resume_session signature Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_github_copilot/_agent.py | 40 +++++++++++++------ python/packages/github_copilot/pyproject.toml | 2 +- .../tests/test_github_copilot_agent.py | 26 ++++++++++++ python/uv.lock | 16 ++++---- 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index cee60dea7f..49d7f0f6ee 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -174,12 +174,6 @@ class GitHubCopilotOptions(TypedDict, total=False): log_level: str """CLI log level. Defaults to GITHUB_COPILOT_LOG_LEVEL environment variable.""" - copilot_home: str - """Directory where the CLI stores session state, configuration, and other - persistent data. Defaults to ~/.copilot when not set. Only applicable when - the SDK spawns the CLI process (ignored when connecting to an external server - via a pre-configured client).""" - on_permission_request: PermissionHandlerType """Permission request handler. Called when Copilot requests permission to perform an action (shell, read, write, etc.). @@ -866,7 +860,7 @@ async def _get_or_create_session( try: if agent_session.service_session_id: - return await self._resume_session(agent_session.service_session_id, streaming) + return await self._resume_session(agent_session.service_session_id, streaming, runtime_options) session = await self._create_session(streaming, runtime_options) agent_session.service_session_id = session.session_id @@ -910,21 +904,43 @@ async def _create_session( instruction_directories=instruction_directories, ) - async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSession: - """Resume an existing Copilot session by ID.""" + async def _resume_session( + self, + session_id: str, + streaming: bool, + runtime_options: dict[str, Any] | None = None, + ) -> CopilotSession: + """Resume an existing Copilot session by ID. + + Args: + session_id: The session ID to resume. + streaming: Whether to enable streaming for the session. + runtime_options: Runtime options that take precedence over default_options. + """ if not self._client: raise RuntimeError("GitHub Copilot client not initialized. Call start() first.") - permission_handler: PermissionHandlerType = self._permission_handler or _deny_all_permissions + opts = runtime_options or {} + 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 + provider = opts.get("provider") or self._provider or None + instruction_directories = opts.get("instruction_directories", self._instruction_directories) tools = self._prepare_tools(self._tools) if self._tools else None return await self._client.resume_session( session_id, on_permission_request=permission_handler, streaming=streaming, + model=model or None, + system_message=system_message or None, tools=tools or None, - mcp_servers=self._mcp_servers or None, - provider=self._provider or None, + mcp_servers=mcp_servers or None, + provider=provider or None, + instruction_directories=instruction_directories, ) diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 3e6f4f75b2..0abab27e7b 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.2.2,<2", - "github-copilot-sdk>=1.0.0b1,<=1.0.0b1; python_version >= '3.11'", + "github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'", ] [tool.uv] diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index 65c2ef7d48..321ca9880e 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -944,9 +944,12 @@ async def test_session_resumed_for_same_session( mock_session.session_id, on_permission_request=unittest.mock.ANY, streaming=unittest.mock.ANY, + model=unittest.mock.ANY, + system_message=unittest.mock.ANY, tools=unittest.mock.ANY, mcp_servers=unittest.mock.ANY, provider=unittest.mock.ANY, + instruction_directories=unittest.mock.ANY, ) async def test_session_config_includes_model( @@ -1149,6 +1152,29 @@ async def test_instruction_directories_empty_list_clears_defaults( config = call_args.kwargs assert config["instruction_directories"] == [] + async def test_instruction_directories_override_on_resumed_session( + self, + mock_client: MagicMock, + mock_session: MagicMock, + ) -> None: + """Test that instruction_directories override works on resumed sessions.""" + agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( + client=mock_client, + default_options={"instruction_directories": ["/default/path"]}, + ) + await agent.start() + + # Simulate a session that already has a service_session_id (resume path) + session = AgentSession() + session.service_session_id = "existing-session-id" + + runtime_options: GitHubCopilotOptions = {"instruction_directories": ["/override/path"]} + await agent._get_or_create_session(session, runtime_options=runtime_options) # type: ignore + + call_args = mock_client.resume_session.call_args + config = call_args.kwargs + assert config["instruction_directories"] == ["/override/path"] + class TestGitHubCopilotAgentMCPServers: """Test cases for MCP server configuration.""" diff --git a/python/uv.lock b/python/uv.lock index 53b9062138..f69f8dbba0 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -598,7 +598,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b1,>=1.0.0b1" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, ] [[package]] @@ -2514,19 +2514,19 @@ wheels = [ [[package]] name = "github-copilot-sdk" -version = "1.0.0b1" +version = "1.0.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "python-dateutil", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/78/8d/d3a67c2b0625bd7e676e88243e83d4a3e946079361cc7343ba720887aedd/github_copilot_sdk-1.0.0b1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:64518586314c40ed56753f638b5a6ea61c5178c338ca74246577ff5703f27772", size = 59211810, upload-time = "2026-05-04T12:53:03.519Z" }, - { url = "https://files.pythonhosted.org/packages/54/b0/eb0402143756b97025abf632e06a54b4c22490be453c25875fffb9f6e757/github_copilot_sdk-1.0.0b1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34ee3de569b02c5ef70da8a8c98438f7436dd35740c2f0f8f1b6c2d67dacb6cb", size = 55957273, upload-time = "2026-05-04T12:53:08.092Z" }, - { url = "https://files.pythonhosted.org/packages/db/8e/0dfd7cdf3216381af41e9f305e7c24e4aad5047cd187458400ddea901709/github_copilot_sdk-1.0.0b1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:bd724e12a3bdcb06d3279782e7fc6476543eb465e1461f7435f40704b082fe0a", size = 62044108, upload-time = "2026-05-04T12:53:13.388Z" }, - { url = "https://files.pythonhosted.org/packages/4d/76/484ef06aea1f27bb04aedc538115dae93c50b989b5df1a8b0d974f40b70e/github_copilot_sdk-1.0.0b1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:d16e917ebce34c4b615f409d9c894575130727a87a8aed25cc3fd550d547c523", size = 60273559, upload-time = "2026-05-04T12:53:19.248Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5f/f7d3296221dd600d3dee73c3560bce8cd43e341be58bbbd751030a8e21c3/github_copilot_sdk-1.0.0b1-py3-none-win_amd64.whl", hash = "sha256:e32327ca871597b408379db43d9737789d15185bf6ded034660c396a74141723", size = 54673005, upload-time = "2026-05-04T12:53:24.551Z" }, - { url = "https://files.pythonhosted.org/packages/af/fa/d46acb18d8cd3d99fba3040e5a7467d0f264bf0dfe82087e4addfd0f1834/github_copilot_sdk-1.0.0b1-py3-none-win_arm64.whl", hash = "sha256:e2919074ffcbbf4308f6095bf5b50def6d0d5b154af72c869a3d268cfe59a93a", size = 52475405, upload-time = "2026-05-04T12:53:29.422Z" }, + { url = "https://files.pythonhosted.org/packages/82/fe/2cb98d4b9f57f8062ea72775bde72aed1958305016753f7296398e0ceb45/github_copilot_sdk-1.0.0b2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:1b5941d8b6e3d94d42a5bec6607a26f562e6535d5c981089d23d3d224b94601c", size = 67061619, upload-time = "2026-05-06T20:02:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/57/45/76567821b2d36f81e6bca78c98d265e2762733f765fa51d69602b7f81867/github_copilot_sdk-1.0.0b2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b8f6a087a0cf02bb0d33976e8f8c009578d84d701a0b28d52051304791ac70", size = 63790955, upload-time = "2026-05-06T20:02:12.354Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/684b0da0b1207a2bdf025c22ee075d34a1736d61a4973651035d4fd4d8dc/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f403638c11b82bddb81c94675fc4e8014a1bb2e86a679a39fa167dcc3ad5416a", size = 69538664, upload-time = "2026-05-06T20:02:16.363Z" }, + { url = "https://files.pythonhosted.org/packages/57/1d/80d88ecf83683535d1a16d4817f1683db3b125f52a924ebdfe9764f5e4c3/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:433d16bb31171fee8d3a5b70259c527f63b297e83a8f8761ae1f16f14d641f32", size = 68163648, upload-time = "2026-05-06T20:02:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/32/d3/b72aa2fbb3194b50b53e8cb1484f5606a1f8eedcdb0bfb5747da52079553/github_copilot_sdk-1.0.0b2-py3-none-win_amd64.whl", hash = "sha256:a6e9782dae4c3c2ab3527b45bb5de0f61998104c10e9ff64698280eaf37ab5dd", size = 62649144, upload-time = "2026-05-06T20:02:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e2/be95b8ea0ac11d1ca474e28a59284f4e395c2710734eadfb657f5de8ace2/github_copilot_sdk-1.0.0b2-py3-none-win_arm64.whl", hash = "sha256:2e97d0ce4bad67dc5929091cb429e7bbae7d4643e4908a6af256a41439000740", size = 60374365, upload-time = "2026-05-06T20:02:29.02Z" }, ] [[package]]