AgentTask: structured data collection
AgentTask: structured data collection
Tools return strings. But when you need to collect a complete order item — with a name, quantity, and list of modifications — a string is not enough. You need structured data that your application can validate and process. AgentTask solves this by running a focused conversational loop that collects typed data and returns it to the parent agent.
What you'll learn
- How AgentTask runs a focused sub-conversation to collect structured data
- How to define typed results with Pydantic models and Zod schemas
- How
complete()ends the task and returns the result - How the parent agent receives and uses the task result
What is an AgentTask?
An AgentTask is a mini-agent with a single purpose: collect specific information from the user and return it as a typed object. Think of it as a conversational form. The task has its own instructions and tools, runs its own conversational loop, and when it has everything it needs, it calls complete() with the result. Control then returns to the parent agent.
The key difference from a regular agent is the generic type parameter. AgentTask[OrderItem] means "this task will return an OrderItem when it completes." The framework enforces this — you cannot call complete() with the wrong type.
Defining the output type
Start by defining the data model that represents the task's result. In Python, use Pydantic. In TypeScript, use Zod.
from pydantic import BaseModel
class OrderItem(BaseModel):
name: str
quantity: int
modifications: list[str] = []
unit_price: float = 0.0import { z } from "zod";
const OrderItemSchema = z.object({
name: z.string(),
quantity: z.number().int().min(1),
modifications: z.array(z.string()).default([]),
unitPrice: z.number().default(0),
});
type OrderItem = z.infer<typeof OrderItemSchema>;The output type defines exactly what data the task must collect. Pydantic and Zod both provide validation — if the task tries to complete with missing or invalid data, the framework raises an error rather than silently passing bad data to the parent agent. The modifications field has a default empty list, meaning it is optional; the customer does not have to specify modifications for every item.
Building the task
Now create the AgentTask subclass. It needs instructions that tell the LLM what to collect, and a tool that lets the LLM submit the result when it has gathered everything.
from livekit.agents import AgentTask, function_tool, RunContext
from models import OrderItem
class CollectOrderItem(AgentTask[OrderItem]):
def __init__(self):
super().__init__(
instructions="""Collect one menu item from the customer.
You need to get:
1. The item name (must be from our menu)
2. The quantity
3. Any modifications (e.g., no onions, extra cheese)
Ask for each piece of information conversationally.
Confirm the complete item before submitting.
Keep responses brief — one to two sentences.""",
output_type=OrderItem,
)
@function_tool
async def check_menu(self, context: RunContext, category: str) -> str:
"""Check available menu items and prices for a category."""
menu = {
"appetizers": [("Bruschetta", 12.0), ("Caesar Salad", 10.0)],
"mains": [("Grilled Salmon", 28.0), ("Ribeye Steak", 35.0), ("Pasta Primavera", 18.0)],
"desserts": [("Tiramisu", 10.0), ("Cheesecake", 9.0)],
}
items = menu.get(category, [])
if not items:
return f"No category '{category}' found. Available: appetizers, mains, desserts."
return "\n".join(f"{name}: ${price:.2f}" for name, price in items)
@function_tool
async def confirm_item(
self,
context: RunContext,
name: str,
quantity: int,
unit_price: float,
modifications: list[str] = [],
) -> None:
"""Confirm and submit the order item after the customer agrees."""
self.complete(
OrderItem(
name=name,
quantity=quantity,
modifications=modifications,
unit_price=unit_price,
)
)import { AgentTask, functionTool, RunContext } from "@livekit/agents";
import { z } from "zod";
const OrderItemSchema = z.object({
name: z.string(),
quantity: z.number().int().min(1),
modifications: z.array(z.string()).default([]),
unitPrice: z.number().default(0),
});
type OrderItem = z.infer<typeof OrderItemSchema>;
class CollectOrderItem extends AgentTask<OrderItem> {
constructor() {
super({
instructions: `Collect one menu item from the customer.
You need to get:
1. The item name (must be from our menu)
2. The quantity
3. Any modifications (e.g., no onions, extra cheese)
Ask for each piece of information conversationally.
Confirm the complete item before submitting.
Keep responses brief — one to two sentences.`,
outputType: OrderItemSchema,
});
}
@functionTool({ description: "Check available menu items and prices for a category." })
async checkMenu(context: RunContext, category: string): Promise<string> {
const menu: Record<string, [string, number][]> = {
appetizers: [["Bruschetta", 12.0], ["Caesar Salad", 10.0]],
mains: [["Grilled Salmon", 28.0], ["Ribeye Steak", 35.0], ["Pasta Primavera", 18.0]],
desserts: [["Tiramisu", 10.0], ["Cheesecake", 9.0]],
};
const items = menu[category];
if (!items) return `No category '${category}' found. Available: appetizers, mains, desserts.`;
return items.map(([name, price]) => `${name}: $${price.toFixed(2)}`).join("\n");
}
@functionTool({ description: "Confirm and submit the order item after the customer agrees." })
async confirmItem(
context: RunContext,
name: string,
quantity: number,
unitPrice: number,
modifications: string[] = []
): Promise<void> {
this.complete({ name, quantity, modifications, unitPrice });
}
}The task starts its conversational loop
When the parent agent runs the task, the LLM switches to the task's instructions. It asks the customer what they would like to order.
The LLM collects information
Through natural conversation, the LLM gathers the item name, quantity, and any modifications. It can use the check_menu tool to verify items and look up prices.
The LLM confirms and submits
Once the LLM has all the information and the customer confirms, it calls confirm_item with the structured data. Inside that tool, self.complete() ends the task and returns the OrderItem to the parent.
Tasks have their own conversation context
While a task is running, it has its own instructions and tools. The parent agent's tools are not available — the customer can only interact with what the task provides. This keeps the conversation focused. When the task completes, the parent agent resumes with its own tools and instructions.
Running a task from the parent agent
The parent agent creates a task instance and runs it. The result is the typed object the task collected.
from livekit.agents import Agent, function_tool, RunContext
from collect_order_item import CollectOrderItem
class OrderTakerAgent(Agent):
def __init__(self):
super().__init__(
instructions="""You are the order taker at Bella Vista.
Help customers build their order one item at a time.
After each item, ask if they want to add anything else.""",
)
self.order_items = []
async def on_enter(self):
self.session.generate_reply(
instructions="Welcome the guest and ask what they would like to order."
)
@function_tool
async def add_item(self, context: RunContext) -> str:
"""Start collecting a new order item from the customer."""
task = CollectOrderItem()
result = await task.run(self.session)
self.order_items.append(result)
total = sum(item.unit_price * item.quantity for item in self.order_items)
return (
f"Added: {result.quantity}x {result.name}"
f"{' (' + ', '.join(result.modifications) + ')' if result.modifications else ''}"
f". Running total: ${total:.2f}"
)import { Agent, functionTool, RunContext } from "@livekit/agents";
import { CollectOrderItem, OrderItem } from "./collectOrderItem";
class OrderTakerAgent extends Agent {
private orderItems: OrderItem[] = [];
constructor() {
super({
instructions: `You are the order taker at Bella Vista.
Help customers build their order one item at a time.
After each item, ask if they want to add anything else.`,
});
}
async onEnter() {
this.session.generateReply({
instructions: "Welcome the guest and ask what they would like to order.",
});
}
@functionTool({ description: "Start collecting a new order item from the customer." })
async addItem(context: RunContext): Promise<string> {
const task = new CollectOrderItem();
const result = await task.run(this.session);
this.orderItems.push(result);
const total = this.orderItems.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
const mods = result.modifications.length > 0
? ` (${result.modifications.join(", ")})`
: "";
return `Added: ${result.quantity}x ${result.name}${mods}. Running total: $${total.toFixed(2)}`;
}
}The flow is straightforward: the LLM decides to call add_item, which creates and runs a CollectOrderItem task. The task takes over the conversation, collects the item details through natural dialogue, and calls self.complete() with a validated OrderItem. The await task.run(self.session) call blocks until the task completes, then returns the typed result. The parent agent appends it to the order list and reports back to the LLM.
Handling task cancellation
Sometimes the customer changes their mind mid-task. They might say "Actually, never mind" while the task is collecting an item. You can handle this gracefully.
class CollectOrderItem(AgentTask[OrderItem]):
def __init__(self):
super().__init__(
instructions="""Collect one menu item from the customer.
If the customer says they changed their mind or want to cancel,
use the cancel_item tool.""",
output_type=OrderItem,
)
@function_tool
async def cancel_item(self, context: RunContext) -> None:
"""Cancel this item if the customer changed their mind."""
self.complete(None)class CollectOrderItem extends AgentTask<OrderItem> {
constructor() {
super({
instructions: `Collect one menu item from the customer.
If the customer says they changed their mind or want to cancel,
use the cancel_item tool.`,
outputType: OrderItemSchema,
});
}
@functionTool({ description: "Cancel this item if the customer changed their mind." })
async cancelItem(context: RunContext): Promise<void> {
this.complete(null);
}
}Check for None results
When a task completes with None, the parent agent must handle that case. Check the result before using it: if result is not None: self.order_items.append(result).
What you learned
AgentTask[T]runs a focused conversational loop that collects typed data- The output type is defined with Pydantic (Python) or Zod (TypeScript) for validation
self.complete(result)ends the task and returns the result to the parent- Tasks have their own instructions and tools, separate from the parent agent
task.run(session)blocks until the task completes, returning the typed result- Tasks can be cancelled by calling
self.complete(None)
Test your knowledge
Question 1 of 3
What does the generic type parameter in AgentTask[OrderItem] enforce?
Next up
One order item is useful, but a real order has multiple items and a payment step. In the next chapter, you will use TaskGroup to orchestrate multi-step flows — collecting multiple items, handling regressions, and summarizing context between steps.