fix: preserve handoff agent identity

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-25 00:02:46 +01:00
co-authored by Copilot
parent 7c848113c0
commit 9bfd7837aa
4 changed files with 118 additions and 5 deletions
@@ -22,6 +22,7 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
public static async Task<CopilotAgentBundle> CreateAsync(
RunTurnCommandDto command,
Func<PatternAgentDefinitionDto, PermissionRequest, PermissionInvocation, Task<PermissionRequestResult>> onPermissionRequest,
Action<PatternAgentDefinitionDto, SessionEvent>? onSessionEvent,
CancellationToken cancellationToken)
{
List<IAsyncDisposable> disposables = [];
@@ -53,6 +54,7 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
},
WorkingDirectory = command.ProjectPath,
OnPermissionRequest = (request, invocation) => onPermissionRequest(definition, request, invocation),
OnEvent = evt => onSessionEvent?.Invoke(definition, evt),
Streaming = true,
};
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using Eryx.AgentHost.Contracts;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
namespace Eryx.AgentHost.Services;
@@ -8,6 +9,7 @@ internal sealed class CopilotTurnExecutionState
{
private readonly RunTurnCommandDto _command;
private readonly HashSet<string> _startedAgents = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, AgentIdentity> _observedAgentsByMessageId = new(StringComparer.Ordinal);
private readonly StreamingTranscriptBuffer _transcriptBuffer = new();
private int _fallbackMessageIndex;
@@ -54,6 +56,34 @@ internal sealed class CopilotTurnExecutionState
}
}
public void ObserveSessionEvent(PatternAgentDefinitionDto agentDefinition, SessionEvent sessionEvent)
{
AgentIdentity agent = AgentIdentityResolver.ResolveAgentIdentity(
_command.Pattern,
agentDefinition.Id,
agentDefinition.Name);
switch (sessionEvent)
{
case AssistantMessageDeltaEvent messageDelta when !string.IsNullOrWhiteSpace(messageDelta.Data?.MessageId):
RecordObservedAgentForMessage(agent, messageDelta.Data!.MessageId);
break;
case AssistantMessageEvent assistantMessage when !string.IsNullOrWhiteSpace(assistantMessage.Data?.MessageId):
RecordObservedAgentForMessage(agent, assistantMessage.Data!.MessageId);
break;
case AssistantReasoningDeltaEvent:
ActiveAgent = agent;
break;
}
}
public bool TryResolveObservedAgentForMessage(string? messageId, out AgentIdentity agent)
{
agent = default;
return !string.IsNullOrWhiteSpace(messageId)
&& _observedAgentsByMessageId.TryGetValue(messageId, out agent);
}
public string CreateMessageId(string? messageId)
{
return messageId ?? $"{_command.RequestId}-delta-{_fallbackMessageIndex++}";
@@ -76,6 +106,12 @@ internal sealed class CopilotTurnExecutionState
}
}
private void RecordObservedAgentForMessage(AgentIdentity agent, string messageId)
{
ActiveAgent = agent;
_observedAgentsByMessageId[messageId] = agent;
}
public void UpdateCompletedMessages(
IReadOnlyList<ChatMessage> allMessages,
IReadOnlyList<ChatMessage> inputMessages)
@@ -38,6 +38,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
state.ToolNamesByCallId,
onApproval,
cancellationToken),
(agent, sessionEvent) => state.ObserveSessionEvent(agent, sessionEvent),
cancellationToken);
Workflow workflow = bundle.BuildWorkflow(command.Pattern);
List<ChatMessage> inputMessages = command.Messages.Select(WorkflowTranscriptProjector.ToChatMessage).ToList();
@@ -136,7 +137,12 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
{
AgentIdentity? updateAgent = null;
string authorName = update.ExecutorId;
if (AgentIdentityResolver.TryResolveObservedAgentIdentity(
if (state.TryResolveObservedAgentForMessage(update.Update.MessageId, out AgentIdentity observedMessageAgent))
{
updateAgent = observedMessageAgent;
authorName = observedMessageAgent.AgentName;
}
else if (AgentIdentityResolver.TryResolveObservedAgentIdentity(
command.Pattern,
update.ExecutorId,
state.ActiveAgent,