Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/packages/core/agent_framework/github/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- GitHubCopilotAgent
- GitHubCopilotOptions
- GitHubCopilotSettings
- RawGitHubCopilotAgent
"""

import importlib
Expand All @@ -18,6 +19,7 @@
"GitHubCopilotAgent": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
"GitHubCopilotOptions": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
"GitHubCopilotSettings": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
"RawGitHubCopilotAgent": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
}


Expand Down
2 changes: 2 additions & 0 deletions python/packages/core/agent_framework/github/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ from agent_framework_github_copilot import (
GitHubCopilotAgent,
GitHubCopilotOptions,
GitHubCopilotSettings,
RawGitHubCopilotAgent,
)

__all__ = [
"GitHubCopilotAgent",
"GitHubCopilotOptions",
"GitHubCopilotSettings",
"RawGitHubCopilotAgent",
]
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import importlib.metadata

from ._agent import GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings
from ._agent import GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings, RawGitHubCopilotAgent

try:
__version__ = importlib.metadata.version(__name__)
Expand All @@ -13,5 +13,6 @@
"GitHubCopilotAgent",
"GitHubCopilotOptions",
"GitHubCopilotSettings",
"RawGitHubCopilotAgent",
"__version__",
]
164 changes: 144 additions & 20 deletions python/packages/github_copilot/agent_framework_github_copilot/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
from collections.abc import AsyncIterable, Awaitable, Callable, MutableMapping, Sequence
from typing import Any, ClassVar, Generic, Literal, TypedDict, overload

if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
else:
from typing_extensions import Self # pragma: no cover

from agent_framework import (
AgentMiddlewareLayer,
AgentMiddlewareTypes,
AgentResponse,
AgentResponseUpdate,
Expand All @@ -27,6 +33,7 @@
from agent_framework._tools import FunctionTool, ToolTypes
from agent_framework._types import AgentRunInputs, normalize_tools
from agent_framework.exceptions import AgentException
from agent_framework.observability import AgentTelemetryLayer

try:
from copilot import CopilotClient, CopilotSession, SubprocessConfig
Expand Down Expand Up @@ -135,8 +142,11 @@ class GitHubCopilotOptions(TypedDict, total=False):
)


class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
"""A GitHub Copilot Agent.
class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
"""A GitHub Copilot Agent without telemetry layers.

This is the core GitHub Copilot agent implementation without OpenTelemetry instrumentation.
For most use cases, prefer :class:`GitHubCopilotAgent` which includes telemetry support.

This agent wraps the GitHub Copilot SDK to provide Copilot agentic capabilities
within the Agent Framework. It supports both streaming and non-streaming responses,
Expand All @@ -149,30 +159,19 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):

.. code-block:: python

async with GitHubCopilotAgent() as agent:
async with RawGitHubCopilotAgent() as agent:
response = await agent.run("Hello, world!")
print(response)

With explicitly typed options:

.. code-block:: python

from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions
from agent_framework_github_copilot import RawGitHubCopilotAgent, GitHubCopilotOptions

agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
agent: RawGitHubCopilotAgent[GitHubCopilotOptions] = RawGitHubCopilotAgent(
default_options={"model": "claude-sonnet-4", "timeout": 120}
)

With tools:

.. code-block:: python

def get_weather(city: str) -> str:
return f"Weather in {city} is sunny"


async with GitHubCopilotAgent(tools=[get_weather]) as agent:
response = await agent.run("What's the weather in Seattle?")
"""

AGENT_PROVIDER_NAME: ClassVar[str] = "github.copilot"
Expand Down Expand Up @@ -200,9 +199,9 @@ def __init__(
Keyword Args:
client: Optional pre-configured CopilotClient instance. If not provided,
a new client will be created using the other parameters.
id: ID of the GitHubCopilotAgent.
name: Name of the GitHubCopilotAgent.
description: Description of the GitHubCopilotAgent.
id: ID of the RawGitHubCopilotAgent.
name: Name of the RawGitHubCopilotAgent.
description: Description of the RawGitHubCopilotAgent.
context_providers: Context Providers, to be used by the agent.
middleware: Agent middleware used by the agent.
tools: Tools to use for the agent. Can be functions
Expand Down Expand Up @@ -258,7 +257,7 @@ def __init__(
self._default_options = opts
self._started = False

async def __aenter__(self) -> GitHubCopilotAgent[OptionsT]:
async def __aenter__(self) -> Self:
"""Start the agent when entering async context."""
await self.start()
return self
Expand Down Expand Up @@ -308,14 +307,30 @@ async def stop(self) -> None:

self._started = False

@property
def default_options(self) -> dict[str, Any]:
"""Expose default options including model from settings.

Returns a merged dict of ``_default_options`` with the resolved ``model``
from settings injected under the ``model`` key. This is read by
:class:`AgentTelemetryLayer` to include the model name in span attributes.
"""
opts = dict(self._default_options)
model = self._settings.get("model")
if model:
opts["model"] = model
return opts
Comment thread
droideronline marked this conversation as resolved.

@overload
def run(
self,
messages: AgentRunInputs | None = None,
*,
stream: Literal[False] = False,
session: AgentSession | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
options: OptionsT | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse]: ...

@overload
Expand All @@ -325,7 +340,9 @@ def run(
*,
stream: Literal[True],
session: AgentSession | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
options: OptionsT | None = None,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ...

def run(
Expand All @@ -334,7 +351,9 @@ def run(
*,
stream: bool = False,
session: AgentSession | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
options: OptionsT | None = None,
**kwargs: Any, # type: ignore[override]
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
"""Get a response from the agent.

Expand All @@ -348,7 +367,12 @@ def run(
Keyword Args:
stream: Whether to stream the response. Defaults to False.
session: The conversation session associated with the message(s).
middleware: Not used by this agent directly. Accepted for interface
compatibility; pass middleware via :class:`GitHubCopilotAgent` which
forwards it through :class:`AgentTelemetryLayer`.
options: Runtime options (model, timeout, etc.).
kwargs: Additional keyword arguments for compatibility with the shared agent
interface (e.g. compaction_strategy, tokenizer). Not used by this agent.

Returns:
When stream=False: An Awaitable[AgentResponse].
Expand All @@ -357,6 +381,12 @@ def run(
Raises:
Comment thread
droideronline marked this conversation as resolved.
AgentException: If the request fails.
"""
if middleware:
logger.warning(
"Per-run middleware is not supported by RawGitHubCopilotAgent: the GitHub Copilot SDK "
"handles tool execution internally, so chat/function middleware cannot be injected into "
"the tool call path. Use agent-level middleware via the GitHubCopilotAgent constructor instead."
)
if stream:
ctx_holder: dict[str, Any] = {}

Expand Down Expand Up @@ -767,3 +797,97 @@ async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSess
mcp_servers=self._mcp_servers or None,
provider=self._provider or None,
)


class GitHubCopilotAgent( # type: ignore[misc]
AgentMiddlewareLayer,
AgentTelemetryLayer,
RawGitHubCopilotAgent[OptionsT],
Generic[OptionsT],
):
"""A GitHub Copilot Agent with full middleware and telemetry support.

This is the recommended agent class for most use cases. It includes
middleware support and OpenTelemetry-based telemetry for observability,
with middleware running outside the telemetry span so middleware execution
time is not captured in traces. For a minimal implementation without these
layers, use :class:`RawGitHubCopilotAgent`.

Examples:
Basic usage:

.. code-block:: python

async with GitHubCopilotAgent() as agent:
response = await agent.run("Hello, world!")
print(response)

With explicitly typed options:

.. code-block:: python

from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions

agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
default_options={"model": "claude-sonnet-4-5", "timeout": 120}
)

With observability:

.. code-block:: python

from agent_framework.observability import configure_otel_providers

configure_otel_providers()
async with GitHubCopilotAgent() as agent:
response = await agent.run("Hello, world!")
"""
Comment thread
droideronline marked this conversation as resolved.

def __init__(
self,
instructions: str | None = None,
*,
client: CopilotClient | None = None,
id: str | None = None,
name: str | None = None,
description: str | None = None,
context_providers: Sequence[ContextProvider] | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsT | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize a GitHub Copilot Agent with full middleware and telemetry.

Args:
instructions: System message for the agent.

Keyword Args:
client: Optional pre-configured CopilotClient instance. If not provided,
a new client will be created using the other parameters.
id: ID of the agent.
name: Name of the agent.
description: Description of the agent.
context_providers: Context providers to be used by the agent.
middleware: Agent middleware used by the agent.
tools: Tools to use for the agent. Can be functions or tool definition dicts.
These are converted to Copilot SDK tools internally.
default_options: Default options for the agent. Can include cli_path, model,
timeout, log_level, etc.
env_file_path: Optional path to .env file for loading configuration.
env_file_encoding: Encoding of the .env file, defaults to 'utf-8'.
"""
super().__init__(
instructions,
client=client,
id=id,
name=name,
description=description,
context_providers=context_providers,
middleware=middleware,
tools=tools,
default_options=default_options,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
25 changes: 25 additions & 0 deletions python/packages/github_copilot/tests/test_github_copilot_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,31 @@ def test_instructions_parameter_defaults_to_append_mode(self) -> None:
"content": "Direct instructions",
}

def test_default_options_includes_model_for_telemetry(self) -> None:
"""Test that default_options merges model from settings for AgentTelemetryLayer span attributes."""
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
default_options={"model": "claude-sonnet-4-5", "timeout": 120}
)
opts = agent.default_options
assert opts["model"] == "claude-sonnet-4-5"
Comment thread
droideronline marked this conversation as resolved.
assert "timeout" not in opts # timeout is extracted into _settings, not returned in default_options

def test_default_options_without_model_configured(self) -> None:
"""Test that default_options works correctly when no model is configured."""
agent = GitHubCopilotAgent(instructions="Helper")
opts = agent.default_options
assert "model" not in opts
assert opts.get("system_message") == {"mode": "append", "content": "Helper"}

def test_default_options_returns_independent_copy(self) -> None:
"""Test that mutating the returned dict does not affect internal state."""
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
default_options={"model": "gpt-5.1-mini"}
)
opts = agent.default_options
opts["model"] = "mutated"
assert agent._settings.get("model") == "gpt-5.1-mini"


class TestGitHubCopilotAgentLifecycle:
"""Test cases for agent lifecycle management."""
Expand Down
16 changes: 16 additions & 0 deletions python/samples/02-agents/providers/github_copilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ The following environment variables can be configured:
| `GITHUB_COPILOT_TIMEOUT` | Request timeout in seconds | `60` |
| `GITHUB_COPILOT_LOG_LEVEL` | CLI log level | `info` |

## Observability

`GitHubCopilotAgent` has OpenTelemetry tracing built-in. To enable it, call `configure_otel_providers()` before running the agent:

```python
from agent_framework.observability import configure_otel_providers
from agent_framework.github import GitHubCopilotAgent

configure_otel_providers(enable_console_exporters=True)

async with GitHubCopilotAgent() as agent:
response = await agent.run("Hello!")
```

See the [observability samples](../../../02-agents/observability/) for full examples with OTLP exporters.

## Examples

| File | Description |
Expand Down
Loading