Skip to content
Open
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__",
]
153 changes: 133 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 @@ -27,6 +27,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 @@ -129,8 +130,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 @@ -143,30 +147,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 @@ -194,9 +187,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 @@ -250,7 +243,7 @@ def __init__(
self._default_options = opts
self._started = False

async def __aenter__(self) -> GitHubCopilotAgent[OptionsT]:
async def __aenter__(self) -> RawGitHubCopilotAgent[OptionsT]:
"""Start the agent when entering async context."""
await self.start()
return self
Expand Down Expand Up @@ -300,14 +293,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

@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 @@ -317,7 +326,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 @@ -326,7 +337,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 @@ -340,7 +353,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 @@ -349,6 +367,7 @@ def run(
Raises:
AgentException: If the request fails.
"""
del middleware # not used; accepted for interface compatibility with AgentTelemetryLayer
if stream:
ctx_holder: dict[str, Any] = {}

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


class GitHubCopilotAgent(AgentTelemetryLayer, RawGitHubCopilotAgent[OptionsT], Generic[OptionsT]):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should probably also add the middleware layer here? and I think that is not applied properly in all agents, I will fix that, but telemetry should be inside the middleware so that the time spent on middleware is not captured by the telemtry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch - added middleware explicitly to GitHubCopilotAgent.run() overloads and forwarded it through to super().run() so it reaches AgentTelemetryLayer properly. Agree on the ordering concern - leaving that for your broader fix across agents.

"""A GitHub Copilot Agent with OpenTelemetry instrumentation.

This is the recommended agent class for most use cases. It includes
OpenTelemetry-based telemetry for observability. For a minimal
implementation without telemetry, use :class:`RawGitHubCopilotAgent`.

This agent wraps the GitHub Copilot SDK to provide Copilot agentic capabilities
within the Agent Framework. It supports both streaming and non-streaming responses,
custom tools, and session management.

The agent can be used as an async context manager to ensure proper cleanup:

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", "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!")
"""

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

@overload # type: ignore[override]
def run(
self,
messages: AgentRunInputs | None = None,
*,
stream: Literal[True],
session: AgentSession | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
options: OptionsT | None = None,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ...

def run( # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[override]
self,
messages: AgentRunInputs | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
options: OptionsT | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
"""Run the GitHub Copilot agent with telemetry enabled."""
from typing import cast

super_run = cast(
"Callable[..., Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]]",
super().run,
)
return super_run(
messages=messages,
stream=stream,
session=session,
middleware=middleware,
options=options,
**kwargs,
)
24 changes: 24 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,30 @@ 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"

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