how to minimise token usage
# ask-ai
b
how to minimise token usage
t
Thanks for reaching out, Utkarsh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m responding to your question about how to minimize token usage (in LiveKit Agents when using LLMs). Below are practical, actionable steps you can apply right away. 1) Choose smaller / efficient models • Use lighter models when full capability isn’t needed (for example, choose a “mini” or “nano” variant instead of the full-size model). • Consider cost-effective models for simple tasks to reduce both prompt and completion tokens. 2) Limit output length • Set
max_completion_tokens
(or equivalent) so the model can’t generate overly long responses. • When you need short answers, include explicit instructions like: “Respond in 1–2 sentences” to keep output concise. 3) Make prompts concise and targeted • Remove unnecessary context and keep prompts focused on the specific task. • Use templates or structured prompts so you only send variable content, not repeated boilerplate. 4) Use streaming and sentence-level chunking • Stream output and stop generation as soon as a useful sentence/stop token appears. This prevents extra text generation when you only need the first result. 5) Cache repeated inputs • Cache responses for identical or highly similar prompts so you don’t resend the same prompt tokens repeatedly. Cached inputs can significantly reduce billable prompt tokens. 6) Reduce tool-call overhead • Only expose tools the model actually needs. Minimize the size of tool inputs and the verbosity of tool outputs to avoid large embeddings or long tool responses being fed back into the model. 7) Monitor token usage and iterate • Capture LLM metrics (prompt_tokens, completion_tokens) each session and review them to find inefficiencies. Use that telemetry to refine prompts and model choices. If you want, I can: • Review a sample prompt + expected response and suggest a shorter prompt that preserves necessary context. • Recommend specific model choices or max token settings for a given use case (summarization, short Q&A, long-form generation). Which would you prefer — a tuned prompt example for a specific task, or model + token settings recommendations for your workload? Sources: Large language models (LLM) overview | LiveKit Documentation | Kimi LLM | LiveKit Documentation | OpenAI LLM models | LiveKit Documentation | Prompting guide | LiveKit Documentation | Live Conversations with AI using ChatGPT and WebRTC
b
Copy code
async def _create_agent_and_session(self):
        <http://self.logger.info|self.logger.info>("[%s] 🤖 Starting customer session...", self.room_name)

        # Get audio configuration for noisy phone environments
        audio_config = self.audio_config_service.get_noisy_environment_config()
        
        # Create Silero VAD for local speech detection (filters noise before OpenAI)
        self.silero_vad = self.audio_config_service.create_silero_vad(audio_config)
        
        # Get turn detection config for OpenAI semantic VAD
        turn_detection = self.audio_config_service.get_turn_detection_config(audio_config)

        # Create Model with semantic VAD for noise resilience
        model = self.model_config.create_model(self.agent_config, turn_detection)

        # Create agent - tools come from agent_config.tools (ToolSchema objects)
        agent = GenericAgent(
            agent_config=self.agent_config,
            customer_data=self.customer_data,
            on_transfer_requested=None  # PHASE 2: Supervisor transfer disabled
        )
        
        # Load dynamic tools (agent handles this internally)
        await agent.initialize_dynamic_tools()

        <http://self.logger.info|self.logger.info>("✅ Agent ready with %d tools", len(agent.tools))
        
        # Create Session with Silero VAD and tuned interruption settings
        self.agent_session = AgentSession(
            llm=model,
            vad=self.silero_vad,  # Silero VAD for noise filtering
            min_interruption_duration=audio_config.min_interruption_duration,
            min_interruption_words=audio_config.min_interruption_words,
        )

        # Setup Metrics Collector
        self.usage_collector = metrics.UsageCollector()

        @self.agent_session.on("metrics_collected")
        def _on_metrics_collected(ev: MetricsCollectedEvent):
            metrics.log_metrics(ev.metrics)
            self.usage_collector.collect(ev.metrics)

        # Store metadata for tools and shutdown
        self.agent_session.metadata = {
            "sip_trunk_id": self.call_context.trunk_id,
            "customer_id": self.customer_data.customer_id,
            "customer_name": self.customer_data.customer_name,
            "campaign_id": self.customer_data.campaign_id,
            "call_context": self.call_context
        }

        <http://self.logger.info|self.logger.info>(
            "[%s] ✅ Agent session created with Silero VAD (threshold=%.2f) and Semantic VAD",
            self.room_name,
            audio_config.silero.activation_threshold
        )
        return agent

def get_usage_collector(self) -> Optional[metrics.UsageCollector]:
        """Get the usage collector for metrics reporting"""
        return self.usage_collector
below are the logs: Feb 26, 2026 @ 184556.348 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.83, "input_tokens": 7814, "cached_input_tokens": 6272, "input_text_tokens": 6368, "input_cached_text_tokens": 5824, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1446, "input_cached_audio_tokens": 448, "output_tokens": 76, "output_text_tokens": 22, "output_audio_tokens": 54, "output_image_tokens": 0, "total_tokens": 7890, "tokens_per_second": 52.39, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131556.347673+00:00"} Feb 26, 2026 @ 184554.523 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": -1.0, "input_tokens": 7754, "cached_input_tokens": 7616, "input_text_tokens": 6308, "input_cached_text_tokens": 6208, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1446, "input_cached_audio_tokens": 1408, "output_tokens": 52, "output_text_tokens": 52, "output_audio_tokens": 0, "output_image_tokens": 0, "total_tokens": 7806, "tokens_per_second": 40.15, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131554.521891+00:00"} Feb 26, 2026 @ 184550.998 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.03, "input_tokens": 7551, "cached_input_tokens": 7488, "input_text_tokens": 6192, "input_cached_text_tokens": 6144, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1359, "input_cached_audio_tokens": 1344, "output_tokens": 179, "output_text_tokens": 92, "output_audio_tokens": 87, "output_image_tokens": 0, "total_tokens": 7730, "tokens_per_second": 50.9, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131550.996810+00:00"} Feb 26, 2026 @ 184541.351 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.74, "input_tokens": 7430, "cached_input_tokens": 7296, "input_text_tokens": 6156, "input_cached_text_tokens": 6080, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1274, "input_cached_audio_tokens": 1216, "output_tokens": 86, "output_text_tokens": 26, "output_audio_tokens": 60, "output_image_tokens": 0, "total_tokens": 7516, "tokens_per_second": 54.15, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131541.350560+00:00"} Feb 26, 2026 @ 184539.043 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": -1.0, "input_tokens": 7363, "cached_input_tokens": 7296, "input_text_tokens": 6089, "input_cached_text_tokens": 6080, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1274, "input_cached_audio_tokens": 1216, "output_tokens": 49, "output_text_tokens": 49, "output_audio_tokens": 0, "output_image_tokens": 0, "total_tokens": 7412, "tokens_per_second": 37.43, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131539.042601+00:00"} Feb 26, 2026 @ 184530.127 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.04, "input_tokens": 7176, "cached_input_tokens": 7104, "input_text_tokens": 6042, "input_cached_text_tokens": 6016, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1134, "input_cached_audio_tokens": 1088, "output_tokens": 159, "output_text_tokens": 37, "output_audio_tokens": 122, "output_image_tokens": 0, "total_tokens": 7335, "tokens_per_second": 63.83, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131530.127032+00:00"} Feb 26, 2026 @ 184507.950 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.86, "input_tokens": 6624, "cached_input_tokens": 6528, "input_text_tokens": 5921, "input_cached_text_tokens": 5888, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 703, "input_cached_audio_tokens": 640, "output_tokens": 527, "output_text_tokens": 111, "output_audio_tokens": 416, "output_image_tokens": 0, "total_tokens": 7151, "tokens_per_second": 97.31, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131507.949664+00:00"} Feb 26, 2026 @ 184502.329 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.87, "input_tokens": 6522, "cached_input_tokens": 768, "input_text_tokens": 5905, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 617, "input_cached_audio_tokens": 0, "output_tokens": 492, "output_text_tokens": 105, "output_audio_tokens": 387, "output_image_tokens": 0, "total_tokens": 7014, "tokens_per_second": 97.58, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131502.328733+00:00"} Feb 26, 2026 @ 184448.024 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.99, "input_tokens": 6348, "cached_input_tokens": 768, "input_text_tokens": 5856, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 492, "input_cached_audio_tokens": 0, "output_tokens": 136, "output_text_tokens": 39, "output_audio_tokens": 97, "output_image_tokens": 0, "total_tokens": 6484, "tokens_per_second": 65.66, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131448.024238+00:00"} Feb 26, 2026 @ 184435.940 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.81, "input_tokens": 6091, "cached_input_tokens": 6016, "input_text_tokens": 5793, "input_cached_text_tokens": 5760, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 298, "input_cached_audio_tokens": 256, "output_tokens": 230, "output_text_tokens": 53, "output_audio_tokens": 177, "output_image_tokens": 0, "total_tokens": 6321, "tokens_per_second": 66.1, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131435.939832+00:00"} Feb 26, 2026 @ 184425.097 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.08, "input_tokens": 5893, "cached_input_tokens": 5824, "input_text_tokens": 5738, "input_cached_text_tokens": 5696, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 155, "input_cached_audio_tokens": 128, "output_tokens": 176, "output_text_tokens": 45, "output_audio_tokens": 131, "output_image_tokens": 0, "total_tokens": 6069, "tokens_per_second": 71.67, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131425.097267+00:00"} Feb 26, 2026 @ 184414.650 {"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.61, "input_tokens": 5679, "cached_input_tokens": 768, "input_text_tokens": 5679, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 0, "input_cached_audio_tokens": 0, "output_tokens": 194, "output_text_tokens": 49, "output_audio_tokens": 145, "output_image_tokens": 0, "total_tokens": 5873, "tokens_per_second": 83.29, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131414.649565+00:00"} is there a way to reduce the total token utilized, as you can see the summmation of input_tokens is high
t
Thanks — I’m responding to your question about reducing total token usage (I reviewed your agent/session code and the RealtimeModel metrics you pasted). You already have useful pieces in place (Silero VAD, semantic VAD, metrics collector). Below are focused, practical changes you can try right away to lower the input token sum and overall usage. 1) Reduce prompt/context size • Trim or remove non-essential fields from session metadata and tool inputs before sending to the model. Only include the smallest identifiers needed (e.g.,
customer_id
instead of full
customer_name
or
call_context
) when the model doesn’t need verbose context. • Keep system + instruction prompts minimal and use templates that only insert changing variables rather than resending repeated boilerplate. 2) Increase caching and reuse of cached tokens • Your logs show cached_input_tokens rising but not fully utilized. Ensure prompt components that are constant across turns (system instructions, tool descriptions, static persona) are sent once and marked cacheable so subsequent turns hit the cache. • For audio, try to ensure previously-transcribed or previously-embedded audio segments are flagged cacheable when identical or repeated. 3) Send only relevant transcript chunks • Instead of sending the full conversation each turn, send a rolling window of recent N utterances or a summarized history. For example: a. Keep last 2–3 customer utterances + last agent action b. Provide a short recap / summary tokenized into a single short prompt when longer history is needed • Use your semantic VAD / turn detection to delimit utterances tightly so you don’t include redundant silence or repeated transcriptions. 4) Reduce audio tokenization overhead • Your logs show significant audio tokens. Options: ◦ Lower audio sampling or chunk size sent to the LLM where permissible. ◦ Pre-filter audio with Silero VAD to avoid sending non-speech segments. Tune Silero activation threshold and min_interruption_* to avoid short noise bursts being transcribed and sent. ◦ Transcribe locally to a compact text summary and send that instead of raw audio tokens to the model whenever possible. 5) Limit model output length and frequency • Set
max_completion_tokens
to a conservative value for realtime responses so model output tokens remain small. • If you generate both text and audio outputs, reduce audio generation bitrate/size or use shorter TTS responses. 6) Avoid unnecessary tool calls and verbose tool outputs • Your agent loads dynamic tools — make sure tools only return essential output. If a tool returns long structured data, summarize it before feeding back to the model. • Reduce the number of tools triggered per turn by gating tool invocation with simple heuristics (e.g., only call external DB/tool when confidence is low or explicit intent is detected). 7) Prompt-level brevity instructions • Add an instruction like “Answer in 1–2 short sentences” or “Use bullet points, max 40 tokens” for tasks that don’t need long replies. This reduces completion tokens and discourages verbose context re-sending. 8) Use streaming + early stop heuristics • You already have realtime flow; apply sentence-level or punctuation-based cutoffs so generation stops once a usable sentence is produced (stop on period or when intent-confidence threshold reached). 9) Monitor and iterate with the UsageCollector • Continue collecting
prompt_tokens
and
completion_tokens
per session. Identify high-cost sessions/flows and reproduce them locally to test which prompt or input caused token spikes. • Log which parts of the prompt (metadata, transcript, tool output) contribute most to input token count so you can cut/condense those first. Quick implementation pointers based on your code: • Before constructing the model prompt, run a small compaction function that: ◦ Removes or shortens fields in
self.agent_session.metadata
that aren’t required each turn. ◦ Trims the conversation to a rolling window or summary. • Tune Silero VAD thresholds in
get_noisy_environment_config()
and increase
min_interruption_duration
/
min_interruption_words
slightly to avoid sending many tiny audio-transcript chunks. • When creating your model, pass
extra_kwargs
with a
max_completion_tokens
limit for realtime LLM configuration. If you’d like, I can: • Suggest a concrete prompt compaction function (code) to insert before sending prompts. • Recommend exact numeric values to try for Silero thresholds,
min_interruption_*
,
max_completion_tokens
, or rolling-window sizes — tell me whether you prioritize latency, accuracy, or cost so I can tailor those numbers. Which would you prefer: a short code sample to compact prompts, or specific numeric tuning suggestions for thresholds and token limits?
b
can multi agent approach solve this issue? Let say I have will have initial_greeting_agent, then name_confirmation_agent and then other agents so on based on condition, as the converstaion proceeds the agents gets loaded. Does this approach makes sense, if so, how should we do it?
t
Thanks — I’m responding to your question about using a multi‑agent approach to reduce token usage (I reviewed your code, metrics, and your proposed flow:
initial_greeting_agent
→
name_confirmation_agent
→ others). Short answer: yes, the multi‑agent pattern makes sense and can significantly reduce total tokens when implemented correctly. Below I’ll explain why, outline a practical implementation pattern for LiveKit, and ask a couple of clarifying questions so I can give concrete code/parameter suggestions tailored to your priorities. Why this helps • Each agent only needs a small, focused context (reduces input tokens per call). • You can pick lighter models for simple agents (e.g., confirmation) and stronger models only where needed. • You limit how much conversation history any single LLM call carries by summarizing or truncating before handoff. • Focused agents let you gate tool calls and avoid sending large tool outputs repeatedly. Recommended implementation pattern (practical and actionable) 1. Design small, focused agents ◦ initial_greeting_agent: short system prompt, only asks/collects name. ◦ name_confirmation_agent: focused instructions to confirm and store the name. ◦ task agents (billing/triage/etc.): each has minimal system prompt and receives only necessary context. 1. Use function-based handoffs (LiveKit
@function_tool
) ◦ Implement a
@function_tool
in the source agent that returns the next agent instance and a short transition message. This cleanly hands control and is supported by LiveKit for agent transitions. 1. Persist essential state in session userdata ◦ Store confirmed pieces of info (e.g.,
caller_name
,
customer_id
, flags) in
context.session.userdata
. Next agents read this instead of re-asking or re-sending full history. 1. Reset or truncate chat context on handoff ◦ When creating the next agent, give it a fresh short chat context: ▪︎ One
system
message summarizing key persisted state (1–2 sentences). ▪︎ Last N utterances (rolling window, e.g., 2–3 messages) or a short, token-compressed summary. ◦ Avoid passing full raw transcript/history. 1. Choose models per agent ◦ Use smaller/cheaper models for simple tasks (confirmations, yes/no answers). ◦ Reserve higher-capability models for complex reasoning or long-form generation. 1. Gate tool calls and compress tool outputs ◦ Only call tools when necessary and return summarized, minimal tool results into agent prompts. 1. Tune turn boundaries & VAD ◦ Keep your Silero VAD + semantic VAD but tune thresholds so agents are invoked only on meaningful turns to avoid many tiny prompts. 1. Limit completion tokens and instruct brevity ◦ For simple agents, set
max_completion_tokens
low and include an instruction like “Respond in 1–2 short sentences.” Example flow (conceptual) 1. initial_greeting_agent greets and obtains raw name. If name likely captured → call
transfer_to_name_confirmation()
which: ◦ Saves minimal raw name to
userdata.raw_name
. ◦ Returns an instance of
NameConfirmationAgent
with a compact chat context and a short transition message. 1. NameConfirmationAgent asks a single targeted question (confirm spelling). On confirmation: ◦ Writes
userdata.caller_name
and
userdata.name_confirmed = True
. ◦ Transfers to next task agent with a one-line system summary: “Caller name = X (confirmed).” Key tradeoffs to decide (so I can tailor numbers/code) • Latency vs token cost: Do you need sub-200ms responses, or can you accept some extra latency to run summarization/compression steps? • Accuracy vs brevity: How tolerant are you of short summaries (risk of missing small context) vs. sending more tokens? • Model cost priority: Is minimizing cost paramount, or do you prefer maximizing conversational quality? Two clarifying questions 1. Do you want each agent to run in the same AgentSession (handoff within the same session using
function_tool
) or do you prefer spinning up separate AgentSessions per stage? (I recommend same session +
userdata
for simpler state handling and lower overhead.) 2. Which do you prioritize most: latency, token cost, or accuracy/robustness? If you’d like, I can next provide one of these: • A concise code sample you can drop into your project implementing the
function_tool
handoff + compact context creation, OR • Specific tuning numbers to try (rolling-window size, max_completion_tokens per agent, Silero VAD thresholds) based on whether you prioritize latency, cost, or accuracy. Which would you prefer? Sources: Building multi-agent architectures with LiveKit agents | LiveKit | Workflows | LiveKit Documentation | Introduction | LiveKit Documentation | Building Multi-Agent Architectures with LiveKit Agents | Agents and handoffs | LiveKit Documentation
b
same AgentSession, i prioritize latency, accuracy and then token cost
t
Great — thanks for confirming you want to keep everything in the same AgentSession and that your priority order is latency, accuracy, then token cost. That’s a sensible choice: it keeps state simple and lets you optimize handoffs for low latency while still reducing token bloat. Below is a concise, practical plan and a small, ready-to-adapt code pattern you can drop into your app. It shows: • same AgentSession usage, • deterministic handoffs via
@function_tool
, •
userdata
for persistent state (avoids resending history), • compact context creation for each handoff, • latency-minded tweaks (preemptive generation + lightweight agents where appropriate), • accuracy safeguards (explicit confirmation steps). 1) Core ideas to follow • Persist structured data in
session.userdata
(confirmed name, IDs, flags). Don’t push full transcripts into each prompt. • Use
@function_tool
to return the next agent instance; this is a low-latency, supported handoff mechanism. • On handoff, create a fresh, tiny chat context for the incoming agent containing: ◦ One short system summary (1–2 sentences) of the relevant persisted state. ◦ Last 1–2 utterances if absolutely needed. • Use preemptive generation to reduce perceived latency and tune VAD to avoid sending tiny, noisy chunks. • Pick small/fast models for micro‑agents (confirmations, yes/no), heavier models only for complex tasks. 2) Minimal example (pattern) • Replace model names with ones you use in your infra. This keeps the session, userdata, and handoffs local and fast.
Copy code
from livekit.agents import Agent, AgentSession, function_tool, RunContext
from dataclasses import dataclass, field
from typing import List

@dataclass
class SessionState:
    caller_name: str = None
    name_confirmed: bool = False
    history_summary: str = ""  # compact summary if needed
    # add other minimal flags/ids you need

def create_compact_context(state: SessionState, recent_msgs: List[dict] = None):
    # Returns a small ChatContext for the incoming agent
    ctx = ChatContext()
    # 1-2 sentence system summary of persisted state
    summary = "Session state: "
    if state.name_confirmed:
        summary += f"name={state.caller_name} (confirmed)."
    elif state.caller_name:
        summary += f"name={state.caller_name} (unconfirmed)."
    else:
        summary += "no name known."
    if state.history_summary:
        summary += " " + state.history_summary
    ctx.add_message("system", summary[:400])  # cap length to keep tokens low

    # optional: add last 1-2 user messages only when needed for context
    if recent_msgs:
        for m in recent_msgs[-2:]:
            ctx.add_message(m["role"], m["content"][:800])  # cap each message size

    return ctx

class GreetingAgent(Agent):
    def __init__(self):
        super().__init__(instructions="Greet user briefly and ask for name.")
    @function_tool()
    async def to_name_confirm(self, context: RunContext, raw_name: str = None):
        # Save the raw name into persistent userdata
        context.session.userdata.caller_name = raw_name
        context.session.userdata.name_confirmed = False

        # Prepare next agent with compact context
        next_agent = NameConfirmationAgent()
        next_agent.chat_ctx = create_compact_context(context.session.userdata)
        return next_agent, "Thanks — can you confirm the spelling of your name?"

class NameConfirmationAgent(Agent):
    def __init__(self):
        super().__init__(
            instructions="Confirm the caller's name precisely; ask once then store."
        )

    @function_tool()
    async def confirm_and_proceed(self, context: RunContext, confirmed_name: str = None):
        # Confirmed name stored centrally
        context.session.userdata.caller_name = confirmed_name
        context.session.userdata.name_confirmed = True

        # Minimal summary to pass forward; we intentionally avoid full transcript
        context.session.userdata.history_summary = f"Caller name confirmed: {confirmed_name}."

        # Choose next agent (specialized). Use a lightweight model if the task is simple.
        next_agent = TaskAgent()  # replace with your domain agent
        next_agent.chat_ctx = create_compact_context(context.session.userdata)
        return next_agent, f"Thanks, {confirmed_name}. How can I help you today?"

class TaskAgent(Agent):
    def __init__(self):
        super().__init__(instructions="Handle the user's request with high accuracy.")
        # Optionally override LLM model for heavier reasoning here if needed

    async def on_enter(self):
        # Use userdata instead of re-sending the whole dialogue
        name = self.session.userdata.caller_name
        # Generate a targeted prompt using name + short summary
        await self.session.generate_reply(
            instructions=f"User {name} (confirmed). Proceed to handle their request concisely."
        )

# Creating the AgentSession (single session for the whole call)
session = AgentSession[SessionState](
    userdata=SessionState(),
    stt="deepgram/your-stt",
    llm="openai/your-default-llm",
    tts="your-tts",
    preemptive_generation=True,  # helps latency
    # other session tuning...
)

# Start session with GreetingAgent:
initial_agent = GreetingAgent()
await session.start(agent=initial_agent, room=your_room)
3) Tuning suggestions (latency-first, accuracy-second, token cost third) • Preemptive generation: enabled (helps perceived latency). Use cautiously with noisy audio. • VAD thresholds: raise Silero’s activation threshold and increase
min_interruption_words
slightly to avoid too many tiny transcripts. • Rolling window: pass last 1–2 utterances only when strictly needed. Prefer 1 utterance + summary for most flows. • max_completion_tokens: set per-agent low for confirmations (e.g., 20–60 tokens), higher for TaskAgent if needed. • Model selection: use fast, compact models for micro-agents (confirmations, routing). Use higher-quality models only where complex reasoning is required. • Early-stop heuristics: stop generation once a clear confirmation or intent is detected (e.g., punctuation or confidence threshold). 4) Accuracy safeguards • Always confirm critical fields (name, account numbers) with a dedicated micro-agent — this keeps accuracy high even if the compacted context omits raw transcript. • Persist raw minimal details in
userdata
in case you need to re-run a targeted clarification (instead of resending large history to the model). • For important decisions, add a final verification step: short summary + explicit user confirmation. 5) How to measure success quickly • Use your existing UsageCollector to log prompt vs completion tokens per agent. Track which agent calls dominate prompt tokens and iterate on that agent’s compacting strategy. • Run A/B tests: monolithic vs multi-agent in a controlled load to measure latency, accuracy, and token costs. If you want, I can: • Provide a compact helper function to automatically compress/encode summaries (e.g., template-based summarizer) to insert into
history_summary
. • Suggest exact numeric values for Silero thresholds, rolling-window size, and
max_completion_tokens
tuned to your latency targets — tell me your acceptable latency (e.g., max 300 ms server-side response) and I’ll propose numbers. Which would you like next: a) a compact summary-generation helper (code) to plug into
history_summary
, or b) recommended numeric tuning values for VAD thresholds, rolling-window size, and token limits? Sources: Anthropic Claude LLM plugin guide | LiveKit Documentation | Build voice, video, and physical AI | LiveKit | Workflows | LiveKit Documentation | Agents and handoffs | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKit
b
should we use langchain for this?
t
Short answer: No — don’t reach for LangChain as your default. Use LiveKit’s native multi‑agent handoffs unless you specifically need LangChain’s stateful planning features. Why LiveKit native is the better fit for your priorities (latency → accuracy → token cost) • Lower latency: LiveKit’s agent handoffs via
@function_tool
are tightly integrated and avoid the extra orchestration layer LangChain adds. That matters when you prioritize fast audio→response cycles. • Simpler, accurate state handling: Use
session.userdata
to persist confirmed facts (name, IDs, flags). That keeps accuracy high without re-sending full history in prompts. • Token efficiency by design: The multi-agent pattern in LiveKit lets you create a fresh, minimal chat context for each agent (1–2 sentence system summary + last 1 utterance) so each LLM call is small and focused. When LangChain makes sense • You need complex, stateful planning workflows (LangGraph), multi-step reasoning loops, or you already rely heavily on LangChain chains/tools (retrievers, memory layers) that you want to reuse. • If you require recursive reasoning or long-running multi-step pipelines that maintain complex memories, LangChain can be integrated into a single agent for that specific role — not for whole-call orchestration. Recommended approach for your setup • Keep everything in one AgentSession (as you said). Use
@function_tool
to hand off between micro‑agents (greeting → name confirmation → task agent). • Persist critical data in
session.userdata
so downstream agents don’t need full transcripts. • For each handoff, create a compact chat context: one short system summary (1–2 sentences) + last 0–1 user utterances. • Use small/fast models for micro‑agents (confirmations, routing) and use more capable models only for complex tasks. • Keep preemptive generation enabled and tune VAD to reduce noisy micro-turns. Hybrid option (if you need LangChain features) • If only one part of the flow needs LangChain (e.g., a complex planning agent), embed LangChain inside that single agent. Do not use LangChain to orchestrate the whole multi‑agent call — LiveKit should remain the orchestrator. This gives you LangChain capabilities where required without the latency/token penalty across the whole system. Next steps I can provide • A minimal example showing how to embed LangChain inside a single LiveKit agent (if you need LangChain features for one role), OR • A ready-to-use code snippet for LiveKit-native multi-agent handoffs and compact context creation tailored to your latency/accuracy priorities. Which would you prefer? Sources: LangChain integration guide | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKit | How can I reduce latency in voice agents using STT, TTS and LLM? | LiveKit | Introducing LiveKit Inference: A unified model interface for voice AI | How much can LiveKit scale? | LiveKit
b
or can we simply update the agents instructions, instead of multi agent approach?
t
Good question — yes, you can often get meaningful token savings by tightening a single agent’s instructions, and sometimes that’s the simplest first step. Below I’ll compare both approaches quickly and give concrete, actionable changes you can make if you choose to keep one agent. Short summary • Updating instructions can reduce tokens quickly with minimal architecture changes. • Multi‑agent is still the stronger long‑term solution if you expect long sessions, many distinct phases, or need to use different models per phase. • Since you prioritize latency → accuracy → cost, start with instruction + context compaction; move to multi‑agent only if token/complexity still grows or you need model specialization. When updating a single agent makes sense • Session lengths are short or medium and don’t accumulate huge history. • Tasks are fairly linear and don’t require frequent role-switching or different models. • You want minimal engineering changes and fast wins. Concrete, minimal-change steps to reduce tokens by updating instructions (apply these first) 1) Make the system prompt extremely concise and prescriptive • Replace long persona/boilerplate with a tiny instruction: “You are a concise, accurate voice agent. Reply in 1–2 short sentences unless the user requests more detail.” • If some behavior is needed rarely, keep that logic out of the system prompt and call it only when required (see tool gating). 2) Add explicit brevity & format constraints in turn-level prompts • For confirmations: “Answer in <= 20 tokens. Only confirm or ask to repeat.” • For routing: “Return one of these tokens only: BILLING, TECH, SCHEDULING.” 3) Stop resending static context every turn • Move static instructions, tool descriptions, and persona to a cached system message that is not re-sent every turn. Only send dynamic variables (new utterance, last action). 4) Use summarization instead of full history • Keep a rolling one-line summary in
session.userdata.history_summary
and send that instead of raw transcript. Update the summary when important events happen. 5) Gate tool calls and compress tool outputs • Only call tools when intent confidence is low or when explicit user intent is detected. If a tool returns long results, summarize them before including in a prompt. 6) Cap outputs and set max_completion_tokens • For the agent config, set
max_completion_tokens
low for short tasks (20–60) and only increase for complex tasks. 7) Tune VAD and interruption thresholds • Increase activation threshold and min_interruption settings to avoid many short transcriptions that inflate token counts. 8) Use model selection tactically • If your single agent handles both confirmations and heavy reasoning, prefer a single reasonably capable model to avoid latency from switching — but instruct it to use short replies when adequate. If you must, set up quick calls to a smaller fallback model for one-liners (this is closer to multi-agent). When the single-agent approach will not be enough • Conversation history still grows large despite summarization. • Different phases need different model capabilities (and you want to avoid sending full history to heavy models). • You want deterministic handoffs, clearer testing, or independent QA per sub-task. If you later move to multi‑agent, you’ll benefit from: • Smaller per-call contexts, targeted prompts, and per-agent model selection. • Easier gating of tools and confirmations, which reduces unnecessary tokens more aggressively. Next practical help I can provide • A compact rewrite of your current system prompt + an example of a tight per-turn prompt that you can drop in to reduce tokens immediately. • Or, if you prefer, a small code snippet that implements
history_summary
upkeep and enforces capped replies. Which do you want next: the prompt rewrites or the summary-helper code?
b
Copy code
#CORE SYSTEM IDENTITY (EXPLICIT FEMALE VOICE)
      You are Ritika, a senior female collection specialist at {bank} bank ABC XYZ. Be warm, firm, and solution-focused while staying respectful and compliant.

      ## **LANGUAGE GUARDRAIL (ABSOLUTE - NEVER BREAK):**
      **RULE:** Once customer requests English (or any language), you are FORBIDDEN from speaking ANY word in Hindi/Hinglish for the REST OF THE CALL.
      **ENFORCEMENT:** Before speaking EVERY response, check: "Is customer speaking English?" If YES, translate ALL Hindi words to English.
  

      ## **INDIAN VOICE & TONE (FEMALE DELIVERY):**
      * **Gender (CRITICAL):** You are **FEMALE**. Always use feminine verbs (**"Kar rahi hoon"**, **"Samajh sakti hoon"**, **"Main baat kar rahi thi"**) and describe actions femininely ("Main guide kar rahi hoon").
      * **Script Output:** Write Hindi/Hinglish in **ROMAN SCRIPT** only (e.g., "Main check karti hoon"), never Devanagari.
      * **LANGUAGE (CRITICAL):** Once the customer's language is clear (Hindi/Hinglish, English or any other Customer Mentioned Indian Language), then **IMMEDIATELY switch and STAY in that language for ALL responses**.
      * **TRANSLATION RULE (ABSOLUTE PRIORITY):** If customer is speaking English, you MUST translate ALL Hindi/Hinglish responses in this script to English before speaking. If customer is speaking Tamil, translate to Tamil. Match the customer's language for EVERY response.
      * **LANGUAGE LOCK:** Once you switch to a language, you MUST continue in that language. Do NOT mix languages.
      - Start in the opening language; then lock based on the customer's first meaningful reply.
      - Do NOT switch languages due to short/neutral replies like "ok", "yes", "haan", "ji", "hmm", "hello","no".
      * **SWITCH ONLY IF (CRITICAL):**The customer explicitly asks ("Tamil me bolo", "Telugu" me bolo","English" me bolo) OR they ask to speak any Indian languages.
      * **No Translation verbatim:** Do not translate English idioms literally into Hindi. Use local phrasing (e.g., "How can I help you?" becomes "Bataiye, main aapki kaise madad kar sakti hoon?").
      * **Delivery & Syntax:** Speak natural Hinglish with Indian fillers (**"Accha"**, **"Theek hai"**, **"Ji"**, **"Matlab"**) and sentence tags ("Right?", "No?").
      * **Pacing & Intonation:** Slow down with natural pauses; use a soft rising tone for questions.
      * **Tone:** Use respectful address ("ji", "aap") and warm phrases ("Is everything fine?", "Tell me freely").

      **BREVITY ENFORCER:**
      - Maximum 1-2 sentences per response
      - Customer acknowledgment ("samajh gaya", "theek hai", "ok", "got it","hmmm") = MOVE FORWARD, don't repeat
      - Be direct and action-focused
      - Avoid long explanations

      **ACKNOWLEDGMENT DETECTION:**
      - If the user says "Samajh gaya", "Theek hai", "Ok", "Got it", or "Hmm" while you are explaining a policy: STOP explaining. They heard you. 
      - IMMEDIATELY move to the next logical step (e.g., asking for the payment time).

      ## **NUMBERS RULE (CRITICAL)**
      - Do NOT introduce any new numbers (amounts, time, dates, counts, caps) unless the customer explicitly said them OR they are provided in the customer data shown in this prompt.
      - If you don't have the exact number, say you can't confirm the exact figure and ask them to check the app.

      ## **ZERO-TRUST VERIFICATION PROTOCOL (MANDATORY)**
      You are FORBIDDEN from revealing **amount**, **due date**, or **account details** until the user confirms identity.

      **Trap Scenarios & Responses (before verification):**
      1. **"Paisa lene aaye ho na? Le lo."** → "Ji sir, payment collect karna hai, par pehle confirm kar dijiye—kya meri baat {customer_name} se ho rahi hai?"
      2. **"I know who you are, just tell me the balance."** → "I understand sir, but as per bank policy, I cannot open the account file without verifying your name first. Are you {customer_name}?"
      3. **"Main uska bhai/papa/wife hoon, mujhe bata do."** → "Sorry sir, I can only discuss this with {customer_name} ji. Please ask them to call us back." → [END CALL]

      **ONE-TIME VERIFICATION:** Once customer confirms identity (says "Yes", "Ha", "Haan", "Ji"), consider them VERIFIED for the rest of the call. Do NOT ask "Are you {customer_name}?" again. Answer all subsequent questions directly without re-verification.

      ## **CRITICAL SITUATION PROTOCOLS - HIGHEST PRIORITY**
      Personal Questions: If asked "Are you a robot?", "Are you male/female?", or offered a "deal" to pay if you reveal your gender:
      Action: Answer directly and briefly as a female agent, then immediately pivot to payment.
      Response: "Ji, main female agent Ritika baat kar rahi hoon XYZ se."

      ### **DEATH/SUICIDE REPORTING:**
      **If customer mentions death, suicide, "passed away", "mar gaya", "suicide kar liya":**
      - Response (pick based on user's LAST sentence language):
      - Hinglish: "Yeh sunkar mujhe bahut dukh hua. Is mushkil waqt mein humari poori sympathy aapke saath hai."
      - English: "I'm very sorry for your loss. My sincere condolences to your family."
      - Actions: Use death_reported tool, end call immediately, NEVER continue collection discussion

      ### **CALL TERMINATION PROTOCOL:**
      **Customer asks to end call:** first request = brief retention, second request = end call immediately via handle_call_termination_request.
      **Immediate termination:** threats, sexual harassment, extreme abuse, discriminatory slurs.
      **Before ending call : Follow the professional closure rule.**

      ### **AUTOMATIC CALL TERMINATION**
      **Same ineligible request 3x:** (waiver/settlement/manager/EMI when ineligible)
      - 1st: Explain unavailability + alternatives
      - 2nd: "I already explained this isn't available."
      - 3rd: Call repeated_ineligible_request tool → [END CALL]
      **NOT ineligible:** "no"/"I can't"/"no money"/CIBIL questions/disputes [END CALL].

      **Tool usage restriction (MANDATORY):** Only call **repeated_ineligible_request** when the customer explicitly asks for **waiver / settlement / manager**, OR asks for **EMI when EMI is not eligible**. Do NOT call it for payment refusal, CIBIL questions, disputes, "no money", or vague complaints.
      **CRITICAL (Avoid wrong termination):** Do NOT treat a customer saying "no" / "I can't" / "I don't have money" / asking a CIBIL question as an ineligible request. Only apply this rule when the customer explicitly asks for an unavailable option (e.g., waiver/settlement/EMI when not eligible/manager) repeatedly.

      # VOICEMAIL DETECTION (CHECK FIRST)
      **Triggers:** "leave message", beep/tone, robotic voice, "not available", 5+ sec silence
      **Action:** 
      "Hi {customer_name}, main Ritika {bank} XYZ se. Aapke account ke baare mein call kiya tha. Please call back."→ end_call IMMEDIATELY
      **DO NOT wait, DO NOT say "Hello", DO NOT continue conversation**


      ## **ANTI-LOOP & REPETITION CHECK (CRITICAL):**

      **Before EVERY response:**

      1. **Check last 2 responses (cross-response):**
        - Already asked this question? → Change approach or move forward
        - Already stated payment amount/CIBIL? → Ask action question instead
        - Customer said "Samajh gaya"/"Theek hai"/"Ok"? → MOVE FORWARD

      2. **Check CURRENT response (intra-response):**
        - Same sentence appears twice? → DELETE duplicates
        - Amount/date mentioned twice? → Keep only first mention

      **Rule:** Each sentence/fact appears ONCE per response. Never repeat across responses.

      **Example:**
      ❌ "Rahul, Rs.19,344 due hai. Rahul, Rs.19,344 due hai."
      ✅ "Rahul, Rs.19,344 payment 2 February ko due hai. Kya resolve kar sakte hain?"


      ## **STRUCTURED CALL FLOW**
      ### **CURRENT CUSTOMER SNAPSHOT**
      - Customer: {customer_name}
      - Outstanding: {outstanding_balance}
      - Minimum due: {minimum_due_amount}
      - Due: {due_date}
      - Days past due: {days_past_due}
      - EMI eligible: {emi_eligible}
      - EMI interest rate: {emi_interest_rate}
      - EMI options: {emi_tenure_options}
      - Waiver eligible: {waiver_eligible}
      - Waiver amount: {waiver_amount}

      ### **PHASE 1: CALL INITIATION & VERIFICATION**

      **INTRODUCTION DELIVERY RULE (STRICT):**
      1. **Deliver the FULL opening statement without stopping** - do NOT pause or stop for any interruption during intro.
      2. **Only after intro is complete**, listen for customer response and proceed to verification.

      **Opening (choose based on language):**
      ** "Hello, main Ritika baat kar rahi hoon {bank} bank XYZ Credit Card se. Kya meri baat {customer_name} ji se ho rahi hai?"

      **WAIT FOR VERIFICATION - DO NOT PROCEED WITHOUT IT**

      **Verification Responses:**
      - Customer says: Yes / Haan / Ji / Boliye / Bol raha hu / Avunandi → verification_status = VERIFIED → Proceed to Phase 2
      - Customer asks "Kiska payment?" or "Kis baare mein?" → "Pehle confirm karein - kya aap {customer_name} hain?"
      - No / Wrong person → "Kya {customer_name} available hain?" → If no: record_call_outcome(status='wrong_number')
      - 3rd party → "Sorry, main sirf {customer_name} ji se discuss kar sakti hoon." → end_call
      - Refuses/Dodges (TWO-STRIKE RULE):
        - Strike 1: "Security ke liye details batane se pehle confirm karna jaroori hoga—kya aap {customer_name} hain?"
        - Strike 2: "Verification ke bina main details share nahi kar sakti." → end_call

      **FORBIDDEN until VERIFIED:** You CANNOT say {outstanding_balance}, {due_date}, or any account details.

      
      ## PHASE 2: ACCOUNT DISCLOSURE (ONLY AFTER VERIFIED)
      **After verification confirmed:**
      "{customer_name} ji, aapka {outstanding_balance} payment {due_date} se due hai. Kya aaj resolve kar sakte hain?"

      ## PHASE 3: CUSTOMER RESPONSES (PRIORITY ORDER)

      ### PRIORITY 0: REPEAT REQUEST (HIGHEST)
      **Triggers:** "Kya bola?", "Repeat please", "Phir se bolo", "Awaaz nahi aayi", "Sunai nahi diya"
      **Action:** 
      1. Identify the KEY POINT of your last response (not the entire response)
      2. Repeat ONLY that key point in 1 simple sentence
      3. Speak slower and clearer
### PRIORITY 1: PAYMENT CONFIRMED
      **Triggers:** "paid", "payment done", "already paid", "maine kar diya", "amount bhara", "jama kar diya"

      **ATOMIC SEQUENCE (DO NOT DEVIATE):**
      1. Say: "Thanks for the update. Kis medium se payment kiya - UPI, app, ya bank transfer?"
      2. **WAIT** for customer response
      3. Say: "Perfect! System mein reflect hone mein time lagta hai. Have a great day."
      4. Call: record_call_outcome(status='payment_claimed')
      5. Call: end_call
      6. **HARD STOP - Generate ZERO text after this point**
this is the prompt, it has more characters which i am not able to put right now due to some constrain
t
Thanks — I’m responding to your question about whether you can just update the single agent’s instructions (you pasted a long, complex system prompt) instead of moving to multi‑agent. Short answer: yes — you can often get large wins by compacting and restructuring that prompt, and that’s the lowest‑effort first step. Below I’ll (1) highlight the main risks of your current prompt, (2) give concrete, safe compaction strategies that preserve accuracy/compliance, and (3) provide a short compacted example and next steps you can use immediately. Why compacting the single agent prompt is a good first step • Fast to implement with no architecture change. • Keeps latency low (no extra handoffs). • Good when you prioritize latency and accuracy over absolute token cost. • If done correctly (structured state + explicit verification rules), it can achieve most of the benefits of a multi‑agent system while remaining simple. Main risks in the current long prompt • Large, repeated system text increases input_tokens every turn if sent each request. • Complex conditional rules (language locks, multiple critical protocols) are easy for an LLM to mishandle if prompts repeat redundant context every turn. • Verbose rules raise the chance of token overshoot and slow inference. • If static content is re-sent every turn, caching may not be applied effectively. How to compact safely while preserving accuracy/compliance 1. Move static, rarely changing rules into cached system messages ◦ Put immutable policies (ZERO-TRUST VERIFICATION, NUMBERS RULE, CRITICAL SITUATION PROTOCOLS, VOICEMAIL DETECTION, TERMINATION rules) into a cached system message that you send once or mark cacheable. Do not re-send every turn. 1. Keep per-turn system prompt minimal and actionable ◦ Per turn send only what the model absolutely needs: tiny system summary + the immediate user utterance. ◦ Example per-turn system line: “Ritika, female collections agent. Confirm identity first. Max 1–2 short sentences. Follow cached policies.” 1. Replace long procedural prose with bulletized, machine-readable instructions ◦ Use short numbered rules with direct guards (e.g., RULE: Do not speak Hindi if user switched to English). LLMs follow concise guarded tokens more reliably than long paragraphs. 1. Use structured flags in session.userdata rather than re-sent narrative ◦ Persist: name_confirmed, customer_name, language_locked, last_action, critical_flag. Downstream prompts use these short flags instead of verbose history. 1. Move examples and long trap-scenarios to external docs & only send an ID ◦ If you need the LLM to reference “trap scenarios,” send a small token like
policy_v2_trap_handling
and keep the long text cached on the server or referenced by ID. 1. Enforce brevity with explicit per-turn instruction and token caps ◦ Per-turn: “Reply in 1–2 sentences. Max 40 tokens.” Also set
max_completion_tokens
on the LLM call. 1. Summarize history into a single sentence ◦ Keep
history_summary
in userdata and update it only when something important occurs (verified name, payment claim, ineligible request). Send that 1‑line summary instead of full transcript. 1. Use deterministic checks before sending prompts ◦ Perform small pre-check logic server-side (language detection, voicemail detection, repeated request counters). If the check triggers a deterministic action (end call, play intro), do it without an LLM call. 1. For critical actions, separate LLM intent detection from response generation ◦ First send a tiny intent-only call (very small prompt) to decide routing; only when needed, run a generation call with the compacted context. Compact prompt pattern (what you actually send each turn) • Cached system (sent once / cacheable): ◦ policy_v2 (contains ZERO-TRUST, NUMBERS RULE, termination rules, voicemail rules) • Per-turn system (short): ◦ “Ritika: female collections specialist, warm & firm. Verify identity first. Use language_lock if set. Reply 1–2 sentences. Obey policy_v2.” • User message: the raw utterance or preprocessed ASR text • Session flags: name_confirmed=True/False, customer_name=..., language_locked=en/hi/none • Max tokens: low for confirmations (20–40), higher for complex responses. Short compact example (drop-in) • Cached policy stored on server under key
policy_v2
(not re-sent). • Per-turn system message (send every turn, tiny): ◦ "Ritika (female collections). Verify identity before disclosing amounts. Use language lock: {language_locked}. Replies: 1–2 sentences. Obey policy_v2." • User: "Haan, main Rahul bol raha hoon" • Flags: language_locked=hi (set after server determines language) • LLM config: max_completion_tokens=40 Concrete minimal refactor of your prompt (keeps rules but saves tokens) • Cached policy (policy_v2): full original long text stored server-side. Only referenced by key. • Live per-turn system: ◦ “Ritika — female collections agent. Verify identity first. Max 1–2 short sentences. Follow policy_v2. Language_lock={lang}.” • Short helper rules (1–2 lines) included only if state requires them, e.g., if verified: ◦ “VERIFIED: You may disclose outstanding_balance and due_date from session metadata.” Server-side logic to reduce churn (pseudo): 1. On ASR result: ◦ run language detection, voicemail detector, and simple intent classifier locally. ◦ update session.userdata flags. 1. If voicemail -> play canned message + end_call without LLM. 2. If ASR shows verification phrase -> set name_confirmed and only then allow send of short Disclose prompt to LLM (with
VERIFIED
system line). 3. Otherwise send compact per-turn prompt + small
max_completion_tokens
. Why this preserves accuracy • Critical rules remain authoritative in
policy_v2
so the agent remains compliant. • Per-turn brevity + server-side deterministic checks reduce LLM hallucination risk while lowering token count. • Explicit verification gating ensures you never disclose protected info accidentally. When to switch to multi-agent later • If your single-agent flows become many distinct phased processes that require different model capabilities (e.g., heavy reasoning for disputes), then split into agents. • If you see one agent’s prompts still dominating tokens after compaction, that’s a clear signal to move to multi-agent. If you want, I can: • Produce a compacted, production-ready version of your full prompt broken into a cacheable policy + per-turn system message (I can do that by compressing your long prompt into a stored
policy_v2
and a 2–3 line per-turn system string). • Provide the small server-side pre-check code (voicemail detection, language detector, verification detectors) to avoid unnecessary LLM calls. Which would you like next: (A) I compress your full prompt into a cached policy + short per-turn prompt and return both, or (B) I give the small server-side pre-check code snippets to gate LLM calls?
b
ompress your full prompt into a cached policy + short per-turn prompt and return both,
t
Thanks — I compressed your full prompt into two pieces you can use immediately: • Cached policy (send once and store server-side under a key like
policy_v2
). This preserves every critical rule, protocol, and example but is not re-sent every turn. • Short per-turn system prompt (send each LLM call). This is tiny, enforces immediate behavior, and references the cached policy. Instructions: store the cached policy on your server (or as a cacheable system message). For every LLM call, send only the short per-turn system prompt, the user utterance (ASR text), and session flags (e.g.,
language_locked
,
name_confirmed
,
customer_name
,
history_summary
). Set conservative
max_completion_tokens
per agent role. 1) Cached policy (store as
policy_v2
, do NOT send each turn) • Title: policy_v2 — Ritika collections policy (cached) • Purpose: authoritative rules for identity, language locking, disclosure, safety, termination, voicemail, anti-loop, and tool usage. • Content (store exact text below; do not re-send each turn): policy_v2: • Identity & persona ◦ Agent identity: Ritika — senior female collections specialist for {bank} (use this metadata only for tone & personalization). ◦ Always act warm, firm, respectful, and compliant. • Language guardrails (absolute) ◦ Determine customer language on first meaningful reply (ignore short/neutral tokens like “ok”, “yes”, “hmm”). ◦ When customer explicitly requests a language (e.g., “Tamil me bolo”, “English”): switch to that language and lock for the rest of the call. ◦ If customer speaks English, do NOT include any Hindi/Hinglish in future responses for the rest of the call. Translate any internal Hindi content to English before output. ◦ Never mix languages after lock. Use natural local phrasing; avoid literal translations of idioms. ◦ When using Hindi/Hinglish: use ROMAN SCRIPT only (e.g., “Main check karti hoon”). Use respectful forms: “ji”, “aap”, feminine verbs/phrasing. • Brevity & response format ◦ Maximum 1–2 sentences per response unless the user requests more detail. ◦ Include short acknowledgments (e.g., “samajh gaya”, “theek hai”) and move forward—do not repeat. ◦ Explicit per-turn rule: reply concisely and action-focused. • Numbers & disclosure rules (critical) ◦ Do NOT introduce any new numbers (amounts, dates, counts) unless provided in session metadata or said by the customer. ◦ ZERO-TRUST: do not reveal outstanding_balance, due_date, or account details until identity is VERIFIED. ◦ Once a user confirms identity (explicit “Yes”, “Haan”, “Ji”, similar), mark VERIFIED and do not re-ask verification. After VERIFIED, the agent may disclose account details from session metadata. • Verification & one-time verification policy ◦ Always ask for identity verification before disclosing account details. ◦ One-time verification: a single explicit confirmation marks the user verified for the remainder of the call. ◦ Two-strike dodge rule: if user dodges or refuses to verify twice, end per the specified strikes. • Tool usage and termination rules ◦ ONLY call
repeated_ineligible_request
tool when the customer explicitly asks repeatedly for waiver/settlement/manager or EMI when not eligible. Do NOT call for “no money”, CIBIL questions, disputes, or vague complaints. ◦ Termination triggers: immediate call termination for threats, sexual harassment, extreme abuse, discriminatory slurs. ◦ Follow professional closure rules before ending call when appropriate. • Voicemail detection (deterministic server-side check) ◦ Detect via: “leave message”, robotic voice, beep, “not available”, or 5+ sec silence. ◦ On detection: play canned voicemail script and end call immediately. Do not attempt conversation. • Critical situation handling ◦ If user asks “Are you a robot?” or asks agent gender: respond briefly as female agent and pivot back to payment. Example: “Ji, main female agent Ritika baat kar rahi hoon XYZ se.” ◦ If death/suicide reported: use
death_reported
tool immediately, issue condolence in the detected language, end call; NEVER continue collection discussion. • Anti-loop & repetition checks ◦ Before each response, check last two responses and the planned response: ▪︎ If already asked the same question or shared same fact, change approach or move forward. ▪︎ Do not repeat sentences or duplicate facts in a response. ▪︎ If customer says an acknowledgment phrase (“Samajh gaya”, “Theek hai”, etc.) while explaining, STOP further explanation and move forward. • Structured call flow summary (for server use) ◦ Phase 1: Introduction & verification — Play full opening (do not pause) then listen. Do not proceed without verification. ◦ Phase 2: Account disclosure — Only after VERIFIED. Use session metadata fields for amounts/dates. ◦ Phase 3: Responses — prioritized actions (repeat request, payment confirmed, etc.) with deterministic short replies and tool calls as defined. • Examples & traps ◦ Provide exact short replies for traps (e.g., “I know who you are, just tell me the balance.” → “I understand sir, but as per bank policy, I cannot open the account file without verifying your name first. Are you {customer_name}?”) ◦ Example “voicemail” script, termination flows, and two-strike responses are included. • Logging & audit ◦ Always log verification events, tool invocations, and termination actions for audit. (Keep the full original text archived in
policy_v2
for compliance & training; you may expand this policy over time but avoid sending it per turn.) 2) Short per-turn system prompt (send every LLM call; tiny) • Use this exact, short system prompt each turn. Replace variables inline. Per-turn system prompt: Ritika — female collections specialist. Follow cached policy
policy_v2
. Verify identity before revealing account details. Language_lock={language_locked}. name_confirmed={name_confirmed}. Reply in 1–2 short sentences. Do not introduce new numbers. Max 40 tokens. 3) Per-turn inputs you must send (minimal) • user_message: the ASR-transcribed text (preprocessed for profanity trimming). • session flags (server-side small JSON): ◦ language_locked: one of {none, en, hi, ta, ...} (set after first meaningful reply / server language detection). ◦ name_confirmed: boolean. ◦ customer_name: string (if known). ◦ history_summary: one-line summary from server (if relevant). ◦ verification_attempts: small counter for the two-strike rule. • metadata: outstanding_balance, due_date, emi_eligible, etc. — only include in the prompt IF name_confirmed==True. Otherwise keep in server metadata but do not pass to LLM. 4) Server-side deterministic checks to run before calling the LLM • Voicemail detector (phrase + silence detection) → handle and end call without LLM. • Language detector on the first meaningful utterance → set language_locked. • Simple verification detector (look for “Yes/Haan/Ji” patterns) → set name_confirmed locally. If detected, call a tiny “disclosure_approval” LLM prompt (or directly allow disclosure) instead of full prompt generation. • Trap phrase checks (e.g., “I know who you are, just tell me the balance”) → return canned policy-based short reply without LLM where feasible. 5) LLM configuration & limits • For confirmation/routing micro-tasks: max_completion_tokens = 20–40. • For task agent (complex reasoning): max_completion_tokens = 80–180. • Use streaming + early stop when you detect punctuation/period to lower perceived latency. 6) Example per-turn payload (when NOT verified) • system: (short per-turn system prompt above with language_locked and name_confirmed=false) • user: "Haan main Rahul bol raha hoon" • metadata: { customer_name: "Rahul", name_confirmed: false } • llm_config: max_completion_tokens=30 7) Example per-turn payload (when VERIFIED) • system: (short per-turn prompt with name_confirmed=true) • user: "Main aaj payment kar sakta hoon" • metadata: { customer_name: "Rahul", outstanding_balance: "Rs.19,344", due_date: "2026-02-02", name_confirmed: true } • llm_config: max_completion_tokens=60 Why this reduces tokens and keeps compliance • The big policy text is only stored once (policy_v2) and not re-sent every turn, removing thousands of repeated tokens. • Per-turn prompts are tiny (one short system line + ASR), so the LLM receives minimal input tokens each turn. • Server-side deterministic checks and state flags avoid many unnecessary LLM calls. • Critical rules remain authoritative (in policy_v2) preserving accuracy and compliance. Next steps & optional helpers I can provide • A drop-in server snippet to store/fetch
policy_v2
and construct the per-turn prompt. • A compact summary updater that maintains
history_summary
when key events happen (verification, payment claim, ineligible request). • Exact regex lists for verification phrases and voicemail detection to run before the LLM. Which helper would you like first: (A) server snippet for caching policy_v2 and building per-turn payloads
b
how should i refactor and use the updated prompt for that it works correctly
t
Thanks — I’ll walk you through a clear, step‑by‑step refactor so the cached policy + short per‑turn prompt works reliably in your LiveKit agent flow. I’ll keep each step short and actionable and include a practical code pattern you can drop into your existing service. High‑level plan 1. Store the long policy once (server-side) as
policy_v2
(never re-send each turn). 2. Send only the tiny per‑turn system prompt + ASR + a few session flags to the LLM each call. 3. Run deterministic prechecks server‑side (voicemail, language detection, verification detect) before calling the LLM. 4. Use
session.userdata
flags to avoid resending protected metadata until VERIFIED. 5. Log verification/tool actions for audit. Step-by-step refactor (with code patterns) 1) Persist the cached policy (server) • Store your long prompt text in a secure server-side store or in memory keyed as
policy_v2
. Do NOT include it in per-turn LLM payloads. • Keep it auditable and versioned for compliance. Python-ish example (Flask/fastapi style):
Copy code
# policy_store.py
POLICIES = {}

def store_policy(key: str, text: str):
    POLICIES[key] = text

def get_policy(key: str) -> str:
    return POLICIES[key]
Call once at startup:
Copy code
store_policy("policy_v2", "<<< full long policy text >>>")
2) Add server-side deterministic prechecks (run before any LLM call) • Language detection on first meaningful utterance -> set language_locked. • Voicemail detection (phrases or >5 sec silence) -> play canned message and end call immediately. • Verification detection (patterns: "Yes", "Haan", "Ji", "Yes I am X") -> set name_confirmed and avoid extra LLM calls where possible. • Trap phrase detection -> return canned short reply if matching common traps. Example precheck function:
Copy code
def prechecks(asr_text: str, session):
    # voicemail heuristics
    if is_voicemail(asr_text) or session.silence_seconds > 5:
        play_voicemail_and_end(session)
        return {"action": "handled_voicemail"}

    # language detection (first meaningful reply)
    if session.userdata.language_locked == "none":
        lang = detect_language(asr_text)
        if lang in ["en","hi","ta",...]:
            session.userdata.language_locked = lang

    # verification detection
    if looks_like_verification(asr_text):
        session.userdata.name_confirmed = True
        session.userdata.verification_attempts += 1
        return {"action": "verified"}

    # trap detection
    if looks_like_trap(asr_text):
        return {"action": "trap_response", "reply": canned_trap_reply(session)}
    return {"action": "continue"}
3) Short per‑turn system prompt + minimal payload • Build this per LLM call. Do NOT include policy_v2 text — just reference it by name if needed. Per‑turn prompt template: Ritika — female collections specialist. Follow cached policy
policy_v2
. Verify identity before revealing account details. Language_lock={language_locked}. name_confirmed={name_confirmed}. Reply in 1–2 short sentences. Do not introduce new numbers. Max 40 tokens. Construct the LLM payload:
Copy code
def build_payload(session, user_asr):
    system = f"Ritika — female collections specialist. Follow cached policy `policy_v2`. Verify identity before revealing account details. Language_lock={session.userdata.language_locked}. name_confirmed={session.userdata.name_confirmed}. Reply in 1–2 short sentences. Do not introduce new numbers. Max 40 tokens."

    # include protected metadata only if verified
    metadata = {}
    if session.userdata.name_confirmed:
        metadata.update({
            "customer_name": session.metadata.get("customer_name"),
            "outstanding_balance": session.metadata.get("outstanding_balance"),
            "due_date": session.metadata.get("due_date"),
        })

    payload = {
        "system": system,
        "user_message": user_asr,
        "flags": {
            "language_locked": session.userdata.language_locked,
            "name_confirmed": session.userdata.name_confirmed,
            "verification_attempts": session.userdata.verification_attempts
        },
        "metadata": metadata
    }
    return payload
4) Use session.userdata to hold minimal structured state • Example fields: language_locked (none/en/hi/...), name_confirmed (bool), customer_name (string), verification_attempts (int), history_summary (1‑line). • Update flags from deterministic checks and only send tiny flags to LLM. Example:
Copy code
class SessionUserData:
    def __init__(self):
        self.language_locked = "none"
        self.name_confirmed = False
        self.customer_name = None
        self.verification_attempts = 0
        self.history_summary = ""
5) Only include sensitive metadata if name_confirmed==True • Keep outstanding_balance etc. in server-side metadata, but never send to LLM until verified. This enforces ZERO-TRUST. 6) Compact history: maintain history_summary • Append only meaningful events: ◦ "name confirmed" ◦ "payment claimed" ◦ "ineligible request repeated" • Update summary server‑side and send as a single short sentence when needed. Example updater:
Copy code
def update_history_summary(session, event: str):
    session.userdata.history_summary = (session.userdata.history_summary + " | " + event).strip()[:400]
7) LLM call and max token settings • Use small max_completion_tokens for micro tasks: ◦ Confirmations / routing: 20–40 tokens ◦ Short disclosures after verified: 40–60 ◦ Complex reasoning (rare): 80–180 • Enable streaming and early-stop heuristics. 8) Example end‑to‑end flow (simplified) 1. ASR text arrives -> run prechecks. 2. If prechecks handled (voicemail/verification/trap), respond deterministically without full LLM or with tiny intent LLM. 3. Else build payload using build_payload() and call LLM with small
max_completion_tokens
. 4. Update session.userdata/state from LLM output (e.g., if LLM indicates verification turn). 5. Log tokens via UsageCollector for tuning. 9) Testing & rollout • Unit test deterministic prechecks (voicemail, verification, trap phrases). These should never call LLM. • A/B test compact prompt vs current full prompt on a subset of traffic. Monitor: ◦ latency (TTFT / tokens_per_second) ◦ accuracy (verification success, policy violations) ◦ token counts (prompt_tokens, completion_tokens) • Use UsageCollector logs to find which flows still have high prompt tokens and iterate. 10) Example small code snippet integrating into your AgentSession (pseudo)
Copy code
@session.on("asr_result")
async def handle_asr(asr_text, session):
    pre = prechecks(asr_text, session)
    if pre["action"] == "handled_voicemail":
        return
    if pre["action"] == "trap_response":
        await session.send_reply(pre["reply"])
        return
    if pre["action"] == "verified":
        # optionally send a short confirmation reply or allow disclosure
        await session.send_reply("Thank you — verification complete.")
        return

    payload = build_payload(session, asr_text)
    # choose max tokens based on flags or role
    max_toks = 30 if not session.userdata.name_confirmed else 60
    response = await call_llm(payload, max_completion_tokens=max_toks)
    # post-process checks (dedupe, anti-loop)
    resp = dedupe_and_apply_policy(response, session)
    await session.send_reply(resp)
Quick checklist to confirm correct behavior • [ ] policy_v2 stored server-side (not sent each turn). • [ ] server prechecks implemented (voicemail, language detection, verification detection, trap detection). • [ ] session.userdata fields exist and are updated deterministically. • [ ] per-turn system prompt is tiny and references
policy_v2
. • [ ] sensitive metadata only sent when name_confirmed==True. • [ ]
max_completion_tokens
set per micro-task. • [ ] UsageCollector logs prompt/completion tokens per agent call. If you want, I can: • Provide a drop‑in GitHub‑style snippet that integrates exactly with your code shown earlier (I’ll adapt variable names like
self.agent_session
,
self.usage_collector
, and
self.agent_config
). • Propose exact regex lists for verification phrases and voicemail triggers to paste into prechecks. Which do you want next: (A) a drop‑in snippet for your existing class, or (B) the regex/language-detection helpers for prechecks?
b
does livekit dispatch request need trunk_id>
t
Short answer: No — LiveKit agent dispatch requests do not require a
trunk_id
. Details: • Agent dispatch assigns an agent to a LiveKit room (via AgentDispatch APIs) and requires the agent name and room; it does not need SIP trunk identifiers. •
trunk_id
is only required for SIP/telephony operations: configuring inbound/outbound trunks, creating SIP participants, or SIP-based dispatch rules that explicitly route phone calls through a trunk. • If you’re dispatching an agent to handle a room or a non‑SIP interaction, omit
trunk_id
. If your flow involves SIP (inbound call routing or outbound dialing), include the appropriate `sip_trunk_id`/`trunk_id` in the SIP-related API calls. Example: • Agent dispatch (no trunk_id needed)
Copy code
await lkapi.agent_dispatch.create_dispatch(
    api.CreateAgentDispatchRequest(
        agent_name="test-agent",
        room="my-room",
        metadata='{"user_id":"12345"}'
    )
)
• SIP participant / outbound call (trunk_id required)
Copy code
await lkapi.sip.create_sip_participant(
    api.CreateSIPParticipantRequest(
        room_name="my-room",
        sip_trunk_id="ST_outbound_abc",
        sip_call_to="+1234567890",
        participant_identity="caller"
    )
)
If you tell me whether your dispatch is for a SIP phone call or for a regular room/agent assignment, I can suggest the exact API fields you should send. Sources: Agent dispatch | LiveKit Documentation | Outbound Calling Script | LiveKit Documentation | Agents telephony integration | LiveKit Documentation
b
we are dispatching for sip participant but right now we are passing the trunk on scheduler which is hampering our design, we want to know whether we can dispatch the job from scheduler but inside the create sip participant in orchestrator we choose the sip trunk
t
Yes — you can dispatch the job from your scheduler without a
trunk_id
and choose the SIP trunk dynamically inside the orchestrator when you call
create_sip_participant
. That decoupling is a good pattern and solves the design issue you described. Why this works • Dispatching an agent/job is orthogonal to how you place the SIP call. Agent dispatch needs agent_name + room and optional metadata; the SIP trunk is only required when creating a SIP participant for the call. • Deferring trunk selection to the orchestrator keeps scheduling lightweight and lets the orchestrator pick the best trunk per-call (region, load, provider, business rules). Recommended pattern (practical) 1. Scheduler: create dispatch/job with minimal metadata ◦ Include routing metadata the orchestrator needs (phone_number, region, priority, campaign_id), but omit trunk_id. 1. Orchestrator (inside the agent or orchestrator service) ◦ On job start, pick the trunk using business logic (region, load, failover, latency, provider costs, legal requirements). ◦ Call
create_sip_participant(..., sip_trunk_id=chosen_trunk, sip_call_to=phone_number, ...)
there. 1. Store trunk decision for auditing & retry ◦ Log chosen trunk, reason, and any errors for observability and retries. Example decision logic (conceptual) • If phone_number country == IN -> choose India-optimized trunk • If campaign_priority == high -> choose premium trunk • If trunk health shows high latency -> pick next available trunk Example (pseudo-Python)
Copy code
# scheduler -> creates job without trunk_id
scheduler.create_dispatch(agent_name="outbound-agent", room="r123", metadata={
    "phone_number":"+91xxxx", "region":"IN", "priority":"standard", "campaign":"CAMP1"
})

# orchestrator / agent
def choose_trunk(metadata):
    if metadata["priority"] == "premium":
        return os.getenv("PREMIUM_SIP_TRUNK")
    if metadata["region"] == "IN":
        return os.getenv("IN_SIP_TRUNK")
    return os.getenv("DEFAULT_SIP_TRUNK")

async def make_call(job):
    phone = job.metadata["phone_number"]
    trunk = choose_trunk(job.metadata)
    await api.sip.create_sip_participant(
        room_name=job.room,
        sip_trunk_id=trunk,
        sip_call_to=phone,
        participant_identity=f"caller-{job.id}"
    )
Best practices & considerations • Pass routing metadata with the scheduled job so the orchestrator can make an informed trunk choice (region, priority, campaign, failover rules). • Validate trunk availability and implement fallback selection. Handle errors & retries if trunk creation fails. • Enforce permissions: ensure the orchestrator has the rights to create SIP participants and use the configured trunks. • Audit & observability: log trunk choice and call outcome for troubleshooting and cost analysis. • Compliance: ensure trunk selection meets regulatory/geographic constraints for the destination number. • Idempotency: if scheduler re-dispatches the job, ensure the orchestrator handles duplicate job attempts (use job idempotency keys). If you’d like, I can: • Provide a ready-to-drop-in code snippet that fits your existing orchestrator (I can adapt to your class names and APIs), or • Propose a small trunk-selection priority table (region → trunk mapping, health-based fallback) you can use immediately. Which would you prefer? Sources: Outbound Calling Script | LiveKit Documentation | Agents telephony integration | LiveKit Documentation | Agent dispatch | LiveKit Documentation | server-sdk-kotlin | livekit/livekit_composite | DeepWiki | SIP Lifecycle Management Agent | LiveKit Documentation
b
if all AgentServers are down, what will happen to the dispatched job?
t
Thanks — I’m responding to your question about what happens to a dispatched job if all AgentServers are down. Short answer: the dispatch will not be processed and you must handle retries/fallbacks on the client/orchestrator side. What happens • Job assignment will fail because there are no AgentServers available to accept the job. The dispatch request will be rejected or time out. • LiveKit does not automatically queue and replay dispatches when all AgentServers are offline. The system requires an available AgentServer to accept and run the job. • When AgentServers come back online, they do not automatically resume previously failed dispatch attempts — new dispatch requests must be issued. Recommended mitigations (practical, prioritized) 1. Add redundancy (high impact) ◦ Run multiple AgentServers for each agent_name across zones/instances so a single outage won’t exhaust capacity. 1. Implement retries with backoff in your scheduler (medium impact) ◦ On dispatch failure, retry with exponential backoff and a max retry window. Log failures and surface alerts after N retries. 1. Fallback handling in scheduler/orchestrator (high impact) ◦ If dispatch fails repeatedly, mark the job as “failed” and run fallback flows: notify an operator, enqueue for manual processing, or route to an alternative channel (outbound SIP queue, SMS, or callback list). 1. Health checks & auto-scaling (high impact) ◦ Monitor AgentServer health and register/unregister capacity. Auto-scale AgentServers when utilization crosses thresholds so capacity is available. 1. Graceful degradation / user messaging (low effort) ◦ If the job represents a customer-facing call, notify the customer of temporary delay and optionally schedule a retry window. 1. Audit & observability (critical) ◦ Log dispatch attempts, failures, chosen retry strategy, and final outcome. Track metrics (failure rate, mean time to recover) and create alerts for sustained outages. Implementation ideas / snippets • Retry with backoff (pseudo):
Copy code
def dispatch_with_retries(dispatch_payload, max_retries=5):
    delay = 1
    for attempt in range(max_retries):
        try:
            return lkapi.agent_dispatch.create_dispatch(dispatch_payload)
        except TransientDispatchError:
            time.sleep(delay)
            delay *= 2
    # after retries fail
    mark_job_failed(dispatch_payload)
    alert_ops(dispatch_payload)
    return None
• Fallback queue: push failed dispatches to a durable queue (Redis, SQS) for manual/automated reprocessing once servers recover. Decision points to pick • Retry policy: how many retries and total retry window are acceptable for your business SLA? (e.g., 3 retries over 2 minutes vs. 12 retries over 24 hours) • Failover behavior: immediate human notification vs. automated requeue only. If you want, I can: • Provide a concrete retry policy and sample code integrated with your scheduler (including exponential backoff, jitter, and alerting), OR • Draft an operational runbook for outage scenarios (detection, retries, notifications, and fallback routing). Which would you like next? Sources: Agent dispatch | LiveKit Documentation | Agents Framework | livekit/livekit_composite | DeepWiki