From 7e37d751160bd7883e974e934ecd13f803ad7744 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Tue, 7 Apr 2026 14:53:08 +0200 Subject: [PATCH] fix: emit completed activity for sequential agents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/CopilotTurnExecutionState.cs | 18 +++++++ .../Services/CopilotWorkflowRunner.cs | 1 + .../CopilotTurnExecutionStateTests.cs | 16 +++++++ .../CopilotWorkflowRunnerTests.cs | 48 +++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs b/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs index 0b0aa52..00a9835 100644 --- a/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs +++ b/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs @@ -54,6 +54,11 @@ internal sealed class CopilotTurnExecutionState } } + public void QueueCompletedActivity(AgentIdentity agent) + { + _pendingEvents.Enqueue(CreateCompletedActivity(agent)); + } + public void ApplyEvent(SidecarEventDto evt) { if (evt is AgentActivityEventDto activity @@ -310,6 +315,19 @@ internal sealed class CopilotTurnExecutionState }; } + private AgentActivityEventDto CreateCompletedActivity(AgentIdentity agent) + { + return new AgentActivityEventDto + { + Type = "agent-activity", + RequestId = _command.RequestId, + SessionId = _command.SessionId, + ActivityType = "completed", + AgentId = agent.AgentId, + AgentName = agent.AgentName, + }; + } + private MessageReclassifiedEventDto CreateMessageReclassifiedEvent(string messageId) { return new MessageReclassifiedEventDto diff --git a/sidecar/src/Aryx.AgentHost/Services/CopilotWorkflowRunner.cs b/sidecar/src/Aryx.AgentHost/Services/CopilotWorkflowRunner.cs index 488281a..dc0b338 100644 --- a/sidecar/src/Aryx.AgentHost/Services/CopilotWorkflowRunner.cs +++ b/sidecar/src/Aryx.AgentHost/Services/CopilotWorkflowRunner.cs @@ -375,6 +375,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner out AgentIdentity completedAgent)) { TraceHandoff(command, $"Executor completed: {completed.ExecutorId} -> {completedAgent.AgentName} ({completedAgent.AgentId})."); + state.QueueCompletedActivity(completedAgent); state.ClearActiveAgentIfMatching(completedAgent); } else diff --git a/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs index df0667b..d982068 100644 --- a/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs +++ b/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs @@ -94,6 +94,22 @@ public sealed class CopilotTurnExecutionStateTests Assert.Equal("handoff_to_specialist", toolName); } + [Fact] + public void QueueCompletedActivity_QueuesCompletedAgentActivity() + { + RunTurnCommandDto command = CreateCommand(); + CopilotTurnExecutionState state = new(command); + + state.QueueCompletedActivity(new AgentIdentity("agent-1", "Primary")); + + AgentActivityEventDto activity = Assert.Single(state.DrainPendingEvents().OfType()); + Assert.Equal("completed", activity.ActivityType); + Assert.Equal("agent-1", activity.AgentId); + Assert.Equal("Primary", activity.AgentName); + Assert.Equal(command.RequestId, activity.RequestId); + Assert.Equal(command.SessionId, activity.SessionId); + } + [Fact] public void ObserveSessionEvent_AssistantMessageWithToolRequests_QueuesMessageReclassifiedEvent() { diff --git a/sidecar/tests/Aryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs index aac50a4..3fbfe81 100644 --- a/sidecar/tests/Aryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs +++ b/sidecar/tests/Aryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs @@ -945,6 +945,54 @@ public sealed class CopilotWorkflowRunnerTests Assert.Equal("InvalidOperationException", diagnostic.ExceptionType); } + [Fact] + public async Task HandleWorkflowEventAsync_QueuesCompletedActivityForCompletedExecutor() + { + RunTurnCommandDto command = CreateApprovalCommand(); + CopilotTurnExecutionState state = new(command); + WorkflowNodeDto primaryAgent = command.Workflow.GetAgentNodes()[0]; + + state.ObserveSessionEvent( + primaryAgent, + SessionEvent.FromJson( + """ + { + "type": "assistant.message_delta", + "data": { + "messageId": "msg-1", + "deltaContent": "Working" + }, + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-03-27T00:00:00Z" + } + """)); + _ = state.DrainPendingEvents(); + + MethodInfo handleWorkflowEvent = typeof(CopilotWorkflowRunner).GetMethod( + "HandleWorkflowEventAsync", + BindingFlags.NonPublic | BindingFlags.Static)!; + Task handleTask = (Task)handleWorkflowEvent.Invoke( + null, + [ + command, + new ExecutorCompletedEvent("agent-1", null), + Array.Empty(), + state, + (Func)(_ => Task.CompletedTask), + (Func)(_ => Task.CompletedTask), + ])!; + + bool shouldEndTurn = await handleTask; + + Assert.False(shouldEndTurn); + Assert.Null(state.ActiveAgent); + + AgentActivityEventDto completed = Assert.Single(state.DrainPendingEvents().OfType()); + Assert.Equal("completed", completed.ActivityType); + Assert.Equal("agent-1", completed.AgentId); + Assert.Equal("Primary", completed.AgentName); + } + [Fact] public async Task HandleWorkflowEventAsync_EmitsWorkflowWarningDiagnostic() {