Tuesday, May 26, 2026

Microsoft Agent Framework: Deterministic Multi-Agent Orchestrations with .NET Durable Functions

In this post, let's have a look at building deterministic multi-agent orchestrations with Microsoft Agent Framework in .NET using Durable Functions to coordinate multiple agents.

A couple of earlier posts set the stage, if you're jumping in here, those are worth reading first:
We've got an Azure Functions app hosting Microsoft Agent Framework agents, backed by Durable Functions on Durable Task Scheduler (DTS). Now we want to coordinate multiple agents in a controlled way.

Why a Durable Functions orchestrator?

A popular pattern for multi-agent systems is to let one agent route between other agents, the "agent-as-orchestrator" pattern. That is great for flexibility, but the routing decisions are non-deterministic, the same input can take different paths on different runs, which makes auditing, retries, and replay harder.

When we want deterministic behavior, same input, same path, with retries and replay handled by the platform, we can put the orchestration logic in a Durable Functions orchestrator. The agents stay the "smart" bits; the orchestrator is plain C# code that decides what runs when, fully replayable.

The example

We'll build on the weekend planner from the previous posts, with a twist:
  • A weather-assessor agent decides whether the weather is suitable for outdoor activities.
  • If yes, an itinerary-planner agent builds a full weekend itinerary.
  • If no, we short-circuit and call an activity to suggest indoor options instead.
Two agents, one orchestrator, deterministic branching.

Naming the agents

Since the orchestrator resolves agents by name, let's keep them in a small constants class to avoid magic strings:
internal static class Agents
{
    public const string WeatherAssessor = "weather-assessor";

    public const string ItineraryPlanner = "itinerary-planner";
}
Registering the agents

Same shape as the previous post, just two agents this time. Each has its own instructions and tools (tools code omitted for brevity, same weather/activities/date functions as before):
using Azure.AI.Projects;
using Azure.Identity;
using MafSamples.DurableOrchestrator.Constants;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Hosting;

var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_ENDPOINT")!;
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL")!;

// For local development, using AzureCliCredential
var credential = new AzureCliCredential();
var projectClient = new AIProjectClient(new Uri(endpoint), credential);

// Agent 1 — assesses whether the weather is suitable for outdoor activities
AIAgent weatherAssessor = projectClient
    .AsAIAgent(
        model: deploymentName,
        name: Agents.WeatherAssessor,
        instructions: """
            You determine whether the weather in a given city is suitable for outdoor leisure activities this weekend.
            Use the available tools to check the weather and current date.
            Respond with a structured result: IsSuitableForOutdoors and a short Reason.
            """,
        tools: [
            AIFunctionFactory.Create(Tools.GetWeather),
            AIFunctionFactory.Create(Tools.GetCurrentDate)
        ]);

// Agent 2 — builds the weekend itinerary
AIAgent itineraryPlanner = projectClient
    .AsAIAgent(
        model: deploymentName,
        name: Agents.ItineraryPlanner,
        instructions: """
            You build a weekend itinerary using the available activities for a given city.
            Include the date of the weekend in the itinerary.
            Respond with a structured result containing a Summary string.
            """,
        tools: [
            AIFunctionFactory.Create(Tools.GetActivities),
            AIFunctionFactory.Create(Tools.GetCurrentDate)
        ]);

// Register the agents so orchestrations can resolve them via context.GetAgent(name)
using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableAgents(options =>
    {
        options.AddAIAgent(weatherAssessor);
        options.AddAIAgent(itineraryPlanner);
    })
    .Build();

app.Run();


The orchestrator

This is the new piece. Each agent is resolved from the orchestration context, and we drive the flow with regular C#, including the if/else branch:
using MafSamples.DurableOrchestrator.Constants;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;

public static class WeekendPlanOrchestrations
{
    [Function(nameof(WeekendPlanOrchestration))]
    public static async Task<string> WeekendPlanOrchestration(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        WeekendRequest request = context.GetInput<WeekendRequest>()!;

        // 1. Ask the weather-assessor whether the weather is suitable for outdoor activities
        DurableAIAgent weatherAgent = context.GetAgent(Agents.WeatherAssessor);
        AgentSession weatherSession = await weatherAgent.CreateSessionAsync();

        AgentResponse<WeatherAssessment> weatherResponse = await weatherAgent.RunAsync<WeatherAssessment>(
            message: $"Is the weather in {request.City} suitable for outdoor activities this weekend?",
            session: weatherSession);

        WeatherAssessment assessment = weatherResponse.Result;

        if (!assessment.IsSuitableForOutdoors)
        {
            return await context.CallActivityAsync<string>(nameof(NotifyIndoorPlan), assessment.Reason);
        }

        // 2. Ask the itinerary-planner to build a plan
        DurableAIAgent plannerAgent = context.GetAgent(Agents.ItineraryPlanner);
        AgentSession plannerSession = await plannerAgent.CreateSessionAsync();

        AgentResponse<WeekendItinerary> plannerResponse = await plannerAgent.RunAsync<WeekendItinerary>(
            message: $"Create a weekend itinerary for {request.City}",
            session: plannerSession);

        return plannerResponse.Result.Summary;
    }

    [Function(nameof(NotifyIndoorPlan))]
    public static string NotifyIndoorPlan([ActivityTrigger] string reason)
    {
        Console.WriteLine($"[Activity] Suggesting indoor plan. Reason: {reason}");
        return $"Bad weather for outdoor activities ({reason}). Try indoor options like museums or movies.";
    }
}

// Structured outputs requested from the two agents inside the orchestration
public record WeatherAssessment(bool IsSuitableForOutdoors, string Reason);

public record WeekendItinerary(string Summary);
A few things worth pointing out:
  • context.GetAgent(name) resolves a DurableAIAgent: an agent that runs inside the orchestration with its state persisted durably.
  • CreateSessionAsync starts a fresh conversation session for this orchestration run.
  • RunAsync<T> sends a message and gets a structured response back, typed to our record (WeatherAssessment, WeekendItinerary).
  • The if branch is deterministic: on replay, the same WeatherAssessment will always lead to the same path.
  • CallActivityAsync on the "bad weather" branch shows that we can still mix in regular Durable activities alongside agent calls.
Kicking it off

An HTTP starter accepts a city and schedules a new orchestration instance:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask.Client;
using System.Net;

public static class WeekendPlanStarter
{
    [Function(nameof(StartWeekendPlan))]
    public static async Task<HttpResponseData> StartWeekendPlan(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "weekend-plan")] HttpRequestData req,
        [DurableClient] DurableTaskClient client)
    {
        WeekendRequest? request = await req.ReadFromJsonAsync<WeekendRequest>();
        if (request is null || string.IsNullOrWhiteSpace(request.City))
        {
            HttpResponseData badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
            await badRequest.WriteStringAsync("Body must include a non-empty 'city'.");
            return badRequest;
        }

        string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
            nameof(WeekendPlanOrchestrations.WeekendPlanOrchestration), request);

        return await client.CreateCheckStatusResponseAsync(req, instanceId);
    }
}

// API contract for the HTTP starter
public record WeekendRequest(string City);
Send a request and we're off:
POST http://localhost:7100/api/weekend-plan
Content-Type: application/json

{
    "city": "Auckland"
}
  
We will get a response with 202 Accepted with Orchestration response.

Now we get to see the real benefit of running on DTS. We can open up DTS Dashboard and the Orchestrations tab shows the orchestration instance and its full history, every agent call, every activity, replays and all:
DTS: Orchestrations
All the AI happens inside individual agents; all the "what runs when" lives in code we can read, test, and reason about. That trade-off, less agent autonomy in exchange for more predictability is often exactly what you want in a production workflow.

Complete source code:

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment