Python: Updated instructions/system_message logic in GitHub Copilot agent (#3625)

* Updated instructions handling

* Small improvement

* Included runtime options in session creation logic
This commit is contained in:
Dmytro Struk
2026-02-02 21:40:35 -08:00
committed by GitHub
Unverified
parent 98cd72839e
commit 73033c300f
9 changed files with 206 additions and 65 deletions
@@ -31,6 +31,7 @@ from copilot.types import (
PermissionRequestResult,
ResumeSessionConfig,
SessionConfig,
SystemMessageConfig,
ToolInvocation,
ToolResult,
)
@@ -57,8 +58,9 @@ logger = logging.getLogger("agent_framework.github_copilot")
class GitHubCopilotOptions(TypedDict, total=False):
"""GitHub Copilot-specific options."""
instructions: str
"""System message to append to the session."""
system_message: SystemMessageConfig
"""System message configuration for the session. Use mode 'append' to add to the default
system prompt, or 'replace' to completely override it."""
cli_path: str
"""Path to the Copilot CLI executable. Defaults to GITHUB_COPILOT_CLI_PATH environment variable
@@ -139,6 +141,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
def __init__(
self,
instructions: str | None = None,
*,
client: CopilotClient | None = None,
id: str | None = None,
@@ -157,6 +160,9 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
) -> None:
"""Initialize the GitHub Copilot Agent.
Args:
instructions: System message for the agent.
Keyword Args:
client: Optional pre-configured CopilotClient instance. If not provided,
a new client will be created using the other parameters.
@@ -188,7 +194,10 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
# Parse options
opts: dict[str, Any] = dict(default_options) if default_options else {}
instructions = opts.pop("instructions", None)
# Handle instructions - direct parameter takes precedence over default_options.system_message
self._prepare_system_message(instructions, opts)
cli_path = opts.pop("cli_path", None)
model = opts.pop("model", None)
timeout = opts.pop("timeout", None)
@@ -208,7 +217,6 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
except ValidationError as ex:
raise ServiceInitializationError("Failed to create GitHub Copilot settings.", ex) from ex
self._instructions = instructions
self._tools = normalize_tools(tools)
self._permission_handler = on_permission_request
self._mcp_servers = mcp_servers
@@ -302,7 +310,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
opts: dict[str, Any] = dict(options) if options else {}
timeout = opts.pop("timeout", None) or self._settings.timeout or DEFAULT_TIMEOUT_SECONDS
session = await self._get_or_create_session(thread, streaming=False)
session = await self._get_or_create_session(thread, streaming=False, runtime_options=opts)
input_messages = normalize_messages(messages)
prompt = "\n".join([message.text for message in input_messages])
@@ -365,7 +373,9 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
if not thread:
thread = self.get_new_thread()
session = await self._get_or_create_session(thread, streaming=True)
opts: dict[str, Any] = dict(options) if options else {}
session = await self._get_or_create_session(thread, streaming=True, runtime_options=opts)
input_messages = normalize_messages(messages)
prompt = "\n".join([message.text for message in input_messages])
@@ -400,6 +410,29 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
finally:
unsubscribe()
@staticmethod
def _prepare_system_message(
instructions: str | None,
opts: dict[str, Any],
) -> None:
"""Prepare system message configuration in opts.
If instructions is provided, it takes precedence for content.
If system_message is also provided, its mode is preserved.
Modifies opts in place.
Args:
instructions: Direct instructions parameter for content.
opts: Options dictionary to modify.
"""
opts_system_message = opts.pop("system_message", None)
if instructions is not None:
# Use instructions for content, but preserve mode from system_message if provided
mode = opts_system_message.get("mode", "append") if opts_system_message else "append"
opts["system_message"] = {"mode": mode, "content": instructions}
elif opts_system_message is not None:
opts["system_message"] = opts_system_message
def _prepare_tools(
self,
tools: list[ToolProtocol | MutableMapping[str, Any]],
@@ -459,12 +492,14 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
self,
thread: AgentThread,
streaming: bool = False,
runtime_options: dict[str, Any] | None = None,
) -> CopilotSession:
"""Get an existing session or create a new one for the thread.
Args:
thread: The conversation thread.
streaming: Whether to enable streaming for the session.
runtime_options: Runtime options from run/run_stream that take precedence.
Returns:
A CopilotSession instance.
@@ -479,33 +514,47 @@ class GitHubCopilotAgent(BaseAgent, Generic[TOptions]):
if thread.service_thread_id:
return await self._resume_session(thread.service_thread_id, streaming)
session = await self._create_session(streaming)
session = await self._create_session(streaming, runtime_options)
thread.service_thread_id = session.session_id
return session
except Exception as ex:
raise ServiceException(f"Failed to create GitHub Copilot session: {ex}") from ex
async def _create_session(self, streaming: bool) -> CopilotSession:
"""Create a new Copilot session."""
async def _create_session(
self,
streaming: bool,
runtime_options: dict[str, Any] | None = None,
) -> CopilotSession:
"""Create a new Copilot session.
Args:
streaming: Whether to enable streaming for the session.
runtime_options: Runtime options that take precedence over default_options.
"""
if not self._client:
raise ServiceException("GitHub Copilot client not initialized. Call start() first.")
opts = runtime_options or {}
config: SessionConfig = {"streaming": streaming}
if self._settings.model:
config["model"] = self._settings.model # type: ignore[typeddict-item]
model = opts.get("model") or self._settings.model
if model:
config["model"] = model # type: ignore[typeddict-item]
if self._instructions:
config["system_message"] = {"mode": "append", "content": self._instructions}
system_message = opts.get("system_message") or self._default_options.get("system_message")
if system_message:
config["system_message"] = system_message
if self._tools:
config["tools"] = self._prepare_tools(self._tools)
if self._permission_handler:
config["on_permission_request"] = self._permission_handler
permission_handler = opts.get("on_permission_request") or self._permission_handler
if permission_handler:
config["on_permission_request"] = permission_handler
if self._mcp_servers:
config["mcp_servers"] = self._mcp_servers
mcp_servers = opts.get("mcp_servers") or self._mcp_servers
if mcp_servers:
config["mcp_servers"] = mcp_servers
return await self._client.create_session(config)
@@ -135,12 +135,52 @@ class TestGitHubCopilotAgentInit:
agent = GitHubCopilotAgent(tools=[my_tool])
assert len(agent._tools) == 1 # type: ignore
def test_init_with_instructions(self) -> None:
"""Test initialization with custom instructions."""
def test_init_with_instructions_parameter(self) -> None:
"""Test initialization with instructions parameter."""
agent = GitHubCopilotAgent(instructions="You are a helpful assistant.")
assert agent._default_options.get("system_message") == { # type: ignore
"mode": "append",
"content": "You are a helpful assistant.",
}
def test_init_with_system_message_in_default_options(self) -> None:
"""Test initialization with system_message object in default_options."""
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
default_options={"instructions": "You are a helpful assistant."}
default_options={"system_message": {"mode": "append", "content": "You are a helpful assistant."}}
)
assert agent._instructions == "You are a helpful assistant." # type: ignore
assert agent._default_options.get("system_message") == { # type: ignore
"mode": "append",
"content": "You are a helpful assistant.",
}
def test_init_with_system_message_replace_mode(self) -> None:
"""Test initialization with system_message in replace mode."""
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
default_options={"system_message": {"mode": "replace", "content": "Custom system prompt."}}
)
assert agent._default_options.get("system_message") == { # type: ignore
"mode": "replace",
"content": "Custom system prompt.",
}
def test_instructions_parameter_takes_precedence_for_content(self) -> None:
"""Test that direct instructions parameter takes precedence for content but preserves mode."""
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
instructions="Direct instructions",
default_options={"system_message": {"mode": "replace", "content": "Options system_message"}},
)
assert agent._default_options.get("system_message") == { # type: ignore
"mode": "replace",
"content": "Direct instructions",
}
def test_instructions_parameter_defaults_to_append_mode(self) -> None:
"""Test that instructions parameter defaults to append mode when no system_message provided."""
agent = GitHubCopilotAgent(instructions="Direct instructions")
assert agent._default_options.get("system_message") == { # type: ignore
"mode": "append",
"content": "Direct instructions",
}
class TestGitHubCopilotAgentLifecycle:
@@ -462,10 +502,10 @@ class TestGitHubCopilotAgentSessionManagement:
mock_client: MagicMock,
mock_session: MagicMock,
) -> None:
"""Test that session config includes instructions."""
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
"""Test that session config includes instructions from direct parameter."""
agent = GitHubCopilotAgent(
instructions="You are a helpful assistant.",
client=mock_client,
default_options={"instructions": "You are a helpful assistant."},
)
await agent.start()
@@ -476,6 +516,31 @@ class TestGitHubCopilotAgentSessionManagement:
assert config["system_message"]["mode"] == "append"
assert config["system_message"]["content"] == "You are a helpful assistant."
async def test_runtime_options_take_precedence_over_default(
self,
mock_client: MagicMock,
mock_session: MagicMock,
) -> None:
"""Test that runtime options from run() take precedence over default_options."""
agent = GitHubCopilotAgent(
instructions="Default instructions",
client=mock_client,
)
await agent.start()
runtime_options: GitHubCopilotOptions = {
"system_message": {"mode": "replace", "content": "Runtime instructions"}
}
await agent._get_or_create_session( # type: ignore
AgentThread(),
runtime_options=runtime_options,
)
call_args = mock_client.create_session.call_args
config = call_args[0][0]
assert config["system_message"]["mode"] == "replace"
assert config["system_message"]["content"] == "Runtime instructions"
async def test_session_config_includes_streaming_flag(
self,
mock_client: MagicMock,