From 7f6cd1d0b8f1cf07e7a2e44a9e2f5eb32bb61c1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:18:22 +0000 Subject: [PATCH 1/3] Initial plan From 8b65d49cded5f04158ea261e0e8e1b80bf843ddd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:25:09 +0000 Subject: [PATCH 2/3] Add AssistantReasoningEvent and AssistantReasoningDeltaEvent support to GitHubCopilotAgent Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../GitHubCopilotAgent.cs | 36 +++ .../GitHubCopilotAgentReasoningTests.cs | 272 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentReasoningTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs index 41236fcc55..c1d4b5012c 100644 --- a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs @@ -182,6 +182,14 @@ protected override async IAsyncEnumerable RunCoreStreamingA channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage)); break; + case AssistantReasoningDeltaEvent reasoningDeltaEvent: + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(reasoningDeltaEvent)); + break; + + case AssistantReasoningEvent reasoningEvent: + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(reasoningEvent)); + break; + case AssistantUsageEvent usageEvent: channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(usageEvent)); break; @@ -339,6 +347,34 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantUsageEvent usa }; } + private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantReasoningDeltaEvent reasoningDeltaEvent) + { + TextReasoningContent reasoningContent = new(reasoningDeltaEvent.Data?.DeltaContent ?? string.Empty) + { + RawRepresentation = reasoningDeltaEvent + }; + + return new AgentResponseUpdate(ChatRole.Assistant, [reasoningContent]) + { + AgentId = this.Id, + CreatedAt = reasoningDeltaEvent.Timestamp + }; + } + + private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantReasoningEvent reasoningEvent) + { + TextReasoningContent reasoningContent = new(reasoningEvent.Data?.Content ?? string.Empty) + { + RawRepresentation = reasoningEvent + }; + + return new AgentResponseUpdate(ChatRole.Assistant, [reasoningContent]) + { + AgentId = this.Id, + CreatedAt = reasoningEvent.Timestamp + }; + } + private static AdditionalPropertiesDictionary? GetAdditionalCounts(AssistantUsageEvent usageEvent) { if (usageEvent.Data is null) diff --git a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentReasoningTests.cs b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentReasoningTests.cs new file mode 100644 index 0000000000..7d45fdfc80 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentReasoningTests.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Reflection; +using GitHub.Copilot.SDK; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.GitHub.Copilot.UnitTests; + +/// +/// Unit tests for the reasoning event handling. +/// +public sealed class GitHubCopilotAgentReasoningTests +{ + /// + /// Tests that ConvertToAgentResponseUpdate correctly handles AssistantReasoningDeltaEvent. + /// + [Fact] + public void ConvertToAgentResponseUpdate_WithReasoningDeltaEvent_CreatesTextReasoningContent() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false); + + // Create an AssistantReasoningDeltaEvent using reflection + AssistantReasoningDeltaEvent reasoningDeltaEvent = new() + { + Data = new AssistantReasoningDeltaData + { + ReasoningId = "reasoning-123", + DeltaContent = "Thinking step " + }, + Timestamp = DateTimeOffset.UtcNow + }; + + // Act - Use reflection to call the private method + MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod( + "ConvertToAgentResponseUpdate", + BindingFlags.NonPublic | BindingFlags.Instance, + null, + [typeof(AssistantReasoningDeltaEvent)], + null); + + Assert.NotNull(method); + + AgentResponseUpdate? result = method.Invoke(agent, [reasoningDeltaEvent]) as AgentResponseUpdate; + + // Assert + Assert.NotNull(result); + Assert.Equal(ChatRole.Assistant, result.Role); + Assert.NotEmpty(result.Contents); + + AIContent content = result.Contents[0]; + Assert.IsType(content); + + TextReasoningContent reasoningContent = (TextReasoningContent)content; + Assert.Equal("Thinking step ", reasoningContent.Text); + Assert.Equal(reasoningDeltaEvent, reasoningContent.RawRepresentation); + Assert.Equal(agent.Id, result.AgentId); + Assert.Equal(reasoningDeltaEvent.Timestamp, result.CreatedAt); + } + + /// + /// Tests that ConvertToAgentResponseUpdate correctly handles AssistantReasoningEvent. + /// + [Fact] + public void ConvertToAgentResponseUpdate_WithReasoningEvent_CreatesTextReasoningContent() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false); + + // Create an AssistantReasoningEvent using reflection + AssistantReasoningEvent reasoningEvent = new() + { + Data = new AssistantReasoningData + { + ReasoningId = "reasoning-456", + Content = "Complete reasoning content" + }, + Timestamp = DateTimeOffset.UtcNow + }; + + // Act - Use reflection to call the private method + MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod( + "ConvertToAgentResponseUpdate", + BindingFlags.NonPublic | BindingFlags.Instance, + null, + [typeof(AssistantReasoningEvent)], + null); + + Assert.NotNull(method); + + AgentResponseUpdate? result = method.Invoke(agent, [reasoningEvent]) as AgentResponseUpdate; + + // Assert + Assert.NotNull(result); + Assert.Equal(ChatRole.Assistant, result.Role); + Assert.NotEmpty(result.Contents); + + AIContent content = result.Contents[0]; + Assert.IsType(content); + + TextReasoningContent reasoningContent = (TextReasoningContent)content; + Assert.Equal("Complete reasoning content", reasoningContent.Text); + Assert.Equal(reasoningEvent, reasoningContent.RawRepresentation); + Assert.Equal(agent.Id, result.AgentId); + Assert.Equal(reasoningEvent.Timestamp, result.CreatedAt); + } + + /// + /// Tests that ConvertToAgentResponseUpdate handles null data in AssistantReasoningDeltaEvent. + /// + [Fact] + public void ConvertToAgentResponseUpdate_WithNullDataInReasoningDeltaEvent_CreatesEmptyTextReasoningContent() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false); + + // Create an AssistantReasoningDeltaEvent with null data + AssistantReasoningDeltaEvent reasoningDeltaEvent = new() + { + Data = null!, + Timestamp = DateTimeOffset.UtcNow + }; + + // Act - Use reflection to call the private method + MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod( + "ConvertToAgentResponseUpdate", + BindingFlags.NonPublic | BindingFlags.Instance, + null, + [typeof(AssistantReasoningDeltaEvent)], + null); + + Assert.NotNull(method); + + AgentResponseUpdate? result = method.Invoke(agent, [reasoningDeltaEvent]) as AgentResponseUpdate; + + // Assert + Assert.NotNull(result); + Assert.Equal(ChatRole.Assistant, result.Role); + Assert.NotEmpty(result.Contents); + + AIContent content = result.Contents[0]; + Assert.IsType(content); + + TextReasoningContent reasoningContent = (TextReasoningContent)content; + Assert.Equal(string.Empty, reasoningContent.Text); + } + + /// + /// Tests that ConvertToAgentResponseUpdate handles null content in AssistantReasoningEvent. + /// + [Fact] + public void ConvertToAgentResponseUpdate_WithNullDataInReasoningEvent_CreatesEmptyTextReasoningContent() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false); + + // Create an AssistantReasoningEvent with null data + AssistantReasoningEvent reasoningEvent = new() + { + Data = null!, + Timestamp = DateTimeOffset.UtcNow + }; + + // Act - Use reflection to call the private method + MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod( + "ConvertToAgentResponseUpdate", + BindingFlags.NonPublic | BindingFlags.Instance, + null, + [typeof(AssistantReasoningEvent)], + null); + + Assert.NotNull(method); + + AgentResponseUpdate? result = method.Invoke(agent, [reasoningEvent]) as AgentResponseUpdate; + + // Assert + Assert.NotNull(result); + Assert.Equal(ChatRole.Assistant, result.Role); + Assert.NotEmpty(result.Contents); + + AIContent content = result.Contents[0]; + Assert.IsType(content); + + TextReasoningContent reasoningContent = (TextReasoningContent)content; + Assert.Equal(string.Empty, reasoningContent.Text); + } + + /// + /// Tests that ConvertToAgentResponseUpdate handles null DeltaContent in AssistantReasoningDeltaEvent. + /// + [Fact] + public void ConvertToAgentResponseUpdate_WithNullDeltaContent_CreatesEmptyTextReasoningContent() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false); + + // Create an AssistantReasoningDeltaEvent with null DeltaContent + AssistantReasoningDeltaEvent reasoningDeltaEvent = new() + { + Data = new AssistantReasoningDeltaData + { + ReasoningId = "reasoning-789", + DeltaContent = null! + }, + Timestamp = DateTimeOffset.UtcNow + }; + + // Act - Use reflection to call the private method + MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod( + "ConvertToAgentResponseUpdate", + BindingFlags.NonPublic | BindingFlags.Instance, + null, + [typeof(AssistantReasoningDeltaEvent)], + null); + + Assert.NotNull(method); + + AgentResponseUpdate? result = method.Invoke(agent, [reasoningDeltaEvent]) as AgentResponseUpdate; + + // Assert + Assert.NotNull(result); + AIContent content = result.Contents[0]; + TextReasoningContent reasoningContent = Assert.IsType(content); + Assert.Equal(string.Empty, reasoningContent.Text); + } + + /// + /// Tests that ConvertToAgentResponseUpdate handles null Content in AssistantReasoningEvent. + /// + [Fact] + public void ConvertToAgentResponseUpdate_WithNullContent_CreatesEmptyTextReasoningContent() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + GitHubCopilotAgent agent = new(copilotClient, sessionConfig: null, ownsClient: false); + + // Create an AssistantReasoningEvent with null Content + AssistantReasoningEvent reasoningEvent = new() + { + Data = new AssistantReasoningData + { + ReasoningId = "reasoning-999", + Content = null! + }, + Timestamp = DateTimeOffset.UtcNow + }; + + // Act - Use reflection to call the private method + MethodInfo? method = typeof(GitHubCopilotAgent).GetMethod( + "ConvertToAgentResponseUpdate", + BindingFlags.NonPublic | BindingFlags.Instance, + null, + [typeof(AssistantReasoningEvent)], + null); + + Assert.NotNull(method); + + AgentResponseUpdate? result = method.Invoke(agent, [reasoningEvent]) as AgentResponseUpdate; + + // Assert + Assert.NotNull(result); + AIContent content = result.Contents[0]; + TextReasoningContent reasoningContent = Assert.IsType(content); + Assert.Equal(string.Empty, reasoningContent.Text); + } +} From 1a4fe694a12cfeab64ce6056017c52b1e8caddba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:30:18 +0000 Subject: [PATCH 3/3] Add AssistantReasoningEvent support to Python GitHubCopilotAgent Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../agent_framework_github_copilot/_agent.py | 16 ++++ .../tests/test_github_copilot_agent.py | 76 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 8bcfa9a5ba..12d85d59fc 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -382,6 +382,22 @@ def event_handler(event: SessionEvent) -> None: raw_representation=event, ) queue.put_nowait(update) + elif event.type == SessionEventType.ASSISTANT_REASONING_DELTA: + if event.data.delta_content: + update = AgentResponseUpdate( + role=Role.ASSISTANT, + contents=[Content.from_text_reasoning(event.data.delta_content)], + raw_representation=event, + ) + queue.put_nowait(update) + elif event.type == SessionEventType.ASSISTANT_REASONING: + if event.data.content: + update = AgentResponseUpdate( + role=Role.ASSISTANT, + contents=[Content.from_text_reasoning(event.data.content)], + raw_representation=event, + ) + queue.put_nowait(update) elif event.type == SessionEventType.SESSION_IDLE: queue.put_nowait(None) elif event.type == SessionEventType.SESSION_ERROR: 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 f53aa0fe59..acee15dbab 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -101,6 +101,24 @@ def session_error_event() -> SessionEvent: ) +@pytest.fixture +def assistant_reasoning_delta_event() -> SessionEvent: + """Create a mock assistant reasoning delta event.""" + return create_session_event( + SessionEventType.ASSISTANT_REASONING_DELTA, + delta_content="Thinking step ", + ) + + +@pytest.fixture +def assistant_reasoning_event() -> SessionEvent: + """Create a mock assistant reasoning event.""" + return create_session_event( + SessionEventType.ASSISTANT_REASONING, + content="Complete reasoning content", + ) + + class TestGitHubCopilotAgentInit: """Test cases for GitHubCopilotAgent initialization.""" @@ -418,6 +436,64 @@ def mock_on(handler: Any) -> Any: assert agent._started is True # type: ignore mock_client.start.assert_called_once() + async def test_run_stream_with_reasoning_delta( + self, + mock_client: MagicMock, + mock_session: MagicMock, + assistant_reasoning_delta_event: SessionEvent, + session_idle_event: SessionEvent, + ) -> None: + """Test streaming with reasoning delta events.""" + events = [assistant_reasoning_delta_event, 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_stream("Hello"): + responses.append(update) + + assert len(responses) == 1 + assert isinstance(responses[0], AgentResponseUpdate) + assert responses[0].role == Role.ASSISTANT + # Check that the content is TextReasoningContent by checking its type + assert responses[0].contents[0].type == "text_reasoning" + assert responses[0].contents[0].text == "Thinking step " + + async def test_run_stream_with_reasoning_complete( + self, + mock_client: MagicMock, + mock_session: MagicMock, + assistant_reasoning_event: SessionEvent, + session_idle_event: SessionEvent, + ) -> None: + """Test streaming with complete reasoning events.""" + events = [assistant_reasoning_event, 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_stream("Hello"): + responses.append(update) + + assert len(responses) == 1 + assert isinstance(responses[0], AgentResponseUpdate) + assert responses[0].role == Role.ASSISTANT + # Check that the content is TextReasoningContent by checking its type + assert responses[0].contents[0].type == "text_reasoning" + assert responses[0].contents[0].text == "Complete reasoning content" + class TestGitHubCopilotAgentSessionManagement: """Test cases for session management."""