Skip to content

Commit 63c0a51

Browse files
Python: Add OpenTelemetry integration for GitHubCopilotAgent (microsoft#5142)
* Python: Add OpenTelemetry integration for GitHubCopilotAgent - Split GitHubCopilotAgent into RawGitHubCopilotAgent (core, no OTel) and GitHubCopilotAgent(AgentTelemetryLayer, RawGitHubCopilotAgent) with tracing - Add default_options property to expose model for span attributes - Export RawGitHubCopilotAgent from all public namespaces - Add github_copilot_with_observability.py sample and update README * Python: Fix OTEL_SERVICE_NAME default in GitHub Copilot README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Python: Add unit tests for RawGitHubCopilotAgent.default_options property * Python: Address review feedback on GitHubCopilotAgent OTel integration - Add middleware param to GitHubCopilotAgent.run() overloads so per-call middleware is explicitly forwarded through AgentTelemetryLayer - Remove github_copilot_with_observability.py sample per feedback; replace with inline snippet + link to observability samples in README * Python: Address review feedback on log_level and session kwargs typing - Add middleware param to RawGitHubCopilotAgent.run() overloads for interface compatibility with AgentTelemetryLayer - Fix import in README observability snippet to use agent_framework.github * Python: Add AgentMiddlewareLayer to GitHubCopilotAgent MRO Follow FoundryAgent pattern: AgentMiddlewareLayer runs outside the telemetry span so middleware execution time is not captured in traces. Overloads removed as AgentMiddlewareLayer.run() handles dispatch via MRO. * Python: Add explicit __init__ to GitHubCopilotAgent for auto-complete and docstrings * Python: Address review feedback on middleware warning and test assertions - Add assert "timeout" not in opts to test_default_options_includes_model_for_telemetry to document the intentional asymmetry where timeout is extracted into _settings and not returned in default_options. - Replace silent del middleware with a logged warning when per-run middleware is passed to RawGitHubCopilotAgent, making it clear that the GitHub Copilot SDK handles tool execution internally and chat/function middleware cannot be injected. * Python: Use Self for __aenter__ return type in RawGitHubCopilotAgent Address review feedback: use typing.Self (3.11+) / typing_extensions.Self (3.10) for __aenter__ so subclasses like GitHubCopilotAgent get the correct return type from async context manager usage. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b00465d commit 63c0a51

6 files changed

Lines changed: 191 additions & 21 deletions

File tree

‎python/packages/core/agent_framework/github/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- GitHubCopilotAgent
1010
- GitHubCopilotOptions
1111
- GitHubCopilotSettings
12+
- RawGitHubCopilotAgent
1213
"""
1314

1415
import importlib
@@ -18,6 +19,7 @@
1819
"GitHubCopilotAgent": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
1920
"GitHubCopilotOptions": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
2021
"GitHubCopilotSettings": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
22+
"RawGitHubCopilotAgent": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
2123
}
2224

2325

‎python/packages/core/agent_framework/github/__init__.pyi‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ from agent_framework_github_copilot import (
44
GitHubCopilotAgent,
55
GitHubCopilotOptions,
66
GitHubCopilotSettings,
7+
RawGitHubCopilotAgent,
78
)
89

910
__all__ = [
1011
"GitHubCopilotAgent",
1112
"GitHubCopilotOptions",
1213
"GitHubCopilotSettings",
14+
"RawGitHubCopilotAgent",
1315
]

‎python/packages/github_copilot/agent_framework_github_copilot/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import importlib.metadata
44

5-
from ._agent import GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings
5+
from ._agent import GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings, RawGitHubCopilotAgent
66

77
try:
88
__version__ = importlib.metadata.version(__name__)
@@ -13,5 +13,6 @@
1313
"GitHubCopilotAgent",
1414
"GitHubCopilotOptions",
1515
"GitHubCopilotSettings",
16+
"RawGitHubCopilotAgent",
1617
"__version__",
1718
]

‎python/packages/github_copilot/agent_framework_github_copilot/_agent.py‎

Lines changed: 144 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
from collections.abc import AsyncIterable, Awaitable, Callable, MutableMapping, Sequence
1010
from typing import Any, ClassVar, Generic, Literal, TypedDict, overload
1111

12+
if sys.version_info >= (3, 11):
13+
from typing import Self # pragma: no cover
14+
else:
15+
from typing_extensions import Self # pragma: no cover
16+
1217
from agent_framework import (
18+
AgentMiddlewareLayer,
1319
AgentMiddlewareTypes,
1420
AgentResponse,
1521
AgentResponseUpdate,
@@ -27,6 +33,7 @@
2733
from agent_framework._tools import FunctionTool, ToolTypes
2834
from agent_framework._types import AgentRunInputs, normalize_tools
2935
from agent_framework.exceptions import AgentException
36+
from agent_framework.observability import AgentTelemetryLayer
3037

3138
try:
3239
from copilot import CopilotClient, CopilotSession, SubprocessConfig
@@ -135,8 +142,11 @@ class GitHubCopilotOptions(TypedDict, total=False):
135142
)
136143

137144

138-
class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
139-
"""A GitHub Copilot Agent.
145+
class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
146+
"""A GitHub Copilot Agent without telemetry layers.
147+
148+
This is the core GitHub Copilot agent implementation without OpenTelemetry instrumentation.
149+
For most use cases, prefer :class:`GitHubCopilotAgent` which includes telemetry support.
140150
141151
This agent wraps the GitHub Copilot SDK to provide Copilot agentic capabilities
142152
within the Agent Framework. It supports both streaming and non-streaming responses,
@@ -149,30 +159,19 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
149159
150160
.. code-block:: python
151161
152-
async with GitHubCopilotAgent() as agent:
162+
async with RawGitHubCopilotAgent() as agent:
153163
response = await agent.run("Hello, world!")
154164
print(response)
155165
156166
With explicitly typed options:
157167
158168
.. code-block:: python
159169
160-
from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions
170+
from agent_framework_github_copilot import RawGitHubCopilotAgent, GitHubCopilotOptions
161171
162-
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
172+
agent: RawGitHubCopilotAgent[GitHubCopilotOptions] = RawGitHubCopilotAgent(
163173
default_options={"model": "claude-sonnet-4", "timeout": 120}
164174
)
165-
166-
With tools:
167-
168-
.. code-block:: python
169-
170-
def get_weather(city: str) -> str:
171-
return f"Weather in {city} is sunny"
172-
173-
174-
async with GitHubCopilotAgent(tools=[get_weather]) as agent:
175-
response = await agent.run("What's the weather in Seattle?")
176175
"""
177176

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

261-
async def __aenter__(self) -> GitHubCopilotAgent[OptionsT]:
260+
async def __aenter__(self) -> Self:
262261
"""Start the agent when entering async context."""
263262
await self.start()
264263
return self
@@ -308,14 +307,30 @@ async def stop(self) -> None:
308307

309308
self._started = False
310309

310+
@property
311+
def default_options(self) -> dict[str, Any]:
312+
"""Expose default options including model from settings.
313+
314+
Returns a merged dict of ``_default_options`` with the resolved ``model``
315+
from settings injected under the ``model`` key. This is read by
316+
:class:`AgentTelemetryLayer` to include the model name in span attributes.
317+
"""
318+
opts = dict(self._default_options)
319+
model = self._settings.get("model")
320+
if model:
321+
opts["model"] = model
322+
return opts
323+
311324
@overload
312325
def run(
313326
self,
314327
messages: AgentRunInputs | None = None,
315328
*,
316329
stream: Literal[False] = False,
317330
session: AgentSession | None = None,
331+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
318332
options: OptionsT | None = None,
333+
**kwargs: Any,
319334
) -> Awaitable[AgentResponse]: ...
320335

321336
@overload
@@ -325,7 +340,9 @@ def run(
325340
*,
326341
stream: Literal[True],
327342
session: AgentSession | None = None,
343+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
328344
options: OptionsT | None = None,
345+
**kwargs: Any,
329346
) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ...
330347

331348
def run(
@@ -334,7 +351,9 @@ def run(
334351
*,
335352
stream: bool = False,
336353
session: AgentSession | None = None,
354+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
337355
options: OptionsT | None = None,
356+
**kwargs: Any, # type: ignore[override]
338357
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
339358
"""Get a response from the agent.
340359
@@ -348,7 +367,12 @@ def run(
348367
Keyword Args:
349368
stream: Whether to stream the response. Defaults to False.
350369
session: The conversation session associated with the message(s).
370+
middleware: Not used by this agent directly. Accepted for interface
371+
compatibility; pass middleware via :class:`GitHubCopilotAgent` which
372+
forwards it through :class:`AgentTelemetryLayer`.
351373
options: Runtime options (model, timeout, etc.).
374+
kwargs: Additional keyword arguments for compatibility with the shared agent
375+
interface (e.g. compaction_strategy, tokenizer). Not used by this agent.
352376
353377
Returns:
354378
When stream=False: An Awaitable[AgentResponse].
@@ -357,6 +381,12 @@ def run(
357381
Raises:
358382
AgentException: If the request fails.
359383
"""
384+
if middleware:
385+
logger.warning(
386+
"Per-run middleware is not supported by RawGitHubCopilotAgent: the GitHub Copilot SDK "
387+
"handles tool execution internally, so chat/function middleware cannot be injected into "
388+
"the tool call path. Use agent-level middleware via the GitHubCopilotAgent constructor instead."
389+
)
360390
if stream:
361391
ctx_holder: dict[str, Any] = {}
362392

@@ -767,3 +797,97 @@ async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSess
767797
mcp_servers=self._mcp_servers or None,
768798
provider=self._provider or None,
769799
)
800+
801+
802+
class GitHubCopilotAgent( # type: ignore[misc]
803+
AgentMiddlewareLayer,
804+
AgentTelemetryLayer,
805+
RawGitHubCopilotAgent[OptionsT],
806+
Generic[OptionsT],
807+
):
808+
"""A GitHub Copilot Agent with full middleware and telemetry support.
809+
810+
This is the recommended agent class for most use cases. It includes
811+
middleware support and OpenTelemetry-based telemetry for observability,
812+
with middleware running outside the telemetry span so middleware execution
813+
time is not captured in traces. For a minimal implementation without these
814+
layers, use :class:`RawGitHubCopilotAgent`.
815+
816+
Examples:
817+
Basic usage:
818+
819+
.. code-block:: python
820+
821+
async with GitHubCopilotAgent() as agent:
822+
response = await agent.run("Hello, world!")
823+
print(response)
824+
825+
With explicitly typed options:
826+
827+
.. code-block:: python
828+
829+
from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions
830+
831+
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
832+
default_options={"model": "claude-sonnet-4-5", "timeout": 120}
833+
)
834+
835+
With observability:
836+
837+
.. code-block:: python
838+
839+
from agent_framework.observability import configure_otel_providers
840+
841+
configure_otel_providers()
842+
async with GitHubCopilotAgent() as agent:
843+
response = await agent.run("Hello, world!")
844+
"""
845+
846+
def __init__(
847+
self,
848+
instructions: str | None = None,
849+
*,
850+
client: CopilotClient | None = None,
851+
id: str | None = None,
852+
name: str | None = None,
853+
description: str | None = None,
854+
context_providers: Sequence[ContextProvider] | None = None,
855+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
856+
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
857+
default_options: OptionsT | None = None,
858+
env_file_path: str | None = None,
859+
env_file_encoding: str | None = None,
860+
) -> None:
861+
"""Initialize a GitHub Copilot Agent with full middleware and telemetry.
862+
863+
Args:
864+
instructions: System message for the agent.
865+
866+
Keyword Args:
867+
client: Optional pre-configured CopilotClient instance. If not provided,
868+
a new client will be created using the other parameters.
869+
id: ID of the agent.
870+
name: Name of the agent.
871+
description: Description of the agent.
872+
context_providers: Context providers to be used by the agent.
873+
middleware: Agent middleware used by the agent.
874+
tools: Tools to use for the agent. Can be functions or tool definition dicts.
875+
These are converted to Copilot SDK tools internally.
876+
default_options: Default options for the agent. Can include cli_path, model,
877+
timeout, log_level, etc.
878+
env_file_path: Optional path to .env file for loading configuration.
879+
env_file_encoding: Encoding of the .env file, defaults to 'utf-8'.
880+
"""
881+
super().__init__(
882+
instructions,
883+
client=client,
884+
id=id,
885+
name=name,
886+
description=description,
887+
context_providers=context_providers,
888+
middleware=middleware,
889+
tools=tools,
890+
default_options=default_options,
891+
env_file_path=env_file_path,
892+
env_file_encoding=env_file_encoding,
893+
)

‎python/packages/github_copilot/tests/test_github_copilot_agent.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,31 @@ def test_instructions_parameter_defaults_to_append_mode(self) -> None:
189189
"content": "Direct instructions",
190190
}
191191

192+
def test_default_options_includes_model_for_telemetry(self) -> None:
193+
"""Test that default_options merges model from settings for AgentTelemetryLayer span attributes."""
194+
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
195+
default_options={"model": "claude-sonnet-4-5", "timeout": 120}
196+
)
197+
opts = agent.default_options
198+
assert opts["model"] == "claude-sonnet-4-5"
199+
assert "timeout" not in opts # timeout is extracted into _settings, not returned in default_options
200+
201+
def test_default_options_without_model_configured(self) -> None:
202+
"""Test that default_options works correctly when no model is configured."""
203+
agent = GitHubCopilotAgent(instructions="Helper")
204+
opts = agent.default_options
205+
assert "model" not in opts
206+
assert opts.get("system_message") == {"mode": "append", "content": "Helper"}
207+
208+
def test_default_options_returns_independent_copy(self) -> None:
209+
"""Test that mutating the returned dict does not affect internal state."""
210+
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
211+
default_options={"model": "gpt-5.1-mini"}
212+
)
213+
opts = agent.default_options
214+
opts["model"] = "mutated"
215+
assert agent._settings.get("model") == "gpt-5.1-mini"
216+
192217

193218
class TestGitHubCopilotAgentLifecycle:
194219
"""Test cases for agent lifecycle management."""

‎python/samples/02-agents/providers/github_copilot/README.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ The following environment variables can be configured:
2424
| `GITHUB_COPILOT_TIMEOUT` | Request timeout in seconds | `60` |
2525
| `GITHUB_COPILOT_LOG_LEVEL` | CLI log level | `info` |
2626

27+
## Observability
28+
29+
`GitHubCopilotAgent` has OpenTelemetry tracing built-in. To enable it, call `configure_otel_providers()` before running the agent:
30+
31+
```python
32+
from agent_framework.observability import configure_otel_providers
33+
from agent_framework.github import GitHubCopilotAgent
34+
35+
configure_otel_providers(enable_console_exporters=True)
36+
37+
async with GitHubCopilotAgent() as agent:
38+
response = await agent.run("Hello!")
39+
```
40+
41+
See the [observability samples](../../../02-agents/observability/) for full examples with OTLP exporters.
42+
2743
## Examples
2844

2945
| File | Description |

0 commit comments

Comments
 (0)