Files
aryx/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs
T
David KayaandCopilot 805e369b67 refactor: remove legacy patterns system, unify on workflows
Remove the entire patterns domain model, IPC channels, sidecar services,
renderer components, and tests. Sessions now bind exclusively to workflows
via workflowId. Builtin workflows replace builtin patterns.

Backend:
- Make AgentNodeConfig standalone (no longer extends PatternAgentDefinition)
- Add WorkflowOrchestrationMode and WorkflowExecutionDefinition
- Create builtin workflows (single-agent, sequential, concurrent, handoff, group-chat)
- Rewrite session model config helpers for workflow-only
- Remove pattern IPC channels, handlers, and preload bindings
- Merge createSession/createWorkflowSession into single method
- Remove sidecar PatternGraphResolver, PatternValidator, pattern DTOs
- Add workspace migration for legacy sessions (patternId -> workflowId)

Frontend:
- Delete PatternEditor, pattern-graph components, patternGraph lib
- Delete NewSessionModal (session creation uses workflows directly)
- Remove PatternsSection from SettingsPanel
- Update App.tsx, ChatPane, ActivityPanel, Sidebar, RunTimeline,
  AgentConfigFields, InlinePills, sessionActivity to use workflow types
- Delete pattern.ts domain module

78 files changed across backend and frontend.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-06 22:37:00 +02:00

708 lines
26 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"},"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.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName));
Assert.Equal("view", toolName);
}
[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 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",
},
},
};
}
}