calm-greece-1021
12/26/2025, 11:11 AMtall-airline-16756
12/26/2025, 11:11 AMtall-airline-16756
12/26/2025, 11:11 AMchat_ctx) or metadata (job.metadata, room.metadata, participant attributes).
Minimal examples
• Function tool (direct return)
@function_tool()
async def do_simple_task(self, context: RunContext, input: str):
# perform quick computation or external call
return f"Result for {input}"
• Agent handoff
class ParentAgent(Agent):
@function_tool()
async def call_specialist(self, context: RunContext, data: str):
return SpecialistAgent(), "Handing off to specialist"
class SpecialistAgent(Agent):
async def on_enter(self) -> None:
# do work
result = "specialist result"
# hand back to parent with result message
return ParentAgent(), f"Returning result: {result}"
Passing data and best practices
• Pass inputs and results via chat_ctx messages or metadata fields so both agents can access conversation state.
• Keep agents stateless where possible; store persistent state in metadata.
• Use function tool for fast/synchronous needs; use handoff for complex or multi-turn processing.
• If you need, I can convert one of the examples into a fuller implementation for your language/runtime or show how to store/read results in job.metadata or room.metadata.
Which pattern should I expand into a full example for you?
Sources: Agents and handoffs | LiveKit docs | External data and RAG | LiveKit docs | Tool definition and use | LiveKit docs | Building Multi-Agent Architectures with LiveKit Agents | LiveKit Docscalm-greece-1021
12/26/2025, 11:13 AMtall-airline-16756
12/26/2025, 11:13 AM@function_tool() on the parent agent (or as a callable helper the tool invokes).
• The tool executes the logic and returns the result directly to the parent LLM flow.
• Pass any needed context via the tool arguments or `chat_ctx`/metadata.
Minimal example (Python-style)
from livekit.agents import Agent, function_tool, RunContext
class ParentAgent(Agent):
def __init__(self):
super().__init__(instructions="You are the parent agent.")
@function_tool()
async def call_specialist(self, context: RunContext, task_input: str):
# Option A: simple inline logic
result = f"Processed inline: {task_input[::-1]}" # example operation
return result
# Option B: delegate to a helper/sub-module (still a direct return)
# result = await specialist_helper.process(task_input)
# return result
# Example helper (if you want separation but still a direct return)
class SpecialistHelper:
@staticmethod
async def process(data: str) -> str:
# external API call, computation, DB lookup, etc.
return f"Specialist result for {data}"
Best practices
• Keep tools idempotent and stateless; store long-term state in metadata if needed.
• Validate inputs in the tool and return structured results (JSON/string) that the parent agent can parse.
• Use chat_ctx or metadata only when you need shared context beyond function arguments.
• For longer-running or multi-turn work, use the agent-handoff pattern instead.
Would you like this adapted to a specific runtime (TypeScript/Node, Python async framework) or an example showing structured JSON returns and parsing?calm-greece-1021
12/26/2025, 11:16 AMtall-airline-16756
12/26/2025, 11:16 AM@function_tool calling a sub‑agent instance, awaiting its work, and returning the sub‑agent’s result back to the parent.
Python example
from livekit.agents import Agent, function_tool, RunContext
class SpecialistAgent(Agent):
def __init__(self, payload: str):
super().__init__(instructions="Specialist agent")
self.payload = payload
self.result = None
async def run_task(self):
# perform specialist work (sync/async external calls allowed)
self.result = f"specialist processed: {self.payload[::-1]}"
return self.result
class ParentAgent(Agent):
@function_tool()
async def call_specialist(self, context: RunContext, task_input: str):
# instantiate sub-agent
specialist = SpecialistAgent(task_input)
# run its work directly and get result
result = await specialist.run_task()
# return result directly to parent LLM flow
return result
Notes
• This keeps the tool-call pattern (parent calls a function tool) while actually using a sub‑agent object to encapsulate logic.
• For multi‑turn conversational handoffs (sub‑agent needs conversational control), use the agent-handoff pattern instead (return the sub‑agent instance from the tool and let it hand back control when done).
• If you want TypeScript/Node or an example that integrates chat_ctx or metadata storage, tell me which and I’ll expand it. Which language/runtime should I produce next?calm-greece-1021
12/26/2025, 11:17 AMtall-airline-16756
12/26/2025, 11:17 AMfrom livekit.agents import Agent, function_tool, AgentSession, AgentInputContext
# Sub-agent that uses its own LLM to process input and produce a result
class SubAgent(Agent):
def __init__(self, prompt: str):
super().__init__(instructions="You are a specialist agent.")
self.prompt = prompt
self.result = None
async def run_and_return(self):
# start a short session for the sub-agent to call its LLM and produce output
session = AgentSession()
await session.start(agent=self, input_ctx=AgentInputContext(text_input=self.prompt))
await session.wait_for_completion()
# read last message produced by the sub-agent's LLM from chat_ctx
last_msg = self.session.chat_ctx.messages[-1] if self.session and self.session.chat_ctx.messages else None
self.result = last_msg.content[0] if last_msg and last_msg.content else None
return self.result
# Parent agent with a function-tool that instantiates and awaits the sub-agent
class ParentAgent(Agent):
@function_tool()
async def call_subagent(self, context, user_text: str):
sub = SubAgent(user_text)
# run the sub-agent and get its LLM-produced result
result = await sub.run_and_return()
# return the result directly to the parent LLM flow
return result
Key points/practices
• The sub-agent runs its own AgentSession so it can call its LLM independently.
• The parent tool awaits the sub-agent’s completion and returns the sub-agent’s final content as the tool result.
• Exchange inputs via the tool arguments; exchange outputs via the tool return value (or structured JSON).
• Use chat_ctx or metadata (job.metadata / room.metadata) if you need richer shared state across agents.
• For multi‑turn conversational control, prefer the handoff pattern (return agent instance), but for synchronous function-style results use the pattern above.
Would you like this translated to TypeScript/Node, or an example that returns structured JSON and shows parsing in the parent?
Sources: Agents and handoffs | LiveKit docs | Workflows | LiveKit docscalm-greece-1021
12/26/2025, 11:20 AMtall-airline-16756
12/26/2025, 11:21 AMwait_for_completion() — thanks for pointing that out. Here are the correct patterns to get a sub‑agent to communicate with an LLM and return results.
Two supported approaches
1) Agent handoff (tool returns an agent instance)
• Use when the sub‑agent needs conversational control or multi‑turn interaction.
• Implement the parent tool to return `SubAgent()`; LiveKit performs a handoff and the sub‑agent runs. When it finishes (via on_exit or returning control), conversation context and any shared state are available to the parent.
• Pass results via shared state (e.g., session.userdata, job.metadata, room.metadata) or by putting the result into the chat context before exit.
Minimal example (handoff)
@function_tool()
async def call_specialist(self, ctx):
return SpecialistAgent(), "Handing off to specialist"
class SpecialistAgent(Agent):
async def on_enter(self):
# call LLM, produce result, store it
await self.session.generate_reply("Processing...")
self.session.userdata["specialist_result"] = "result text"
# return control (implicitly or explicitly) to parent when done
2) AgentTask (blocking, awaitable, returns typed result)
• Use when you need a synchronous-like call that returns a value the parent can await.
• Implement AgentTask to run LLM work and call `self.complete(result)`; then result = await SubTask() in the parent.
Minimal example (AgentTask)
class SummaryTask(AgentTask[dict]):
async def on_enter(self):
# run LLM work, then complete with a result
summary = await self.llm.generate("Summarize X")
self.complete({"summary": summary})
# In parent agent
result = await SummaryTask()
# result is the dict produced by the task
Why AgentSession.wait_for_completion() is wrong
• AgentSession does not expose wait_for_completion(). Attempting to await it is unsupported.
• Use handoffs (return agent instances) for conversational control, or AgentTask for awaitable, result-returning work.
Passing results back
• For handoffs: write result to session.userdata or chat messages before sub-agent exit so the parent can read it after control returns.
• For tasks: return the result via self.complete(...) and consume it from the parent via await.
If you’d like, I can:
• Convert one of these patterns into a fuller Python or TypeScript example tailored to your runtime, or
• Show exactly how to store/read session.userdata or put a structured JSON result into chat context for the parent to consume.
Which would help you most?
Sources: Agents and handoffs | LiveKit docs | Tool definition and use | LiveKit docs | Tasks and task groups | LiveKit docs