agent-framework-github-copilot 1.0.0b260429__tar.gz → 1.0.0b260514__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-github-copilot
3
- Version: 1.0.0b260429
3
+ Version: 1.0.0b260514
4
4
  Summary: GitHub Copilot integration for Microsoft Agent Framework.
5
5
  Author-email: Microsoft <af-support@microsoft.com>
6
6
  Requires-Python: >=3.10
@@ -16,8 +16,8 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Typing :: Typed
18
18
  License-File: LICENSE
19
- Requires-Dist: agent-framework-core>=1.2.2,<2
20
- Requires-Dist: github-copilot-sdk>=0.2.1,<=0.2.1; python_version >= '3.11'
19
+ Requires-Dist: agent-framework-core>=1.4.0,<2
20
+ Requires-Dist: github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'
21
21
  Project-URL: homepage, https://aka.ms/agent-framework
22
22
  Project-URL: issues, https://github.com/microsoft/agent-framework/issues
23
23
  Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
@@ -4,9 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import contextlib
7
+ import inspect
7
8
  import logging
8
9
  import sys
9
- from collections.abc import AsyncIterable, Awaitable, Callable, MutableMapping, Sequence
10
+ from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, Sequence
10
11
  from typing import Any, ClassVar, Generic, Literal, TypedDict, overload
11
12
 
12
13
  if sys.version_info >= (3, 11):
@@ -59,6 +60,59 @@ DEFAULT_TIMEOUT_SECONDS: float = 60.0
59
60
  PermissionHandlerType = Callable[[PermissionRequest, dict[str, str]], PermissionRequestResult]
60
61
  """Type for permission request handlers."""
61
62
 
63
+
64
+ FunctionApprovalCallback = Callable[[Content], "bool | Awaitable[bool]"]
65
+ """Callback invoked by the agent before executing a FunctionTool that requires approval.
66
+
67
+ The callback receives a ``FunctionCallContent`` describing the pending call
68
+ (``name``, ``arguments``, and a synthetic ``call_id``) and must return ``True``
69
+ to allow execution or ``False`` to deny it. Both synchronous and ``await``-able
70
+ return values are supported.
71
+
72
+ The Copilot CLI manages its own tool-calling loop, so the framework cannot
73
+ round-trip a ``FunctionApprovalRequestContent`` / ``FunctionApprovalResponseContent``
74
+ pair the way the standard chat-client pipeline does. This callback is the
75
+ agent-level enforcement point for tools declared with
76
+ ``approval_mode="always_require"``: when no callback is configured the agent
77
+ denies these calls by default.
78
+
79
+ Note: this is independent of ``on_permission_request``, which gates the
80
+ Copilot SDK's *built-in* shell/file actions; ``on_function_approval`` gates
81
+ agent-framework ``FunctionTool`` calls.
82
+ """
83
+
84
+
85
+ async def _resolve_function_approval(
86
+ callback: FunctionApprovalCallback | None,
87
+ func_tool: FunctionTool,
88
+ arguments: Mapping[str, Any] | None,
89
+ ) -> bool:
90
+ """Run the agent-level approval callback for a pending tool call.
91
+
92
+ Returns ``True`` only when ``callback`` is configured and explicitly returns
93
+ a truthy value. A missing callback or any callback failure is treated as a
94
+ denial so the secure-by-default policy holds even if the user code raises.
95
+ """
96
+ if callback is None:
97
+ return False
98
+ request = Content.from_function_call(
99
+ call_id=f"af-copilot-approval::{func_tool.name}",
100
+ name=func_tool.name,
101
+ arguments=None if arguments is None else dict(arguments),
102
+ )
103
+ try:
104
+ outcome = callback(request)
105
+ if inspect.isawaitable(outcome):
106
+ outcome = await outcome
107
+ except Exception:
108
+ logger.exception(
109
+ "on_function_approval callback raised for tool '%s'; denying execution.",
110
+ func_tool.name,
111
+ )
112
+ return False
113
+ return bool(outcome)
114
+
115
+
62
116
  logger = logging.getLogger("agent_framework.github_copilot")
63
117
 
64
118
 
@@ -86,12 +140,18 @@ class GitHubCopilotSettings(TypedDict, total=False):
86
140
  Can be set via environment variable GITHUB_COPILOT_TIMEOUT.
87
141
  log_level: CLI log level.
88
142
  Can be set via environment variable GITHUB_COPILOT_LOG_LEVEL.
143
+ copilot_home: Directory where the CLI stores session state, configuration,
144
+ and other persistent data. Can be set via environment variable
145
+ GITHUB_COPILOT_COPILOT_HOME. Defaults to ~/.copilot when not set.
146
+ Only applicable when the SDK spawns the CLI process (ignored when
147
+ connecting to an external server via a pre-configured client).
89
148
  """
90
149
 
91
150
  cli_path: str | None
92
151
  model: str | None
93
152
  timeout: float | None
94
153
  log_level: str | None
154
+ copilot_home: str | None
95
155
 
96
156
 
97
157
  class GitHubCopilotOptions(TypedDict, total=False):
@@ -133,6 +193,20 @@ class GitHubCopilotOptions(TypedDict, total=False):
133
193
  instead of the default GitHub Copilot backend.
134
194
  """
135
195
 
196
+ instruction_directories: list[str]
197
+ """Additional directories to search for custom instruction files.
198
+ Lets applications point the CLI at project-specific or team-shared instruction
199
+ files beyond the default locations.
200
+ """
201
+
202
+ on_function_approval: FunctionApprovalCallback
203
+ """Approval callback for ``FunctionTool`` instances declared with
204
+ ``approval_mode="always_require"``. The callback is awaited (sync or async)
205
+ inside the SDK tool-handler before the tool is executed; a falsy return
206
+ value denies the call. If omitted, calls to such tools are denied with an
207
+ explanatory message returned to the model. This is independent of
208
+ ``on_permission_request``, which gates the Copilot SDK's built-in actions."""
209
+
136
210
 
137
211
  OptionsT = TypeVar(
138
212
  "OptionsT",
@@ -238,6 +312,9 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
238
312
  on_permission_request: PermissionHandlerType | None = opts.pop("on_permission_request", None)
239
313
  mcp_servers: dict[str, MCPServerConfig] | None = opts.pop("mcp_servers", None)
240
314
  provider: ProviderConfig | None = opts.pop("provider", None)
315
+ instruction_directories: list[str] | None = opts.pop("instruction_directories", None)
316
+ on_function_approval: FunctionApprovalCallback | None = opts.pop("on_function_approval", None)
317
+ copilot_home = opts.pop("copilot_home", None)
241
318
 
242
319
  self._settings = load_settings(
243
320
  GitHubCopilotSettings,
@@ -246,14 +323,17 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
246
323
  model=model,
247
324
  timeout=timeout,
248
325
  log_level=log_level,
326
+ copilot_home=copilot_home,
249
327
  env_file_path=env_file_path,
250
328
  env_file_encoding=env_file_encoding,
251
329
  )
252
330
 
253
331
  self._tools = normalize_tools(tools)
254
332
  self._permission_handler = on_permission_request
333
+ self._function_approval_handler: FunctionApprovalCallback | None = on_function_approval
255
334
  self._mcp_servers = mcp_servers
256
335
  self._provider = provider
336
+ self._instruction_directories = instruction_directories
257
337
  self._default_options = opts
258
338
  self._started = False
259
339
 
@@ -282,10 +362,13 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
282
362
  if self._client is None:
283
363
  cli_path = self._settings.get("cli_path") or None
284
364
  log_level = self._settings.get("log_level") or None
365
+ copilot_home = self._settings.get("copilot_home") or None
285
366
 
286
367
  subprocess_kwargs: dict[str, Any] = {"cli_path": cli_path}
287
368
  if log_level:
288
369
  subprocess_kwargs["log_level"] = log_level
370
+ if copilot_home:
371
+ subprocess_kwargs["copilot_home"] = copilot_home
289
372
  self._client = CopilotClient(SubprocessConfig(**subprocess_kwargs))
290
373
 
291
374
  try:
@@ -425,6 +508,12 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
425
508
  session = self.create_session()
426
509
 
427
510
  opts: dict[str, Any] = dict(options) if options else {}
511
+ if "on_function_approval" in opts:
512
+ raise ValueError(
513
+ "on_function_approval is a security-sensitive option and must be set "
514
+ "via default_options at agent construction time. It cannot be overridden "
515
+ "per run."
516
+ )
428
517
  timeout = opts.get("timeout") or self._settings.get("timeout") or DEFAULT_TIMEOUT_SECONDS
429
518
 
430
519
  input_messages = normalize_messages(messages)
@@ -453,13 +542,14 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
453
542
  # send_and_wait returns only the final ASSISTANT_MESSAGE event;
454
543
  # other events (deltas, tool calls) are handled internally by the SDK.
455
544
  if response_event and response_event.type == SessionEventType.ASSISTANT_MESSAGE:
456
- message_id = response_event.data.message_id
545
+ data: Any = response_event.data
546
+ message_id = data.message_id
457
547
 
458
- if response_event.data.content:
548
+ if data.content:
459
549
  response_messages.append(
460
550
  Message(
461
551
  role="assistant",
462
- contents=[Content.from_text(response_event.data.content)],
552
+ contents=[Content.from_text(data.content)],
463
553
  message_id=message_id,
464
554
  raw_representation=response_event,
465
555
  )
@@ -504,6 +594,12 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
504
594
  session = self.create_session()
505
595
 
506
596
  opts: dict[str, Any] = dict(options) if options else {}
597
+ if "on_function_approval" in opts:
598
+ raise ValueError(
599
+ "on_function_approval is a security-sensitive option and must be set "
600
+ "via default_options at agent construction time. It cannot be overridden "
601
+ "per run."
602
+ )
507
603
 
508
604
  input_messages = normalize_messages(messages)
509
605
 
@@ -527,12 +623,13 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
527
623
 
528
624
  def event_handler(event: SessionEvent) -> None:
529
625
  if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA:
530
- if event.data.delta_content:
626
+ data: Any = event.data
627
+ if data.delta_content:
531
628
  update = AgentResponseUpdate(
532
629
  role="assistant",
533
- contents=[Content.from_text(event.data.delta_content)],
534
- response_id=event.data.message_id,
535
- message_id=event.data.message_id,
630
+ contents=[Content.from_text(data.delta_content)],
631
+ response_id=data.message_id,
632
+ message_id=data.message_id,
536
633
  raw_representation=event,
537
634
  )
538
635
  queue.put_nowait(update)
@@ -576,7 +673,8 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
576
673
  elif event.type == SessionEventType.SESSION_IDLE:
577
674
  queue.put_nowait(None)
578
675
  elif event.type == SessionEventType.SESSION_ERROR:
579
- error_msg = event.data.message or "Unknown error"
676
+ error_data: Any = event.data
677
+ error_msg = error_data.message or "Unknown error"
580
678
  queue.put_nowait(AgentException(f"GitHub Copilot session error: {error_msg}"))
581
679
 
582
680
  unsubscribe = copilot_session.on(event_handler)
@@ -681,10 +779,33 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
681
779
 
682
780
  def _tool_to_copilot_tool(self, ai_func: FunctionTool) -> CopilotTool:
683
781
  """Convert an FunctionTool to a Copilot SDK tool."""
782
+ approval_handler = self._function_approval_handler
783
+ requires_approval = ai_func.approval_mode == "always_require"
684
784
 
685
785
  async def handler(invocation: ToolInvocation) -> ToolResult:
686
786
  args: dict[str, Any] = invocation.arguments or {}
687
787
  try:
788
+ if requires_approval and not await _resolve_function_approval(approval_handler, ai_func, args):
789
+ deny_text = (
790
+ f"Tool '{ai_func.name}' requires human approval "
791
+ "(approval_mode='always_require') and the request was denied."
792
+ if approval_handler is not None
793
+ else (
794
+ f"Tool '{ai_func.name}' requires human approval "
795
+ "(approval_mode='always_require') but no on_function_approval "
796
+ "callback is configured on the agent; the request was denied."
797
+ )
798
+ )
799
+ logger.info(
800
+ "Denying execution of tool '%s' (approval_mode='always_require', %s)",
801
+ ai_func.name,
802
+ "callback denied" if approval_handler is not None else "no callback configured",
803
+ )
804
+ return ToolResult(
805
+ text_result_for_llm=deny_text,
806
+ result_type="failure",
807
+ error="approval_denied",
808
+ )
688
809
  if ai_func.input_model:
689
810
  args_instance = ai_func.input_model(**args)
690
811
  result = await ai_func.invoke(arguments=args_instance)
@@ -739,7 +860,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
739
860
 
740
861
  try:
741
862
  if agent_session.service_session_id:
742
- return await self._resume_session(agent_session.service_session_id, streaming)
863
+ return await self._resume_session(agent_session.service_session_id, streaming, runtime_options)
743
864
 
744
865
  session = await self._create_session(streaming, runtime_options)
745
866
  agent_session.service_session_id = session.session_id
@@ -769,6 +890,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
769
890
  )
770
891
  mcp_servers = opts.get("mcp_servers") or self._mcp_servers or None
771
892
  provider = opts.get("provider") or self._provider or None
893
+ instruction_directories = opts.get("instruction_directories", self._instruction_directories)
772
894
  tools = self._prepare_tools(self._tools) if self._tools else None
773
895
 
774
896
  return await self._client.create_session(
@@ -779,23 +901,46 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
779
901
  tools=tools or None,
780
902
  mcp_servers=mcp_servers or None,
781
903
  provider=provider or None,
904
+ instruction_directories=instruction_directories,
782
905
  )
783
906
 
784
- async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSession:
785
- """Resume an existing Copilot session by ID."""
907
+ async def _resume_session(
908
+ self,
909
+ session_id: str,
910
+ streaming: bool,
911
+ runtime_options: dict[str, Any] | None = None,
912
+ ) -> CopilotSession:
913
+ """Resume an existing Copilot session by ID.
914
+
915
+ Args:
916
+ session_id: The session ID to resume.
917
+ streaming: Whether to enable streaming for the session.
918
+ runtime_options: Runtime options that take precedence over default_options.
919
+ """
786
920
  if not self._client:
787
921
  raise RuntimeError("GitHub Copilot client not initialized. Call start() first.")
788
922
 
789
- permission_handler: PermissionHandlerType = self._permission_handler or _deny_all_permissions
923
+ opts = runtime_options or {}
924
+ model = opts.get("model") or self._settings.get("model") or None
925
+ system_message = opts.get("system_message") or self._default_options.get("system_message") or None
926
+ permission_handler: PermissionHandlerType = (
927
+ opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions
928
+ )
929
+ mcp_servers = opts.get("mcp_servers") or self._mcp_servers or None
930
+ provider = opts.get("provider") or self._provider or None
931
+ instruction_directories = opts.get("instruction_directories", self._instruction_directories)
790
932
  tools = self._prepare_tools(self._tools) if self._tools else None
791
933
 
792
934
  return await self._client.resume_session(
793
935
  session_id,
794
936
  on_permission_request=permission_handler,
795
937
  streaming=streaming,
938
+ model=model or None,
939
+ system_message=system_message or None,
796
940
  tools=tools or None,
797
- mcp_servers=self._mcp_servers or None,
798
- provider=self._provider or None,
941
+ mcp_servers=mcp_servers or None,
942
+ provider=provider or None,
943
+ instruction_directories=instruction_directories,
799
944
  )
800
945
 
801
946
 
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
4
4
  authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
- version = "1.0.0b260429"
7
+ version = "1.0.0b260514"
8
8
  license-files = ["LICENSE"]
9
9
  urls.homepage = "https://aka.ms/agent-framework"
10
10
  urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,8 @@ classifiers = [
23
23
  "Typing :: Typed",
24
24
  ]
25
25
  dependencies = [
26
- "agent-framework-core>=1.2.2,<2",
27
- "github-copilot-sdk>=0.2.1,<=0.2.1; python_version >= '3.11'",
26
+ "agent-framework-core>=1.4.0,<2",
27
+ "github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'",
28
28
  ]
29
29
 
30
30
  [tool.uv]