Dynamic tool management
Dynamic tool management
So far, every agent has had a fixed set of tools for its entire lifetime. But real conversations change shape. After a customer places their order, you might want to offer a coupon tool. During payment, you might want to remove the "add item" tool so the customer cannot modify the order. LiveKit Agents lets you add, remove, and reconfigure tools and instructions on the fly.
What you'll learn
- How to add tools to a running agent with
update_tools() - How to remove tools to restrict what the agent can do
- How to change agent behavior mid-conversation with
update_instructions() - When and why to use dynamic tools instead of agent handoffs
Adding tools dynamically
The update_tools() method lets you change the tool set of a running agent. The next time the LLM generates a response, it will see the updated tool list.
Here is a practical example: after the customer's order is confirmed, you dynamically add an apply_coupon tool.
from livekit.agents import Agent, function_tool, RunContext, Tool
async def apply_coupon_handler(context: RunContext, code: str) -> str:
"""Apply a coupon code to the order."""
valid_coupons = {
"WELCOME10": 10,
"LUNCH20": 20,
"PASTA15": 15,
}
discount = valid_coupons.get(code.upper())
if discount is None:
return f"Coupon code '{code}' is not valid."
return f"Coupon applied! {discount}% discount on your order."
apply_coupon_tool = Tool.create(
name="apply_coupon",
description="Apply a coupon or discount code to the current order",
handler=apply_coupon_handler,
parameters={
"code": {
"type": "string",
"description": "The coupon code to apply",
}
},
)
class OrderTakerAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are the order taker at Bella Vista.
Help customers build their order.""",
)
self.order_confirmed = False
@function_tool
async def confirm_order(self, context: RunContext) -> str:
"""Confirm the customer's complete order."""
self.order_confirmed = True
# Dynamically add the coupon tool now that the order is confirmed
await self.session.update_tools([apply_coupon_tool])
# Update instructions to mention the new capability
await self.session.update_instructions(
"""You are the order taker at Bella Vista.
The order has been confirmed. Ask the customer if they have a coupon code.
If they do, use the apply_coupon tool. Then proceed to payment."""
)
state = self.session.userdata["state"]
return f"Order confirmed: {state.order_summary}. Ask if they have a coupon code."import { Agent, functionTool, RunContext, Tool } from "@livekit/agents";
const applyCouponTool = Tool.create({
name: "apply_coupon",
description: "Apply a coupon or discount code to the current order",
handler: async (context: RunContext, { code }: { code: string }) => {
const validCoupons: Record<string, number> = {
WELCOME10: 10,
LUNCH20: 20,
PASTA15: 15,
};
const discount = validCoupons[code.toUpperCase()];
if (discount === undefined) {
return `Coupon code '${code}' is not valid.`;
}
return `Coupon applied! ${discount}% discount on your order.`;
},
parameters: {
code: { type: "string", description: "The coupon code to apply" },
},
});
class OrderTakerAgent extends Agent {
private orderConfirmed = false;
constructor() {
super({
instructions: `You are the order taker at Bella Vista.
Help customers build their order.`,
});
}
@functionTool({ description: "Confirm the customer's complete order." })
async confirmOrder(context: RunContext): Promise<string> {
this.orderConfirmed = true;
await this.session.updateTools([applyCouponTool]);
await this.session.updateInstructions(
`You are the order taker at Bella Vista.
The order has been confirmed. Ask the customer if they have a coupon code.
If they do, use the apply_coupon tool. Then proceed to payment.`
);
const state = this.session.userdata.state;
return `Order confirmed: ${state.orderSummary}. Ask if they have a coupon code.`;
}
}After confirm_order runs, two things change. First, update_tools() adds the coupon tool to the agent's available tools. The LLM will see it in its next request and can decide to use it. Second, update_instructions() changes the agent's system prompt to mention coupons. Both changes take effect immediately — the very next LLM generation will use the new tools and instructions.
Removing tools
Sometimes you need to take tools away. After the coupon is applied and payment begins, you might want to remove the ordering tools so the customer cannot accidentally add more items.
class OrderTakerAgent(Agent):
def __init__(self):
super().__init__(
instructions="You are the order taker at Bella Vista...",
)
@function_tool
async def add_item(self, context: RunContext, name: str, quantity: int) -> str:
"""Add an item to the order."""
return f"Added {quantity}x {name}"
@function_tool
async def remove_item(self, context: RunContext, name: str) -> str:
"""Remove an item from the order."""
return f"Removed {name}"
@function_tool
async def proceed_to_payment(self, context: RunContext) -> str:
"""Lock the order and proceed to payment."""
# Remove ordering tools — keep only payment-related tools
await self.session.update_tools([])
await self.session.update_instructions(
"""The order is locked. Collect payment information.
Ask how the customer would like to pay: card, cash, or mobile.
Do not allow order modifications at this point."""
)
return "Order is locked. Proceeding to payment."class OrderTakerAgent extends Agent {
constructor() {
super({
instructions: "You are the order taker at Bella Vista...",
});
}
@functionTool({ description: "Add an item to the order." })
async addItem(context: RunContext, name: string, quantity: number): Promise<string> {
return `Added ${quantity}x ${name}`;
}
@functionTool({ description: "Remove an item from the order." })
async removeItem(context: RunContext, name: string): Promise<string> {
return `Removed ${name}`;
}
@functionTool({ description: "Lock the order and proceed to payment." })
async proceedToPayment(context: RunContext): Promise<string> {
await this.session.updateTools([]);
await this.session.updateInstructions(
`The order is locked. Collect payment information.
Ask how the customer would like to pay: card, cash, or mobile.
Do not allow order modifications at this point.`
);
return "Order is locked. Proceeding to payment.";
}
}update_tools() replaces the tool list
update_tools([]) removes all dynamically added tools. If you want to keep some tools and remove others, pass the tools you want to keep. The method replaces the dynamic tool list — it does not merge with it. Tools defined as decorated methods on the agent class are not affected by update_tools().
Updating instructions
update_instructions() replaces the agent's system prompt entirely. This is powerful but requires care — you need to include everything the agent needs to know in the new instructions, not just the changes.
class OrderTakerAgent(Agent):
PHASE_ORDERING = """You are the order taker at Bella Vista.
Help the customer build their order. Suggest popular items.
After each item, ask if they want anything else.
Available tools: add_item, remove_item, check_menu."""
PHASE_REVIEW = """You are the order taker at Bella Vista.
Read the complete order back to the customer.
Ask if everything looks correct.
Available tools: confirm_order, remove_item."""
PHASE_PAYMENT = """You are the order taker at Bella Vista.
The order is confirmed. Collect payment.
Ask how they would like to pay: card, cash, or mobile.
Ask about tip (suggest 15%, 18%, or 20%).
Do not allow order changes."""
def __init__(self):
super().__init__(instructions=self.PHASE_ORDERING)
@function_tool
async def review_order(self, context: RunContext) -> str:
"""Switch to order review phase."""
await self.session.update_instructions(self.PHASE_REVIEW)
state = self.session.userdata["state"]
return f"Current order: {state.order_summary}. Confirm with the customer."
@function_tool
async def confirm_order(self, context: RunContext) -> str:
"""Confirm the order and switch to payment phase."""
await self.session.update_instructions(self.PHASE_PAYMENT)
await self.session.update_tools([])
return "Order confirmed. Collecting payment."class OrderTakerAgent extends Agent {
static PHASE_ORDERING = `You are the order taker at Bella Vista.
Help the customer build their order. Suggest popular items.
After each item, ask if they want anything else.
Available tools: add_item, remove_item, check_menu.`;
static PHASE_REVIEW = `You are the order taker at Bella Vista.
Read the complete order back to the customer.
Ask if everything looks correct.
Available tools: confirm_order, remove_item.`;
static PHASE_PAYMENT = `You are the order taker at Bella Vista.
The order is confirmed. Collect payment.
Ask how they would like to pay: card, cash, or mobile.
Ask about tip (suggest 15%, 18%, or 20%).
Do not allow order changes.`;
constructor() {
super({ instructions: OrderTakerAgent.PHASE_ORDERING });
}
@functionTool({ description: "Switch to order review phase." })
async reviewOrder(context: RunContext): Promise<string> {
await this.session.updateInstructions(OrderTakerAgent.PHASE_REVIEW);
const state = this.session.userdata.state;
return `Current order: ${state.orderSummary}. Confirm with the customer.`;
}
@functionTool({ description: "Confirm the order and switch to payment phase." })
async confirmOrder(context: RunContext): Promise<string> {
await this.session.updateInstructions(OrderTakerAgent.PHASE_PAYMENT);
await this.session.updateTools([]);
return "Order confirmed. Collecting payment.";
}
}Using phase constants keeps your instruction changes organized and readable. Each phase has a complete, self-contained instruction set. When you switch phases, you swap the entire prompt rather than trying to patch individual sentences. This approach is predictable and easy to debug — you can read each phase's instructions independently and understand exactly what the agent will do.
When to use dynamic tools vs. agent handoffs
Dynamic tools and agent handoffs both change what the agent can do. Here is how to decide between them:
Same personality, different capabilities: use dynamic tools
If the agent's tone and role stay the same but its available actions change (ordering phase to payment phase), dynamic tools keep the conversation smooth. The customer does not notice a transition.
Different personality or role: use an agent handoff
If the conversation needs a fundamentally different tone (warm greeter to methodical order taker), an agent handoff is cleaner. Each agent class encapsulates its own personality and logic.
Quick capability change: use dynamic tools
Adding a coupon tool after order confirmation is a quick change that does not warrant a full agent handoff. Dynamic tools keep the overhead low.
Complex state reset: use an agent handoff
If switching phases requires reinitializing state, resetting conversation context, or loading entirely different configuration, a handoff is more appropriate.
Combine both approaches
In the restaurant system, we use agent handoffs for major role changes (Greeter to OrderTaker to Feedback) and dynamic tools for phase changes within a single role (ordering to review to payment within the OrderTaker). This gives you the best of both worlds.
What you learned
update_tools()adds or removes tools from a running agentupdate_instructions()replaces the agent's system prompt entirely- Phase-based instruction constants keep dynamic behavior organized
- Use dynamic tools for capability changes within the same role; use agent handoffs for role changes
Test your knowledge
Question 1 of 3
What does update_tools([]) do to an agent's tool set?
Next up
You have built the complete restaurant ordering system. In the final chapter, you will learn how to test the entire multi-agent workflow — asserting on handoffs, validating task results, and running it all in CI.