fix: emit completed activity for sequential agents

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-07 14:53:08 +02:00
co-authored by Copilot
parent 8797186a73
commit 7e37d75116
4 changed files with 83 additions and 0 deletions
@@ -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
@@ -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
@@ -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<AgentActivityEventDto>());
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()
{
@@ -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<bool> handleTask = (Task<bool>)handleWorkflowEvent.Invoke(
null,
[
command,
new ExecutorCompletedEvent("agent-1", null),
Array.Empty<ChatMessage>(),
state,
(Func<TurnDeltaEventDto, Task>)(_ => Task.CompletedTask),
(Func<SidecarEventDto, Task>)(_ => Task.CompletedTask),
])!;
bool shouldEndTurn = await handleTask;
Assert.False(shouldEndTurn);
Assert.Null(state.ActiveAgent);
AgentActivityEventDto completed = Assert.Single(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
Assert.Equal("completed", completed.ActivityType);
Assert.Equal("agent-1", completed.AgentId);
Assert.Equal("Primary", completed.AgentName);
}
[Fact]
public async Task HandleWorkflowEventAsync_EmitsWorkflowWarningDiagnostic()
{