polite-dog-12152
05/05/2026, 6:34 AMtall-airline-16756
05/05/2026, 6:35 AMtall-airline-16756
05/05/2026, 6:35 AMwait_until_answered=True, a successful create_sip_participant response means the call was answered; if the API call errors or times out, the call was not answered.
• If you need non-blocking tracking, use SIP participant status/events or handle the response object's status field to determine answer state.
What to do in your code
1. Keep wait_until_answered=True if you want the call creation to only succeed when answered. Wrap the call in try/except and treat exceptions/timeouts as "not answered".
2. Inspect the SIP response object for status/state fields (the SDK returns status info on the created SIP participant). Log or return that field instead of printing sip.s.
3. For async/non-blocking flows, set wait_until_answered=False and:
◦ Subscribe to SIP/participant events or poll the participant state to detect when it transitions to answered/connected.
◦ Or add logic to detect silence/voicemail (e.g., simple speech detection timeout) if you need to distinguish voicemail vs answered.
Example (minimal change to your function)
1. Use try/except to capture a failed/timeout create:
try:
sip = await lk.sip.create_sip_participant(..., wait_until_answered=True, ...)
# success → answered
except Exception as e:
# failure/timeout → not answered
1. Or for non-blocking:
sip = await lk.sip.create_sip_participant(..., wait_until_answered=False, ...)
# then check sip.status or listen for participant state events to see when/if it becomes 'answered' or 'connected'
Questions to narrow this down
• Do you want the call flow to block until answered (current wait_until_answered=True) or remain async and notify later?
• Do you have access to participant state/events in your runtime (so I can show a sample using event callbacks or polling)?
Tell me which approach you want and I’ll provide a concrete code sample.
Sources: Preserving Caller ID During SIP Transfers in LiveKit Agents | Accepting calls overview | LiveKit Documentation | Making calls overview | LiveKit Documentation, with interactive elementspolite-dog-12152
05/06/2026, 1:45 PMtall-airline-16756
05/06/2026, 1:45 PMllm_node. I can show a concise pattern and a short code example, but first a quick clarification so I give the right sample:
• Which language/runtime are you using (Python or TS)?
• Does your semantic cache expose async methods like lookup(query) -> Optional[str] and store(query, response)? If not, what are the method names/signatures?
If you're using Python and your cache has async `lookup`/`store`, here's the minimal pattern to override llm_node and use the cache:
async def llm_node(self, chat_ctx: llm.ChatContext, tools: list, model_settings) -> AsyncIterable[llm.ChatChunk]:
# 1) Build a cache key from the incoming context (message text, metadata, etc.)
cache_key = self.semantic_cache.build_key(chat_ctx)
# 2) Try cache hit
cached = await self.semantic_cache.lookup(cache_key)
if cached:
# Return cached text as a single ChatChunk (or stream if you prefer)
yield llm.ChatChunk(text=cached)
return
# 3) No hit — call default LLM behavior and stream results, while capturing them
collected = []
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
collected.append(chunk.text)
yield chunk
# 4) Store combined response in cache
full_response = "".join(collected)
await self.semantic_cache.store(cache_key, full_response)
What I can provide next
• A TypeScript equivalent if you use TS.
• A version that returns chunked streaming cached responses.
• Help adapting this to your actual cache method names or key schema.
Tell me runtime and your cache method signatures and I’ll adapt the snippet.
Sources: Pipeline nodes and hooks | LiveKit Documentation, with interactive elementspolite-dog-12152
05/06/2026, 1:47 PMtall-airline-16756
05/06/2026, 1:47 PMllm_node. The example assumes your cache exposes async lookup(key) and store(key, value) methods and that chat_ctx.messages[-1].content gives the user prompt text. Adapt keys and message extraction to your schema.
import asyncio
from typing import AsyncIterable
from livekit import llm # adjust import to your SDK
from livekit.agents import Agent # adjust import path
from livekit.agents import llm as llm_types # for ChatChunk type if needed
# Simple in-memory semantic cache (replace with vector DB or Redis in prod)
class SemanticCache:
def __init__(self):
self._store: dict[str, str] = {}
async def build_key(self, chat_ctx) -> str:
# Example: use last user message as key (customize with embeddings/metadata)
return chat_ctx.messages[-1].content.strip()
async def lookup(self, key: str) -> str | None:
# Real semantic cache would do similarity search; this is exact match
await asyncio.sleep(0) # simulate async I/O
return self._store.get(key)
async def store(self, key: str, value: str):
await asyncio.sleep(0) # simulate async I/O
self._store[key] = value
# Example Agent subclass with llm_node override
class CachingAgent(Agent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.semantic_cache = SemanticCache()
async def llm_node(
self,
chat_ctx: llm.ChatContext,
tools: list,
model_settings
) -> AsyncIterable[llm.ChatChunk]:
# 1) Build cache key
cache_key = await self.semantic_cache.build_key(chat_ctx)
# 2) Try cache hit
cached = await self.semantic_cache.lookup(cache_key)
if cached:
# Return cached response as a single ChatChunk
yield llm.ChatChunk(text=cached)
return
# 3) No cache hit — call default LLM and stream results while collecting
collected_chunks: list[str] = []
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
# chunk may be str or ChatChunk depending on SDK; handle both
text = chunk.text if hasattr(chunk, "text") else str(chunk)
collected_chunks.append(text)
yield chunk
# 4) Store the combined response in cache
full_response = "".join(collected_chunks)
await self.semantic_cache.store(cache_key, full_response)
If you want:
• A version that uses embeddings & similarity search for fuzzy cache hits.
• Streaming cached responses (yielding multiple small chunks).
• Integration with a real vector DB (e.g., Pinecone, Weaviate, RedisVector).
Tell me which and I’ll extend the snippet.
, with interactive elementspolite-dog-12152
05/15/2026, 6:14 AMtall-airline-16756
05/15/2026, 6:14 AMconversation_item_added) or poll the chat state.
• Use the planner to research/summarize, then inject results into the main agent via update_chat_ctx() so the main agent can use them on the next turn.
Minimal Python pattern
1. BackgroundPlanner: holds its own LLM and tools, subscribes to events, runs async tasks for research/summaries.
2. On new message: planner runs research, generates summary, then calls main_agent_session.update_chat_ctx(...) or creates a system message with findings.
3. Main agent remains fast; it sees injected context on next inference.
If you'd like, I can:
• Provide a concise Python example showing a BackgroundPlanner class that listens for conversation_item_added, calls a web-search tool, summarizes with another LLM, and injects the summary.
• Or tailor the example to specific tools/models you plan to use.
Which do you want—full example or a sketch tailored to your tools/models?
Sources: How to Build a Background Observer for Voice AI Guardrails | LiveKit | Tool definition and use | LiveKit Documentation, with interactive elementspolite-dog-12152
05/15/2026, 6:19 AMtall-airline-16756
05/15/2026, 6:19 AMimport asyncio
from livekit.agents import AgentSession, JobContext, llm, inference
from livekit.agents.events import ChatContextEvent
# --- Simple planner tools (replace with real implementations) ---
class WebSearchTool:
async def search(self, query: str) -> str:
await asyncio.sleep(0) # simulate async I/O
return f"Search results for: {query}"
class SummarizerTool:
async def summarize(self, text: str) -> str:
await asyncio.sleep(0)
return f"Summary: {text[:200]}"
# --- Background planner ---
class BackgroundPlanner:
def __init__(self, session: AgentSession, planner_llm: inference.LLM):
self.session = session
self.planner_llm = planner_llm
self.search_tool = WebSearchTool()
self.summary_tool = SummarizerTool()
# subscribe to chat events
self.session.event_queue.subscribe(ChatContextEvent)
self._task = asyncio.create_task(self._loop())
async def _loop(self):
async for event in self.session.event_queue.disposable_iter():
if not isinstance(event, ChatContextEvent) or event.added_item is None:
continue
item = event.added_item
# only react to user messages
if getattr(item, "role", "") == "user":
text = item.content
# run research/summarization in background
asyncio.create_task(self._research_and_inject(text))
async def _research_and_inject(self, user_text: str):
# 1) Research
results = await self.search_tool.search(user_text)
# 2) Summarize results using planner LLM (or summarizer tool)
# Example uses planner_llm.chat() — adapt to your SDK's API
chat_ctx = llm.ChatContext()
chat_ctx.add_message(role="system", content="You are a research assistant.")
chat_ctx.add_message(role="user", content=f"Please summarize these findings:\n\n{results}")
completion = await self.planner_llm.chat(chat_ctx=chat_ctx)
planner_summary = completion.message.content if completion and completion.message else await self.summary_tool.summarize(results)
# 3) Inject the summary into the main agent's chat context
new_ctx = self.session.chat_ctx.copy()
new_ctx.add_message(role="system", content=f"[PLANNER UPDATE]: {planner_summary}")
await self.session.update_chat_ctx(new_ctx)
# --- Example main session setup ---
async def start_session(ctx: JobContext):
session = AgentSession(
stt=inference.STT(model="some-stt"),
llm=inference.LLM(model="gpt-4o-mini"), # main agent LLM (fast)
tts=inference.TTS(model="some-tts"),
)
# planner uses a separate, stronger model
planner_llm = inference.LLM(model="gpt-4.1")
BackgroundPlanner(session=session, planner_llm=planner_llm)
await session.start(agent=YourMainAgent(), room=ctx.room)
Would you like this adapted to:
• use a real web-search tool (e.g., Google / Bing wrapper), or
• stream planner updates as multiple messages instead of a single system message? Sources: How to Build a Background Observer for Voice AI Guardrails | LiveKit | Tool definition and use | LiveKit Documentation, with interactive elementspolite-dog-12152
05/15/2026, 6:20 AMtall-airline-16756
05/15/2026, 6:20 AMpolite-dog-12152
05/15/2026, 6:20 AMtall-airline-16756
05/15/2026, 6:21 AMimport asyncio
from typing import Any
from livekit.agents import Agent, AgentSession, function_tool
from livekit.agents.llm.async_toolset import AsyncToolset, AsyncRunContext
from livekit.agents import inference, llm
# --- Async toolset ---
class ResearchToolset(AsyncToolset):
def __init__(self):
super().__init__(id="research")
@function_tool()
async def perform_research(self, ctx: AsyncRunContext, query: str) -> str:
# progress update visible in chat context while running
await ctx.update(f"Starting research for: {query}")
# simulate long-running research (replace with real web search / API calls)
await asyncio.sleep(2)
results = f"Fake search results for: {query}"
await ctx.update("Research complete, summarizing...")
# simulate summarization
await asyncio.sleep(1)
summary = f"Summary of '{query}': {results[:200]}"
# final return becomes the tool result
return summary
# --- Main agent (no changes needed for tool calling) ---
class MyAgent(Agent):
def __init__(self):
super().__init__(instructions="You are a helpful assistant.")
# Agent-level tools are cancelled on handoff; prefer session-level tools if persistence needed
# --- Start session with async tool registered on the session ---
async def start_session():
session = AgentSession(
stt=inference.STT(model="some-stt"),
llm=inference.LLM(model="gpt-4o-mini"), # main agent LLM
tts=inference.TTS(model="some-tts"),
tools=[ResearchToolset()], # register async tools on session (persist across handoffs)
)
# Start the agent session (replace with your actual session.start usage)
await session.start(agent=MyAgent(), room="room-name")
# --- How it works ---
# - The LLM (main agent) will only call `perform_research` when it decides the tool is needed
# (based on prompt/context or explicit user command).
# - The tool runs asynchronously; use `await ctx.update(...)` to send progress updates into chat.
# - You can rely on the LLM to invoke the tool via ReAct-style reasoning, or prompt it explicitly:
# User: "Research: best restaurants in Seattle"
# The agent will decide to call the async tool and return the tool result when ready.
# Run example (in real code this is driven by your server framework)
# asyncio.run(start_session())
Notes:
• Tools defined with AsyncToolset are only available in Python.
• Register the tool on AgentSession (not Agent) if you want it to persist across agent handoffs.
• The LLM decides when to call the tool; you can encourage on-demand use by prompting (e.g., "Research: ...") or by detecting an intent and letting the model call the tool.
• Use ctx.update() to push progress messages into the chat without blocking the agent.
Want a version that:
• Shows how to explicitly trigger the tool by intent detection in code, or
• Demonstrates returning intermediate streaming updates to the user? Which would help most? Sources: Async tools | LiveKit Documentation | The ReAct Pattern for Voice Agents and How AI Agents Think, Act, and Respon..., with interactive elementspolite-dog-12152
05/15/2026, 6:22 AMResearchToolset
i want seaprate llm work as sub agenttall-airline-16756
05/15/2026, 6:23 AMimport asyncio
from livekit.agents import Agent, AgentSession, function_tool, llm, inference
from livekit.agents.llm.async_toolset import AsyncToolset, AsyncRunContext
from livekit.agents.events import ChatContextEvent
# --- Async toolset used by the sub-agent ---
class ResearchToolset(AsyncToolset):
def __init__(self):
super().__init__(id="research")
@function_tool()
async def perform_research(self, ctx: AsyncRunContext, query: str) -> str:
await ctx.update(f"Planner: starting research for '{query}'")
await asyncio.sleep(2) # replace with real web search
results = f"Fake search results for: {query}"
await ctx.update("Planner: summarizing results...")
await asyncio.sleep(1)
return f"Summary of '{query}': {results[:200]}"
# --- Main agent (fast, responsive) ---
class MainAgent(Agent):
def __init__(self):
super().__init__(instructions="You are the primary assistant.")
# --- Sub-agent / Background planner that uses its own LLM and tools ---
class BackgroundPlanner:
def __init__(self, session: AgentSession, planner_llm: inference.LLM):
self.session = session
self.planner_llm = planner_llm
# Register tools on the session so they persist across handoffs
# (Session already created with tools=[ResearchToolset()], see start_session)
self.session.event_queue.subscribe(ChatContextEvent)
self._task = asyncio.create_task(self._loop())
async def _loop(self):
async for ev in self.session.event_queue.disposable_iter():
if not isinstance(ev, ChatContextEvent) or ev.added_item is None:
continue
item = ev.added_item
# Trigger policy: sub-agent acts only on explicit "PLAN:" commands
if getattr(item, "role", "") == "user" and item.content.startswith("PLAN:"):
query = item.content[len("PLAN:"):].strip()
asyncio.create_task(self._run_planner(query))
async def _run_planner(self, query: str):
# Build a small chat context for the planner
chat_ctx = llm.ChatContext()
chat_ctx.add_message(role="system", content="You are a planning assistant.")
chat_ctx.add_message(role="user", content=f"Research: {query}")
# Call planner's LLM; allow it to call async tools (ResearchToolset)
# The planner_llm should be configured to allow tool use (handled by session tools)
completion = await self.planner_llm.chat(chat_ctx=chat_ctx)
# If planner's LLM directly returns result, inject it; otherwise the async tool's
# progress/return will already be added to the session via ctx.update() from the tool.
planner_out = completion.message.content if completion and completion.message else None
if planner_out:
new_ctx = self.session.chat_ctx.copy()
new_ctx.add_message(role="system", content=f"[PLANNER RESULT]: {planner_out}")
await self.session.update_chat_ctx(new_ctx)
# --- Start session wiring: register async tool on session and start both agents ---
async def start_session():
session = AgentSession(
stt=inference.STT(model="some-stt"),
llm=inference.LLM(model="gpt-4o-mini"), # main agent LLM (fast)
tts=inference.TTS(model="some-tts"),
tools=[ResearchToolset()], # session-level async tools
)
# Planner uses a separate, more capable LLM
planner_llm = inference.LLM(model="gpt-4.1") # planner/sub-agent LLM
# Attach the background planner (sub-agent)
BackgroundPlanner(session=session, planner_llm=planner_llm)
# Start main agent
await session.start(agent=MainAgent(), room="room-name")
# Example trigger:
# - User sends: "PLAN: best electric cars 2026"
# - Planner sees "PLAN:" command, runs research tool (async), and injects summary when ready.
Notes:
• The planner uses its own LLM instance (planner_llm) separate from the main agent LLM.
• The async tool (ResearchToolset) is registered on the AgentSession so the planner can call it; it runs only when invoked by the planner (or when the LLM decides to).
• Triggering is demonstrated with a "PLAN:" user prefix to run the planner on demand; you can replace this with intent detection, a special system message, or an explicit call from the main agent.
• Progress updates from the async tool use ctx.update() and appear in the chat context while the tool runs.
• If you want the main agent to wait for the planner result synchronously, have the main agent detect the intent and either call the planner LLM directly or wait for injected results before responding.
Want this adapted to intent-based triggers (no special prefix) or to stream planner updates as separate assistant messages?
, with interactive elementspolite-dog-12152
05/15/2026, 6:25 AMtall-airline-16756
05/15/2026, 6:25 AMctx.update().
• On-demand loading patterns: enable lazy tool discovery or proxies to avoid large tool lists hurting performance.
• Resource lifecycle: override setup/aclose for connections or cleanup when a toolset is activated/deactivated.
When to use
• Group related APIs or operations you want the LLM to call via function tools.
• Provide background/long-running actions (use AsyncToolset).
• Keep tool availability flexible across sessions and agent handoffs.
If you want, I can:
• Show a minimal Toolset vs AsyncToolset example, or
• Show how to register a Toolset only when the planner/sub-agent is invoked. Which do you prefer? Sources: Toolsets | LiveKit Documentation | The ReAct Pattern for Voice Agents and How AI Agents Think, Act, and Respon..., with interactive elements