mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-27 23:18:46 +02:00
fix: preserve handoff agent identity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -451,11 +451,59 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
Assert.Equal("A plus", first.Content);
|
||||
},
|
||||
second =>
|
||||
{
|
||||
Assert.Equal("msg-2", second.MessageId);
|
||||
Assert.Equal("Implementer", second.AuthorName);
|
||||
Assert.Equal("B", second.Content);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObserveSessionEvent_TracksObservedAgentByMessageId()
|
||||
{
|
||||
CopilotTurnExecutionState state = new(CreateHandoffCommand());
|
||||
SessionEvent sessionEvent = SessionEvent.FromJson(
|
||||
"""
|
||||
{
|
||||
Assert.Equal("msg-2", second.MessageId);
|
||||
Assert.Equal("Implementer", second.AuthorName);
|
||||
Assert.Equal("B", second.Content);
|
||||
});
|
||||
"type": "assistant.message_delta",
|
||||
"data": {
|
||||
"messageId": "msg-7",
|
||||
"deltaContent": "Done."
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"timestamp": "2026-03-24T00:00:00Z"
|
||||
}
|
||||
""");
|
||||
|
||||
state.ObserveSessionEvent(CreateAgent("agent-handoff-ux", "UX Specialist"), sessionEvent);
|
||||
|
||||
Assert.True(state.TryResolveObservedAgentForMessage("msg-7", out AgentIdentity observedAgent));
|
||||
Assert.Equal("agent-handoff-ux", observedAgent.AgentId);
|
||||
Assert.Equal("UX Specialist", observedAgent.AgentName);
|
||||
Assert.Equal("agent-handoff-ux", state.ActiveAgent?.AgentId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObserveSessionEvent_UsesReasoningDeltasToTrackActiveAgent()
|
||||
{
|
||||
CopilotTurnExecutionState state = new(CreateHandoffCommand());
|
||||
SessionEvent sessionEvent = SessionEvent.FromJson(
|
||||
"""
|
||||
{
|
||||
"type": "assistant.reasoning_delta",
|
||||
"data": {
|
||||
"reasoningId": "reasoning-1",
|
||||
"deltaContent": "Planning."
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"timestamp": "2026-03-24T00:00:00Z"
|
||||
}
|
||||
""");
|
||||
|
||||
state.ObserveSessionEvent(CreateAgent("agent-handoff-ux", "UX Specialist"), sessionEvent);
|
||||
|
||||
Assert.Equal("agent-handoff-ux", state.ActiveAgent?.AgentId);
|
||||
Assert.Equal("UX Specialist", state.ActiveAgent?.AgentName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -770,6 +818,27 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
};
|
||||
}
|
||||
|
||||
private static RunTurnCommandDto CreateHandoffCommand()
|
||||
{
|
||||
return new RunTurnCommandDto
|
||||
{
|
||||
RequestId = "turn-1",
|
||||
SessionId = "session-1",
|
||||
Pattern = new PatternDefinitionDto
|
||||
{
|
||||
Id = "pattern-handoff",
|
||||
Name = "Handoff Flow",
|
||||
Mode = "handoff",
|
||||
Availability = "available",
|
||||
Agents =
|
||||
[
|
||||
CreateAgent("agent-handoff-triage", "Triage"),
|
||||
CreateAgent("agent-handoff-ux", "UX Specialist"),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static RunTurnCommandDto CreateApprovalCommand()
|
||||
{
|
||||
return new RunTurnCommandDto
|
||||
|
||||
Reference in New Issue
Block a user