Skip to content

Commit 926c4ef

Browse files
committed
Python: support Copilot skill directories and provider tools
1 parent 485af07 commit 926c4ef

3 files changed

Lines changed: 303 additions & 28 deletions

File tree

python/packages/github_copilot/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,24 @@ pip install agent-framework-github-copilot --pre
99
## GitHub Copilot Agent
1010

1111
The GitHub Copilot agent enables integration with GitHub Copilot, allowing you to interact with Copilot's agentic capabilities through the Agent Framework.
12+
13+
### Native Copilot skills
14+
15+
You can load Copilot CLI-native skills by passing `skill_directories` in `default_options`:
16+
17+
```python
18+
from copilot.session import PermissionRequestResult
19+
from agent_framework_github_copilot import GitHubCopilotAgent
20+
21+
22+
def approve_all(_request, _context):
23+
return PermissionRequestResult(kind="approved")
24+
25+
26+
agent = GitHubCopilotAgent(
27+
default_options={
28+
"on_permission_request": approve_all,
29+
"skill_directories": ["./skills"],
30+
}
31+
)
32+
```

python/packages/github_copilot/agent_framework_github_copilot/_agent.py

Lines changed: 127 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
import sys
99
from collections.abc import AsyncIterable, Awaitable, Callable, MutableMapping, Sequence
10-
from typing import Any, ClassVar, Generic, Literal, TypedDict, overload
10+
from typing import Any, ClassVar, Generic, Literal, TypedDict, cast, overload
1111

1212
from agent_framework import (
1313
AgentMiddlewareTypes,
@@ -120,6 +120,9 @@ class GitHubCopilotOptions(TypedDict, total=False):
120120
Supports both local (stdio) and remote (HTTP/SSE) servers.
121121
"""
122122

123+
skill_directories: list[str]
124+
"""Directories containing Copilot-native ``SKILL.md`` files to load into the session."""
125+
123126

124127
OptionsT = TypeVar(
125128
"OptionsT",
@@ -232,6 +235,7 @@ def __init__(
232235
log_level = opts.pop("log_level", None)
233236
on_permission_request: PermissionHandlerType | None = opts.pop("on_permission_request", None)
234237
mcp_servers: dict[str, MCPServerConfig] | None = opts.pop("mcp_servers", None)
238+
skill_directories: list[str] | None = opts.pop("skill_directories", None)
235239

236240
self._settings = load_settings(
237241
GitHubCopilotSettings,
@@ -247,6 +251,7 @@ def __init__(
247251
self._tools = normalize_tools(tools)
248252
self._permission_handler = on_permission_request
249253
self._mcp_servers = mcp_servers
254+
self._skill_directories = skill_directories
250255
self._default_options = opts
251256
self._started = False
252257

@@ -395,7 +400,12 @@ async def _run_impl(
395400

396401
# NOTE: session is created after providers run so that future provider-contributed
397402
# tools/config could be folded into runtime_options before session creation.
398-
copilot_session = await self._get_or_create_session(session, streaming=False, runtime_options=opts)
403+
copilot_session = await self._get_or_create_session(
404+
session,
405+
streaming=False,
406+
runtime_options=opts,
407+
session_tools=session_context.tools,
408+
)
399409

400410
# Build the prompt from the full set of messages in the session context,
401411
# so that any context/history provider-injected messages are included.
@@ -473,7 +483,12 @@ async def _stream_updates(
473483

474484
# NOTE: session is created after providers run so that future provider-contributed
475485
# tools/config could be folded into runtime_options before session creation.
476-
copilot_session = await self._get_or_create_session(session, streaming=True, runtime_options=opts)
486+
copilot_session = await self._get_or_create_session(
487+
session,
488+
streaming=True,
489+
runtime_options=opts,
490+
session_tools=session_context.tools,
491+
)
477492

478493
if _ctx_holder is not None:
479494
_ctx_holder["session_context"] = session_context
@@ -677,18 +692,101 @@ async def handler(invocation: ToolInvocation) -> ToolResult:
677692
parameters=ai_func.parameters(),
678693
)
679694

695+
@staticmethod
696+
def _get_tool_name(tool: ToolTypes | CopilotTool) -> str | None:
697+
"""Extract a tool name for duplicate detection."""
698+
if isinstance(tool, dict):
699+
tool_dict = cast(dict[str, Any], tool)
700+
func = tool_dict.get("function")
701+
if isinstance(func, dict):
702+
func_dict = cast(dict[str, Any], func)
703+
name = func_dict.get("name")
704+
return name if isinstance(name, str) else None
705+
return None
706+
707+
name = getattr(tool, "name", None)
708+
return name if isinstance(name, str) else None
709+
710+
@staticmethod
711+
def _resolve_option(
712+
opts: dict[str, Any],
713+
key: str,
714+
default: Any = None,
715+
) -> Any:
716+
"""Resolve a runtime option while preserving explicit empty collections."""
717+
if key not in opts:
718+
return default
719+
value = opts[key]
720+
return default if value is None else value
721+
722+
def _resolve_tools(
723+
self,
724+
session_tools: Sequence[ToolTypes | Callable[..., Any] | CopilotTool] | None = None,
725+
) -> list[CopilotTool] | None:
726+
"""Merge agent and provider tools using core uniqueness rules."""
727+
merged_tools: list[ToolTypes | CopilotTool] = []
728+
seen_tools: dict[str, ToolTypes | CopilotTool] = {}
729+
730+
for tool_group in (self._tools, normalize_tools(session_tools)):
731+
for tool in tool_group:
732+
tool_name = self._get_tool_name(tool)
733+
if tool_name is None:
734+
merged_tools.append(tool)
735+
continue
736+
737+
existing = seen_tools.get(tool_name)
738+
if existing is None:
739+
seen_tools[tool_name] = tool
740+
merged_tools.append(tool)
741+
continue
742+
743+
if existing is tool:
744+
continue
745+
746+
raise ValueError(f"Duplicate tool name '{tool_name}'. Tool names must be unique.")
747+
748+
return self._prepare_tools(merged_tools) if merged_tools else None
749+
750+
def _build_session_config(
751+
self,
752+
*,
753+
streaming: bool,
754+
runtime_options: dict[str, Any] | None = None,
755+
session_tools: Sequence[ToolTypes | Callable[..., Any] | CopilotTool] | None = None,
756+
) -> dict[str, Any]:
757+
"""Build shared Copilot session configuration for create and resume paths."""
758+
opts = runtime_options or {}
759+
model = self._resolve_option(opts, "model", self._settings.get("model"))
760+
system_message = self._resolve_option(opts, "system_message", self._default_options.get("system_message"))
761+
permission_handler = self._resolve_option(opts, "on_permission_request", self._permission_handler)
762+
mcp_servers = self._resolve_option(opts, "mcp_servers", self._mcp_servers)
763+
skill_directories = self._resolve_option(opts, "skill_directories", self._skill_directories)
764+
tools = self._resolve_tools(session_tools)
765+
766+
return {
767+
"on_permission_request": permission_handler or _deny_all_permissions,
768+
"streaming": streaming,
769+
"model": model,
770+
"system_message": system_message,
771+
"tools": tools,
772+
"mcp_servers": mcp_servers,
773+
"skill_directories": skill_directories,
774+
}
775+
680776
async def _get_or_create_session(
681777
self,
682778
agent_session: AgentSession,
683779
streaming: bool = False,
684780
runtime_options: dict[str, Any] | None = None,
781+
session_tools: Sequence[ToolTypes | Callable[..., Any] | CopilotTool] | None = None,
685782
) -> CopilotSession:
686783
"""Get an existing session or create a new one for the session.
687784
688785
Args:
689786
agent_session: The conversation session.
690787
streaming: Whether to enable streaming for the session.
691788
runtime_options: Runtime options from run that take precedence.
789+
session_tools: Tools contributed for this invocation by context providers.
692790
693791
Returns:
694792
A CopilotSession instance.
@@ -701,9 +799,14 @@ async def _get_or_create_session(
701799

702800
try:
703801
if agent_session.service_session_id:
704-
return await self._resume_session(agent_session.service_session_id, streaming)
802+
return await self._resume_session(
803+
agent_session.service_session_id,
804+
streaming,
805+
runtime_options=runtime_options,
806+
session_tools=session_tools,
807+
)
705808

706-
session = await self._create_session(streaming, runtime_options)
809+
session = await self._create_session(streaming, runtime_options, session_tools=session_tools)
707810
agent_session.service_session_id = session.session_id
708811
return session
709812
except Exception as ex:
@@ -713,46 +816,42 @@ async def _create_session(
713816
self,
714817
streaming: bool,
715818
runtime_options: dict[str, Any] | None = None,
819+
session_tools: Sequence[ToolTypes | Callable[..., Any] | CopilotTool] | None = None,
716820
) -> CopilotSession:
717821
"""Create a new Copilot session.
718822
719823
Args:
720824
streaming: Whether to enable streaming for the session.
721825
runtime_options: Runtime options that take precedence over default_options.
826+
session_tools: Tools contributed for this invocation by context providers.
722827
"""
723828
if not self._client:
724829
raise RuntimeError("GitHub Copilot client not initialized. Call start() first.")
725830

726-
opts = runtime_options or {}
727-
model = opts.get("model") or self._settings.get("model") or None
728-
system_message = opts.get("system_message") or self._default_options.get("system_message") or None
729-
permission_handler: PermissionHandlerType = (
730-
opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions
731-
)
732-
mcp_servers = opts.get("mcp_servers") or self._mcp_servers or None
733-
tools = self._prepare_tools(self._tools) if self._tools else None
734-
735831
return await self._client.create_session(
736-
on_permission_request=permission_handler,
737-
streaming=streaming,
738-
model=model or None,
739-
system_message=system_message or None,
740-
tools=tools or None,
741-
mcp_servers=mcp_servers or None,
832+
**self._build_session_config(
833+
streaming=streaming,
834+
runtime_options=runtime_options,
835+
session_tools=session_tools,
836+
)
742837
)
743838

744-
async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSession:
839+
async def _resume_session(
840+
self,
841+
session_id: str,
842+
streaming: bool,
843+
runtime_options: dict[str, Any] | None = None,
844+
session_tools: Sequence[ToolTypes | Callable[..., Any] | CopilotTool] | None = None,
845+
) -> CopilotSession:
745846
"""Resume an existing Copilot session by ID."""
746847
if not self._client:
747848
raise RuntimeError("GitHub Copilot client not initialized. Call start() first.")
748849

749-
permission_handler: PermissionHandlerType = self._permission_handler or _deny_all_permissions
750-
tools = self._prepare_tools(self._tools) if self._tools else None
751-
752850
return await self._client.resume_session(
753851
session_id,
754-
on_permission_request=permission_handler,
755-
streaming=streaming,
756-
tools=tools or None,
757-
mcp_servers=self._mcp_servers or None,
852+
**self._build_session_config(
853+
streaming=streaming,
854+
runtime_options=runtime_options,
855+
session_tools=session_tools,
856+
),
758857
)

0 commit comments

Comments
 (0)