From 41da08bf6f49c1bd16db40b925d6862ae6ef7b7a Mon Sep 17 00:00:00 2001 From: Copilot Date: Sun, 31 May 2026 22:07:31 +0000 Subject: [PATCH 1/2] Add regression test for MCP tool result event projection (#5897) Add a streaming test that exercises multiple consecutive MCP tool invocations with JSON payloads, verifying that TOOL_EXECUTION_START and TOOL_EXECUTION_COMPLETE events are projected as function_call and function_result content items with correct call_id, tool name, and result payload. Removes unrelated formatting-only changes from prior iteration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/test_github_copilot_agent.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index a0f0caef72..168d9fd9ca 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -920,6 +920,87 @@ def mock_on(handler: Any) -> Any: assert responses[3].role == "assistant" assert responses[3].contents[0].type == "text" + async def test_run_streaming_multiple_tool_calls_with_json_results( + self, + mock_client: MagicMock, + mock_session: MagicMock, + session_idle_event: SessionEvent, + ) -> None: + """Test that multiple tool calls with JSON results are all projected correctly.""" + json_payloads = [ + '{"users": [{"id": "u1", "name": "Alice"}, {"id": "u2", "name": "Bob"}]}', + '{"tickets": [{"id": "INC001", "state": "open", "short_description": "VPN issue"}]}', + '{"groups": [{"id": "g1", "displayName": "IT Admins", "members": 42}]}', + ] + tool_names = ["msgraph_list_users", "sn_query_table", "msgraph_list_groups"] + + events: list[SessionEvent] = [] + for i, (name, payload) in enumerate(zip(tool_names, json_payloads)): + call_id = f"call_{i:03d}" + + # Tool start + start_data = MagicMock() + start_data.tool_call_id = call_id + start_data.tool_name = name + start_data.arguments = {"query": f"test_{i}"} + events.append( + SessionEvent( + data=start_data, + id=uuid4(), + timestamp=datetime.now(timezone.utc), + type=SessionEventType.TOOL_EXECUTION_START, + ) + ) + + # Tool complete + complete_data = MagicMock() + complete_data.tool_call_id = call_id + complete_data.result = ToolExecutionCompleteResult(content=payload) + complete_data.success = True + complete_data.error = None + events.append( + SessionEvent( + data=complete_data, + id=uuid4(), + timestamp=datetime.now(timezone.utc), + type=SessionEventType.TOOL_EXECUTION_COMPLETE, + ) + ) + + events.append(session_idle_event) + + def mock_on(handler: Any) -> Any: + for event in events: + handler(event) + return lambda: None + + mock_session.on = mock_on + + agent = GitHubCopilotAgent(client=mock_client) + responses: list[AgentResponseUpdate] = [] + async for update in agent.run("List users, tickets, and groups", stream=True): + responses.append(update) + + # Should have 3 tool calls + 3 tool results = 6 updates + assert len(responses) == 6 + + # Verify each pair: function_call followed by function_result + for i in range(3): + call_update = responses[i * 2] + result_update = responses[i * 2 + 1] + call_id = f"call_{i:03d}" + + assert call_update.role == "assistant" + assert call_update.contents[0].type == "function_call" + assert call_update.contents[0].call_id == call_id + assert call_update.contents[0].name == tool_names[i] + + assert result_update.role == "tool" + assert result_update.contents[0].type == "function_result" + assert result_update.contents[0].call_id == call_id + assert result_update.contents[0].result == json_payloads[i] + assert result_update.contents[0].exception is None + class TestGitHubCopilotAgentSessionManagement: """Test cases for session management.""" From 82ab52632f9ea4a309d1fe2666fdefad5180ce48 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sun, 31 May 2026 22:29:54 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Address=20review=20feedback=20for=20#5897:?= =?UTF-8?q?=20Python:=20.NET:=20[Bug]:=20GitHubCopilotAgent=20silently=20d?= =?UTF-8?q?rops=20MCP=20tool=20result=20events=20=E2=80=94=20projected=20a?= =?UTF-8?q?s=20opaque=20AIContent=20instead=20of=20FunctionResultContent,?= =?UTF-8?q?=20causing=20models=20to=20fabricate=20timeout=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/packages/core/agent_framework/_skills.py | 7 ++----- python/packages/core/tests/core/test_skills.py | 8 ++++---- python/samples/02-agents/harness/harness_research.py | 5 ++++- python/uv.lock | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index 683302b13a..5e313f20d9 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -2134,9 +2134,7 @@ async def _run_script( ), FunctionTool( name="read_skill_resource", - description=( - "Reads a resource associated with a skill, such as references, assets, or dynamic data." - ), + description=("Reads a resource associated with a skill, such as references, assets, or dynamic data."), func=_read_resource, input_model={ "type": "object", @@ -2173,8 +2171,7 @@ async def _run_script( "type": "object", "additionalProperties": True, "description": ( - "Named arguments as key-value pairs " - '(e.g. {"length": 24, "uppercase": true}).' + 'Named arguments as key-value pairs (e.g. {"length": 24, "uppercase": true}).' ), }, { diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index 17fb2cf5ce..31f679c367 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -4086,8 +4086,8 @@ async def test_full_skill_content_contains_scripts(self) -> None: async def test_content_is_cached(self) -> None: skill = _MinimalClassSkill() - content1 = (await skill.get_content()) - content2 = (await skill.get_content()) + content1 = await skill.get_content() + content2 = await skill.get_content() assert content1 is content2 def test_resources_are_lazy_cached(self) -> None: @@ -5587,8 +5587,8 @@ class TestInlineSkillContentCaching: async def test_content_cached_after_first_access(self) -> None: """InlineSkill.content returns the same object on subsequent accesses.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body") - first = (await skill.get_content()) - second = (await skill.get_content()) + first = await skill.get_content() + second = await skill.get_content() assert first is second # Same object (cached) assert "test-skill" in first diff --git a/python/samples/02-agents/harness/harness_research.py b/python/samples/02-agents/harness/harness_research.py index f1cb66228a..977c26f049 100644 --- a/python/samples/02-agents/harness/harness_research.py +++ b/python/samples/02-agents/harness/harness_research.py @@ -109,7 +109,10 @@ async def main() -> None: print(f"\n [calling tool: {content.name}]", flush=True) print(" ", end="", flush=True) # Show web search activity when the result arrives with action details. - elif content.type in ("search_tool_call", "search_tool_result") and getattr(content, "tool_name", None) == "web_search": + elif ( + content.type in ("search_tool_call", "search_tool_result") + and getattr(content, "tool_name", None) == "web_search" + ): action = None if content.type == "search_tool_result" and isinstance(content.result, dict): action = content.result.get("action", {}) diff --git a/python/uv.lock b/python/uv.lock index a67c495e62..3ef58900c5 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -606,7 +606,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=1.0.0b2,<=1.0.0b2" }, ] [[package]]