Chapter 130m

AI-native routing & multi-agent orchestration

AI-Native Routing: Replace the IVR with Intent Detection

Traditional IVRs force callers through DTMF menu trees — "press 1 for billing, press 2 for support." The entire premise of AI voice agents is that this is no longer necessary. The agent understands what the caller wants from natural conversation and routes them to the right place. This chapter builds that system using multi-agent orchestration.

Intent detectionMulti-agent handoffState machineDepartment agents

What you'll learn

  • Why AI intent detection replaces DTMF menu trees entirely
  • How to design a multi-agent call flow as a state machine
  • Building department-specific agents with isolated instructions and tools
  • Orchestrating agent handoffs that preserve conversation context
  • Managing the complete call lifecycle from greeting to resolution

The AI advantage over traditional IVR

A traditional IVR presents a fixed menu. The caller must listen to every option, remember a number, and press it. Three levels deep, callers are frustrated before they reach a human. AI-native routing eliminates this entirely:

Traditional IVRAI-Native Routing
"Press 1 for billing, 2 for support...""How can I help you today?"
Caller must listen to all optionsCaller states their need naturally
Fixed menu depth (3+ levels)Single conversational turn
DTMF only — no natural languageFull natural language understanding
Misroutes require starting overAgent can re-route mid-conversation
What's happening

The key insight is that the greeting agent IS the IVR. Instead of a menu tree, you have an LLM that understands intent from natural speech. "I got overcharged on my last invoice" routes to billing. "My product stopped working" routes to support. No menus, no numbers, no frustrated callers. The agent even handles ambiguous requests — "I need to change something on my account" — by asking a clarifying question, something a DTMF tree cannot do.

Designing the state machine

An AI-native call flow is a directed graph. Each node is a state — greeting, department routing, specialized handling, escalation — and edges are triggered by the LLM's understanding of caller intent.

call_states.pypython
from enum import Enum
from dataclasses import dataclass, field


class CallState(Enum):
  GREETING = "greeting"
  BILLING = "billing"
  SUPPORT = "support"
  SALES = "sales"
  TRANSFER_TO_HUMAN = "transfer_to_human"
  GOODBYE = "goodbye"


@dataclass
class CallContext:
  """Shared state passed between agents during a call."""
  caller_id: str
  caller_name: str | None = None
  account_number: str | None = None
  current_state: CallState = CallState.GREETING
  previous_states: list[CallState] = field(default_factory=list)
  intent_history: list[str] = field(default_factory=list)

  def transition(self, new_state: CallState, reason: str = ""):
      self.previous_states.append(self.current_state)
      self.current_state = new_state
      if reason:
          self.intent_history.append(reason)


# Valid transitions — prevents nonsensical jumps
VALID_TRANSITIONS: dict[CallState, set[CallState]] = {
  CallState.GREETING: {CallState.BILLING, CallState.SUPPORT, CallState.SALES, CallState.GOODBYE},
  CallState.BILLING: {CallState.TRANSFER_TO_HUMAN, CallState.SUPPORT, CallState.GOODBYE},
  CallState.SUPPORT: {CallState.TRANSFER_TO_HUMAN, CallState.BILLING, CallState.GOODBYE},
  CallState.SALES: {CallState.TRANSFER_TO_HUMAN, CallState.GOODBYE},
  CallState.TRANSFER_TO_HUMAN: {CallState.GOODBYE},
  CallState.GOODBYE: set(),
}

No department selection node needed

Notice there is no "department_select" state. The greeting agent detects intent and routes directly. The caller says what they need, and the LLM figures out where to send them. This is the fundamental difference from a traditional IVR — intent detection replaces explicit menu navigation.

Building department agents

Each department gets its own agent with focused instructions and tools. The greeting agent routes callers based on natural language. Department agents handle domain-specific requests. Each agent knows only what it needs to know.

agents.pypython
from livekit.agents import Agent, AgentSession, function_tool, RunContext


class GreetingAgent(Agent):
  def __init__(self):
      super().__init__(
          instructions="""You are the front desk of Acme Corp. Greet the caller warmly
          and determine which department they need from their natural speech.
          Use the route_to_department tool once you understand their intent.
          If the intent is ambiguous, ask ONE clarifying question — never present
          a menu of options. Keep the greeting under 15 seconds.""",
          tools=[route_to_department],
      )


class BillingAgent(Agent):
  def __init__(self):
      super().__init__(
          instructions="""You are a billing specialist at Acme Corp. Help callers with
          invoices, payment status, refunds, and account balance inquiries.
          Always verify the caller's account number before sharing details.
          If you cannot resolve the issue, use transfer_to_human to escalate.""",
          tools=[check_balance, process_refund, transfer_to_human],
      )


class SupportAgent(Agent):
  def __init__(self):
      super().__init__(
          instructions="""You are a technical support agent at Acme Corp. Help callers
          troubleshoot product issues. Walk callers through solutions step by step.
          If the issue requires hands-on intervention, escalate to a human agent.""",
          tools=[lookup_ticket, create_ticket, transfer_to_human],
      )


AGENT_MAP = {
  "billing": BillingAgent,
  "support": SupportAgent,
  "sales": SalesAgent,
}


@function_tool
async def route_to_department(context: RunContext, department: str, reason: str) -> str:
  """Route the caller to a specific department based on their stated need.

  Args:
      department: The department to route to (billing, support, sales).
      reason: Brief description of the caller's intent.
  """
  call_ctx: CallContext = context.userdata
  state_map = {
      "billing": CallState.BILLING,
      "support": CallState.SUPPORT,
      "sales": CallState.SALES,
  }
  target = state_map.get(department.lower())
  if target is None:
      return f"Unknown department '{department}'. Valid options: billing, support, sales."

  call_ctx.transition(target, reason)

  # Perform the actual agent handoff
  agent_cls = AGENT_MAP.get(department.lower())
  if agent_cls:
      context.session.update_agent(agent_cls())

  return f"Routing to {department}. Reason: {reason}"
agents.tstypescript
import { Agent, AgentSession, RunContext } from "@livekit/agents";

class GreetingAgent extends Agent {
constructor() {
  super({
    instructions: `You are the front desk of Acme Corp. Greet the caller warmly
      and determine which department they need from their natural speech.
      Use the route_to_department tool once you understand their intent.
      If the intent is ambiguous, ask ONE clarifying question — never present
      a menu of options. Keep the greeting under 15 seconds.`,
    tools: [routeToDepartment],
  });
}
}

class BillingAgent extends Agent {
constructor() {
  super({
    instructions: `You are a billing specialist at Acme Corp. Help callers with
      invoices, payment status, refunds, and account balance inquiries.
      Always verify the caller's account number before sharing details.
      If you cannot resolve the issue, use transferToHuman to escalate.`,
    tools: [checkBalance, processRefund, transferToHuman],
  });
}
}

class SupportAgent extends Agent {
constructor() {
  super({
    instructions: `You are a technical support agent at Acme Corp. Help callers
      troubleshoot product issues. Walk callers through solutions step by step.
      If the issue requires hands-on intervention, escalate to a human agent.`,
    tools: [lookupTicket, createTicket, transferToHuman],
  });
}
}

Agent instructions must be strict boundaries

Each agent should only know about its own domain. The billing agent should not attempt to troubleshoot product issues. Clear boundaries prevent confused responses and make it obvious when a transfer is needed. This also makes each agent independently testable — you measure billing resolution rate separately from support resolution rate.

Orchestrating handoffs

The entrypoint creates the call context and starts the greeting agent. When the greeting agent detects intent and calls the routing tool, the controller swaps to the appropriate department agent while preserving conversation context.

entrypoint.pypython
from livekit.agents import AgentServer, AgentSession

server = AgentServer()

AGENT_MAP = {
  CallState.GREETING: GreetingAgent,
  CallState.BILLING: BillingAgent,
  CallState.SUPPORT: SupportAgent,
  CallState.SALES: SalesAgent,
}


@server.rtc_session
async def entrypoint(session: AgentSession):
  call_ctx = CallContext(caller_id=session.room.name)

  await session.start(
      agent=GreetingAgent(),
      room=session.room,
      userdata=call_ctx,
  )
  # The GreetingAgent's route_to_department tool calls
  # context.session.update_agent() to swap in the department
  # agent — the handoff happens automatically when the LLM
  # calls the routing tool.

Complete flow walkthrough

Here is what happens when a caller dials in and says "I got overcharged on my last invoice."

1

Caller connects, GreetingAgent activates

The entrypoint creates a CallContext and starts the GreetingAgent. It says: "Thank you for calling Acme Corp. How can I help you today?"

2

AI detects intent from natural speech

The caller says: "I got overcharged on my last invoice." The LLM recognizes billing intent and calls route_to_department(department="billing", reason="invoice overcharge dispute"). No menu, no button press — just natural conversation.

3

State machine transitions, agent swaps

The CallContext updates from GREETING to BILLING. The controller swaps to BillingAgent via session.update_agent(), passing the conversation history.

4

BillingAgent takes over with full context

The BillingAgent receives the conversation context. It says: "I can help with that overcharge. Could you provide your account number so I can pull up the invoice?"

5

Resolution or escalation

If the agent resolves the issue, it transitions to GOODBYE. If not, it calls transfer_to_human, connecting the caller to a live representative with full context.

What's happening

The power of multi-agent orchestration is separation of concerns. Each agent is small, focused, and testable. The greeting agent is measured on routing accuracy. The billing agent is measured on resolution rate. You can update one agent's instructions without touching the others. The state machine ensures the overall flow remains coherent even as individual agents evolve.

Test your knowledge

Question 1 of 3

What is the fundamental advantage of AI intent detection over a DTMF menu tree in a contact center?

What you learned

  • AI intent detection replaces DTMF menu trees — callers state their need naturally and the LLM routes them
  • A multi-agent call flow is modeled as a state machine with valid transition constraints
  • Each department agent has focused instructions and isolated tools for separation of concerns
  • Agent handoffs preserve CallContext (caller ID, account, intent history) across transitions
  • The greeting agent IS the IVR replacement — one conversational turn instead of multi-level menus

Next up

Not every call can be resolved by AI. The next chapter builds the handoff from AI to human agents — with full context transfer so the human never asks the caller to repeat themselves.

Concepts covered
Intent detectionMulti-agent handoffState machineDepartment agents