Files
aryx/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs
T
David KayaandCopilot 2c165e453f fix: extract tool arguments from ToolExecutionStartEvent in sidecar
The CopilotTurnExecutionState was ignoring ToolExecutionStartData.Arguments
when creating tool-calling activity events, despite the Copilot SDK providing
them. This caused the activity panel to show 'Viewed a file' / 'Used rg'
with no contextual details (file paths, search patterns, etc.).

The fix adds NormalizeRawToolArguments to WorkflowRequestInfoInterpreter for
converting the raw object? payload (typically JsonElement) into the normalized
dictionary, then passes ToolExecutionStartData.Arguments through
CreateToolCallingActivity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-08 10:47:55 +02:00

741 lines
28 KiB
C#

using Aryx.AgentHost.Contracts;
using Aryx.AgentHost.Services;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
namespace Aryx.AgentHost.Tests;
public sealed class CopilotTurnExecutionStateTests
{
[Fact]
public void ObserveSessionEvent_McpOauthRequired_SetsActiveAgent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
new McpOauthRequiredEvent
{
Data = new McpOauthRequiredData
{
RequestId = "oauth-request-1",
ServerName = "Example MCP",
ServerUrl = "https://example.com/mcp",
},
});
Assert.True(state.ActiveAgent.HasValue);
Assert.Equal("agent-1", state.ActiveAgent.Value.AgentId);
Assert.Equal("Primary", state.ActiveAgent.Value.AgentName);
}
[Fact]
public void ObserveSessionEvent_AssistantMessageDelta_QueuesThinkingActivity()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.message_delta",
"data": {
"messageId": "msg-1",
"deltaContent": "Hello"
},
"id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
AgentActivityEventDto activity = Assert.Single(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
Assert.Equal("thinking", activity.ActivityType);
Assert.Equal("agent-1", activity.AgentId);
Assert.Equal("Primary", activity.AgentName);
Assert.True(state.TryResolveObservedAgentForMessage("msg-1", out AgentIdentity observedAgent));
Assert.Equal("agent-1", observedAgent.AgentId);
}
[Fact]
public void ObserveSessionEvent_ToolExecutionStart_TracksToolNameByCallIdAndQueuesToolActivity()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""{"type":"tool.execution_start","data":{"toolCallId":"tool-call-1","toolName":"view","arguments":{"path":"/src/main.ts","view_range":[10,20]}},"id":"33333333-3333-3333-3333-333333333333","timestamp":"2026-03-27T00:00:00Z"}"""));
AgentActivityEventDto toolActivity = Assert.Single(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
Assert.Equal("tool-calling", toolActivity.ActivityType);
Assert.Equal("view", toolActivity.ToolName);
Assert.Equal("tool-call-1", toolActivity.ToolCallId);
Assert.NotNull(toolActivity.ToolArguments);
Assert.Equal("/src/main.ts", toolActivity.ToolArguments["path"]);
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName));
Assert.Equal("view", toolName);
}
[Fact]
public void ObserveSessionEvent_ToolExecutionStart_WithoutArguments_SetsToolArgumentsToNull()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""{"type":"tool.execution_start","data":{"toolCallId":"tool-call-1","toolName":"view"},"id":"33333333-3333-3333-3333-333333333333","timestamp":"2026-03-27T00:00:00Z"}"""));
AgentActivityEventDto toolActivity = Assert.Single(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
Assert.Null(toolActivity.ToolArguments);
}
[Fact]
public void ObserveSessionEvent_ToolExecutionStart_DoesNotQueueToolActivityForHandoffTools()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""{"type":"tool.execution_start","data":{"toolCallId":"tool-call-1","toolName":"handoff_to_specialist"},"id":"1ce9d1dc-68f1-4df5-9728-f97017233279","timestamp":"2026-03-27T00:00:00Z"}"""));
Assert.Empty(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName));
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()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.message",
"data": {
"messageId": "msg-2",
"content": "Let me search for that.",
"toolRequests": [
{
"toolCallId": "tool-call-1",
"name": "rg",
"arguments": {
"pattern": "identifierUri"
}
}
]
},
"id": "3f75988b-8e69-4c90-a203-6b01d1c1f90b",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
IReadOnlyList<SidecarEventDto> pending = state.DrainPendingEvents();
AgentActivityEventDto thinking = Assert.Single(pending.OfType<AgentActivityEventDto>());
Assert.Equal("thinking", thinking.ActivityType);
MessageReclassifiedEventDto reclassified = Assert.Single(pending.OfType<MessageReclassifiedEventDto>());
Assert.Equal("session-1", reclassified.SessionId);
Assert.Equal("msg-2", reclassified.MessageId);
Assert.Equal("thinking", reclassified.NewKind);
}
[Fact]
public void ObserveSessionEvent_ToolExecutionStart_ReclassifiesLastObservedMessageOnce()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.message_delta",
"data": {
"messageId": "msg-3",
"deltaContent": "Searching"
},
"id": "0b65f0e9-d0fb-417e-ab5c-7a3343d8581b",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
_ = state.DrainPendingEvents();
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "tool.execution_start",
"data": {
"toolCallId": "tool-call-1",
"toolName": "rg"
},
"id": "8f33240e-bd3f-475c-aeb6-a4b7908e47b0",
"timestamp": "2026-03-27T00:00:01Z"
}
"""));
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "tool.execution_start",
"data": {
"toolCallId": "tool-call-2",
"toolName": "view"
},
"id": "a23f9c9a-f947-4282-866d-f599451c3899",
"timestamp": "2026-03-27T00:00:02Z"
}
"""));
IReadOnlyList<SidecarEventDto> pending = state.DrainPendingEvents();
AgentActivityEventDto[] toolActivities = [.. pending.OfType<AgentActivityEventDto>().Where(activity => activity.ActivityType == "tool-calling")];
Assert.Equal(2, toolActivities.Length);
Assert.Contains(toolActivities, activity => activity.ToolCallId == "tool-call-1" && activity.ToolName == "rg");
Assert.Contains(toolActivities, activity => activity.ToolCallId == "tool-call-2" && activity.ToolName == "view");
MessageReclassifiedEventDto reclassified = Assert.Single(pending.OfType<MessageReclassifiedEventDto>());
Assert.Equal("msg-3", reclassified.MessageId);
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? firstToolName));
Assert.Equal("rg", firstToolName);
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-2", out string? secondToolName));
Assert.Equal("view", secondToolName);
}
[Fact]
public void ObserveSessionEvent_AssistantMessageWithoutToolRequests_DoesNotQueueMessageReclassifiedEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.message",
"data": {
"messageId": "msg-4",
"content": "Final answer."
},
"id": "d07fe954-1258-4f6a-bf79-1550d6143ed0",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
Assert.Empty(state.DrainPendingEvents().OfType<MessageReclassifiedEventDto>());
}
[Fact]
public async Task EmitThinkingIfNeeded_DoesNotDuplicateQueuedThinkingActivity()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.reasoning_delta",
"data": {
"reasoningId": "reasoning-1",
"deltaContent": "Planning"
},
"id": "22222222-2222-2222-2222-222222222222",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
List<AgentActivityEventDto> activities = [.. state.DrainPendingEvents().OfType<AgentActivityEventDto>()];
await state.EmitThinkingIfNeeded(
new AgentIdentity("agent-1", "Primary"),
sidecarEvent =>
{
activities.Add(Assert.IsType<AgentActivityEventDto>(sidecarEvent));
return Task.CompletedTask;
});
AgentActivityEventDto thinking = Assert.Single(activities);
Assert.Equal("thinking", thinking.ActivityType);
Assert.Equal("agent-1", thinking.AgentId);
}
[Fact]
public void ObserveSessionEvent_AssistantIntent_QueuesIntentEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.intent",
"data": {
"intent": "Searching incident playbooks"
},
"id": "64cf59fe-63f0-4217-adf4-9bd6b3a80452",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
IReadOnlyList<SidecarEventDto> pending = state.DrainPendingEvents();
AgentActivityEventDto thinking = Assert.Single(pending.OfType<AgentActivityEventDto>());
Assert.Equal("thinking", thinking.ActivityType);
AssistantIntentEventDto intent = Assert.Single(pending.OfType<AssistantIntentEventDto>());
Assert.Equal("session-1", intent.SessionId);
Assert.Equal("agent-1", intent.AgentId);
Assert.Equal("Searching incident playbooks", intent.Intent);
}
[Fact]
public void ObserveSessionEvent_AssistantReasoningDelta_QueuesReasoningDeltaEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.reasoning_delta",
"data": {
"reasoningId": "reasoning-2",
"deltaContent": "Searching logs."
},
"id": "bd269258-5e5d-46b6-bf3f-bd8cba793b1a",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
IReadOnlyList<SidecarEventDto> pending = state.DrainPendingEvents();
AgentActivityEventDto thinking = Assert.Single(pending.OfType<AgentActivityEventDto>());
Assert.Equal("thinking", thinking.ActivityType);
ReasoningDeltaEventDto reasoning = Assert.Single(pending.OfType<ReasoningDeltaEventDto>());
Assert.Equal("session-1", reasoning.SessionId);
Assert.Equal("agent-1", reasoning.AgentId);
Assert.Equal("reasoning-2", reasoning.ReasoningId);
Assert.Equal("Searching logs.", reasoning.ContentDelta);
}
[Fact]
public void DrainPendingMcpOauthRequests_ReturnsQueuedRequestsAndClearsQueue()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
McpOauthRequiredEventDto request = new()
{
Type = "mcp-oauth-required",
RequestId = command.RequestId,
SessionId = command.SessionId,
OauthRequestId = "oauth-request-1",
ServerName = "Example MCP",
ServerUrl = "https://example.com/mcp",
};
state.EnqueuePendingMcpOauthRequest(request);
IReadOnlyList<McpOauthRequiredEventDto> firstDrain = state.DrainPendingMcpOauthRequests();
IReadOnlyList<McpOauthRequiredEventDto> secondDrain = state.DrainPendingMcpOauthRequests();
McpOauthRequiredEventDto drained = Assert.Single(firstDrain);
Assert.Equal("oauth-request-1", drained.OauthRequestId);
Assert.Empty(secondDrain);
}
[Fact]
public void ObserveSessionEvent_SubagentStarted_QueuesSubagentEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "subagent.started",
"data": {
"toolCallId": "tool-call-1",
"agentName": "designer",
"agentDisplayName": "Designer",
"agentDescription": "Design specialist"
},
"id": "44444444-4444-4444-4444-444444444444",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
SubagentEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<SubagentEventDto>());
Assert.Equal("started", evt.EventKind);
Assert.Equal("tool-call-1", evt.ToolCallId);
Assert.Equal("designer", evt.CustomAgentName);
Assert.Equal("Designer", evt.CustomAgentDisplayName);
}
[Fact]
public void ObserveSessionEvent_SkillInvoked_QueuesSkillEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "skill.invoked",
"data": {
"name": "reviewer",
"path": "C:\\skills\\reviewer\\SKILL.md",
"content": "# Reviewer",
"allowedTools": ["view"],
"pluginName": "aryx-plugin",
"pluginVersion": "1.0.0"
},
"id": "55555555-5555-5555-5555-555555555555",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
SkillInvokedEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<SkillInvokedEventDto>());
Assert.Equal("reviewer", evt.SkillName);
Assert.Equal(@"C:\skills\reviewer\SKILL.md", evt.Path);
Assert.Equal(["view"], evt.AllowedTools);
Assert.Equal("aryx-plugin", evt.PluginName);
}
[Fact]
public void ObserveSessionEvent_HookStart_QueuesHookLifecycleEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(command.Workflow.GetAgentNodes()[0], CreateHookStartEvent());
HookLifecycleEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<HookLifecycleEventDto>());
Assert.Equal("start", evt.Phase);
Assert.Equal("postToolUse", evt.HookType);
Assert.Equal("hook-1", evt.HookInvocationId);
Assert.NotNull(evt.Input);
}
[Fact]
public void ObserveSessionEvent_HookEnd_QueuesHookLifecycleEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(command.Workflow.GetAgentNodes()[0], CreateHookEndEvent());
HookLifecycleEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<HookLifecycleEventDto>());
Assert.Equal("end", evt.Phase);
Assert.Equal("postToolUse", evt.HookType);
Assert.Equal("hook-1", evt.HookInvocationId);
Assert.True(evt.Success);
}
[Fact]
public void ObserveSessionEvent_HookLifecycleEvents_AreSuppressedWhenConfigured()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command)
{
SuppressHookLifecycleEvents = true,
};
state.ObserveSessionEvent(command.Workflow.GetAgentNodes()[0], CreateHookStartEvent());
state.ObserveSessionEvent(command.Workflow.GetAgentNodes()[0], CreateHookEndEvent());
Assert.Empty(state.DrainPendingEvents());
}
[Fact]
public void ObserveSessionEvent_AssistantUsage_QueuesAssistantUsageEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.usage",
"data": {
"model": "gpt-5.4",
"inputTokens": 1200,
"outputTokens": 300,
"cacheReadTokens": 50,
"cacheWriteTokens": 10,
"cost": 0.42,
"duration": 8200,
"quotaSnapshots": {
"premium_interactions": {
"entitlementRequests": 50,
"usedRequests": 12,
"remainingPercentage": 76,
"overage": 0,
"overageAllowedWithExhaustedQuota": true,
"resetDate": "2026-04-01T00:00:00Z"
}
},
"copilotUsage": {
"tokenDetails": [
{
"batchSize": 1,
"costPerBatch": 1,
"tokenCount": 1500,
"tokenType": "input"
}
],
"totalNanoAiu": 1200000000
}
},
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
AssistantUsageEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<AssistantUsageEventDto>());
Assert.Equal("session-1", evt.SessionId);
Assert.Equal("agent-1", evt.AgentId);
Assert.Equal("Primary", evt.AgentName);
Assert.Equal("gpt-5.4", evt.Model);
Assert.Equal(1200, evt.InputTokens);
Assert.Equal(300, evt.OutputTokens);
Assert.Equal(0.42, evt.Cost);
Assert.Equal(8200, evt.Duration);
Assert.Equal(1200000000, evt.TotalNanoAiu);
QuotaSnapshotDto snapshot = Assert.Single(evt.QuotaSnapshots!.Values);
Assert.Equal(50, snapshot.EntitlementRequests);
Assert.Equal(12, snapshot.UsedRequests);
Assert.Equal(76, snapshot.RemainingPercentage);
}
[Fact]
public void ObserveSessionEvent_SessionCompactionComplete_QueuesCompactionEvent()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "session.compaction_complete",
"data": {
"success": true,
"preCompactionTokens": 1000,
"postCompactionTokens": 400,
"messagesRemoved": 8,
"tokensRemoved": 600,
"summaryContent": "Compacted summary",
"checkpointNumber": 2,
"checkpointPath": "C:\\Users\\me\\.copilot\\session-state\\checkpoint-2.json"
},
"id": "77777777-7777-7777-7777-777777777777",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
SessionCompactionEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<SessionCompactionEventDto>());
Assert.Equal("complete", evt.Phase);
Assert.True(evt.Success);
Assert.Equal(1000, evt.PreCompactionTokens);
Assert.Equal(400, evt.PostCompactionTokens);
Assert.Equal("Compacted summary", evt.SummaryContent);
}
[Fact]
public void ObserveSessionEvent_PendingMessagesModified_QueuesPendingMessageSignal()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "pending_messages.modified",
"data": {},
"id": "88888888-8888-8888-8888-888888888888",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
PendingMessagesModifiedEventDto evt = Assert.Single(state.DrainPendingEvents().OfType<PendingMessagesModifiedEventDto>());
Assert.Equal("session-1", evt.SessionId);
Assert.Equal("agent-1", evt.AgentId);
}
private static SessionEvent CreateHookStartEvent()
{
return SessionEvent.FromJson(
"""
{
"type": "hook.start",
"data": {
"hookInvocationId": "hook-1",
"hookType": "postToolUse",
"input": {
"toolName": "view"
}
},
"id": "66666666-6666-6666-6666-666666666666",
"timestamp": "2026-03-27T00:00:00Z"
}
""");
}
private static SessionEvent CreateHookEndEvent()
{
return SessionEvent.FromJson(
"""
{
"type": "hook.end",
"data": {
"hookInvocationId": "hook-1",
"hookType": "postToolUse",
"success": true,
"output": {
"status": "ok"
}
},
"id": "99999999-9999-9999-9999-999999999999",
"timestamp": "2026-03-27T00:00:00Z"
}
""");
}
[Fact]
public void FinalizeCompletedMessages_TagsReclassifiedMessagesAsThinking()
{
RunTurnCommandDto command = CreateCommand();
CopilotTurnExecutionState state = new(command);
// Simulate assistant message with tool requests → triggers reclassification
state.ObserveSessionEvent(
command.Workflow.GetAgentNodes()[0],
SessionEvent.FromJson(
"""
{
"type": "assistant.message",
"data": {
"messageId": "msg-intermediate",
"content": "Let me search...",
"toolRequests": [
{
"toolCallId": "tool-call-1",
"name": "grep",
"arguments": {}
}
]
},
"id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
state.DrainPendingEvents();
// Build completed messages with a reclassified and a non-reclassified message
ChatMessage intermediateMsg = new(ChatRole.Assistant, "Let me search...");
intermediateMsg.MessageId = "msg-intermediate";
intermediateMsg.AuthorName = "Primary";
ChatMessage finalMsg = new(ChatRole.Assistant, "Here are the results.");
finalMsg.MessageId = "msg-final";
finalMsg.AuthorName = "Primary";
state.UpdateCompletedMessages([intermediateMsg, finalMsg], []);
IReadOnlyList<ChatMessageDto> messages = state.FinalizeCompletedMessages();
ChatMessageDto intermediate = Assert.Single(messages, m => m.Id == "msg-intermediate");
Assert.Equal("thinking", intermediate.MessageKind);
ChatMessageDto final_ = Assert.Single(messages, m => m.Id == "msg-final");
Assert.Null(final_.MessageKind);
}
private static RunTurnCommandDto CreateCommand()
{
return new RunTurnCommandDto
{
RequestId = "turn-1",
SessionId = "session-1",
Workflow = new WorkflowDefinitionDto
{
Id = "workflow-1",
Name = "Execution State Workflow",
Graph = new WorkflowGraphDto
{
Nodes =
[
new WorkflowNodeDto
{
Id = "agent-1",
Kind = "agent",
Label = "Primary",
Config = new WorkflowNodeConfigDto
{
Kind = "agent",
Id = "agent-1",
Name = "Primary",
Model = "gpt-5.4",
Instructions = "Help with the request.",
},
},
],
},
Settings = new WorkflowSettingsDto
{
OrchestrationMode = "single",
},
},
};
}
}