Chapter 215m

Building the greeter agent

Building the greeter agent

Every great restaurant experience starts at the front door. In this chapter, you will build the Greeter Agent — the first thing a customer interacts with. It welcomes diners, answers quick questions, and hands off to the Order Taker when the guest is ready. Along the way, you will learn how to subclass Agent, use on_enter() for automatic greetings, and create a tool that triggers an agent handoff.

Agent subclasson_enter()Tool-based handoff

What you'll learn

  • How to create a custom agent by subclassing Agent
  • How to use on_enter() to greet the user automatically
  • How to define a tool that triggers an agent handoff
  • How to wire up the agent with a session entrypoint

Subclassing Agent

In Course 1.1, you created agents by passing instructions directly to the Agent() constructor. That works for simple cases, but when an agent needs its own tools, lifecycle hooks, and custom behavior, subclassing is cleaner.

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


class GreeterAgent(Agent):
  def __init__(self):
      super().__init__(
          instructions="""You are a friendly host at Bella Vista, an Italian restaurant.
          Your job is to welcome guests, answer quick questions about hours
          and wait times, and transfer them to the order taker when they are
          ready to order.

          Keep responses warm and brief — one to two sentences.
          Never use markdown, bullet points, or emoji.
          If the guest asks to see the menu or place an order, transfer them
          to the order taker right away.""",
      )
greeterAgent.tstypescript
import { Agent, functionTool, RunContext } from "@livekit/agents";

class GreeterAgent extends Agent {
constructor() {
  super({
    instructions: `You are a friendly host at Bella Vista, an Italian restaurant.
      Your job is to welcome guests, answer quick questions about hours
      and wait times, and transfer them to the order taker when they are
      ready to order.

      Keep responses warm and brief — one to two sentences.
      Never use markdown, bullet points, or emoji.
      If the guest asks to see the menu or place an order, transfer them
      to the order taker right away.`,
  });
}
}
What's happening

Subclassing Agent lets you co-locate instructions, tools, and lifecycle hooks in a single class. The super().__init__() call passes your instructions to the base class. Everything else — tools, on_enter(), on_exit() — becomes a method on the class.

Greeting with on_enter()

When an agent takes over a conversation — either at the start or after a handoff — LiveKit calls its on_enter() method. This is where you put automatic behavior that should happen before the user speaks.

For the greeter, you want an immediate welcome message so the customer does not sit in silence wondering if anyone is there.

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


class GreeterAgent(Agent):
  def __init__(self):
      super().__init__(
          instructions="""You are a friendly host at Bella Vista, an Italian restaurant.
          Your job is to welcome guests, answer quick questions about hours
          and wait times, and transfer them to the order taker when they are
          ready to order.

          Keep responses warm and brief — one to two sentences.
          Never use markdown, bullet points, or emoji.
          If the guest asks to see the menu or place an order, transfer them
          to the order taker right away.""",
      )

  async def on_enter(self):
      self.session.generate_reply(
          instructions="Greet the guest warmly. Welcome them to Bella Vista and ask how you can help them today."
      )
greeterAgent.tstypescript
import { Agent, functionTool, RunContext } from "@livekit/agents";

class GreeterAgent extends Agent {
constructor() {
  super({
    instructions: `You are a friendly host at Bella Vista, an Italian restaurant.
      Your job is to welcome guests, answer quick questions about hours
      and wait times, and transfer them to the order taker when they are
      ready to order.

      Keep responses warm and brief — one to two sentences.
      Never use markdown, bullet points, or emoji.
      If the guest asks to see the menu or place an order, transfer them
      to the order taker right away.`,
  });
}

async onEnter() {
  this.session.generateReply({
    instructions: "Greet the guest warmly. Welcome them to Bella Vista and ask how you can help them today.",
  });
}
}

generate_reply() is non-blocking

generate_reply() tells the LLM to produce a response using the given instructions as additional context. It does not block — the method returns immediately and the LLM generates the greeting asynchronously. The instructions parameter is a one-time addition to the system prompt for just this generation; it does not change the agent's base instructions.

Adding tools to the greeter

The greeter needs two informational tools and one handoff tool. The informational tools let the LLM answer questions about hours and wait times using real data instead of guessing.

greeter_agent.pypython
class GreeterAgent(Agent):
  def __init__(self):
      super().__init__(
          instructions="""You are a friendly host at Bella Vista, an Italian restaurant.
          Your job is to welcome guests, answer quick questions about hours
          and wait times, and transfer them to the order taker when they are
          ready to order.

          Keep responses warm and brief — one to two sentences.
          Never use markdown, bullet points, or emoji.
          If the guest asks to see the menu or place an order, transfer them
          to the order taker right away.

          You have tools to check restaurant hours and current wait times.
          Always use them rather than guessing.""",
      )

  async def on_enter(self):
      self.session.generate_reply(
          instructions="Greet the guest warmly. Welcome them to Bella Vista and ask how you can help them today."
      )

  @function_tool
  async def check_hours(self, context: RunContext) -> str:
      """Check the restaurant's operating hours."""
      return "Bella Vista is open Monday through Saturday, 11 AM to 10 PM. Closed on Sundays."

  @function_tool
  async def check_wait_time(self, context: RunContext) -> str:
      """Check the current estimated wait time for a table."""
      # In production, query your reservation system
      return "Current wait time is approximately 15 minutes."
greeterAgent.tstypescript
class GreeterAgent extends Agent {
constructor() {
  super({
    instructions: `You are a friendly host at Bella Vista, an Italian restaurant.
      Your job is to welcome guests, answer quick questions about hours
      and wait times, and transfer them to the order taker when they are
      ready to order.

      Keep responses warm and brief — one to two sentences.
      Never use markdown, bullet points, or emoji.
      If the guest asks to see the menu or place an order, transfer them
      to the order taker right away.

      You have tools to check restaurant hours and current wait times.
      Always use them rather than guessing.`,
  });
}

async onEnter() {
  this.session.generateReply({
    instructions: "Greet the guest warmly. Welcome them to Bella Vista and ask how you can help them today.",
  });
}

@functionTool({ description: "Check the restaurant's operating hours." })
async checkHours(context: RunContext): Promise<string> {
  return "Bella Vista is open Monday through Saturday, 11 AM to 10 PM. Closed on Sundays.";
}

@functionTool({ description: "Check the current estimated wait time for a table." })
async checkWaitTime(context: RunContext): Promise<string> {
  return "Current wait time is approximately 15 minutes.";
}
}
What's happening

When tools are defined as methods on an Agent subclass using the @function_tool decorator, they are automatically registered with that agent. You do not need to pass them in a tools=[] list — the framework discovers them through the decorator.

The handoff tool

The most important tool on the greeter is the one that transfers the guest to the order taker. In LiveKit Agents, you trigger a handoff by calling self.hand_off() with an instance of the target agent.

greeter_agent.pypython
from order_taker_agent import OrderTakerAgent


class GreeterAgent(Agent):
  # ... instructions, on_enter, check_hours, check_wait_time ...

  @function_tool
  async def transfer_to_order_taker(self, context: RunContext) -> str:
      """Transfer the guest to the order taker when they are ready to order."""
      self.hand_off(OrderTakerAgent())
      return "Transferring you to our order taker now."
greeterAgent.tstypescript
import { OrderTakerAgent } from "./orderTakerAgent";

class GreeterAgent extends Agent {
// ... instructions, onEnter, checkHours, checkWaitTime ...

@functionTool({ description: "Transfer the guest to the order taker when they are ready to order." })
async transferToOrderTaker(context: RunContext): Promise<string> {
  this.handOff(new OrderTakerAgent());
  return "Transferring you to our order taker now.";
}
}
1

The LLM decides to hand off

When the guest says something like "I'd like to order" or "Can I see the menu?", the LLM reads the instructions ("transfer them to the order taker right away") and decides to call transfer_to_order_taker.

2

hand_off() is called

self.hand_off(OrderTakerAgent()) creates a new instance of the Order Taker and tells the session to switch to it. The current agent's on_exit() will fire, then the new agent's on_enter() will fire.

3

The return value is spoken

The string "Transferring you to our order taker now." is fed back to the LLM, which uses it to generate a natural transition message before the new agent takes over.

Always instantiate the target agent

self.hand_off() takes an instance, not a class. Write self.hand_off(OrderTakerAgent()), not self.hand_off(OrderTakerAgent). Passing the class itself will raise an error.

Wiring up the entrypoint

With the GreeterAgent complete, connect it to a session entrypoint so it runs when a user connects.

agent.pypython
from livekit.agents import AgentServer, AgentSession
from livekit.plugins import openai, deepgram, cartesia
from greeter_agent import GreeterAgent

server = AgentServer()


@server.rtc_session
async def entrypoint(session: AgentSession):
  await session.start(
      agent=GreeterAgent(),
      room=session.room,
      stt=deepgram.STT(model="nova-3"),
      llm=openai.LLM(model="gpt-4o-mini"),
      tts=cartesia.TTS(voice="<voice-id>"),
  )


if __name__ == "__main__":
  server.run()
agent.tstypescript
import { AgentServer, AgentSession } from "@livekit/agents";
import { DeepgramSTT } from "@livekit/plugins-deepgram";
import { OpenAILLM } from "@livekit/plugins-openai";
import { CartesiaTTS } from "@livekit/plugins-cartesia";
import { GreeterAgent } from "./greeterAgent";

const server = new AgentServer();

server.rtcSession(async (session: AgentSession) => {
await session.start({
  agent: new GreeterAgent(),
  room: session.room,
  stt: new DeepgramSTT({ model: "nova-3" }),
  llm: new OpenAILLM({ model: "gpt-4o-mini" }),
  tts: new CartesiaTTS({ voice: "<voice-id>" }),
});
});

server.run();
What's happening

The entrypoint creates a GreeterAgent instance and passes it as the starting agent. When the session begins, the framework calls on_enter() on the greeter, which triggers the welcome message. From there, the conversation flows naturally until the guest is ready to order and the handoff tool fires.

What you learned

  • Subclassing Agent lets you co-locate instructions, tools, and lifecycle hooks in one class
  • on_enter() runs automatically when an agent takes over — use it for greetings and setup
  • generate_reply() asks the LLM to produce a response with optional one-time instructions
  • Tools defined as decorated methods on the agent subclass are auto-registered
  • self.hand_off(TargetAgent()) transfers the conversation to a new agent instance

Test your knowledge

Question 1 of 3

What does on_enter() do when an agent takes over a conversation?

Next up

The greeter's tools are simple — string in, string out. In the next chapter, you will go beyond decorators and learn how to create tools programmatically, define complex parameter schemas, manage tools with Toolset, and control tool behavior with ToolFlag.

Concepts covered
Agent subclasson_enter()Tool-based handoff