77import logging
88import sys
99from 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
1212from 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
124127OptionsT = 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