blue-school-51670
02/16/2026, 1:20 PMclass Assistant(Agent):
def __init__(self, instructions: str) -> None:
super().__init__(
instructions=instructions,
tools=[
end_call,
book_appointment_tool,
# cancel_appointment_tool,
],
)
async def on_enter(self) -> None:
<http://logger.info|logger.info>("On enter called")
welcom_inst = get_prompt(
TRIAGE_AGENT_WELCOME_PROMPT,
variables={"current_datetime": datetime.now().astimezone(pytz.timezone("Asia/Kolkata")).strftime("%Y-%m-%d %A %H:%M %p")},
)
# <http://logger.info|logger.info>("Welcome instructions: %s", welcom_inst)
await self.session.generate_reply(
instructions=welcom_inst,
allow_interruptions=True,
)
async def entrypoint(ctx: agents.JobContext):
metadata_dict: Dict[str, Any] = json.loads(ctx.job.metadata) if ctx.job.metadata else {}
<http://logger.info|logger.info>("Metadata: %s", metadata_dict)
call_type = metadata_dict.get("call_type", "web_call") # web_call or phone_call
await ctx.connect()
<http://logger.info|logger.info>("Connected to the room")
if call_type == "phone_call":
<http://logger.info|logger.info>("Phone call detected")
<http://logger.info|logger.info>("Waiting for participant to connect...")
participant = await ctx.wait_for_participant()
<http://logger.info|logger.info>("Participant attributes: %s", participant.attributes)
sip_number = participant.attributes.get("sip.phoneNumber")[3:]
<http://logger.info|logger.info>("SIP number: %s", sip_number)
else:
<http://logger.info|logger.info>("Web call detected")
sip_number = "1111111111"
# Initialize S3 storage for session data
s3_storage = S3SessionStorage()
# Use room name as session ID for consistent folder naming
session_id = ctx.room.name
session = AgentSession(
# Saarika STT - Converts speech to text
stt=sarvam.STT(
language="unknown",
model="saaras:v3",
mode="codemix"
), # Auto-detects language,
llm=openai.LLM(model="gpt-4.1"),
# Bulbul TTS - Converts text to speech
tts=sarvam.TTS(
target_language_code="en-IN",
model="bulbul:v3",
speaker="simran",
pace=1.15
),
vad = silero.VAD.load(),
turn_detection="vad",
user_away_timeout=10.0,
)
current_datetime_str = (
datetime.now()
.astimezone(pytz.timezone("Asia/Kolkata"))
.strftime("%Y-%m-%d %A %H:%M %p")
)
if call_type == "web_call":
<http://logger.info|logger.info>("Waiting for participant to connect...")
participant = await ctx.wait_for_participant()
<http://logger.info|logger.info>("Participant attributes: %s", participant.attributes)
await session.start(
room=ctx.room,
agent=Assistant(
instructions=get_prompt(
TRIAGE_AGENT_PROMPT_NAME,
variables={"current_datetime": current_datetime_str, "sip_number": sip_number}
),
),
)tall-airline-16756
02/16/2026, 1:20 PMtall-airline-16756
02/16/2026, 1:20 PMAssistant agent (so it “gets passed into” or executed by the agent).
• How to let the agent’s prompt/LLM trigger the task by exposing it as a function_tool.
Short summary up front
• You don’t pass a task as a tool. Instead you instantiate and await the task inside an agent method (for example on_enter) or wrap task creation in a @function_tool() that the LLM can call.
• Tasks take temporary control of the session and return only when they call complete(). Await the task to get the typed result (e.g., BehavioralResults).
• To let the LLM invoke the task from the conversation, wrap task startup in a function_tool, include that tool in the agent’s tool list, and provide a clear description so the LLM knows when to call it.
Concrete changes to your code
1) Import/define the task and results
• Ensure BehavioralTask and BehavioralResults are defined in your codebase (or imported from your tasks module). Example from docs:
@dataclass
class BehavioralResults:
strengths: str
weaknesses: str
work_style: str
class BehavioralTask(AgentTask[BehavioralResults]):
def __init__(self, chat_ctx=None) -> None:
super().__init__("Collect strengths, weaknesses, and work style in any order.")
self._results = {}
self.chat_ctx = chat_ctx
@function_tool()
async def record_strengths(self, strengths_summary: str):
self._results["strengths"] = strengths_summary
self._check_completion()
@function_tool()
async def record_weaknesses(self, weaknesses_summary: str):
self._results["weaknesses"] = weaknesses_summary
self._check_completion()
@function_tool()
async def record_work_style(self, work_style: str):
self._results["work_style"] = work_style
self._check_completion()
def _check_completion(self):
required_keys = {"strengths", "weaknesses", "work_style"}
if self._results.keys() == required_keys:
self.complete(BehavioralResults(**self._results))
2) Run the task directly inside your agent (simple, explicit)
• In your Assistant.on_enter() (or another agent method) instantiate and await the task. The session will be controlled by the task until it completes.
Example (add inside on_enter after greeting):
# after session.generate_reply(welcome...)
try:
task = BehavioralTask(chat_ctx=self.chat_ctx)
result: BehavioralResults = await task # awaiting the task runs it until complete()
<http://logger.info|logger.info>("Behavioral task completed: %s", result)
await self.session.generate_reply(f"Thanks — I've recorded your responses.")
except Exception as e:
logger.error("Behavioral task error: %s", e)
await self.session.generate_reply("I couldn't complete the behavioral questions.")
Notes:
• Passing chat_ctx=self.chat_ctx keeps the task in the same conversational context.
• Awaiting the task gives you the typed BehavioralResults once complete() is called.
3) Let the LLM trigger the task via a function_tool (recommended if you want the agent prompt to cause the task)
• Create a function_tool wrapper that instantiates and awaits the task. Put that tool in the agent’s tools list so the LLM can call it when appropriate.
Example wrapper:
@function_tool()
async def start_behavioral_interview(self) -> str:
"""Begin a behavioral interview to collect strengths, weaknesses, and work style."""
try:
task = BehavioralTask(chat_ctx=self.chat_ctx)
result: BehavioralResults = await task
# Save or attach result to session state if you want
self.session.userdata["behavioral_results"] = result
return f"Behavioral profile collected."
except Exception as e:
return f"Failed to collect behavioral info: {e}"
Then include it in `Assistant`:
class Assistant(Agent):
def __init__(self, instructions: str) -> None:
super().__init__(
instructions=instructions,
tools=[
end_call,
book_appointment_tool,
self.start_behavioral_interview, # include the tool reference
],
)
How the LLM will decide to invoke it
• The LLM decides to call a tool when the tool’s description fits the user intent. Make the tool docstring clear about when to call it (what it does and what it returns).
• When called, the function runs, the task takes control of the session, and the tool returns the result string when done.
Behavior and lifecycle reminders
• Tasks temporarily take control of the session and only return when they call complete().
• Only one task runs at a time.
• Awaiting the task inside a method pauses that method until the task completes.
Where to add this in your existing entrypoint
• You already create session = AgentSession(...) and pass Assistant(...) into await session.start(...).
• Add the task invocation either:
◦ Inside Assistant.on_enter() (runs each session on entry), or
◦ Expose the wrapper tool and let the LLM invoke it when the conversation indicates it should run.
If you want, I can:
• Show a combined, runnable file that integrates your provided entrypoint and the task wrapper.
• Create the exact wrapper function signature fitting your Agent framework (if you share how function_tool() is decorated in your codebase).
• Suggest prompt text for the agent instructions so the LLM is more likely to call the tool at the right moment.
Which would you prefer next: a full example file integrating your entrypoint with BehavioralTask, or a minimal wrapper tailored to your current Assistant class?
Sources: Tasks and task groups | LiveKit Documentation | Workflows | LiveKit Documentationblue-school-51670
02/16/2026, 1:25 PMtall-airline-16756
02/16/2026, 1:25 PMon_enter or wrapping task startup in a @function_tool() and letting the LLM call it are the two most straightforward and common patterns.
• Other places you can start/await a task:
◦ Inside other agent lifecycle methods (e.g., any handler you implement on the agent) — wherever you have access to self.chat_ctx / self.session and can await the task.
◦ From custom functions or helpers called by your agent code (for example, a conversational state handler, an event callback, or a turn-processing function).
◦ From your external code that controls the session (the entrypoint) — you can invoke a task before or after starting the agent session if you pass the correct chat/session context.
• Important behavior to remember:
◦ Tasks take temporary control of the session and return only when complete() is called, so start them where pausing the agent/session is acceptable.
◦ Only one task runs at a time in a session.
◦ If you want the LLM to decide to start a task during conversation, expose task-starting code as a tool with a clear description so the LLM will call it.
If you tell me where in your flow you want the task to run (automatically at session start, only when the user asks for it, or triggered by some external event), I can recommend the best placement and show the exact code change.blue-school-51670
02/16/2026, 1:29 PMtall-airline-16756
02/16/2026, 1:29 PMcomplete(), the main agent resumes. At that point the task’s conversation history is added into the main agent’s context and the agent also receives the task’s final return value (e.g., the BehavioralResults dataclass). So the agent has full knowledge of what was discussed, not only the final summarized fields.
Why this design is useful
• It avoids duplicate questioning: the agent won’t interrupt or re-ask things the task already covered.
• It keeps the task’s logic and prompts scoped to the task while still allowing the main agent to act on the detailed results and the full conversation afterward.
If you want to control visibility or behavior further
• You can capture or persist the task result (or parts of the conversation) into session state before the agent resumes.
• If you only want the agent to see a summarized result rather than the full conversation, you can have the task return a summary or store only selected fields in session.userdata before completing.
Would you like an example showing how the task conversation history is merged into the agent context after completion, or a snippet that persists just a summary instead of the full conversation?
Sources: Tasks and task groups | LiveKit Documentationblue-school-51670
02/16/2026, 1:31 PMtall-airline-16756
02/16/2026, 1:32 PMblue-school-51670
02/16/2026, 1:35 PMtall-airline-16756
02/16/2026, 1:36 PMfrom dataclasses import dataclass
from livekit.agents import AgentTask, function_tool
@dataclass
class BehavioralResult:
strengths: str
weaknesses: str
work_style: str
class BehavioralTask(AgentTask[BehavioralResult]):
def __init__(self, chat_ctx=None):
super().__init__("Collect strengths, weaknesses, and work style.")
self._results = {}
self.chat_ctx = chat_ctx
async def on_enter(self):
await self.session.generate_reply(instructions="Hi — I'll ask a few quick questions about strengths, weaknesses, and work style.")
@function_tool()
async def record_strengths(self, strengths: str):
self._results["strengths"] = strengths
await self._maybe_prompt_or_finish()
@function_tool()
async def record_weaknesses(self, weaknesses: str):
self._results["weaknesses"] = weaknesses
await self._maybe_prompt_or_finish()
@function_tool()
async def record_work_style(self, work_style: str):
self._results["work_style"] = work_style
await self._maybe_prompt_or_finish()
async def _maybe_prompt_or_finish(self):
required = {"strengths", "weaknesses", "work_style"}
missing = required - set(self._results.keys())
if not missing:
# All collected — return typed result
self.complete(BehavioralResult(**self._results))
else:
# Prompt for next missing item (simple round-robin)
next_key = next(iter(missing))
prompts = {
"strengths": "Could you tell me your top strengths?",
"weaknesses": "What are areas you want to improve?",
"work_style": "How do you prefer to work — alone or in teams?"
}
await self.session.generate_reply(instructions=prompts[next_key])
2) Wrap task start in a function_tool that persists only a summary/result
• This is the key piece: the function_tool runs the task, awaits its completion, stores only the structured result or a short summary in session.userdata (or another session store), and returns a short message. The main agent will not receive the full task conversation—only what you persist.
from livekit.agents import function_tool
class Assistant(Agent):
def __init__(self, instructions: str) -> None:
super().__init__(instructions=instructions, tools=[end_call, book_appointment_tool])
# register the tool method so LLM can call it
# depending on your framework, you may need to decorate and attach differently
@function_tool()
async def start_behavioral_task(self) -> str:
"""
Start a behavioral interview. Stores a short summary and structured result in session.userdata.
Returns a short status string for the agent to include in the conversation.
"""
try:
task = BehavioralTask(chat_ctx=self.chat_ctx)
result: BehavioralResult = await task # await the task — it runs until complete()
# Persist only the structured fields and a 1-2 line summary
summary = f"Strengths: {result.strengths}; Weaknesses: {result.weaknesses}; Work style: {result.work_style}"
# Save into session state (so main agent can access later)
self.session.userdata["behavioral_result"] = {
"strengths": result.strengths,
"weaknesses": result.weaknesses,
"work_style": result.work_style,
"summary": summary,
}
return "Behavioral interview completed and summarized."
except Exception as e:
return f"Behavioral interview failed: {e}"
3) How the main agent reads the persisted summary after the task completes
• The main agent will resume after the tool returns. Because you persisted only the structured fields and a short summary, the main agent sees only that data (not the task’s raw conversation).
Example usage in your agent flow (e.g., in on_enter or wherever appropriate):
async def on_enter(self) -> None:
# Welcome message, etc.
await self.session.generate_reply(instructions="Welcome...")
# Option A: start the behavioral task immediately in code:
# result_message = await self.start_behavioral_task()
# await self.session.generate_reply(instructions=result_message)
# Option B: expose start_behavioral_task as a tool and let the LLM call it.
# If LLM calls it, the tool will run the task and persist the summary.
# Later, when you want to use the summary:
br = self.session.userdata.get("behavioral_result")
if br:
await self.session.generate_reply(
instructions=f"Got the candidate profile. Summary: {br['summary']}"
)
Notes and important behavior
• While the BehavioralTask runs, the task’s LLM handles the conversation; the main agent’s LLM is paused and does not receive intermediate messages.
• Because the wrapper persists only selected fields and a short summary into session.userdata, the main agent will not see the full task conversation. It will have (a) the structured result fields, and (b) a short natural-language summary if you saved one.
• If you need no summary at all, store only the structured fields and avoid creating the natural-language summary string.
• If you want an automated short summary (rather than composing it yourself), you can call a summarization model inside the task or after obtaining the result and store only that summary.
Would you like this put into a single combined file using your original entrypoint/session code so you can drop it in directly?
Sources: Tasks and task groups | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKitblue-school-51670
02/16/2026, 2:12 PMtall-airline-16756
02/16/2026, 2:13 PMsession.userdata. I’ll show:
• A short prompt template for the main agent’s instructions.
• How to reference the persisted summary and structured fields.
• A couple of small variants depending on desired agent behavior (summarize next steps vs. propose actions).
Use whichever fits your flow; keep the main prompt short and explicitly tell the agent how to use behavioral_result data.
Primary prompt template (recommended)
• Purpose: Agent resumes after behavioral interview, has access to session.userdata["behavioral_result"] which contains strengths, weaknesses, work_style, and summary.
• Behavior: Acknowledge the summary, act on the content, and offer next steps.
Prompt text: "You are an interviewer assistant. The system has already collected a behavioral profile and stored it as structured fields in session state: strengths, weaknesses, work_style, and a one-line summary available as behavioral_result['summary']. When you reply:
1. Acknowledge you received the profile.
2. Present a concise recap using the summary (do not repeat full internal dialogue).
3. Suggest the next recommended action (e.g., schedule next interview, propose role fit, ask a follow-up question).
4. Keep tone professional and helpful; if uncertain, ask one clarifying question."
How to use it in code (pseudo-usage)
• After task completes and you persist result:
◦ br = self.session.userdata.get("behavioral_result")
◦ Inject br['summary'] (and fields if needed) into the prompt variables before calling the LLM.
Example reply-generation instruction (concrete) "Thanks — I have the candidate profile: {summary}. Based on this, give a 2–3 sentence assessment of role fit and propose one concrete next step (schedule technical interview, request portfolio, or send follow-up questionnaire). If you need clarification, ask one short question."
Variants
• Concise assessment + next step (use when agent drives workflow):
"You have the summary: {summary}. Provide a one-sentence assessment of fit, then recommend the single best next action."
• Human-friendly recap + ask permission to proceed (use when agent should confirm with caller):
"Recap the profile in one sentence from {summary}, then ask the caller: 'Would you like me to schedule the next step?'"
Tips
• Keep the prompt explicit about where data lives (session.userdata["behavioral_result"]) so developers know what to inject.
• Avoid instructing the LLM to re-process the task conversation — it does not have the raw chat.
• If you want the agent to reference specific fields, pass them as variables:
◦ strengths: {strengths}, weaknesses: {weaknesses}, work_style: {work_style}.
• Limit final output length (e.g., "limit to 2–3 sentences") to keep responses focused.
If you want, I can:
• Produce a ready-to-drop prompt string with placeholders filled from your session data.
• Show how to programmatically assemble the prompt inside your on_enter using your exact entrypoint code. Which would you prefer?blue-school-51670
02/16/2026, 2:21 PMtall-airline-16756
02/16/2026, 2:22 PMtask.chat_ctx = ..., but chat_ctx is a read-only property on the Task class (no setter). You must not assign to chat_ctx after instantiation.
How to fix it (two safe patterns)
• Pattern A — pass context into the task constructor and store it on your own instance field (recommended).
• Pattern B — avoid assigning chat_ctx and instead use self.session or self.chat_ctx provided by the runtime once the task runs.
Minimal corrected examples
Pattern A — pass a context object explicitly and store it on your own attribute
class BehavioralTask(AgentTask[BehavioralResult]):
def __init__(self, caller_ctx=None):
super().__init__("Collect strengths, weaknesses, and work style.")
self._results = {}
# store provided context on a custom attribute; do NOT assign to self.chat_ctx
self.caller_ctx = caller_ctx
async def on_enter(self):
# Use self.session to send messages inside the task.
await self.session.generate_reply(instructions="Hi — I'll ask a few quick questions...")
When you create the task, pass your context object (if needed):
# inside your tool or agent method
task = BehavioralTask(caller_ctx=self.chat_ctx) # safe: stores on caller_ctx
result = await task
Pattern B — rely on the task runtime's self.chat_ctx or self.session (no manual assignment)
• Do not try to set self.chat_ctx. The task receives session/chat context when it's executed by the framework; inside the task use self.chat_ctx or self.session for runtime operations without assigning to them.
class BehavioralTask(AgentTask[BehavioralResult]):
async def on_enter(self):
# self.chat_ctx and self.session are available to use (read-only)
await self.session.generate_reply(instructions="Starting behavioral questions...")
Why you saw the error in your earlier code
• You initialized the task with BehavioralTask(chat_ctx=self.chat_ctx) and your task's init tried to assign self.chat_ctx = chat_ctx. Because chat_ctx is defined as a property without a setter on the base class, assignment raises that error. The fix is to use a different attribute name (e.g., caller_ctx) or to avoid assigning to chat_ctx entirely.
Recommended change for your earlier code
• Change your task init signature to accept caller_ctx (or caller_chat_ctx) and store it on that attribute, or remove the parameter and use self.session inside the task.
Example adapted to your wrapper tool (minimal)
# wrapper tool inside your Assistant
@function_tool()
async def start_behavioral_task(self) -> str:
# pass context in a non-conflicting attribute name
task = BehavioralTask(caller_ctx=self.chat_ctx)
result: BehavioralResult = await task
# persist only the summary/result
summary = f"Strengths: {result.strengths}; Weaknesses: {result.weaknesses}; Work style: {result.work_style}"
self.session.userdata["behavioral_result"] = {
"strengths": result.strengths,
"weaknesses": result.weaknesses,
"work_style": result.work_style,
"summary": summary,
}
return "Behavioral interview completed and summarized."
Quick checklist to avoid similar errors
• Never assign to self.chat_ctx or other read-only framework properties.
• Use distinct names like caller_ctx, caller_chat_ctx, or source_ctx for any context you want to pass in.
• Prefer using self.session and self.chat_ctx inside task methods for runtime I/O; they are available during task execution.
If you’d like, I can:
• Update your BehavioralTask code to use caller_ctx and return the summarized result, and then show the exact place to call it from your existing entrypoint and Assistant class.
• Or produce a single combined file that replaces chat_ctx assignments and runs end-to-end in your current setup.
Which would you prefer?blue-school-51670
02/16/2026, 2:33 PMtall-airline-16756
02/16/2026, 2:34 PMcomplete(). You cannot synchronously start and await another task from inside that active task without breaking the lifecycle or session ownership.
• Task tools are scoped to the task that defines them, so they aren’t direct entry points for launching a different task in the same synchronous call.
Recommended patterns to chain tasks
1) TaskGroup (recommended for ordered flows)
• Use TaskGroup to register multiple tasks and await the group. TaskGroup runs tasks in sequence (or however you configure it) and returns typed results for each task.
• This keeps session control and context merging correct.
Example idea:
• Create TaskGroup, add BehavioralTask then another task (e.g., ScreeningTask), await TaskGroup, then read results.task_results["behavioral"] and results.task_results["screening"].
2) Agent-level orchestration (explicit sequential awaits)
• Have the Agent (or a parent controller) await TaskA, then based on its result await TaskB.
• This gives you conditional, programmatic control over which task runs next.
Example idea:
• result_a = await BehavioralTask(...)
• if result_a indicates follow-up needed: result_b = await ScreeningTask(...)
3) Trigger next task via tool + return to agent (hand-off pattern)
• A task can finish and a tool (or the agent) can then start the next task. For example, the inner task calls complete() with its result; the agent or a wrapper tool then starts the next task.
• Use this if you need the LLM or user to confirm before proceeding.
4) Shared state / multi-step collection inside one task
• If the goal is stepwise collection, you can keep everything inside one task using multiple @function_tool() handlers and session.userdata to accumulate data, then complete() once ready.
• Useful when steps are tightly coupled and you don’t need separate task lifecycles.
Practical constraints and tips
• Tools defined inside a task are scoped to that task. They don’t automatically let you start a new task; they’re for the task’s own LLM to use.
• Always avoid trying to assign framework read-only properties (e.g., self.chat_ctx) — use different attribute names or rely on self.session inside tasks.
• If you need to limit what the parent agent sees after a task, use TaskGroup defaults (summarize_chat_ctx=True) or persist only the summary/result into session.userdata.
• For complex branching, prefer agent-level orchestration or TaskGroup with conditional logic—this keeps session ownership and merging predictable.
If you want, I can:
• Show a short code example using TaskGroup to chain BehavioralTask → FollowupTask, or
• Show agent-level code that awaits TaskA then conditionally starts TaskB, including how to persist only summarized results.
Which example would be most helpful: TaskGroup-based chaining or agent-level sequential awaits?
Sources: Tasks and task groups | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKitblue-school-51670
02/16/2026, 2:39 PMtall-airline-16756
02/16/2026, 2:39 PMcomplete(result) (or you explicitly terminate it).
Common scenarios and what to do
1) User switches topic mid-task
• Default behavior: the task will attempt to steer the conversation back to required questions (if its instructions say to).
• How to strengthen this:
◦ Add clear task instructions like “Ignore unrelated requests and re‑ask the missing question” so the task LLM prioritizes completion.
◦ Validate inputs in your @function_tool() handlers and re-prompt if the response is off-topic.
◦ If the new topic truly needs attention, have a design that allows the task to yield (see “Controlled handoff” below).
2) User gives no answer or ambiguous answer
• Default behavior: task can re-prompt for clarification (you control re-prompt logic).
• How to handle timeouts or repeated ambiguity:
◦ Implement an inactivity timeout at your application level (e.g., track last user utterance time). On timeout you can:
▪︎ Re-prompt once or twice, or
▪︎ Call self.complete() with a partial result or error code, or
▪︎ Return control to the main agent with an explicit “incomplete” status so it can decide next steps.
◦ In `@function_tool()`s, validate the input and re-ask if invalid (avoid silently accepting bad values).
3) User insists on changing flow (wants different task or action)
• Preferred approach: let the task ask a single clarifying question like “Do you want to stop this questionnaire and do X instead?” If user confirms, complete with an appropriate result (e.g., cancelled) and let the main agent decide next steps.
• Keep the user in control: provide an explicit escape phrase (e.g., “change topic” or “stop and schedule”) that the task recognizes and uses to complete() early with a special flag.
Patterns you can implement
• Clear instructions and focused prompts
◦ Tell the task’s LLM: “If the user goes off-topic, politely re-direct to this task. Do not perform unrelated actions unless the user explicitly asks to stop.”
• Input validation in tools
◦ Each @function_tool() should validate values and re-prompt for missing/invalid inputs rather than accepting garbage.
• Timeouts & graceful fallback
◦ Use an async timer or a background watcher for inactivity.
◦ On expiry: re-prompt once, then either complete with partial data or return a “timed_out”/“incomplete” result so the agent can handle it.
• Controlled handoff / confirmation
◦ Offer an explicit confirmation question when the user deviates: “I can pause this survey. Would you like me to stop and handle that request?”
◦ If user confirms pause, call complete() with a status and let the main agent run the new flow.
• Persist progress for recovery
◦ Save intermediate progress in session.userdata (or DB). If the user switches away and returns, you can resume where you left off or continue from the main agent.
• Use TaskGroup or agent orchestration for complex flows
◦ For multi-step or nested processes, orchestrate tasks from the agent (or TaskGroup) so tasks finish cleanly and the agent can route the user to another task or action when needed.
Example decision flow (conceptual)
1. Task asks required question.
2. User gives unrelated input:
◦ Task re-directs: “I’m collecting X; please answer this. If you want a different service, say ‘change’.”
1. If user says “change”:
◦ Task completes with result {"status": "cancelled_by_user"}.
◦ Main agent sees this and starts the other requested flow.
1. If user doesn’t respond in N seconds:
◦ Task re-prompts once.
◦ On second timeout, complete with {"status":"timed_out","partial": {...}} and let agent decide.
Practical tips (short)
• Explicit escape phrases make handoffs predictable.
• Persist partial results so users don’t need to repeat info.
• Keep prompts short and confirm when you’re about to switch flows.
• Test edge cases (silence, off-topic, repeated invalid answers) and instrument logging to tune thresholds.
Would you like:
• A small code snippet that implements a timeout + escape-phrase handling inside BehavioralTask, or
• A TaskGroup/agent-level example that shows how to resume or switch flows cleanly?
Pick one and I’ll provide the concrete snippet you can drop into your current code.
Sources: Collect recording consent with tasks | LiveKit Documentation | Tasks and task groups | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKit