Skip to content

Conversation

@dmytrostruk
Copy link
Member

@dmytrostruk dmytrostruk commented Jan 23, 2026

Motivation and Context

This PR adds agent-framework-github-copilot package that wraps the GitHub Copilot SDK to provide Copilot agentic capabilities within the Agent Framework.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

@dmytrostruk dmytrostruk self-assigned this Jan 23, 2026
Copilot AI review requested due to automatic review settings January 23, 2026 03:06
@dmytrostruk dmytrostruk added documentation Improvements or additions to documentation python labels Jan 23, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new Python package agent-framework-github-copilot that integrates the GitHub Copilot SDK into the Microsoft Agent Framework, enabling developers to create agents powered by GitHub Copilot with support for streaming responses, custom tools, session management, and configurable permissions for system operations.

Changes:

  • New agent-framework-github-copilot package with GithubCopilotAgent implementation
  • Support for streaming and non-streaming responses with custom function tools
  • Permission handling system for shell, file operations, MCP, and URL access
  • Comprehensive test suite and sample applications demonstrating various usage patterns
  • Integration with core package through lazy imports and stub files

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
python/uv.lock Added github-copilot-sdk dependency (v0.1.15)
python/pyproject.toml Added agent-framework-github-copilot to workspace dependencies
python/packages/core/pyproject.toml Added to 'all' extras group
python/packages/core/agent_framework/github_copilot/__init__.py Lazy import mechanism for GitHub Copilot package
python/packages/core/agent_framework/github_copilot/__init__.pyi Type stub file for IDE support
python/packages/github_copilot/pyproject.toml Package configuration and metadata
python/packages/github_copilot/LICENSE MIT license file
python/packages/github_copilot/README.md Package documentation with examples
python/packages/github_copilot/agent_framework_github_copilot/__init__.py Package exports and version management
python/packages/github_copilot/agent_framework_github_copilot/_settings.py Settings class for configuration management
python/packages/github_copilot/agent_framework_github_copilot/_agent.py Main GithubCopilotAgent implementation with streaming, tools, and sessions
python/packages/github_copilot/tests/__init__.py Test package marker
python/packages/github_copilot/tests/conftest.py Pytest fixtures for testing
python/packages/github_copilot/tests/test_github_copilot_agent.py Comprehensive unit tests for agent functionality
python/samples/getting_started/agents/github_copilot/README.md Sample documentation
python/samples/getting_started/agents/github_copilot/github_copilot_basic.py Basic usage example with tools
python/samples/getting_started/agents/github_copilot/github_copilot_with_shell.py Shell permissions example
python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py File operations example
python/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py Multiple permissions example
Comments suppressed due to low confidence (12)

python/packages/github_copilot/README.md:129

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
For more comprehensive examples, see the [Github Copilot examples](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/agents/github_copilot/) which demonstrate:

python/samples/getting_started/agents/github_copilot/github_copilot_basic.py:4

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding throughout the documentation.
Github Copilot Agent Basic Example

python/packages/github_copilot/agent_framework_github_copilot/_agent.py:316

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
            raise ServiceException(f"Failed to start Github Copilot client: {ex}") from ex

    async def stop(self) -> None:
        """Stop the Copilot client and clean up resources.

        This method destroys all active sessions created by this agent and stops
        the Copilot client. It is called automatically when using the agent as
        an async context manager.

        Note:
            Only sessions created by this agent instance (stored in self._sessions)
            are destroyed. Sessions created elsewhere are not affected.
        """
        for session in self._sessions.values():
            with contextlib.suppress(Exception):
                await session.destroy()

        self._sessions.clear()

        if self._client and self._owns_client:
            with contextlib.suppress(Exception):
                await self._client.stop()

        self._started = False

    async def run(
        self,
        messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
        *,
        thread: AgentThread | None = None,
        tools: ToolProtocol
        | Callable[..., Any]
        | MutableMapping[str, Any]
        | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
        | None = None,
        options: TOptions | None = None,
        **kwargs: Any,
    ) -> AgentResponse:
        """Get a response from the agent.

        This method returns the final result of the agent's execution
        as a single AgentResponse object. The caller is blocked until
        the final result is available.

        Args:
            messages: The message(s) to send to the agent.

        Keyword Args:
            thread: The conversation thread associated with the message(s).
            tools: Additional tools to use for this specific run.
            options: Runtime options (model, timeout, etc.).
            kwargs: Additional keyword arguments.

        Returns:
            An agent response item.

        Raises:
            ServiceException: If the request fails.
        """
        if not self._started:
            await self.start()

        if not thread:
            thread = self.get_new_thread()

        opts: dict[str, Any] = dict(options) if options else {}
        timeout = opts.pop("timeout", None) or self._settings.timeout or DEFAULT_TIMEOUT_SECONDS

        merged_tools = self._merge_tools(tools)
        session = await self._get_or_create_session(thread, merged_tools, streaming=False)
        input_messages = normalize_messages(messages)
        prompt = "\n".join([message.text for message in input_messages])

        try:
            response_event = await session.send_and_wait({"prompt": prompt}, timeout=timeout)
        except Exception as ex:
            raise ServiceException(f"Github Copilot request failed: {ex}") from ex

python/samples/getting_started/agents/github_copilot/README.md:1

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
# Github Copilot Agent Examples

python/samples/getting_started/agents/github_copilot/README.md:17

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
2. **Github Copilot Subscription**: An active Github Copilot subscription

python/packages/github_copilot/agent_framework_github_copilot/_agent.py:84

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated throughout the codebase to match GitHub's official branding.
    """A Github Copilot Agent.

python/packages/github_copilot/agent_framework_github_copilot/_agent.py:550

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
            ServiceException: If the request fails.
        """
        if not self._started:
            await self.start()

        if not thread:
            thread = self.get_new_thread()

        merged_tools = self._merge_tools(tools)
        session = await self._get_or_create_session(thread, merged_tools, streaming=True)
        input_messages = normalize_messages(messages)
        prompt = "\n".join([message.text for message in input_messages])

        import asyncio

        completion_event = asyncio.Event()
        updates: list[AgentResponseUpdate] = []
        errors: list[Exception] = []

        def event_handler(event: SessionEvent) -> None:
            if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
                delta_content = event.data.delta_content or ""
                if delta_content:
                    updates.append(
                        AgentResponseUpdate(
                            role=Role.ASSISTANT,
                            contents=[Content.from_text(delta_content)],
                            response_id=event.data.message_id,
                            message_id=event.data.message_id,
                            raw_representation=event,
                        )
                    )
            elif event.type == SessionEventType.SESSION_IDLE:
                completion_event.set()
            elif event.type == SessionEventType.SESSION_ERROR:
                error_msg = event.data.message or "Unknown error"
                errors.append(ServiceException(f"Github Copilot session error: {error_msg}"))
                completion_event.set()

        unsubscribe = session.on(event_handler)

        try:
            await session.send({"prompt": prompt})
            await completion_event.wait()

            if errors:
                raise errors[0]

            for update in updates:
                yield update
        finally:
            unsubscribe()

    def _create_permission_handler(
        self,
        allowed_permissions: Sequence[str] | None,
    ) -> PermissionHandlerType | None:
        """Create a permission handler from a list of allowed permission kinds."""
        if not allowed_permissions:
            return None

        allowed_set = set(allowed_permissions)

        def handler(
            request: PermissionRequest,
            context: dict[str, str],
        ) -> PermissionRequestResult:
            kind = request.get("kind")
            if kind in allowed_set:
                return PermissionRequestResult(kind="approved")
            return PermissionRequestResult(kind="denied-interactively-by-user")

        return handler

    def _merge_tools(
        self,
        runtime_tools: ToolProtocol
        | Callable[..., Any]
        | MutableMapping[str, Any]
        | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
        | None,
    ) -> list[ToolProtocol | MutableMapping[str, Any]]:
        """Merge runtime tools with default tools."""
        result = list(self._tools)
        result.extend(normalize_tools(runtime_tools))
        return result

    def _convert_tools_to_copilot_tools(
        self,
        tools: list[ToolProtocol | MutableMapping[str, Any]],
    ) -> list[CopilotTool]:
        """Convert Agent Framework tools to Copilot SDK tools.

        Args:
            tools: List of Agent Framework tools.

        Returns:
            List of Copilot SDK tools.
        """
        copilot_tools: list[CopilotTool] = []

        for tool in tools:
            if isinstance(tool, AIFunction):
                copilot_tools.append(self._ai_function_to_copilot_tool(tool))  # type: ignore

        return copilot_tools

    def _ai_function_to_copilot_tool(self, ai_func: AIFunction[Any, Any]) -> CopilotTool:
        """Convert an AIFunction to a Copilot SDK tool."""

        async def handler(invocation: ToolInvocation) -> ToolResult:
            args = invocation.get("arguments", {})
            try:
                if ai_func.input_model:
                    args_instance = ai_func.input_model(**args)
                    result = await ai_func.invoke(arguments=args_instance)
                else:
                    result = await ai_func.invoke(arguments=args)
                return ToolResult(
                    textResultForLlm=str(result),
                    resultType="success",
                    toolTelemetry={},
                )
            except Exception as e:
                return ToolResult(
                    textResultForLlm=f"Error: {e}",
                    resultType="failure",
                    error=str(e),
                    toolTelemetry={},
                )

        return CopilotTool(
            name=ai_func.name,
            description=ai_func.description,
            handler=handler,
            parameters=ai_func.parameters(),
        )

    async def _get_or_create_session(
        self,
        thread: AgentThread,
        tools: list[ToolProtocol | MutableMapping[str, Any]],
        streaming: bool = False,
    ) -> CopilotSession:
        """Get an existing session or create a new one for the thread.

        Args:
            thread: The conversation thread.
            tools: Tools to register with the session.
            streaming: Whether to enable streaming for the session.

        Returns:
            A CopilotSession instance.

        Raises:
            ServiceException: If the session cannot be created.
        """
        if not self._client:
            raise ServiceException("Github Copilot client not initialized. Call start() first.")

        if thread.service_thread_id and thread.service_thread_id in self._sessions:
            return self._sessions[thread.service_thread_id]

        config: SessionConfig = {"streaming": streaming}

        if self._settings.model:
            config["model"] = self._settings.model  # type: ignore[typeddict-item]

        if self._instructions:
            config["system_message"] = {"mode": "append", "content": self._instructions}

        if tools:
            config["tools"] = self._convert_tools_to_copilot_tools(tools)

        if self._permission_handler:
            config["on_permission_request"] = self._permission_handler

        try:
            session = await self._client.create_session(config)
            thread.service_thread_id = session.session_id
            self._sessions[session.session_id] = session
            return session
        except Exception as ex:
            raise ServiceException(f"Failed to create Github Copilot session: {ex}") from ex

python/packages/github_copilot/README.md:11

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
## Github Copilot Agent

The Github Copilot agent enables integration with Github Copilot, allowing you to interact with Copilot's agentic capabilities through the Agent Framework.

python/packages/github_copilot/README.md:18

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
Before using the Github Copilot agent, you need:

1. **Github Copilot CLI**: The Copilot CLI must be installed and authenticated
2. **Github Copilot Subscription**: An active Github Copilot subscription

python/samples/getting_started/agents/github_copilot/github_copilot_with_shell.py:4

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
Github Copilot Agent with Shell Permissions

python/samples/getting_started/agents/github_copilot/github_copilot_with_file_operations.py:53

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
    print("=== Github Copilot Agent with File Operation Permissions ===\n")

python/samples/getting_started/agents/github_copilot/github_copilot_with_multiple_permissions.py:27

  • The correct branding is "GitHub" (with capital H), not "Github". This should be updated to match GitHub's official branding.
    print("=== Github Copilot Agent with Multiple Permissions ===\n")

@markwallace-microsoft
Copy link
Member

markwallace-microsoft commented Jan 23, 2026

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
TOTAL17029255884% 
report-only-changed-files is enabled. No files were changed during this commit :)

Python Unit Test Overview

Tests Skipped Failures Errors Time
3361 213 💤 0 ❌ 0 🔥 1m 6s ⏱️

Copy link
Member

@eavanvalkenburg eavanvalkenburg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll review the implementation later, but loving this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants