Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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__",
]
159 changes: 139 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 @@ -10,6 +10,7 @@
from typing import Any, ClassVar, Generic, Literal, TypedDict, overload

from agent_framework import (
AgentMiddlewareLayer,
AgentMiddlewareTypes,
AgentResponse,
AgentResponseUpdate,
Expand All @@ -27,6 +28,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 +137,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 +154,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 +194,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 +252,7 @@ def __init__(
self._default_options = opts
self._started = False

async def __aenter__(self) -> GitHubCopilotAgent[OptionsT]:
async def __aenter__(self) -> RawGitHubCopilotAgent[OptionsT]:
Comment thread
droideronline marked this conversation as resolved.
Outdated
"""Start the agent when entering async context."""
await self.start()
return self
Expand Down Expand Up @@ -308,14 +302,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 +335,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 +346,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 +362,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 +376,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 +792,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