diff --git a/sidecar/src/Aryx.AgentHost/Services/WorkflowTranscriptProjector.cs b/sidecar/src/Aryx.AgentHost/Services/WorkflowTranscriptProjector.cs index ed25810..11cf656 100644 --- a/sidecar/src/Aryx.AgentHost/Services/WorkflowTranscriptProjector.cs +++ b/sidecar/src/Aryx.AgentHost/Services/WorkflowTranscriptProjector.cs @@ -1,5 +1,6 @@ using System.Text; using Aryx.AgentHost.Contracts; +using GitHub.Copilot.SDK; using Microsoft.Extensions.AI; namespace Aryx.AgentHost.Services; @@ -85,7 +86,7 @@ internal static class WorkflowTranscriptProjector assistantMessages.Count - messageIndex, command.Pattern, fallbackAgent); - string content = message.Text ?? matchedSegment?.Content ?? string.Empty; + string content = ResolveProjectedContent(message, matchedSegment); if (string.IsNullOrWhiteSpace(content)) { continue; @@ -127,7 +128,9 @@ internal static class WorkflowTranscriptProjector { return new ChatMessageDto { - Id = matchedSegment?.MessageId ?? $"{command.RequestId}-final-{fallbackOutputIndex}", + Id = matchedSegment?.MessageId + ?? message.MessageId + ?? $"{command.RequestId}-final-{fallbackOutputIndex}", Role = message.Role == ChatRole.System ? "system" : "assistant", AuthorName = ResolveProjectedAuthorName( command.Pattern, @@ -139,6 +142,35 @@ internal static class WorkflowTranscriptProjector }; } + private static string ResolveProjectedContent( + ChatMessage message, + TranscriptSegment? matchedSegment) + { + return FirstNonBlank( + message.Text, + matchedSegment?.Content, + TryGetAssistantMessageContent(message)) + ?? string.Empty; + } + + private static string? TryGetAssistantMessageContent(ChatMessage message) + { + if (TryGetAssistantMessageData(message.RawRepresentation, out AssistantMessageData? assistantMessageData)) + { + return assistantMessageData?.Content; + } + + foreach (AIContent content in message.Contents) + { + if (TryGetAssistantMessageData(content.RawRepresentation, out assistantMessageData)) + { + return assistantMessageData?.Content; + } + } + + return null; + } + private static ChatMessageDto CreateProjectedMessageFromSegment( RunTurnCommandDto command, TranscriptSegment segment, @@ -360,11 +392,57 @@ internal static class WorkflowTranscriptProjector return fallbackAgent.Value.AgentName; } + if (fallbackAgent.HasValue + && string.IsNullOrWhiteSpace(primaryIdentifier) + && string.IsNullOrWhiteSpace(fallbackIdentifier)) + { + return fallbackAgent.Value.AgentName; + } + + if (pattern.Agents.Count == 1 + && string.IsNullOrWhiteSpace(primaryIdentifier) + && string.IsNullOrWhiteSpace(fallbackIdentifier)) + { + PatternAgentDefinitionDto singleAgent = pattern.Agents[0]; + return AgentIdentityResolver.ResolveDisplayAuthorName(pattern, singleAgent.Id, singleAgent.Name); + } + return AgentIdentityResolver.ResolveDisplayAuthorName( pattern, primaryIdentifier, fallbackIdentifier); } + + private static bool TryGetAssistantMessageData( + object? rawRepresentation, + out AssistantMessageData? assistantMessageData) + { + switch (rawRepresentation) + { + case AssistantMessageEvent assistantMessage when !string.IsNullOrWhiteSpace(assistantMessage.Data.Content): + assistantMessageData = assistantMessage.Data; + return true; + case AssistantMessageData data when !string.IsNullOrWhiteSpace(data.Content): + assistantMessageData = data; + return true; + default: + assistantMessageData = null; + return false; + } + } + + private static string? FirstNonBlank(params string?[] values) + { + foreach (string? value in values) + { + if (!string.IsNullOrWhiteSpace(value)) + { + return value; + } + } + + return null; + } } internal sealed class StreamingTranscriptBuffer diff --git a/sidecar/tests/Aryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs index ab4024d..e5fd180 100644 --- a/sidecar/tests/Aryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs +++ b/sidecar/tests/Aryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs @@ -160,6 +160,50 @@ public sealed class CopilotWorkflowRunnerTests Assert.Equal("Hello", message.Content); } + [Fact] + public void ProjectCompletedMessages_UsesFinalAssistantPayloadWhenStreamingTextIsMissing() + { + RunTurnCommandDto command = new() + { + RequestId = "turn-1", + SessionId = "session-1", + Pattern = new PatternDefinitionDto + { + Id = "pattern-single", + Name = "Single Agent", + Mode = "single", + Availability = "available", + Agents = + [ + CreateAgent(id: "agent-single-primary", name: "Primary Agent"), + ], + }, + }; + + IReadOnlyList messages = WorkflowTranscriptProjector.ProjectCompletedMessages( + command, + [ + new ChatMessage(ChatRole.Assistant, string.Empty) + { + MessageId = "msg-1", + RawRepresentation = new AssistantMessageEvent + { + Data = new AssistantMessageData + { + MessageId = "msg-1", + Content = "Hello from the final assistant payload.", + }, + }, + }, + ], + []); + + ChatMessageDto message = Assert.Single(messages); + Assert.Equal("msg-1", message.Id); + Assert.Equal("Primary Agent", message.AuthorName); + Assert.Equal("Hello from the final assistant payload.", message.Content); + } + [Fact] public void ProjectCompletedMessages_PreservesSequentialConversationHistory() {