agent-framework-github-copilot 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_framework_github_copilot/__init__.py +18 -0
- agent_framework_github_copilot/_agent.py +1364 -0
- agent_framework_github_copilot-1.0.0.dist-info/METADATA +87 -0
- agent_framework_github_copilot-1.0.0.dist-info/RECORD +6 -0
- agent_framework_github_copilot-1.0.0.dist-info/WHEEL +4 -0
- agent_framework_github_copilot-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
import warnings
|
|
11
|
+
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, Sequence
|
|
12
|
+
from typing import Any, ClassVar, Generic, Literal, TypedDict, cast, overload
|
|
13
|
+
|
|
14
|
+
from agent_framework import (
|
|
15
|
+
AgentMiddlewareLayer,
|
|
16
|
+
AgentMiddlewareTypes,
|
|
17
|
+
AgentResponse,
|
|
18
|
+
AgentResponseUpdate,
|
|
19
|
+
AgentSession,
|
|
20
|
+
BaseAgent,
|
|
21
|
+
Content,
|
|
22
|
+
ContextProvider,
|
|
23
|
+
HistoryProvider,
|
|
24
|
+
Message,
|
|
25
|
+
ResponseStream,
|
|
26
|
+
SessionContext,
|
|
27
|
+
UsageDetails,
|
|
28
|
+
add_usage_details,
|
|
29
|
+
normalize_messages,
|
|
30
|
+
)
|
|
31
|
+
from agent_framework._settings import load_settings
|
|
32
|
+
from agent_framework._tools import FunctionTool, ToolTypes
|
|
33
|
+
from agent_framework._types import (
|
|
34
|
+
AgentRunInputs,
|
|
35
|
+
_get_data_bytes_as_str, # pyright: ignore[reportPrivateUsage]
|
|
36
|
+
normalize_tools,
|
|
37
|
+
)
|
|
38
|
+
from agent_framework.exceptions import AgentException, ContentError
|
|
39
|
+
from agent_framework.observability import AgentTelemetryLayer
|
|
40
|
+
|
|
41
|
+
if sys.version_info >= (3, 11):
|
|
42
|
+
from typing import Self # pragma: no cover
|
|
43
|
+
else:
|
|
44
|
+
from typing_extensions import Self # pragma: no cover
|
|
45
|
+
if sys.version_info >= (3, 13):
|
|
46
|
+
from typing import TypeVar # pragma: no cover
|
|
47
|
+
else:
|
|
48
|
+
from typing_extensions import TypeVar # pragma: no cover
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
from copilot import CopilotClient, CopilotSession, RuntimeConnection
|
|
52
|
+
from copilot.generated.rpc import PermissionDecisionUserNotAvailable
|
|
53
|
+
from copilot.session import (
|
|
54
|
+
Attachment,
|
|
55
|
+
BlobAttachment,
|
|
56
|
+
MCPServerConfig,
|
|
57
|
+
PermissionRequestResult,
|
|
58
|
+
PreToolUseHandler,
|
|
59
|
+
PreToolUseHookOutput,
|
|
60
|
+
ProviderConfig,
|
|
61
|
+
SessionHooks,
|
|
62
|
+
SystemMessageConfig,
|
|
63
|
+
)
|
|
64
|
+
from copilot.session_events import AssistantUsageData, PermissionRequest, SessionEvent, SessionEventType
|
|
65
|
+
from copilot.tools import Tool as CopilotTool
|
|
66
|
+
from copilot.tools import ToolInvocation, ToolResult
|
|
67
|
+
except ImportError as _copilot_import_error:
|
|
68
|
+
raise ImportError(
|
|
69
|
+
"GitHubCopilotAgent requires the 'github-copilot-sdk' package, which is only available on Python 3.11+. "
|
|
70
|
+
"Please use Python 3.11 or later."
|
|
71
|
+
) from _copilot_import_error
|
|
72
|
+
|
|
73
|
+
DEFAULT_TIMEOUT_SECONDS: float = 60.0
|
|
74
|
+
"""Default timeout in seconds for Copilot requests."""
|
|
75
|
+
|
|
76
|
+
PermissionHandlerType = Callable[
|
|
77
|
+
[PermissionRequest, dict[str, str]], "PermissionRequestResult | Awaitable[PermissionRequestResult]"
|
|
78
|
+
]
|
|
79
|
+
"""Type for permission request handlers. Supports both sync and async callbacks."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
FunctionApprovalCallback = Callable[[Content], "bool | Awaitable[bool]"]
|
|
83
|
+
"""Deprecated approval callback for ``FunctionTool`` instances declared with
|
|
84
|
+
``approval_mode="always_require"``.
|
|
85
|
+
|
|
86
|
+
.. deprecated::
|
|
87
|
+
Use the SDK ``on_pre_tool_use`` hook together with ``on_permission_request``
|
|
88
|
+
instead. The default ``on_pre_tool_use`` hook returns ``"ask"`` for
|
|
89
|
+
``always_require`` tools and routes the decision to ``on_permission_request``.
|
|
90
|
+
|
|
91
|
+
The callback receives a ``FunctionCallContent`` describing the pending call
|
|
92
|
+
(``name``, ``arguments``, and a synthetic ``call_id``) and must return ``True``
|
|
93
|
+
to allow execution or ``False`` to deny it. Both synchronous and ``await``-able
|
|
94
|
+
return values are supported.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _resolve_function_approval(
|
|
99
|
+
callback: FunctionApprovalCallback | None,
|
|
100
|
+
func_tool: FunctionTool,
|
|
101
|
+
arguments: Mapping[str, Any] | None,
|
|
102
|
+
) -> bool:
|
|
103
|
+
"""Run the deprecated agent-level approval callback for a pending tool call.
|
|
104
|
+
|
|
105
|
+
Returns ``True`` only when ``callback`` is configured and explicitly returns
|
|
106
|
+
a truthy value. A missing callback or any callback failure is treated as a
|
|
107
|
+
denial so the secure-by-default policy holds even if the user code raises.
|
|
108
|
+
"""
|
|
109
|
+
if callback is None:
|
|
110
|
+
return False
|
|
111
|
+
request = Content.from_function_call(
|
|
112
|
+
call_id=f"af-copilot-approval::{func_tool.name}",
|
|
113
|
+
name=func_tool.name,
|
|
114
|
+
arguments=None if arguments is None else dict(arguments),
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
outcome = callback(request)
|
|
118
|
+
if inspect.isawaitable(outcome):
|
|
119
|
+
outcome = await outcome
|
|
120
|
+
except Exception:
|
|
121
|
+
logger.exception(
|
|
122
|
+
"on_function_approval callback raised for tool '%s'; denying execution.",
|
|
123
|
+
func_tool.name,
|
|
124
|
+
)
|
|
125
|
+
return False
|
|
126
|
+
return bool(outcome)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
logger = logging.getLogger("agent_framework.github_copilot")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _deny_all_permissions(
|
|
133
|
+
_request: PermissionRequest,
|
|
134
|
+
_invocation: dict[str, str],
|
|
135
|
+
) -> PermissionRequestResult:
|
|
136
|
+
"""Default permission handler that denies all requests."""
|
|
137
|
+
return PermissionDecisionUserNotAvailable()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class GitHubCopilotSettings(TypedDict, total=False):
|
|
141
|
+
"""GitHub Copilot model settings.
|
|
142
|
+
|
|
143
|
+
Settings are resolved in this order: explicit keyword arguments, values from an
|
|
144
|
+
explicitly provided .env file, then environment variables with the prefix
|
|
145
|
+
'GITHUB_COPILOT_'.
|
|
146
|
+
|
|
147
|
+
Keys:
|
|
148
|
+
cli_path: Path to the Copilot CLI executable.
|
|
149
|
+
Can be set via environment variable GITHUB_COPILOT_CLI_PATH.
|
|
150
|
+
model: Model to use (e.g., "gpt-5", "claude-sonnet-4").
|
|
151
|
+
Can be set via environment variable GITHUB_COPILOT_MODEL.
|
|
152
|
+
timeout: Request timeout in seconds.
|
|
153
|
+
Can be set via environment variable GITHUB_COPILOT_TIMEOUT.
|
|
154
|
+
log_level: CLI log level.
|
|
155
|
+
Can be set via environment variable GITHUB_COPILOT_LOG_LEVEL.
|
|
156
|
+
base_directory: Directory where the CLI stores session state, configuration,
|
|
157
|
+
and other persistent data. Can be set via environment variable
|
|
158
|
+
GITHUB_COPILOT_BASE_DIRECTORY. Defaults to ~/.copilot when not set.
|
|
159
|
+
Only applicable when the SDK spawns the CLI process (ignored when
|
|
160
|
+
connecting to an external server via a pre-configured client).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
cli_path: str | None
|
|
164
|
+
model: str | None
|
|
165
|
+
timeout: float | None
|
|
166
|
+
log_level: str | None
|
|
167
|
+
base_directory: str | None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class GitHubCopilotOptions(TypedDict, total=False):
|
|
171
|
+
"""GitHub Copilot-specific options.
|
|
172
|
+
|
|
173
|
+
The keys below have first-class typing and inline documentation because they are
|
|
174
|
+
the commonly used options. They are **not** an exhaustive list: any other
|
|
175
|
+
parameter accepted by the Copilot SDK's ``create_session`` (for example
|
|
176
|
+
``reasoning_effort``, ``context_tier``, ``enable_citations``, ``available_tools``,
|
|
177
|
+
``memory``, ...) may also be supplied and is forwarded verbatim to the SDK. An
|
|
178
|
+
unrecognized parameter name surfaces as a ``TypeError`` from the SDK, so typos are
|
|
179
|
+
caught rather than silently ignored.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
system_message: SystemMessageConfig
|
|
183
|
+
"""System message configuration for the session. Use mode 'append' to add to the default
|
|
184
|
+
system prompt, or 'replace' to completely override it."""
|
|
185
|
+
|
|
186
|
+
cli_path: str
|
|
187
|
+
"""Path to the Copilot CLI executable. Defaults to GITHUB_COPILOT_CLI_PATH environment variable
|
|
188
|
+
or 'copilot' in PATH."""
|
|
189
|
+
|
|
190
|
+
model: str
|
|
191
|
+
"""Model to use (e.g., "gpt-5", "claude-sonnet-4"). Defaults to GITHUB_COPILOT_MODEL environment variable."""
|
|
192
|
+
|
|
193
|
+
timeout: float
|
|
194
|
+
"""Request timeout in seconds. Defaults to GITHUB_COPILOT_TIMEOUT environment variable or 60 seconds."""
|
|
195
|
+
|
|
196
|
+
log_level: str
|
|
197
|
+
"""CLI log level. Defaults to GITHUB_COPILOT_LOG_LEVEL environment variable."""
|
|
198
|
+
|
|
199
|
+
on_permission_request: PermissionHandlerType
|
|
200
|
+
"""Permission request handler.
|
|
201
|
+
Called when Copilot requests permission to perform an action (shell, read, write, etc.).
|
|
202
|
+
Takes a PermissionRequest and context dict, returns PermissionRequestResult.
|
|
203
|
+
If not provided, all permission requests will be denied by default.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
mcp_servers: dict[str, MCPServerConfig]
|
|
207
|
+
"""MCP (Model Context Protocol) server configurations.
|
|
208
|
+
A dictionary mapping server names to their configurations.
|
|
209
|
+
Supports both local (stdio) and remote (HTTP/SSE) servers.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
provider: ProviderConfig
|
|
213
|
+
"""Custom API provider configuration for BYOK (Bring Your Own Key) scenarios.
|
|
214
|
+
Allows routing requests through your own OpenAI, Azure, or Anthropic endpoint
|
|
215
|
+
instead of the default GitHub Copilot backend.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
instruction_directories: list[str]
|
|
219
|
+
"""Additional directories to search for custom instruction files.
|
|
220
|
+
Lets applications point the CLI at project-specific or team-shared instruction
|
|
221
|
+
files beyond the default locations.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
skill_directories: list[str]
|
|
225
|
+
"""Directories containing SKILL.md files to load into the Copilot CLI session.
|
|
226
|
+
These are loaded natively by the Copilot CLI process, letting applications point
|
|
227
|
+
the CLI at project-specific or team-shared skills beyond the default locations.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
disabled_skills: list[str]
|
|
231
|
+
"""Names of skills to disable for the session.
|
|
232
|
+
Lets applications opt out of specific skills that would otherwise be discovered
|
|
233
|
+
from ``skill_directories`` or the default locations.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
base_directory: str
|
|
237
|
+
"""Directory where the CLI stores session state, configuration, and other persistent data."""
|
|
238
|
+
|
|
239
|
+
on_pre_tool_use: PreToolUseHandler
|
|
240
|
+
"""Pre-tool-use hook handler for the Copilot SDK.
|
|
241
|
+
|
|
242
|
+
Called by the Copilot SDK before any tool is executed. The handler receives a
|
|
243
|
+
``PreToolUseHookInput`` and a context dict, and returns a ``PreToolUseHookOutput``
|
|
244
|
+
(or ``None`` to defer). Returning ``{"permissionDecision": "ask"}`` routes the
|
|
245
|
+
decision to ``on_permission_request``; ``"allow"`` / ``"deny"`` gate the call
|
|
246
|
+
directly.
|
|
247
|
+
|
|
248
|
+
If you do **not** supply this hook, the agent installs a default ``on_pre_tool_use``
|
|
249
|
+
hook that returns ``"ask"`` for ``FunctionTool`` instances declared with
|
|
250
|
+
``approval_mode="always_require"`` (deferring all other tools), so those tools are
|
|
251
|
+
gated through ``on_permission_request``. If you **do** supply your own hook, it
|
|
252
|
+
takes precedence and **you** are responsible for enforcing approval for any
|
|
253
|
+
``always_require`` tool; the agent logs a warning naming such tools."""
|
|
254
|
+
|
|
255
|
+
on_function_approval: FunctionApprovalCallback
|
|
256
|
+
"""Deprecated approval callback for ``FunctionTool`` instances declared with
|
|
257
|
+
``approval_mode="always_require"``.
|
|
258
|
+
|
|
259
|
+
.. deprecated::
|
|
260
|
+
Use ``on_pre_tool_use`` together with ``on_permission_request`` instead.
|
|
261
|
+
When neither this callback nor ``on_pre_tool_use`` is set, the agent
|
|
262
|
+
installs a default ``on_pre_tool_use`` hook that returns ``"ask"`` for
|
|
263
|
+
``always_require`` tools and routes the decision to ``on_permission_request``.
|
|
264
|
+
|
|
265
|
+
When set, this callback is enforced inside the SDK tool-handler before the tool
|
|
266
|
+
runs; a falsy return value denies the call. Setting it emits a
|
|
267
|
+
``DeprecationWarning``. It is **mutually exclusive** with ``on_pre_tool_use`` —
|
|
268
|
+
setting both raises ``ValueError``."""
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
OptionsT = TypeVar(
|
|
272
|
+
"OptionsT",
|
|
273
|
+
bound=TypedDict, # type: ignore[valid-type]
|
|
274
|
+
default="GitHubCopilotOptions",
|
|
275
|
+
covariant=True,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
|
|
280
|
+
"""A GitHub Copilot Agent without telemetry layers.
|
|
281
|
+
|
|
282
|
+
This is the core GitHub Copilot agent implementation without OpenTelemetry instrumentation.
|
|
283
|
+
For most use cases, prefer :class:`GitHubCopilotAgent` which includes telemetry support.
|
|
284
|
+
|
|
285
|
+
This agent wraps the GitHub Copilot SDK to provide Copilot agentic capabilities
|
|
286
|
+
within the Agent Framework. It supports both streaming and non-streaming responses,
|
|
287
|
+
custom tools, and session management.
|
|
288
|
+
|
|
289
|
+
The agent can be used as an async context manager to ensure proper cleanup:
|
|
290
|
+
|
|
291
|
+
Examples:
|
|
292
|
+
Basic usage:
|
|
293
|
+
|
|
294
|
+
.. code-block:: python
|
|
295
|
+
|
|
296
|
+
async with RawGitHubCopilotAgent() as agent:
|
|
297
|
+
response = await agent.run("Hello, world!")
|
|
298
|
+
print(response)
|
|
299
|
+
|
|
300
|
+
With explicitly typed options:
|
|
301
|
+
|
|
302
|
+
.. code-block:: python
|
|
303
|
+
|
|
304
|
+
from agent_framework_github_copilot import RawGitHubCopilotAgent, GitHubCopilotOptions
|
|
305
|
+
|
|
306
|
+
agent: RawGitHubCopilotAgent[GitHubCopilotOptions] = RawGitHubCopilotAgent(
|
|
307
|
+
default_options={"model": "claude-sonnet-4", "timeout": 120}
|
|
308
|
+
)
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
AGENT_PROVIDER_NAME: ClassVar[str] = "github.copilot"
|
|
312
|
+
|
|
313
|
+
def __init__(
|
|
314
|
+
self,
|
|
315
|
+
instructions: str | None = None,
|
|
316
|
+
*,
|
|
317
|
+
client: CopilotClient | None = None,
|
|
318
|
+
id: str | None = None,
|
|
319
|
+
name: str | None = None,
|
|
320
|
+
description: str | None = None,
|
|
321
|
+
context_providers: Sequence[ContextProvider] | None = None,
|
|
322
|
+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
|
|
323
|
+
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
|
324
|
+
default_options: OptionsT | None = None,
|
|
325
|
+
env_file_path: str | None = None,
|
|
326
|
+
env_file_encoding: str | None = None,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Initialize the GitHub Copilot Agent.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
instructions: System message for the agent.
|
|
332
|
+
|
|
333
|
+
Keyword Args:
|
|
334
|
+
client: Optional pre-configured CopilotClient instance. If not provided,
|
|
335
|
+
a new client will be created using the other parameters.
|
|
336
|
+
id: ID of the RawGitHubCopilotAgent.
|
|
337
|
+
name: Name of the RawGitHubCopilotAgent.
|
|
338
|
+
description: Description of the RawGitHubCopilotAgent.
|
|
339
|
+
context_providers: Context Providers, to be used by the agent.
|
|
340
|
+
middleware: Agent middleware used by the agent.
|
|
341
|
+
tools: Tools to use for the agent. Can be functions
|
|
342
|
+
or tool definition dicts. These are converted to Copilot SDK tools internally.
|
|
343
|
+
default_options: Default options for the agent. Can include cli_path, model,
|
|
344
|
+
timeout, log_level, etc.
|
|
345
|
+
env_file_path: Optional path to .env file for loading configuration.
|
|
346
|
+
env_file_encoding: Encoding of the .env file, defaults to 'utf-8'.
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
ValueError: If required configuration is missing or invalid.
|
|
350
|
+
"""
|
|
351
|
+
super().__init__(
|
|
352
|
+
id=id,
|
|
353
|
+
name=name,
|
|
354
|
+
description=description,
|
|
355
|
+
context_providers=context_providers,
|
|
356
|
+
middleware=list(middleware) if middleware else None,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
self._client = client
|
|
360
|
+
self._owns_client = client is None
|
|
361
|
+
|
|
362
|
+
# Parse options
|
|
363
|
+
opts: dict[str, Any] = dict(default_options) if default_options else {}
|
|
364
|
+
|
|
365
|
+
# Handle instructions - direct parameter takes precedence over default_options.system_message
|
|
366
|
+
self._prepare_system_message(instructions, opts)
|
|
367
|
+
|
|
368
|
+
cli_path = opts.pop("cli_path", None)
|
|
369
|
+
model = opts.pop("model", None)
|
|
370
|
+
timeout = opts.pop("timeout", None)
|
|
371
|
+
log_level = opts.pop("log_level", None)
|
|
372
|
+
on_permission_request: PermissionHandlerType | None = opts.pop("on_permission_request", None)
|
|
373
|
+
on_pre_tool_use: PreToolUseHandler | None = opts.pop("on_pre_tool_use", None)
|
|
374
|
+
on_function_approval: FunctionApprovalCallback | None = opts.pop("on_function_approval", None)
|
|
375
|
+
base_directory = opts.pop("base_directory", None)
|
|
376
|
+
|
|
377
|
+
if on_function_approval is not None and on_pre_tool_use is not None:
|
|
378
|
+
raise ValueError(
|
|
379
|
+
"on_function_approval and on_pre_tool_use cannot both be set. "
|
|
380
|
+
"on_function_approval is deprecated; use on_pre_tool_use together with "
|
|
381
|
+
"on_permission_request instead."
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if on_function_approval is not None:
|
|
385
|
+
warnings.warn(
|
|
386
|
+
"on_function_approval is deprecated and will be removed in a future version. "
|
|
387
|
+
"Use the SDK 'on_pre_tool_use' hook together with 'on_permission_request' instead: "
|
|
388
|
+
"the default 'on_pre_tool_use' hook returns 'ask' for approval_mode='always_require' "
|
|
389
|
+
"tools and routes the decision to 'on_permission_request'.",
|
|
390
|
+
DeprecationWarning,
|
|
391
|
+
stacklevel=2,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
self._settings = load_settings(
|
|
395
|
+
GitHubCopilotSettings,
|
|
396
|
+
env_prefix="GITHUB_COPILOT_",
|
|
397
|
+
cli_path=cli_path,
|
|
398
|
+
model=model,
|
|
399
|
+
timeout=timeout,
|
|
400
|
+
log_level=log_level,
|
|
401
|
+
base_directory=base_directory,
|
|
402
|
+
env_file_path=env_file_path,
|
|
403
|
+
env_file_encoding=env_file_encoding,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
self._tools = normalize_tools(tools)
|
|
407
|
+
self._permission_handler = on_permission_request
|
|
408
|
+
self._on_pre_tool_use: PreToolUseHandler | None = on_pre_tool_use
|
|
409
|
+
self._function_approval_handler: FunctionApprovalCallback | None = on_function_approval
|
|
410
|
+
# Remaining options (e.g. mcp_servers, provider, instruction_directories,
|
|
411
|
+
# skill_directories, disabled_skills, and any other create_session parameter)
|
|
412
|
+
# are forwarded verbatim to the Copilot SDK by _build_session_kwargs.
|
|
413
|
+
self._default_options = opts
|
|
414
|
+
self._started = False
|
|
415
|
+
|
|
416
|
+
async def __aenter__(self) -> Self:
|
|
417
|
+
"""Start the agent when entering async context."""
|
|
418
|
+
await self.start()
|
|
419
|
+
return self
|
|
420
|
+
|
|
421
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
422
|
+
"""Stop the agent when exiting async context."""
|
|
423
|
+
await self.stop()
|
|
424
|
+
|
|
425
|
+
async def start(self) -> None:
|
|
426
|
+
"""Start the Copilot client.
|
|
427
|
+
|
|
428
|
+
This method initializes the Copilot client and establishes a connection
|
|
429
|
+
to the Copilot CLI server. It is called automatically when using the
|
|
430
|
+
agent as an async context manager.
|
|
431
|
+
|
|
432
|
+
Raises:
|
|
433
|
+
AgentException: If the client fails to start.
|
|
434
|
+
"""
|
|
435
|
+
if self._started:
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
if self._client is None:
|
|
439
|
+
cli_path = self._settings.get("cli_path") or None
|
|
440
|
+
log_level = self._settings.get("log_level") or None
|
|
441
|
+
base_directory = self._settings.get("base_directory") or None
|
|
442
|
+
|
|
443
|
+
client_kwargs: dict[str, Any] = {}
|
|
444
|
+
if cli_path:
|
|
445
|
+
client_kwargs["connection"] = RuntimeConnection.for_stdio(path=cli_path)
|
|
446
|
+
if log_level:
|
|
447
|
+
client_kwargs["log_level"] = log_level
|
|
448
|
+
if base_directory:
|
|
449
|
+
client_kwargs["base_directory"] = base_directory
|
|
450
|
+
self._client = CopilotClient(**client_kwargs)
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
await self._client.start()
|
|
454
|
+
self._started = True
|
|
455
|
+
except Exception as ex:
|
|
456
|
+
raise AgentException(f"Failed to start GitHub Copilot client: {ex}") from ex
|
|
457
|
+
|
|
458
|
+
async def stop(self) -> None:
|
|
459
|
+
"""Stop the Copilot client and clean up resources.
|
|
460
|
+
|
|
461
|
+
Stops the Copilot client if owned by this agent. The client handles
|
|
462
|
+
session cleanup internally. Called automatically when using the agent
|
|
463
|
+
as an async context manager.
|
|
464
|
+
"""
|
|
465
|
+
if self._client and self._owns_client:
|
|
466
|
+
with contextlib.suppress(Exception):
|
|
467
|
+
await self._client.stop()
|
|
468
|
+
|
|
469
|
+
self._started = False
|
|
470
|
+
|
|
471
|
+
@property
|
|
472
|
+
def default_options(self) -> dict[str, Any]:
|
|
473
|
+
"""Expose default options including model from settings.
|
|
474
|
+
|
|
475
|
+
Returns a merged dict of ``_default_options`` with the resolved ``model``
|
|
476
|
+
from settings injected under the ``model`` key. This is read by
|
|
477
|
+
:class:`AgentTelemetryLayer` to include the model name in span attributes.
|
|
478
|
+
"""
|
|
479
|
+
opts = dict(self._default_options)
|
|
480
|
+
model = self._settings.get("model")
|
|
481
|
+
if model:
|
|
482
|
+
opts["model"] = model
|
|
483
|
+
return opts
|
|
484
|
+
|
|
485
|
+
@overload
|
|
486
|
+
def run(
|
|
487
|
+
self,
|
|
488
|
+
messages: AgentRunInputs | None = None,
|
|
489
|
+
*,
|
|
490
|
+
stream: Literal[False] = False,
|
|
491
|
+
session: AgentSession | None = None,
|
|
492
|
+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
|
|
493
|
+
options: OptionsT | None = None,
|
|
494
|
+
**kwargs: Any,
|
|
495
|
+
) -> Awaitable[AgentResponse]: ...
|
|
496
|
+
|
|
497
|
+
@overload
|
|
498
|
+
def run(
|
|
499
|
+
self,
|
|
500
|
+
messages: AgentRunInputs | None = None,
|
|
501
|
+
*,
|
|
502
|
+
stream: Literal[True],
|
|
503
|
+
session: AgentSession | None = None,
|
|
504
|
+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
|
|
505
|
+
options: OptionsT | None = None,
|
|
506
|
+
**kwargs: Any,
|
|
507
|
+
) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ...
|
|
508
|
+
|
|
509
|
+
def run(
|
|
510
|
+
self,
|
|
511
|
+
messages: AgentRunInputs | None = None,
|
|
512
|
+
*,
|
|
513
|
+
stream: bool = False,
|
|
514
|
+
session: AgentSession | None = None,
|
|
515
|
+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
|
|
516
|
+
options: OptionsT | None = None,
|
|
517
|
+
**kwargs: Any,
|
|
518
|
+
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
|
|
519
|
+
"""Get a response from the agent.
|
|
520
|
+
|
|
521
|
+
This method returns the final result of the agent's execution
|
|
522
|
+
as a single AgentResponse object when stream=False. When stream=True,
|
|
523
|
+
it returns a ResponseStream that yields AgentResponseUpdate objects.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
messages: The message(s) to send to the agent.
|
|
527
|
+
|
|
528
|
+
Keyword Args:
|
|
529
|
+
stream: Whether to stream the response. Defaults to False.
|
|
530
|
+
session: The conversation session associated with the message(s).
|
|
531
|
+
middleware: Not used by this agent directly. Accepted for interface
|
|
532
|
+
compatibility; pass middleware via :class:`GitHubCopilotAgent` which
|
|
533
|
+
forwards it through :class:`AgentTelemetryLayer`.
|
|
534
|
+
options: Runtime options (model, timeout, etc.).
|
|
535
|
+
kwargs: Additional keyword arguments for compatibility with the shared agent
|
|
536
|
+
interface (e.g. compaction_strategy, tokenizer). Not used by this agent.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
When stream=False: An Awaitable[AgentResponse].
|
|
540
|
+
When stream=True: A ResponseStream of AgentResponseUpdate items.
|
|
541
|
+
|
|
542
|
+
Raises:
|
|
543
|
+
AgentException: If the request fails.
|
|
544
|
+
"""
|
|
545
|
+
if middleware:
|
|
546
|
+
logger.warning(
|
|
547
|
+
"Per-run middleware is not supported by RawGitHubCopilotAgent: the GitHub Copilot SDK "
|
|
548
|
+
"handles tool execution internally, so chat/function middleware cannot be injected into "
|
|
549
|
+
"the tool call path. Use agent-level middleware via the GitHubCopilotAgent constructor instead."
|
|
550
|
+
)
|
|
551
|
+
if stream:
|
|
552
|
+
ctx_holder: dict[str, Any] = {}
|
|
553
|
+
|
|
554
|
+
async def _after_run_hook(response: AgentResponse) -> None:
|
|
555
|
+
session_context = ctx_holder.get("session_context")
|
|
556
|
+
sess = ctx_holder.get("session")
|
|
557
|
+
if session_context is not None and sess is not None:
|
|
558
|
+
session_context._response = response
|
|
559
|
+
try:
|
|
560
|
+
await self._run_after_providers(session=sess, context=session_context)
|
|
561
|
+
except Exception:
|
|
562
|
+
logger.exception("Error running after_run providers in streaming result hook")
|
|
563
|
+
|
|
564
|
+
def _finalize(updates: Sequence[AgentResponseUpdate]) -> AgentResponse:
|
|
565
|
+
return AgentResponse.from_updates(updates)
|
|
566
|
+
|
|
567
|
+
return ResponseStream(
|
|
568
|
+
self._stream_updates(messages=messages, session=session, options=options, _ctx_holder=ctx_holder),
|
|
569
|
+
finalizer=_finalize,
|
|
570
|
+
result_hooks=[_after_run_hook],
|
|
571
|
+
)
|
|
572
|
+
return self._run_impl(messages=messages, session=session, options=options)
|
|
573
|
+
|
|
574
|
+
@staticmethod
|
|
575
|
+
def _parse_usage_details_from_copilot(data: AssistantUsageData) -> UsageDetails | None:
|
|
576
|
+
total_token_count = (
|
|
577
|
+
data.input_tokens + data.output_tokens
|
|
578
|
+
if data.input_tokens is not None and data.output_tokens is not None
|
|
579
|
+
else None
|
|
580
|
+
)
|
|
581
|
+
usage_details = UsageDetails(**{
|
|
582
|
+
key: value
|
|
583
|
+
for key, value in {
|
|
584
|
+
"input_token_count": data.input_tokens,
|
|
585
|
+
"output_token_count": data.output_tokens,
|
|
586
|
+
"total_token_count": total_token_count,
|
|
587
|
+
"cache_read_input_token_count": data.cache_read_tokens,
|
|
588
|
+
"cache_creation_input_token_count": data.cache_write_tokens,
|
|
589
|
+
"reasoning_output_token_count": data.reasoning_tokens,
|
|
590
|
+
}.items()
|
|
591
|
+
if value is not None
|
|
592
|
+
})
|
|
593
|
+
return usage_details or None
|
|
594
|
+
|
|
595
|
+
async def _run_impl(
|
|
596
|
+
self,
|
|
597
|
+
messages: AgentRunInputs | None = None,
|
|
598
|
+
*,
|
|
599
|
+
session: AgentSession | None = None,
|
|
600
|
+
options: OptionsT | None = None,
|
|
601
|
+
) -> AgentResponse:
|
|
602
|
+
"""Non-streaming implementation of run."""
|
|
603
|
+
if not self._started:
|
|
604
|
+
await self.start()
|
|
605
|
+
|
|
606
|
+
if not session:
|
|
607
|
+
session = self.create_session()
|
|
608
|
+
|
|
609
|
+
opts: dict[str, Any] = dict(options) if options else {}
|
|
610
|
+
if "on_function_approval" in opts:
|
|
611
|
+
raise ValueError(
|
|
612
|
+
"on_function_approval is a security-sensitive option and must be set "
|
|
613
|
+
"via default_options at agent construction time. It cannot be overridden "
|
|
614
|
+
"per run."
|
|
615
|
+
)
|
|
616
|
+
if "on_pre_tool_use" in opts and self._function_approval_handler is not None:
|
|
617
|
+
raise ValueError(
|
|
618
|
+
"on_pre_tool_use cannot be combined with the deprecated on_function_approval "
|
|
619
|
+
"(set via default_options). Remove on_function_approval and use on_pre_tool_use "
|
|
620
|
+
"together with on_permission_request instead."
|
|
621
|
+
)
|
|
622
|
+
timeout = opts.get("timeout") or self._settings.get("timeout") or DEFAULT_TIMEOUT_SECONDS
|
|
623
|
+
|
|
624
|
+
input_messages = normalize_messages(messages)
|
|
625
|
+
|
|
626
|
+
session_context = await self._run_before_providers(session=session, input_messages=input_messages, options=opts)
|
|
627
|
+
|
|
628
|
+
# Merge provider-contributed tools into runtime_options before session creation.
|
|
629
|
+
if session_context.tools:
|
|
630
|
+
existing = list(opts.get("tools") or [])
|
|
631
|
+
opts["tools"] = existing + list(session_context.tools)
|
|
632
|
+
|
|
633
|
+
copilot_session = await self._get_or_create_session(session, streaming=False, runtime_options=opts)
|
|
634
|
+
usage_details: UsageDetails | None = None
|
|
635
|
+
finish_reason: str | None = None
|
|
636
|
+
model: str | None = None
|
|
637
|
+
|
|
638
|
+
def usage_event_handler(event: SessionEvent) -> None:
|
|
639
|
+
nonlocal usage_details, finish_reason, model
|
|
640
|
+
if event.type != SessionEventType.ASSISTANT_USAGE:
|
|
641
|
+
return
|
|
642
|
+
if isinstance(event.data, AssistantUsageData):
|
|
643
|
+
parsed_usage_details = self._parse_usage_details_from_copilot(event.data)
|
|
644
|
+
if parsed_usage_details:
|
|
645
|
+
usage_details = add_usage_details(usage_details, parsed_usage_details)
|
|
646
|
+
event_finish_reason = (
|
|
647
|
+
"content_filter" if event.data.content_filter_triggered else event.data.finish_reason
|
|
648
|
+
)
|
|
649
|
+
if event_finish_reason:
|
|
650
|
+
finish_reason = event_finish_reason
|
|
651
|
+
if event.data.model:
|
|
652
|
+
model = event.data.model
|
|
653
|
+
else:
|
|
654
|
+
logger.warning(
|
|
655
|
+
"Ignoring GitHub Copilot assistant usage event with unexpected payload type: %s",
|
|
656
|
+
type(event.data).__name__,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
# Build the prompt from the full set of messages in the session context,
|
|
660
|
+
# so that any context/history provider-injected messages are included.
|
|
661
|
+
context_messages = session_context.get_messages(include_input=True)
|
|
662
|
+
prompt = "\n".join([message.text for message in context_messages])
|
|
663
|
+
if session_context.instructions:
|
|
664
|
+
prompt = "\n".join(session_context.instructions) + "\n" + prompt
|
|
665
|
+
attachments = self._prepare_attachments_for_copilot(context_messages)
|
|
666
|
+
|
|
667
|
+
unsubscribe = copilot_session.on(usage_event_handler)
|
|
668
|
+
try:
|
|
669
|
+
response_event = await copilot_session.send_and_wait(prompt, attachments=attachments, timeout=timeout)
|
|
670
|
+
except Exception as ex:
|
|
671
|
+
raise AgentException(f"GitHub Copilot request failed: {ex}") from ex
|
|
672
|
+
finally:
|
|
673
|
+
unsubscribe()
|
|
674
|
+
|
|
675
|
+
response_messages: list[Message] = []
|
|
676
|
+
response_id: str | None = None
|
|
677
|
+
|
|
678
|
+
# send_and_wait returns only the final ASSISTANT_MESSAGE event;
|
|
679
|
+
# other events (deltas, tool calls) are handled internally by the SDK.
|
|
680
|
+
if response_event and response_event.type == SessionEventType.ASSISTANT_MESSAGE:
|
|
681
|
+
data: Any = response_event.data
|
|
682
|
+
message_id = data.message_id
|
|
683
|
+
|
|
684
|
+
if data.content:
|
|
685
|
+
response_messages.append(
|
|
686
|
+
Message(
|
|
687
|
+
role="assistant",
|
|
688
|
+
contents=[Content.from_text(data.content)],
|
|
689
|
+
message_id=message_id,
|
|
690
|
+
raw_representation=response_event,
|
|
691
|
+
)
|
|
692
|
+
)
|
|
693
|
+
response_id = message_id
|
|
694
|
+
|
|
695
|
+
response = AgentResponse(
|
|
696
|
+
messages=response_messages,
|
|
697
|
+
response_id=response_id,
|
|
698
|
+
finish_reason=cast(Any, finish_reason),
|
|
699
|
+
usage_details=usage_details,
|
|
700
|
+
additional_properties={"model": model} if model else None,
|
|
701
|
+
)
|
|
702
|
+
session_context._response = response # type: ignore[assignment]
|
|
703
|
+
await self._run_after_providers(session=session, context=session_context)
|
|
704
|
+
return response
|
|
705
|
+
|
|
706
|
+
async def _stream_updates(
|
|
707
|
+
self,
|
|
708
|
+
messages: AgentRunInputs | None = None,
|
|
709
|
+
*,
|
|
710
|
+
session: AgentSession | None = None,
|
|
711
|
+
options: OptionsT | None = None,
|
|
712
|
+
_ctx_holder: dict[str, Any] | None = None,
|
|
713
|
+
) -> AsyncIterable[AgentResponseUpdate]:
|
|
714
|
+
"""Internal method to stream updates from GitHub Copilot.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
messages: The message(s) to send to the agent.
|
|
718
|
+
|
|
719
|
+
Keyword Args:
|
|
720
|
+
session: The conversation session associated with the message(s).
|
|
721
|
+
options: Runtime options (model, timeout, etc.).
|
|
722
|
+
_ctx_holder: Internal dict populated with session_context and session
|
|
723
|
+
so that the caller (via a ResponseStream result_hook) can run
|
|
724
|
+
after_run providers without duplicating the updates buffer.
|
|
725
|
+
|
|
726
|
+
Yields:
|
|
727
|
+
AgentResponseUpdate items.
|
|
728
|
+
|
|
729
|
+
Raises:
|
|
730
|
+
AgentException: If the request fails.
|
|
731
|
+
"""
|
|
732
|
+
if not self._started:
|
|
733
|
+
await self.start()
|
|
734
|
+
|
|
735
|
+
if not session:
|
|
736
|
+
session = self.create_session()
|
|
737
|
+
|
|
738
|
+
opts: dict[str, Any] = dict(options) if options else {}
|
|
739
|
+
if "on_function_approval" in opts:
|
|
740
|
+
raise ValueError(
|
|
741
|
+
"on_function_approval is a security-sensitive option and must be set "
|
|
742
|
+
"via default_options at agent construction time. It cannot be overridden "
|
|
743
|
+
"per run."
|
|
744
|
+
)
|
|
745
|
+
if "on_pre_tool_use" in opts and self._function_approval_handler is not None:
|
|
746
|
+
raise ValueError(
|
|
747
|
+
"on_pre_tool_use cannot be combined with the deprecated on_function_approval "
|
|
748
|
+
"(set via default_options). Remove on_function_approval and use on_pre_tool_use "
|
|
749
|
+
"together with on_permission_request instead."
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
input_messages = normalize_messages(messages)
|
|
753
|
+
|
|
754
|
+
session_context = await self._run_before_providers(session=session, input_messages=input_messages, options=opts)
|
|
755
|
+
|
|
756
|
+
# Merge provider-contributed tools into runtime_options before session creation.
|
|
757
|
+
if session_context.tools:
|
|
758
|
+
existing = list(opts.get("tools") or [])
|
|
759
|
+
opts["tools"] = existing + list(session_context.tools)
|
|
760
|
+
|
|
761
|
+
copilot_session = await self._get_or_create_session(session, streaming=True, runtime_options=opts)
|
|
762
|
+
|
|
763
|
+
if _ctx_holder is not None:
|
|
764
|
+
_ctx_holder["session_context"] = session_context
|
|
765
|
+
_ctx_holder["session"] = session
|
|
766
|
+
|
|
767
|
+
# Build the prompt from the full session context so provider-injected messages are included.
|
|
768
|
+
context_messages = session_context.get_messages(include_input=True)
|
|
769
|
+
prompt = "\n".join([message.text for message in context_messages])
|
|
770
|
+
if session_context.instructions:
|
|
771
|
+
prompt = "\n".join(session_context.instructions) + "\n" + prompt
|
|
772
|
+
attachments = self._prepare_attachments_for_copilot(context_messages)
|
|
773
|
+
|
|
774
|
+
queue: asyncio.Queue[AgentResponseUpdate | Exception | None] = asyncio.Queue()
|
|
775
|
+
|
|
776
|
+
def event_handler(event: SessionEvent) -> None:
|
|
777
|
+
if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
|
|
778
|
+
data: Any = event.data
|
|
779
|
+
if data.delta_content:
|
|
780
|
+
update = AgentResponseUpdate(
|
|
781
|
+
role="assistant",
|
|
782
|
+
contents=[Content.from_text(data.delta_content)],
|
|
783
|
+
response_id=data.message_id,
|
|
784
|
+
message_id=data.message_id,
|
|
785
|
+
raw_representation=event,
|
|
786
|
+
)
|
|
787
|
+
queue.put_nowait(update)
|
|
788
|
+
elif event.type == SessionEventType.ASSISTANT_USAGE:
|
|
789
|
+
if not isinstance(event.data, AssistantUsageData):
|
|
790
|
+
logger.warning(
|
|
791
|
+
"Ignoring GitHub Copilot assistant usage event with unexpected payload type: %s",
|
|
792
|
+
type(event.data).__name__,
|
|
793
|
+
)
|
|
794
|
+
return
|
|
795
|
+
usage_details = self._parse_usage_details_from_copilot(event.data)
|
|
796
|
+
finish_reason = "content_filter" if event.data.content_filter_triggered else event.data.finish_reason
|
|
797
|
+
model = event.data.model or None
|
|
798
|
+
if usage_details or finish_reason or model:
|
|
799
|
+
update = AgentResponseUpdate(
|
|
800
|
+
contents=[Content.from_usage(usage_details, raw_representation=event.data)]
|
|
801
|
+
if usage_details
|
|
802
|
+
else None,
|
|
803
|
+
finish_reason=cast(Any, finish_reason),
|
|
804
|
+
additional_properties={"model": model} if model else None,
|
|
805
|
+
raw_representation=event,
|
|
806
|
+
)
|
|
807
|
+
queue.put_nowait(update)
|
|
808
|
+
elif event.type == SessionEventType.TOOL_EXECUTION_START:
|
|
809
|
+
tool_call_id = getattr(event.data, "tool_call_id", None) or ""
|
|
810
|
+
tool_name = getattr(event.data, "tool_name", None) or ""
|
|
811
|
+
arguments = getattr(event.data, "arguments", None)
|
|
812
|
+
fc = Content.from_function_call(
|
|
813
|
+
call_id=tool_call_id,
|
|
814
|
+
name=tool_name,
|
|
815
|
+
arguments=arguments,
|
|
816
|
+
raw_representation=event.data,
|
|
817
|
+
)
|
|
818
|
+
update = AgentResponseUpdate(
|
|
819
|
+
role="assistant",
|
|
820
|
+
contents=[fc],
|
|
821
|
+
raw_representation=event,
|
|
822
|
+
)
|
|
823
|
+
queue.put_nowait(update)
|
|
824
|
+
elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE:
|
|
825
|
+
tool_call_id = getattr(event.data, "tool_call_id", None) or ""
|
|
826
|
+
result_obj = getattr(event.data, "result", None)
|
|
827
|
+
result_text = getattr(result_obj, "content", "") if result_obj else ""
|
|
828
|
+
success = getattr(event.data, "success", None)
|
|
829
|
+
error_val = getattr(event.data, "error", None)
|
|
830
|
+
exception = None
|
|
831
|
+
if success is False and error_val is not None:
|
|
832
|
+
exception = error_val.message if hasattr(error_val, "message") else str(error_val)
|
|
833
|
+
fr = Content.from_function_result(
|
|
834
|
+
call_id=tool_call_id,
|
|
835
|
+
result=result_text or "",
|
|
836
|
+
exception=exception,
|
|
837
|
+
raw_representation=event.data,
|
|
838
|
+
)
|
|
839
|
+
update = AgentResponseUpdate(
|
|
840
|
+
role="tool",
|
|
841
|
+
contents=[fr],
|
|
842
|
+
raw_representation=event,
|
|
843
|
+
)
|
|
844
|
+
queue.put_nowait(update)
|
|
845
|
+
elif event.type == SessionEventType.SESSION_IDLE:
|
|
846
|
+
queue.put_nowait(None)
|
|
847
|
+
elif event.type == SessionEventType.SESSION_ERROR:
|
|
848
|
+
error_data: Any = event.data
|
|
849
|
+
error_msg = error_data.message or "Unknown error"
|
|
850
|
+
queue.put_nowait(AgentException(f"GitHub Copilot session error: {error_msg}"))
|
|
851
|
+
|
|
852
|
+
unsubscribe = copilot_session.on(event_handler)
|
|
853
|
+
|
|
854
|
+
try:
|
|
855
|
+
await copilot_session.send(prompt, attachments=attachments)
|
|
856
|
+
|
|
857
|
+
while (item := await queue.get()) is not None:
|
|
858
|
+
if isinstance(item, Exception):
|
|
859
|
+
raise item
|
|
860
|
+
yield item
|
|
861
|
+
finally:
|
|
862
|
+
unsubscribe()
|
|
863
|
+
|
|
864
|
+
async def _run_before_providers(
|
|
865
|
+
self,
|
|
866
|
+
*,
|
|
867
|
+
session: AgentSession,
|
|
868
|
+
input_messages: list[Message],
|
|
869
|
+
options: dict[str, Any],
|
|
870
|
+
) -> SessionContext:
|
|
871
|
+
"""Run before_run on all context providers and return the session context.
|
|
872
|
+
|
|
873
|
+
Creates a SessionContext and invokes ``before_run`` on each provider in
|
|
874
|
+
forward order. ``HistoryProvider`` instances with
|
|
875
|
+
``load_messages=False`` are skipped.
|
|
876
|
+
|
|
877
|
+
Keyword Args:
|
|
878
|
+
session: The conversation session.
|
|
879
|
+
input_messages: The normalized input messages.
|
|
880
|
+
options: Runtime options dict.
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
The SessionContext with provider context populated.
|
|
884
|
+
"""
|
|
885
|
+
session_context = SessionContext(
|
|
886
|
+
session_id=session.session_id,
|
|
887
|
+
service_session_id=session.service_session_id,
|
|
888
|
+
input_messages=input_messages,
|
|
889
|
+
options=options,
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
for provider in self.context_providers:
|
|
893
|
+
if isinstance(provider, HistoryProvider) and not provider.load_messages:
|
|
894
|
+
continue
|
|
895
|
+
await provider.before_run(
|
|
896
|
+
agent=self,
|
|
897
|
+
session=session,
|
|
898
|
+
context=session_context,
|
|
899
|
+
state=session.state.setdefault(provider.source_id, {}),
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
return session_context
|
|
903
|
+
|
|
904
|
+
@staticmethod
|
|
905
|
+
def _prepare_system_message(
|
|
906
|
+
instructions: str | None,
|
|
907
|
+
opts: dict[str, Any],
|
|
908
|
+
) -> None:
|
|
909
|
+
"""Prepare system message configuration in opts.
|
|
910
|
+
|
|
911
|
+
If instructions is provided, it takes precedence for content.
|
|
912
|
+
If system_message is also provided, its mode is preserved.
|
|
913
|
+
Modifies opts in place.
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
instructions: Direct instructions parameter for content.
|
|
917
|
+
opts: Options dictionary to modify.
|
|
918
|
+
"""
|
|
919
|
+
opts_system_message = opts.pop("system_message", None)
|
|
920
|
+
if instructions is not None:
|
|
921
|
+
# Use instructions for content, but preserve mode from system_message if provided
|
|
922
|
+
mode = opts_system_message.get("mode", "append") if opts_system_message else "append"
|
|
923
|
+
opts["system_message"] = {"mode": mode, "content": instructions}
|
|
924
|
+
elif opts_system_message is not None:
|
|
925
|
+
opts["system_message"] = opts_system_message
|
|
926
|
+
|
|
927
|
+
@staticmethod
|
|
928
|
+
def _prepare_attachments_for_copilot(messages: Sequence[Message]) -> list[Attachment] | None:
|
|
929
|
+
"""Convert inline binary message content into Copilot SDK attachments.
|
|
930
|
+
|
|
931
|
+
Scans the outgoing messages for ``data`` content (binary payloads such as
|
|
932
|
+
images or documents carried as base64 data URIs) and maps each one to an
|
|
933
|
+
inline ``blob`` attachment understood by the Copilot SDK.
|
|
934
|
+
|
|
935
|
+
Only base64 ``data:`` content is forwarded as an attachment. Other content
|
|
936
|
+
is not turned into an attachment: text content is already carried in the
|
|
937
|
+
prompt, while remote URIs (for example ``https://`` links) and malformed or
|
|
938
|
+
non-base64 ``data:`` URIs are skipped -- they are neither attached nor added
|
|
939
|
+
to the prompt.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
messages: The messages being sent to the Copilot session.
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
A list of Copilot ``Attachment`` objects, or ``None`` when the messages
|
|
946
|
+
contain no attachable binary content.
|
|
947
|
+
"""
|
|
948
|
+
attachments: list[Attachment] = []
|
|
949
|
+
for message in messages:
|
|
950
|
+
for content in message.contents:
|
|
951
|
+
if content.type != "data":
|
|
952
|
+
continue
|
|
953
|
+
try:
|
|
954
|
+
data_str = _get_data_bytes_as_str(content)
|
|
955
|
+
except ContentError:
|
|
956
|
+
logger.warning(
|
|
957
|
+
"Skipping GitHub Copilot attachment with an unsupported data URI; "
|
|
958
|
+
"only base64-encoded 'data:' URIs can be forwarded as attachments."
|
|
959
|
+
)
|
|
960
|
+
continue
|
|
961
|
+
if not data_str:
|
|
962
|
+
continue
|
|
963
|
+
if not content.media_type:
|
|
964
|
+
logger.warning(
|
|
965
|
+
"Dropping GitHub Copilot attachment with no media type; the Copilot SDK "
|
|
966
|
+
"requires a MIME type for inline binary content."
|
|
967
|
+
)
|
|
968
|
+
continue
|
|
969
|
+
blob: BlobAttachment = {
|
|
970
|
+
"type": "blob",
|
|
971
|
+
"data": data_str,
|
|
972
|
+
"mimeType": content.media_type,
|
|
973
|
+
}
|
|
974
|
+
attachments.append(blob)
|
|
975
|
+
return attachments or None
|
|
976
|
+
|
|
977
|
+
def _prepare_tools(
|
|
978
|
+
self,
|
|
979
|
+
tools: Sequence[ToolTypes | CopilotTool],
|
|
980
|
+
) -> list[CopilotTool]:
|
|
981
|
+
"""Convert Agent Framework tools to Copilot SDK tools.
|
|
982
|
+
|
|
983
|
+
Args:
|
|
984
|
+
tools: List of Agent Framework tools.
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
List of Copilot SDK tools.
|
|
988
|
+
"""
|
|
989
|
+
copilot_tools: list[CopilotTool] = []
|
|
990
|
+
|
|
991
|
+
for tool in tools:
|
|
992
|
+
if isinstance(tool, CopilotTool):
|
|
993
|
+
copilot_tools.append(tool)
|
|
994
|
+
elif isinstance(tool, FunctionTool):
|
|
995
|
+
copilot_tools.append(self._tool_to_copilot_tool(tool))
|
|
996
|
+
elif isinstance(tool, MutableMapping):
|
|
997
|
+
copilot_tools.append(tool) # type: ignore[arg-type]
|
|
998
|
+
# Note: Other tool types (e.g., dict-based hosted tools) are skipped
|
|
999
|
+
|
|
1000
|
+
return copilot_tools
|
|
1001
|
+
|
|
1002
|
+
def _tool_to_copilot_tool(self, ai_func: FunctionTool) -> CopilotTool:
|
|
1003
|
+
"""Convert an FunctionTool to a Copilot SDK tool.
|
|
1004
|
+
|
|
1005
|
+
Approval for tools declared with ``approval_mode="always_require"`` is normally
|
|
1006
|
+
enforced by the Copilot SDK's native ``on_pre_tool_use`` hook (see
|
|
1007
|
+
:meth:`_build_session_hooks`). When the deprecated ``on_function_approval``
|
|
1008
|
+
callback is configured instead, approval is enforced inside this handler for
|
|
1009
|
+
backward compatibility. (``on_function_approval`` and ``on_pre_tool_use`` are
|
|
1010
|
+
mutually exclusive, so only one mechanism is ever active.)
|
|
1011
|
+
"""
|
|
1012
|
+
approval_handler = self._function_approval_handler
|
|
1013
|
+
enforce = approval_handler is not None and ai_func.approval_mode == "always_require"
|
|
1014
|
+
|
|
1015
|
+
async def handler(invocation: ToolInvocation) -> ToolResult:
|
|
1016
|
+
args: dict[str, Any] = invocation.arguments or {}
|
|
1017
|
+
try:
|
|
1018
|
+
if enforce and not await _resolve_function_approval(approval_handler, ai_func, args):
|
|
1019
|
+
logger.info(
|
|
1020
|
+
"Denying execution of tool '%s' (approval_mode='always_require', "
|
|
1021
|
+
"on_function_approval callback denied).",
|
|
1022
|
+
ai_func.name,
|
|
1023
|
+
)
|
|
1024
|
+
return ToolResult(
|
|
1025
|
+
text_result_for_llm=(
|
|
1026
|
+
f"Tool '{ai_func.name}' requires human approval "
|
|
1027
|
+
"(approval_mode='always_require') and the request was denied."
|
|
1028
|
+
),
|
|
1029
|
+
result_type="failure",
|
|
1030
|
+
error="approval_denied",
|
|
1031
|
+
)
|
|
1032
|
+
if ai_func.input_model:
|
|
1033
|
+
args_instance = ai_func.input_model(**args)
|
|
1034
|
+
result = await ai_func.invoke(arguments=args_instance)
|
|
1035
|
+
else:
|
|
1036
|
+
result = await ai_func.invoke(arguments=args)
|
|
1037
|
+
rich = [c for c in result if c.type in ("data", "uri")]
|
|
1038
|
+
if rich:
|
|
1039
|
+
logger.warning(
|
|
1040
|
+
"GitHub Copilot does not support rich tool content; "
|
|
1041
|
+
f"dropping {len(rich)} non-text item(s) from '{ai_func.name}'."
|
|
1042
|
+
)
|
|
1043
|
+
text = "\n".join(c.text for c in result if c.type == "text" and c.text)
|
|
1044
|
+
return ToolResult(
|
|
1045
|
+
text_result_for_llm=text or str(result),
|
|
1046
|
+
result_type="success",
|
|
1047
|
+
)
|
|
1048
|
+
except Exception as e:
|
|
1049
|
+
return ToolResult(
|
|
1050
|
+
text_result_for_llm=f"Error: {e}",
|
|
1051
|
+
result_type="failure",
|
|
1052
|
+
error=str(e),
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
return CopilotTool(
|
|
1056
|
+
name=ai_func.name,
|
|
1057
|
+
description=ai_func.description,
|
|
1058
|
+
handler=handler,
|
|
1059
|
+
parameters=ai_func.parameters(),
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
def _build_session_hooks(
|
|
1063
|
+
self,
|
|
1064
|
+
all_tools: Sequence[ToolTypes | CopilotTool],
|
|
1065
|
+
options: Mapping[str, Any],
|
|
1066
|
+
) -> SessionHooks | None:
|
|
1067
|
+
"""Build the ``SessionHooks`` to pass to the Copilot SDK for this session.
|
|
1068
|
+
|
|
1069
|
+
Approval enforcement for ``FunctionTool`` instances declared with
|
|
1070
|
+
``approval_mode="always_require"`` is delegated to the Copilot SDK's native
|
|
1071
|
+
``on_pre_tool_use`` hook:
|
|
1072
|
+
|
|
1073
|
+
- If the caller supplies their own session hooks -- either the SDK-native
|
|
1074
|
+
``hooks`` dict or the convenience ``on_pre_tool_use`` handler (via per-run
|
|
1075
|
+
``options`` or ``default_options``) -- those take precedence and are used
|
|
1076
|
+
as-is. When both are given, the explicit ``hooks`` dict wins for any key it
|
|
1077
|
+
defines and the ``on_pre_tool_use`` shortcut fills in that key otherwise. A
|
|
1078
|
+
warning is logged naming any approval-required tool that will therefore not
|
|
1079
|
+
be automatically gated, since the caller's hooks are responsible for
|
|
1080
|
+
enforcing approval.
|
|
1081
|
+
- Otherwise, when any approval-required tool is present, a default hook is
|
|
1082
|
+
installed that returns ``"ask"`` for those tools (routing the decision to
|
|
1083
|
+
``on_permission_request``) and defers (``None``) for all other tools.
|
|
1084
|
+
- The default hook is **not** installed when the deprecated
|
|
1085
|
+
``on_function_approval`` callback is configured: in that case approval is
|
|
1086
|
+
enforced inside the tool handler (see :meth:`_tool_to_copilot_tool`) to
|
|
1087
|
+
preserve backward-compatible behavior.
|
|
1088
|
+
- When there are no approval-required tools and no caller hooks, ``None`` is
|
|
1089
|
+
returned so no hooks are registered.
|
|
1090
|
+
|
|
1091
|
+
Args:
|
|
1092
|
+
all_tools: The full set of tools resolved for the session.
|
|
1093
|
+
options: The merged session options (``default_options`` overlaid with
|
|
1094
|
+
per-run ``options``).
|
|
1095
|
+
|
|
1096
|
+
Returns:
|
|
1097
|
+
The hooks to register for the session, or ``None`` if none are needed.
|
|
1098
|
+
"""
|
|
1099
|
+
user_hook: PreToolUseHandler | None = options.get("on_pre_tool_use") or self._on_pre_tool_use
|
|
1100
|
+
caller_hooks: Mapping[str, Any] | None = options.get("hooks")
|
|
1101
|
+
|
|
1102
|
+
# Combine caller-provided hooks: the SDK-native ``hooks`` dict plus the
|
|
1103
|
+
# convenience ``on_pre_tool_use`` shortcut. The explicit dict wins for the
|
|
1104
|
+
# keys it defines; the shortcut only fills in ``on_pre_tool_use`` otherwise.
|
|
1105
|
+
combined: dict[str, Any] = {}
|
|
1106
|
+
if user_hook is not None:
|
|
1107
|
+
combined["on_pre_tool_use"] = user_hook
|
|
1108
|
+
if caller_hooks:
|
|
1109
|
+
combined.update(caller_hooks)
|
|
1110
|
+
|
|
1111
|
+
approval_required_names = {
|
|
1112
|
+
tool.name for tool in all_tools if isinstance(tool, FunctionTool) and tool.approval_mode == "always_require"
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if combined:
|
|
1116
|
+
if approval_required_names:
|
|
1117
|
+
logger.warning(
|
|
1118
|
+
"Custom session hooks are configured, so %d approval-required tool(s) (%s) "
|
|
1119
|
+
"will not be automatically gated by GitHubCopilotAgent. The custom hooks are responsible "
|
|
1120
|
+
"for enforcing approval (for example, by returning a 'deny' or 'ask' decision).",
|
|
1121
|
+
len(approval_required_names),
|
|
1122
|
+
", ".join(sorted(approval_required_names)),
|
|
1123
|
+
)
|
|
1124
|
+
return cast("SessionHooks", combined)
|
|
1125
|
+
|
|
1126
|
+
if not approval_required_names:
|
|
1127
|
+
return None
|
|
1128
|
+
|
|
1129
|
+
# The deprecated on_function_approval callback enforces approval in the tool
|
|
1130
|
+
# handler; don't also install the default ask-hook (which would double-gate).
|
|
1131
|
+
if self._function_approval_handler is not None:
|
|
1132
|
+
return None
|
|
1133
|
+
|
|
1134
|
+
def default_pre_tool_use(
|
|
1135
|
+
hook_input: Mapping[str, Any],
|
|
1136
|
+
_context: Mapping[str, str],
|
|
1137
|
+
) -> PreToolUseHookOutput | None:
|
|
1138
|
+
tool_name = hook_input.get("toolName")
|
|
1139
|
+
if tool_name in approval_required_names:
|
|
1140
|
+
return {
|
|
1141
|
+
"permissionDecision": "ask",
|
|
1142
|
+
"permissionDecisionReason": (
|
|
1143
|
+
f"Tool '{tool_name}' is marked as requiring approval (approval_mode='always_require')."
|
|
1144
|
+
),
|
|
1145
|
+
}
|
|
1146
|
+
return None
|
|
1147
|
+
|
|
1148
|
+
return {"on_pre_tool_use": default_pre_tool_use}
|
|
1149
|
+
|
|
1150
|
+
async def _get_or_create_session(
|
|
1151
|
+
self,
|
|
1152
|
+
agent_session: AgentSession,
|
|
1153
|
+
streaming: bool = False,
|
|
1154
|
+
runtime_options: dict[str, Any] | None = None,
|
|
1155
|
+
) -> CopilotSession:
|
|
1156
|
+
"""Get an existing session or create a new one for the session.
|
|
1157
|
+
|
|
1158
|
+
Args:
|
|
1159
|
+
agent_session: The conversation session.
|
|
1160
|
+
streaming: Whether to enable streaming for the session.
|
|
1161
|
+
runtime_options: Runtime options from run that take precedence.
|
|
1162
|
+
|
|
1163
|
+
Returns:
|
|
1164
|
+
A CopilotSession instance.
|
|
1165
|
+
|
|
1166
|
+
Raises:
|
|
1167
|
+
AgentException: If the session cannot be created.
|
|
1168
|
+
"""
|
|
1169
|
+
if not self._client:
|
|
1170
|
+
raise RuntimeError("GitHub Copilot client not initialized. Call start() first.")
|
|
1171
|
+
|
|
1172
|
+
try:
|
|
1173
|
+
if agent_session.service_session_id:
|
|
1174
|
+
service_session_id = agent_session.service_session_id
|
|
1175
|
+
if not isinstance(service_session_id, str):
|
|
1176
|
+
raise AgentException(
|
|
1177
|
+
"GitHubCopilotAgent expects a string service_session_id for session resumption."
|
|
1178
|
+
)
|
|
1179
|
+
return await self._resume_session(service_session_id, streaming, runtime_options)
|
|
1180
|
+
|
|
1181
|
+
session = await self._create_session(streaming, runtime_options)
|
|
1182
|
+
agent_session.service_session_id = session.session_id
|
|
1183
|
+
return session
|
|
1184
|
+
except Exception as ex:
|
|
1185
|
+
raise AgentException(f"Failed to create GitHub Copilot session: {ex}") from ex
|
|
1186
|
+
|
|
1187
|
+
def _build_session_kwargs(
|
|
1188
|
+
self,
|
|
1189
|
+
streaming: bool,
|
|
1190
|
+
runtime_options: dict[str, Any] | None,
|
|
1191
|
+
) -> dict[str, Any]:
|
|
1192
|
+
"""Assemble keyword arguments for ``create_session`` / ``resume_session``.
|
|
1193
|
+
|
|
1194
|
+
Options are layered: the agent's ``default_options`` first, then per-run
|
|
1195
|
+
``runtime_options`` which override them. Every key is forwarded verbatim to
|
|
1196
|
+
the Copilot SDK, so any ``create_session`` parameter is supported without a
|
|
1197
|
+
dedicated mapping here (an unknown name surfaces as a ``TypeError`` from the
|
|
1198
|
+
SDK). A few keys are handled specially because they need a secure default
|
|
1199
|
+
(``on_permission_request`` defaults to denying all requests) or transforming:
|
|
1200
|
+
``tools`` are merged with the agent's tools and converted to SDK tools, and
|
|
1201
|
+
approval callbacks are turned into ``hooks``.
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
streaming: Whether to enable streaming for the session.
|
|
1205
|
+
runtime_options: Runtime options that take precedence over default_options.
|
|
1206
|
+
|
|
1207
|
+
Returns:
|
|
1208
|
+
The keyword arguments to splat into the SDK session factory.
|
|
1209
|
+
"""
|
|
1210
|
+
opts = runtime_options or {}
|
|
1211
|
+
|
|
1212
|
+
# Passthrough layer: agent defaults first, per-run options override.
|
|
1213
|
+
kwargs: dict[str, Any] = {**self._default_options, **opts}
|
|
1214
|
+
|
|
1215
|
+
# Merge agent-level tools with any caller-supplied tools (from default_options
|
|
1216
|
+
# or per-run options, the latter winning) and convert to SDK tools.
|
|
1217
|
+
all_tools = list(self._tools or []) + list(kwargs.get("tools") or [])
|
|
1218
|
+
kwargs["tools"] = self._prepare_tools(all_tools) if all_tools else None
|
|
1219
|
+
|
|
1220
|
+
kwargs["streaming"] = streaming
|
|
1221
|
+
# model may already be present from per-run options (merged above); otherwise fall
|
|
1222
|
+
# back to the resolved setting (which carries the default_options / env model).
|
|
1223
|
+
if not kwargs.get("model"):
|
|
1224
|
+
kwargs["model"] = self._settings.get("model") or None
|
|
1225
|
+
kwargs["on_permission_request"] = (
|
|
1226
|
+
opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions
|
|
1227
|
+
)
|
|
1228
|
+
kwargs["hooks"] = self._build_session_hooks(all_tools, kwargs)
|
|
1229
|
+
|
|
1230
|
+
# Strip agent-internal and client-level keys that are consumed here or in the
|
|
1231
|
+
# run methods (and settings) but are NOT valid create_session parameters, so
|
|
1232
|
+
# they don't leak through the passthrough layer and raise TypeError.
|
|
1233
|
+
for key in ("on_pre_tool_use", "on_function_approval", "timeout", "cli_path", "log_level", "base_directory"):
|
|
1234
|
+
kwargs.pop(key, None)
|
|
1235
|
+
|
|
1236
|
+
return kwargs
|
|
1237
|
+
|
|
1238
|
+
async def _create_session(
|
|
1239
|
+
self,
|
|
1240
|
+
streaming: bool,
|
|
1241
|
+
runtime_options: dict[str, Any] | None = None,
|
|
1242
|
+
) -> CopilotSession:
|
|
1243
|
+
"""Create a new Copilot session.
|
|
1244
|
+
|
|
1245
|
+
Args:
|
|
1246
|
+
streaming: Whether to enable streaming for the session.
|
|
1247
|
+
runtime_options: Runtime options that take precedence over default_options.
|
|
1248
|
+
"""
|
|
1249
|
+
if not self._client:
|
|
1250
|
+
raise RuntimeError("GitHub Copilot client not initialized. Call start() first.")
|
|
1251
|
+
|
|
1252
|
+
return await self._client.create_session(**self._build_session_kwargs(streaming, runtime_options))
|
|
1253
|
+
|
|
1254
|
+
async def _resume_session(
|
|
1255
|
+
self,
|
|
1256
|
+
session_id: str,
|
|
1257
|
+
streaming: bool,
|
|
1258
|
+
runtime_options: dict[str, Any] | None = None,
|
|
1259
|
+
) -> CopilotSession:
|
|
1260
|
+
"""Resume an existing Copilot session by ID.
|
|
1261
|
+
|
|
1262
|
+
Args:
|
|
1263
|
+
session_id: The session ID to resume.
|
|
1264
|
+
streaming: Whether to enable streaming for the session.
|
|
1265
|
+
runtime_options: Runtime options that take precedence over default_options.
|
|
1266
|
+
"""
|
|
1267
|
+
if not self._client:
|
|
1268
|
+
raise RuntimeError("GitHub Copilot client not initialized. Call start() first.")
|
|
1269
|
+
|
|
1270
|
+
return await self._client.resume_session(session_id, **self._build_session_kwargs(streaming, runtime_options))
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
class GitHubCopilotAgent( # type: ignore[misc]
|
|
1274
|
+
AgentMiddlewareLayer,
|
|
1275
|
+
AgentTelemetryLayer,
|
|
1276
|
+
RawGitHubCopilotAgent[OptionsT],
|
|
1277
|
+
Generic[OptionsT],
|
|
1278
|
+
):
|
|
1279
|
+
"""A GitHub Copilot Agent with full middleware and telemetry support.
|
|
1280
|
+
|
|
1281
|
+
This is the recommended agent class for most use cases. It includes
|
|
1282
|
+
middleware support and OpenTelemetry-based telemetry for observability,
|
|
1283
|
+
with middleware running outside the telemetry span so middleware execution
|
|
1284
|
+
time is not captured in traces. For a minimal implementation without these
|
|
1285
|
+
layers, use :class:`RawGitHubCopilotAgent`.
|
|
1286
|
+
|
|
1287
|
+
Examples:
|
|
1288
|
+
Basic usage:
|
|
1289
|
+
|
|
1290
|
+
.. code-block:: python
|
|
1291
|
+
|
|
1292
|
+
async with GitHubCopilotAgent() as agent:
|
|
1293
|
+
response = await agent.run("Hello, world!")
|
|
1294
|
+
print(response)
|
|
1295
|
+
|
|
1296
|
+
With explicitly typed options:
|
|
1297
|
+
|
|
1298
|
+
.. code-block:: python
|
|
1299
|
+
|
|
1300
|
+
from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions
|
|
1301
|
+
|
|
1302
|
+
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
|
|
1303
|
+
default_options={"model": "claude-sonnet-4-5", "timeout": 120}
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
With observability:
|
|
1307
|
+
|
|
1308
|
+
.. code-block:: python
|
|
1309
|
+
|
|
1310
|
+
from agent_framework.observability import configure_otel_providers
|
|
1311
|
+
|
|
1312
|
+
configure_otel_providers()
|
|
1313
|
+
async with GitHubCopilotAgent() as agent:
|
|
1314
|
+
response = await agent.run("Hello, world!")
|
|
1315
|
+
"""
|
|
1316
|
+
|
|
1317
|
+
def __init__(
|
|
1318
|
+
self,
|
|
1319
|
+
instructions: str | None = None,
|
|
1320
|
+
*,
|
|
1321
|
+
client: CopilotClient | None = None,
|
|
1322
|
+
id: str | None = None,
|
|
1323
|
+
name: str | None = None,
|
|
1324
|
+
description: str | None = None,
|
|
1325
|
+
context_providers: Sequence[ContextProvider] | None = None,
|
|
1326
|
+
middleware: Sequence[AgentMiddlewareTypes] | None = None,
|
|
1327
|
+
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
|
1328
|
+
default_options: OptionsT | None = None,
|
|
1329
|
+
env_file_path: str | None = None,
|
|
1330
|
+
env_file_encoding: str | None = None,
|
|
1331
|
+
) -> None:
|
|
1332
|
+
"""Initialize a GitHub Copilot Agent with full middleware and telemetry.
|
|
1333
|
+
|
|
1334
|
+
Args:
|
|
1335
|
+
instructions: System message for the agent.
|
|
1336
|
+
|
|
1337
|
+
Keyword Args:
|
|
1338
|
+
client: Optional pre-configured CopilotClient instance. If not provided,
|
|
1339
|
+
a new client will be created using the other parameters.
|
|
1340
|
+
id: ID of the agent.
|
|
1341
|
+
name: Name of the agent.
|
|
1342
|
+
description: Description of the agent.
|
|
1343
|
+
context_providers: Context providers to be used by the agent.
|
|
1344
|
+
middleware: Agent middleware used by the agent.
|
|
1345
|
+
tools: Tools to use for the agent. Can be functions or tool definition dicts.
|
|
1346
|
+
These are converted to Copilot SDK tools internally.
|
|
1347
|
+
default_options: Default options for the agent. Can include cli_path, model,
|
|
1348
|
+
timeout, log_level, etc.
|
|
1349
|
+
env_file_path: Optional path to .env file for loading configuration.
|
|
1350
|
+
env_file_encoding: Encoding of the .env file, defaults to 'utf-8'.
|
|
1351
|
+
"""
|
|
1352
|
+
super().__init__(
|
|
1353
|
+
instructions,
|
|
1354
|
+
client=client,
|
|
1355
|
+
id=id,
|
|
1356
|
+
name=name,
|
|
1357
|
+
description=description,
|
|
1358
|
+
context_providers=context_providers,
|
|
1359
|
+
middleware=middleware,
|
|
1360
|
+
tools=tools,
|
|
1361
|
+
default_options=default_options,
|
|
1362
|
+
env_file_path=env_file_path,
|
|
1363
|
+
env_file_encoding=env_file_encoding,
|
|
1364
|
+
)
|