diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
index 053a08f725..a85c126fd6 100644
--- a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
@@ -145,11 +145,12 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
// Ensure the client is started
await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false);
- // Create or resume a session with streaming enabled
+ // Create or resume a session with streaming enabled by default
SessionConfig sessionConfig = this._sessionConfig != null
? CopySessionConfig(this._sessionConfig)
: new SessionConfig { Streaming = true };
+ bool isStreaming = sessionConfig.Streaming ?? true;
CopilotSession copilotSession;
if (typedSession.SessionId is not null)
{
@@ -178,7 +179,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
break;
case AssistantMessageEvent assistantMessage:
- channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage));
+ channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage, isStreaming));
break;
case AssistantUsageEvent usageEvent:
@@ -271,19 +272,20 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
}
///
- /// Copies all supported properties from a source into a new instance
- /// with set to true.
+ /// Copies all supported properties from a source into a new instance,
+ /// preserving from the source (defaulting to true if unset).
///
internal static SessionConfig CopySessionConfig(SessionConfig source)
{
SessionConfig copy = source.Clone();
- copy.Streaming = true;
+ copy.Streaming = source.Streaming ?? true;
return copy;
}
///
/// Copies all supported properties from a source into a new
- /// with set to true.
+ /// , preserving
+ /// from the source (defaulting to true if unset).
///
internal static ResumeSessionConfig CopyResumeSessionConfig(SessionConfig? source)
{
@@ -306,7 +308,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
SkillDirectories = source?.SkillDirectories,
DisabledSkills = source?.DisabledSkills,
InfiniteSessions = source?.InfiniteSessions,
- Streaming = true
+ Streaming = source?.Streaming ?? true
};
}
@@ -325,12 +327,18 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
};
}
- internal AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
+ ///
+ /// Converts an to an .
+ /// When streaming is enabled, text was already delivered via delta events, so only raw metadata is emitted.
+ /// When streaming is disabled, the full message text is emitted as .
+ ///
+ internal AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage, bool isStreaming)
{
- AIContent content = new()
- {
- RawRepresentation = assistantMessage
- };
+ // When streaming, text was already delivered via AssistantMessageDeltaEvent.
+ // When not streaming, this is the only opportunity to emit the response text.
+ AIContent content = isStreaming
+ ? new AIContent { RawRepresentation = assistantMessage }
+ : new TextContent(assistantMessage.Data?.Content ?? string.Empty) { RawRepresentation = assistantMessage };
return new AgentResponseUpdate(ChatRole.Assistant, [content])
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs
index 944f2f30ab..8faa842eb0 100644
--- a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using GitHub.Copilot;
using GitHub.Copilot.Rpc;
@@ -222,7 +223,73 @@ public sealed class GitHubCopilotAgentTests
}
[Fact]
- public void ConvertToAgentResponseUpdate_AssistantMessageEvent_DoesNotEmitTextContent()
+ public void CopySessionConfig_WithStreamingDisabled_PreservesStreamingValue()
+ {
+ // Arrange
+ var source = new SessionConfig
+ {
+ Streaming = false,
+ Model = "gpt-4o",
+ };
+
+ // Act
+ SessionConfig result = GitHubCopilotAgent.CopySessionConfig(source);
+
+ // Assert
+ Assert.False(result.Streaming);
+ }
+
+ [Fact]
+ public void CopySessionConfig_WithStreamingNull_DefaultsToTrue()
+ {
+ // Arrange
+ var source = new SessionConfig
+ {
+ Model = "gpt-4o",
+ };
+
+ // Act
+ SessionConfig result = GitHubCopilotAgent.CopySessionConfig(source);
+
+ // Assert
+ Assert.True(result.Streaming);
+ }
+
+ [Fact]
+ public void CopyResumeSessionConfig_WithStreamingDisabled_PreservesStreamingValue()
+ {
+ // Arrange
+ var source = new SessionConfig
+ {
+ Streaming = false,
+ Model = "gpt-4o",
+ };
+
+ // Act
+ ResumeSessionConfig result = GitHubCopilotAgent.CopyResumeSessionConfig(source);
+
+ // Assert
+ Assert.False(result.Streaming);
+ }
+
+ [Fact]
+ public void CopyResumeSessionConfig_WithStreamingNull_DefaultsToTrue()
+ {
+ // Arrange
+ var source = new SessionConfig
+ {
+ Model = "gpt-4o",
+ };
+
+ // Act
+ ResumeSessionConfig result = GitHubCopilotAgent.CopyResumeSessionConfig(source);
+
+ // Assert
+ Assert.True(result.Streaming);
+ }
+
+ [Fact]
+ public void ConvertToAgentResponseUpdate_AssistantMessageEventWhenStreaming_DoesNotEmitTextContent()
{
var assistantMessage = new AssistantMessageEvent
{
@@ -235,11 +302,84 @@ public sealed class GitHubCopilotAgentTests
CopilotClient copilotClient = new(new CopilotClientOptions());
const string TestId = "agent-id";
var agent = new GitHubCopilotAgent(copilotClient, ownsClient: false, id: TestId, tools: null);
- AgentResponseUpdate result = agent.ConvertToAgentResponseUpdate(assistantMessage);
+ AgentResponseUpdate result = agent.ConvertToAgentResponseUpdate(assistantMessage, isStreaming: true);
- // result.Text need to be empty because the content was already delivered via delta events, and we want to avoid emitting duplicate content in the response update.
- // The content should be delivered through TextContent in the Contents collection instead.
+ // result.Text should be empty because content was already delivered via delta events.
Assert.Empty(result.Text);
Assert.DoesNotContain(result.Contents, c => c is TextContent);
}
+
+ [Fact]
+ public void ConvertToAgentResponseUpdate_AssistantMessageEventWhenNotStreaming_EmitsTextContent()
+ {
+ // Arrange
+ const string ExpectedContent = "Full response text from non-streaming session";
+ var assistantMessage = new AssistantMessageEvent
+ {
+ Data = new AssistantMessageData
+ {
+ MessageId = "msg-789",
+ Content = ExpectedContent
+ }
+ };
+ CopilotClient copilotClient = new(new CopilotClientOptions());
+ const string TestId = "agent-id";
+ var agent = new GitHubCopilotAgent(copilotClient, ownsClient: false, id: TestId, tools: null);
+
+ // Act
+ AgentResponseUpdate result = agent.ConvertToAgentResponseUpdate(assistantMessage, isStreaming: false);
+
+ // Assert - text must be emitted since no delta events precede it in non-streaming mode.
+ Assert.Equal(ExpectedContent, result.Text);
+ Assert.Contains(result.Contents, c => c is TextContent);
+ TextContent textContent = (TextContent)result.Contents.Single(c => c is TextContent);
+ Assert.Equal(ExpectedContent, textContent.Text);
+ Assert.Same(assistantMessage, textContent.RawRepresentation);
+ }
+
+ [Fact]
+ public void ConvertToAgentResponseUpdate_AssistantMessageEventWhenNotStreaming_HandlesEmptyContent()
+ {
+ // Arrange
+ var assistantMessage = new AssistantMessageEvent
+ {
+ Data = new AssistantMessageData
+ {
+ MessageId = "msg-000",
+ Content = string.Empty
+ }
+ };
+ CopilotClient copilotClient = new(new CopilotClientOptions());
+ const string TestId = "agent-id";
+ var agent = new GitHubCopilotAgent(copilotClient, ownsClient: false, id: TestId, tools: null);
+
+ // Act
+ AgentResponseUpdate result = agent.ConvertToAgentResponseUpdate(assistantMessage, isStreaming: false);
+
+ // Assert - should emit empty TextContent rather than throwing.
+ Assert.Empty(result.Text);
+ Assert.Contains(result.Contents, c => c is TextContent);
+ }
+
+ [Fact]
+ public void ConvertToAgentResponseUpdate_AssistantMessageEventWhenNotStreaming_HandlesNullData()
+ {
+ // Arrange
+ var assistantMessage = new AssistantMessageEvent
+ {
+ Data = null!
+ };
+ CopilotClient copilotClient = new(new CopilotClientOptions());
+ const string TestId = "agent-id";
+ var agent = new GitHubCopilotAgent(copilotClient, ownsClient: false, id: TestId, tools: null);
+
+ // Act
+ AgentResponseUpdate result = agent.ConvertToAgentResponseUpdate(assistantMessage, isStreaming: false);
+
+ // Assert - null Data should produce empty TextContent via null-propagation fallback.
+ Assert.Empty(result.Text);
+ Assert.Contains(result.Contents, c => c is TextContent);
+ Assert.Null(result.MessageId);
+ Assert.Null(result.ResponseId);
+ }
}