Chapter 425m

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.

AgentTask[T]Typed resultscomplete()

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.

models.pypython
from pydantic import BaseModel


class OrderItem(BaseModel):
  name: str
  quantity: int
  modifications: list[str] = []
  unit_price: float = 0.0
models.tstypescript
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>;
What's happening

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.

collect_order_item.pypython
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,
          )
      )
collectOrderItem.tstypescript
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 });
}
}
1

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.

2

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.

3

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.

order_taker_agent.pypython
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}"
      )
orderTakerAgent.tstypescript
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)}`;
}
}
What's happening

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.

collect_order_item.pypython
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)
collectOrderItem.tstypescript
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.

Concepts covered
AgentTask[T]Typed resultscomplete()