diff --git a/python/packages/core/agent_framework/github/__init__.py b/python/packages/core/agent_framework/github/__init__.py index 831a838c0d..0ff654fa17 100644 --- a/python/packages/core/agent_framework/github/__init__.py +++ b/python/packages/core/agent_framework/github/__init__.py @@ -9,6 +9,7 @@ - GitHubCopilotAgent - GitHubCopilotOptions - GitHubCopilotSettings +- RawGitHubCopilotAgent """ import importlib @@ -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"), } diff --git a/python/packages/core/agent_framework/github/__init__.pyi b/python/packages/core/agent_framework/github/__init__.pyi index 567ab9490d..f7b68966cf 100644 --- a/python/packages/core/agent_framework/github/__init__.pyi +++ b/python/packages/core/agent_framework/github/__init__.pyi @@ -4,10 +4,12 @@ from agent_framework_github_copilot import ( GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings, + RawGitHubCopilotAgent, ) __all__ = [ "GitHubCopilotAgent", "GitHubCopilotOptions", "GitHubCopilotSettings", + "RawGitHubCopilotAgent", ] diff --git a/python/packages/github_copilot/agent_framework_github_copilot/__init__.py b/python/packages/github_copilot/agent_framework_github_copilot/__init__.py index 432427fd9d..56b46f8dee 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/__init__.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/__init__.py @@ -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__) @@ -13,5 +13,6 @@ "GitHubCopilotAgent", "GitHubCopilotOptions", "GitHubCopilotSettings", + "RawGitHubCopilotAgent", "__version__", ] 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 8015f561d9..b7fc709773 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -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, @@ -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 @@ -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, @@ -149,7 +159,7 @@ 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) @@ -157,22 +167,11 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): .. 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" @@ -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 @@ -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 @@ -308,6 +307,20 @@ 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, @@ -315,7 +328,9 @@ def run( *, stream: Literal[False] = False, session: AgentSession | None = None, + middleware: Sequence[AgentMiddlewareTypes] | None = None, options: OptionsT | None = None, + **kwargs: Any, ) -> Awaitable[AgentResponse]: ... @overload @@ -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( @@ -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. @@ -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]. @@ -357,6 +381,12 @@ def run( Raises: 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] = {} @@ -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!") + """ + + 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, + ) 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 9e1459db93..4d953d9c36 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -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" + 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.""" diff --git a/python/samples/02-agents/providers/github_copilot/README.md b/python/samples/02-agents/providers/github_copilot/README.md index 572ec9c444..5f8ee344d6 100644 --- a/python/samples/02-agents/providers/github_copilot/README.md +++ b/python/samples/02-agents/providers/github_copilot/README.md @@ -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 |