Wednesday, October 7, 2026

Microsoft Agent Framework: Handoff Orchestration with .NET

In this post, let's have a look at handoff orchestration in Microsoft Agent Framework, where the agents themselves decide who should answer next.

Some time back I blogged about Microsoft Agent Framework: Deterministic Multi-Agent Orchestrations with .NET Durable Functions. There, we decided the order. The orchestrator called the first agent, then the second, and it did the same thing on every run.

A handoff is the opposite. We describe who can hand over to whom, and the model picks. That makes it a good fit when the next step depends on what the user just said, rather than on a fixed sequence.

The agents

We need the following packages (latest as of today, it will change).
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.24.0-preview.261006.1" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.24.0" />
The first agent is the weekend planner from Getting Started with Microsoft Agent Framework 1.0.0 in .NET, with one line added to its instructions. The second one is new, it finds restaurants and books tables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_ENDPOINT")!;
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL")!;

// For local development, using AzureCliCredential
var credential = new AzureCliCredential();

AIProjectClient projectClient = new(new Uri(endpoint), credential);

AIAgent activitiesAgent = projectClient.AsAIAgent(
    model: deploymentName,
    name: "activities",
    description: "Plans what to do on the weekend, based on the weather.",
    instructions: """
        You help users plan their weekends and choose the best activities for the given weather.
        If an activity would be unpleasant in weather, don't suggest it.
        Include date of the weekend in response.
        If the user asks about food, restaurants or a table, hand off to the dining agent.
        """,
    tools: [
        AIFunctionFactory.Create(GetWeather, nameof(GetWeather)),
        AIFunctionFactory.Create(GetActivities, nameof(GetActivities)),
        AIFunctionFactory.Create(GetCurrentDate, nameof(GetCurrentDate))
    ]);

AIAgent diningAgent = projectClient.AsAIAgent(
    model: deploymentName,
    name: "dining",
    description: "Finds restaurants and books tables.",
    instructions: """
        You help users find a restaurant and book a table.
        Always look up restaurants before suggesting one, and never invent a restaurant.
        Confirm the restaurant, date, time and number of people before booking.
        If the user asks about activities or the weather, hand off to the activities agent.
        """,
    tools: [
        AIFunctionFactory.Create(GetRestaurants, nameof(GetRestaurants)),
        AIFunctionFactory.Create(BookTable, nameof(BookTable))
    ]);
Note that the tool names are passed explicitly. Local functions in a top level program otherwise get compiler generated names like _Main_g_GetWeather_0_1, and that's what the model sees.

Wiring the handoff

Now we say who starts, and who can hand over to whom.
// The model decides who answers next, not us
Workflow workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(activitiesAgent)
    .WithHandoffs(activitiesAgent, [diningAgent])
    .WithHandoffs(diningAgent, [activitiesAgent])
    // Follow-ups stay with the agent already helping, instead of going back to the first one
    .EnableReturnToPrevious()
    .Build();

// The whole workflow becomes a single agent
AIAgent weekendConcierge = workflow.AsAIAgent(
    name: "weekend-concierge",
    description: "Plans a weekend, and hands off between activities and dining.");
Two things are happening here. The builder quietly gives each agent a handoff tool for every agent it is allowed to hand over to, which is how the model transfers the conversation. And AsAIAgent turns the whole workflow into a single AIAgent, so from the outside it is just an agent like any other.

Running it

One session, so both agents share the same conversation.
// Run the agent, one session so the agents share the conversation
AgentSession session = await weekendConcierge.CreateSessionAsync();

string[] userInputs =
[
    "What should I do this weekend in Auckland?",
    "Where can we eat afterwards?",
    "Book the first one for 2 people at 7pm on Saturday.",
    "Yes, that's correct."
];

foreach (string userInput in userInputs)
{
    Console.WriteLine();
    Console.WriteLine("--- User ---");
    Console.WriteLine(userInput);

    AgentResponse response = await weekendConcierge.RunAsync(userInput, session);

    // Each message carries the name of the agent that wrote it
    foreach (ChatMessage message in response.Messages
        .Where(m => m.Role == ChatRole.Assistant && !string.IsNullOrWhiteSpace(m.Text)))
    {
        Console.WriteLine();
        Console.WriteLine($"--- Agent: {message.AuthorName} ---");
        Console.WriteLine(message.Text);
    }
}
--- User ---
What should I do this weekend in Auckland?
[Tool] Getting current date
[Tool] Getting weather for 'Auckland'.
[Tool] Getting activities for 'Auckland' on '2026-10-10'.
[Tool] Getting activities for 'Auckland' on '2026-10-11'.

--- Agent: activities ---
For this weekend in Auckland (Sat 10 Oct - Sun 11 Oct 2026), the weather is rainy (~18C), so I'd prioritize indoor
or rain-friendly plans and skip things like the beach or hiking.
// Omitted for brevity

--- User ---
Where can we eat afterwards?
[Tool] Getting restaurants for 'Auckland'.

--- Agent: dining ---
Here are a few good post-museum options in Auckland:
- The Domain Kitchen (Modern New Zealand)
- Ponsonby Pasta (Italian)
// Omitted for brevity

--- User ---
Book the first one for 2 people at 7pm on Saturday.

--- Agent: dining ---
Just to confirm before I book: The Domain Kitchen for 2 people on Saturday 2026-10-10 at 19:00 - is that correct?

--- User ---
Yes, that's correct.
[Tool] Booking a table for 2 at 'The Domain Kitchen' on '2026-10-10' at '19:00'.

--- Agent: dining ---
Booked: The Domain Kitchen for 2 people on Saturday 2026-10-10 at 19:00.
Booking reference: BK-1230.
The first turn is answered by the activities agent. The second one mentions food, so it hands over, and from there the dining agent keeps the conversation, including the confirmation and the booking. That's EnableReturnToPrevious doing its job, without it the third and fourth turns would start again at the activities agent.

A few things I learnt

The instructions do most of the work here. The two lines about when to hand off are what make the routing behave, and without them the agents happily answer questions that aren't theirs, or pass the conversation back and forth. It's also worth being explicit about the direction, telling an agent to "hand off to the dining agent" works better than hoping it figures it out.

And notice the confirmation before booking is just the model asking a question. It works, but nothing outside the conversation knows that a booking is waiting on a human. That's a different feature, and a good topic for another post.

The complete sample is here.

More read:

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment