A second agent sounds like double the care. Often it is mainly a double bill. In this lesson, you learn the patterns with which Agent Framework lets several agents work together. Then you build a reviewer that reads through the quote draft from lesson 4, and you use numbers to decide whether the Kladde gets it. The answer can be "no". That's not a failure but the result of a measurement.

Five patterns

Agent Framework comes with five ready-made patterns, all in Microsoft.Agents.AI.Workflows. Each is a workflow like the one in lesson 4, except that the graph is already built:

Pattern How it works In .NET A use
Sequential One agent after the other; each gets the conversation so far. AgentWorkflowBuilder.BuildSequential draft, then review
Parallel All agents get the same question at the same time; the answers are merged. AgentWorkflowBuilder.BuildConcurrent a tender from several angles
Handoff One agent decides which other one takes over, and hands the conversation off. AgentWorkflowBuilder.CreateHandoffBuilderWith pass a request on to specialists
Group chat A manager decides who speaks next; everybody sees the whole conversation. AgentWorkflowBuilder.CreateGroupChatBuilderWith with RoundRobinGroupChatManager writing and reviewing in turn, with a maximum number of rounds
Magentic A manager agent plans, assigns tasks, tracks progress and replans. AgentWorkflowBuilder.CreateMagenticBuilderWith open tasks whose steps nobody knows beforehand

From top to bottom, each pattern gives the model more decisions. In the sequential and the parallel pattern, the process stands in code. In the handoff, a model chooses the next agent. In the group chat, a manager determines the order, as code (RoundRobinGroupChatManager) or as a model. In Magentic, a model plans the whole path. More decisions with the model mean: harder to test, harder to predict what a run will cost, and more places where an indirect attack (T6) can change the direction. For workflows with agents, the same rule applies as in lesson 4: the model only where it's needed.

You know the sequential pattern from lesson 4. The parallel one looks like this:

C#
// parallel.cs
#:package Microsoft.Agents.AI.Workflows@1.28.0

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;

ChatClientAgent technique = new(new FixedModel("Feasible with the existing team."), "You check the technology.", "Technology");
ChatClientAgent deadlines = new(new FixedModel("Submission in five days, that will be tight."), "You check deadlines.", "Deadlines");
Workflow workflow = AgentWorkflowBuilder.BuildConcurrent([technique, deadlines]);

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(
    workflow, new List<ChatMessage> { new(ChatRole.User, "Is tender T-099 suitable for us?") });
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));

await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    if (evt is WorkflowOutputEvent output and not AgentResponseUpdateEvent && output.As<List<ChatMessage>>() is { } messages)
    {
        foreach (ChatMessage message in messages.OrderBy(m => m.AuthorName, StringComparer.Ordinal))
        {
            Console.WriteLine($"[{message.AuthorName}] {message.Text}");
        }

        break;
    }
}

// Stands in for a model and always gives the same answer.
sealed class FixedModel(string answer) : IChatClient
{
    public Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
        Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, answer)));

    public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options = null,
        [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await Task.Yield();
        yield return new ChatResponseUpdate(ChatRole.Assistant, answer);
    }

    public object? GetService(Type serviceType, object? serviceKey = null) => null;

    public void Dispose()
    {
    }
}
PowerShell
dotnet run parallel.cs
[Deadlines] Submission in five days, that will be tight.
[Technology] Feasible with the existing team.

The two agents run at the same time, and the order in which they finish isn't fixed. That's why the app sorts the answers by name before it prints them. As in lesson 4, AgentResponseUpdateEvent is itself a WorkflowOutputEvent; the condition and not AgentResponseUpdateEvent leaves out the pieces of the answers and takes only the result.