Agent Handoff Patterns in .NET: Sequential Routing and State Transfer

Implement multi-agent handoff patterns in Microsoft Agent Framework. Learn sequential routing, triage agents, and conversation context transfer for complex agentic AI workflows.

0

Beyond Single-Agent Thinking

Most developers start with a single agent: one large model with a broad tool set, handling every request that comes its way. It works for simple scenarios. But the moment you need specialized behavior, complex workflows, or clear separation of concerns, a single agent reaches its limits.

Production AI systems rarely work alone. They work in teams. And like any effective team, they need a way to hand work from one specialist to another without losing context or requiring the user to restart the conversation.

This is where the handoff pattern comes in. It lets you build a network of focused agents, each with a narrow responsibility and a minimal tool set, that collaborate seamlessly. The Interview Coach architecture from Microsoft’s Agent Framework documentation is a clear example: a Receptionist routes conversations, a Behavioral Coach handles soft skills, a Technical Coach handles technical depth, and a Summarizer wraps it all up. Each agent knows its job. Each hands off to the next when the work is done.

Handoff vs. Agent-as-Tools: A Critical Distinction

Before diving into implementation, it’s important to understand why handoff is different from simply giving one agent access to other agents as tools.

In the agent-as-tools pattern, a single orchestrator agent decides which other agents to call. It remains in control. It sees all the results. It decides what to do next. This works well for parallel tasks or simple lookups, but it adds cognitive overhead: the orchestrator must reason about when to call which agent, how to interpret results, and how to weave them back into the conversation.

In the handoff pattern, one agent explicitly transfers full control to another. The second agent becomes the active participant in the conversation. The user continues with the new agent as if they’d been talking to it all along. The conversation context moves with the handoff. When that agent’s work is complete, it can hand off to the next agent, or back to the previous one, or to a different specialist altogether.

Handoff is better for sequential workflows, for maintaining conversational flow, and for keeping each agent’s scope clean. It’s also more transparent: both the system and the user understand exactly which agent is active at any moment.

Designing Specialized Agents: Principle of Least Privilege

The foundation of an effective handoff architecture is specialization. Each agent should have a narrow responsibility and only the tools it needs to fulfill that responsibility.

Consider the Receptionist agent in the Interview Coach workflow:

  • Responsibility: Understand the user’s need and route to the right specialist.
  • Tools: None. It only reads the user’s message and makes a routing decision.
  • Output: A clear indication of which agent should take over next.

The Behavioral Coach:

  • Responsibility: Coach on soft skills, communication, and interview confidence.
  • Tools: Perhaps a knowledge base on communication techniques, but not technical documentation.
  • Output: Coaching feedback and a decision to hand off to Technical or Summarizer.

The Technical Coach:

  • Responsibility: Dive deep into technical questions, system design, algorithms.
  • Tools: Technical documentation, code examples, design patterns reference.
  • Output: Technical guidance and readiness to hand off.

This separation has several benefits. First, it reduces hallucination: an agent with fewer tools has fewer ways to go wrong. Second, it makes the system auditable: you know exactly what each agent can do. Third, it makes the system maintainable: if you need to update the Behavioral Coach’s knowledge, you only touch that agent.

When designing your agents, ask: what is the minimal set of tools this agent needs to do its job? If the answer includes tools outside its domain, that’s a signal to split the responsibility across two agents instead.

Building a Triage Agent for Pure Routing

A triage agent is the simplest form of handoff orchestration. Its only job is to read the incoming message and decide which agent should handle it next.

Here’s a conceptual example of how a triage agent might be structured:

public class TriageAgent
{
    private readonly Dictionary<string, string> _agentRegistry;

    public TriageAgent()
    {
        _agentRegistry = new Dictionary<string, string>
        {
            { "behavioral", "BehavioralCoach" },
            { "technical", "TechnicalCoach" },
            { "summary", "Summarizer" }
        };
    }

    public async Task<HandoffDecision> TriageAsync(string userMessage, ConversationContext context)
    {
        var prompt = $@"You are a triage agent. Based on the user's message, decide which specialist should handle this:

User message: {userMessage}
Conversation so far: {context.Summary}

Respond with ONLY one of: behavioral, technical, summary
";

        var response = await InvokeAgentAsync(prompt);
        var selectedAgent = response.Trim().ToLower();

        if (_agentRegistry.TryGetValue(selectedAgent, out var agentName))
        {
            return new HandoffDecision
            {
                TargetAgent = agentName,
                Reason = $"User needs {selectedAgent} guidance",
                PreserveContext = true
            };
        }

        return new HandoffDecision { TargetAgent = "BehavioralCoach" };
    }
}

The triage agent doesn’t do the actual work. It classifies the request and points to the right specialist. This keeps it lightweight and fast.

Notice the PreserveContext flag. This signals to the framework that conversation history should be passed along to the next agent. More on that in a moment.

Sequential Handoffs with AgentWorkflowBuilder

The Microsoft Agent Framework provides the AgentWorkflowBuilder to orchestrate these handoffs. According to the official documentation, you configure handoff relationships using CreateHandoffBuilderWith and WithHandoffs. Here’s a conceptual pattern for a Receptionist to Behavioral to Technical workflow:

// Conceptual pattern showing handoff orchestration structure
var workflow = AgentWorkflowBuilder
    .CreateHandoffBuilderWith(triageAgent)
    .WithHandoffs(triageAgent, new[] { behavioralCoach, technicalCoach })
    .WithHandoffs(new[] { behavioralCoach, technicalCoach }, triageAgent)
    .Build();

// Each agent declares its responsibility and tools
// Receptionist: routes user requests (no tools)
// BehavioralCoach: handles soft skills (communication knowledge base)
// TechnicalCoach: handles technical depth (documentation, code examples)
// Summarizer: wraps up the conversation (minimal tools)

// The framework handles context transfer, message routing, and state management

The workflow is declarative. You define which agents can hand off to which other agents, and the framework manages the mechanics. You can visualize the flow at a glance: Receptionist routes, Behavioral coaches, Technical dives deep, Summarizer wraps up.

Managing Conversation Context During Handoffs

The most critical part of handoff is context transfer. When Agent A hands off to Agent B, Agent B needs to understand the conversation so far. It can’t start from scratch.

There are a few strategies for this:

Full History Transfer

Pass the entire conversation history to the next agent. This is straightforward but can grow the context window if the conversation is long.

public class ConversationContext
{
    public List<Message> FullHistory { get; set; }
    public string CurrentAgentId { get; set; }
    public Dictionary<string, object> SharedState { get; set; }
}

public async Task<string> HandoffAsync(string targetAgent, ConversationContext context)
{
    var nextAgentContext = new ConversationContext
    {
        FullHistory = context.FullHistory,
        CurrentAgentId = targetAgent,
        SharedState = context.SharedState
    };

    return await _agents[targetAgent].ProcessAsync(nextAgentContext);
}

Summarized Context Transfer

Before handing off, summarize the key points of the conversation. This keeps the context window smaller while preserving the essentials.

public async Task<string> HandoffWithSummaryAsync(string targetAgent, ConversationContext context)
{
    var summary = await _summarizer.SummarizeAsync(context.FullHistory);

    var nextAgentContext = new ConversationContext
    {
        FullHistory = context.FullHistory,
        Summary = summary,
        CurrentAgentId = targetAgent,
        SharedState = context.SharedState
    };

    return await _agents[targetAgent].ProcessAsync(nextAgentContext);
}

Structured State Transfer

Extract key facts and decisions into a shared state object. This is the most efficient approach for long conversations.

public class InterviewState
{
    public string CandidateName { get; set; }
    public string Role { get; set; }
    public List<string> BehavioralFeedback { get; set; }
    public List<string> TechnicalTopicsCovered { get; set; }
    public Dictionary<string, string> AreasForImprovement { get; set; }
}

public async Task<string> HandoffWithStructuredStateAsync(string targetAgent, ConversationContext context)
{
    var state = ExtractStateFromHistory(context.FullHistory);

    var nextAgentContext = new ConversationContext
    {
        FullHistory = context.FullHistory,
        SharedState = SerializeState(state),
        CurrentAgentId = targetAgent
    };

    return await _agents[targetAgent].ProcessAsync(nextAgentContext);
}

In practice, you’ll often use a combination: full history for reference, a summary for immediate context, and structured state for key facts. The exact balance depends on your conversation length and complexity.

Implementing Fallback Routing When Conversations Go Off-Script

Not every user message fits neatly into the expected flow. Sometimes a user asks a question that doesn’t belong to the current agent. Sometimes they want to go back to a previous agent. Sometimes they ask something completely unexpected.

A robust handoff system handles these cases gracefully with fallback logic.

Here’s a pattern for implementing fallback routing:

public class FallbackRouter
{
    private readonly Dictionary<string, List<string>> _agentResponsibilities;

    public FallbackRouter()
    {
        _agentResponsibilities = new Dictionary<string, List<string>>
        {
            { "BehavioralCoach", new List<string> { "communication", "confidence", "soft skills" } },
            { "TechnicalCoach", new List<string> { "algorithms", "system design", "code review" } },
            { "Summarizer", new List<string> { "summary", "recap", "next steps" } }
        };
    }

    public async Task<string> DetermineFallbackTargetAsync(string userMessage, string currentAgent)
    {
        if (userMessage.Contains("behavioral") || userMessage.Contains("soft skills"))
        {
            return "BehavioralCoach";
        }
        if (userMessage.Contains("technical") || userMessage.Contains("code"))
        {
            return "TechnicalCoach";
        }
        if (userMessage.Contains("summary") || userMessage.Contains("recap"))
        {
            return "Summarizer";
        }

        var bestAgent = await FindBestAgentAsync(userMessage, _agentResponsibilities);
        return bestAgent ?? currentAgent;
    }
}

The fallback router has two strategies. First, it checks for explicit keywords. If the user says “I want to talk to the technical coach”, honor that request immediately. Second, if there’s no explicit request, it uses semantic matching to find the best agent for the question. If nothing matches, it keeps the conversation with the current agent.

This keeps the user in control while also being smart about routing. It ensures the user gets the right specialist when they need it.

Putting It Together: A Complete Example

Let’s see how all these pieces fit together in a simplified Interview Coach workflow:

public class InterviewCoachOrchestrator
{
    private readonly AgentWorkflow _workflow;
    private readonly FallbackRouter _fallbackRouter;
    private readonly Dictionary<string, Agent> _agents;

    public async Task<string> ProcessUserMessageAsync(string userMessage, ConversationContext context)
    {
        var currentAgent = _agents[context.CurrentAgentId];

        var shouldHandoff = await currentAgent.ShouldHandoffAsync(userMessage, context);

        if (shouldHandoff)
        {
            var targetAgent = await _fallbackRouter.DetermineFallbackTargetAsync(userMessage, context.CurrentAgentId);
            context.CurrentAgentId = targetAgent;
        }

        var response = await currentAgent.ProcessAsync(userMessage, context);

        if (currentAgent.WantsToHandoff(context))
        {
            var nextAgent = await currentAgent.DetermineNextHandoffAsync(context);
            if (!string.IsNullOrEmpty(nextAgent))
            {
                context.CurrentAgentId = nextAgent;
                var transitionResponse = await _agents[nextAgent].HandleHandoffAsync(context);
                response += "

" + transitionResponse;
            }
        }

        return response;
    }
}

This orchestrator manages the full lifecycle of a message: it checks if the current agent should handle it, routes to a different agent if needed, processes the message, and then checks if the agent wants to hand off to the next specialist.

Key Takeaways

The handoff pattern is a powerful way to build complex agentic workflows in .NET. By breaking your system into specialized agents with minimal tool sets, you gain clarity, maintainability, and robustness.

The core principles are:

  • Specialize each agent to a narrow responsibility. Give it only the tools it needs.
  • Use a triage agent to make routing decisions, keeping that logic separate from the work agents.
  • Transfer context explicitly: full history, summaries, or structured state, depending on your needs.
  • Implement fallback routing to handle off-script requests gracefully.
  • Let each agent decide when to hand off, rather than forcing a rigid workflow.

The Interview Coach architecture shows all of this in action. A Receptionist routes, Behavioral and Technical coaches do their specialized work, and a Summarizer ties it all together. Each agent is focused. Each knows when to hand off. The user gets a seamless experience across the entire workflow.

As you build more complex agentic systems, this pattern scales. You can add new specialists, adjust the routing logic, and refine the context transfer strategy without rearchitecting the entire system. That’s the power of thinking about agents as a team, not just a single system.

What’s the difference between handoff and agent-as-tools?

In agent-as-tools, a single orchestrator agent calls other agents as function calls and remains in control. In handoff, one agent explicitly transfers full control to another, and the second agent becomes the active participant in the conversation. Handoff is better for sequential workflows and maintaining conversational flow.

How do I preserve conversation context when handing off between agents?

There are three main strategies: pass the full conversation history, create a summary of key points before handing off, or extract structured state into a shared object. Most systems use a combination, keeping full history for reference, a summary for immediate context, and structured state for critical facts.

Should every agent have tools, or can some be tool-free?

Tool-free agents are perfectly valid, especially for routing or orchestration. A triage agent needs no tools; it only reads the message and makes a routing decision. Specialized work agents (like a Technical Coach) have focused tools relevant to their domain. The principle of least privilege applies: give each agent only what it needs.

What happens if a user asks a question that doesn’t match the current agent’s responsibility?

Implement fallback routing. Check for explicit keywords first (if the user says “I want technical help”, switch agents immediately). If there’s no explicit request, use semantic matching to find the best agent for the question. If nothing matches clearly, keep the conversation with the current agent.

Can agents hand off to agents they’ve handed off to before?

Yes. The workflow can be non-linear. An agent can hand off to a specialist, and that specialist can hand off back to the original agent if needed, or to a different agent entirely. This flexibility allows you to model complex, adaptive workflows that respond to the actual conversation rather than a rigid predetermined path.

Leave a Reply

Your email address will not be published. Required fields are marked *