acoustic-pharmacist-34178
02/23/2026, 8:25 PMtall-airline-16756
02/23/2026, 8:25 PMtall-airline-16756
02/23/2026, 8:26 PMctx.job.metadata and initialize the agent with those instructions.
◦ When to use: Instructions are different per job and you prefer sending them at dispatch time without additional runtime network calls.
◦ Key benefit: Simple and low-latency; no extra runtime fetch required.
• Direct HTTP request in entrypoint (most flexible; watch latency)
◦ What it does: Before starting the agent session, make an HTTP call from your entrypoint to fetch instructions/config.
◦ When to use: You need custom logic to assemble config, or need to call existing APIs to compute instructions.
◦ Caution: Adds startup latency; ensure the fetch completes before ctx.connect() if you want the agent ready with that data immediately.
• Prewarm / preload on server start (good for global/static config)
◦ What it does: Load config once during server prewarm and store in proc.userdata or similar for reuse by sessions.
◦ When to use: Configuration is global, relatively static, and shared across many sessions.
◦ Key benefit: Removes per-session fetch overhead; not suitable for per-user or frequently changing instructions.
Example snippets (conceptual)
1) MCP server in AgentSession
from livekit.agents import mcp
session = AgentSession(
mcp_servers=[ mcp.MCPServerHTTP("<https://your-mcp-server.com>") ],
# other params...
)
2) Read job metadata in entrypoint
async def my_agent(ctx: JobContext):
metadata = json.loads(ctx.job.metadata)
instructions = metadata.get("instructions", "default instructions")
# initialize agent with instructions...
3) Fetch in entrypoint
async def my_agent(ctx: JobContext):
instructions = await fetch_instructions_from_remote()
# then start/connect agent session using instructions
Recommendation summary
• Use MCP when you want a robust, centralized, dynamic source of tools and config for agents.
• Use job metadata for simple per-job instructions with minimal complexity.
• Use direct HTTP fetch when you need custom assembly or external API calls, but be mindful of added latency.
• Use prewarm for global/static config to reduce per-session overhead.
If you’d like, tell me:
• Are your instructions per-user/job or globally shared?
• Do they change often at runtime?
With that I can recommend the exact setup and show a complete code example tailored to your use case.
Sources: Tool definition and use | LiveKit Documentation | External data and RAG | LiveKit Documentation | Server options | LiveKit Documentationacoustic-pharmacist-34178
02/23/2026, 8:33 PMtall-airline-16756
02/23/2026, 8:34 PMuserdata) for the session lifetime.
◦ Persistent external cache (Redis, database) keyed by SIP URI or participant.identity for reuse across sessions.
◦ Preload shared/static instructions in prewarm for global reuse.
Recommended approaches (when to use which)
• External cache (recommended for reuse across reconnects)
◦ Use Redis or a database keyed by the SIP URI (or normalized phone number / participant.identity).
◦ On participant join: check cache first; if present, use cached instructions; if missing, fetch from remote server and write back to cache.
◦ TTLs: give cached instructions a sensible TTL so updates on your remote service eventually propagate.
◦ Benefit: very low latency on reconnects and resilient across process restarts.
• Session-local cache (quick, for a single session)
◦ Store instructions in the agent session’s userdata so other parts of the session can reuse them without repeated fetches.
◦ Use when instructions only need to survive the current session and you want minimal complexity.
• Prewarm/preload (for largely static or shared instructions)
◦ Load global or commonly-used instructions in your server prewarm and serve from proc.userdata.
◦ Use when many participants share the same baseline instructions and you want to avoid per-join fetches.
Implementation outline (example flow)
1. Participant joins; determine key
◦ Use the participant’s SIP URI, phone number, or participant.identity as the cache key.
1. Check cache
◦ Lookup key in Redis/DB. If found, deserialize and use instructions.
1. If not cached, fetch and cache
◦ Fetch from your remote config service (HTTP call or MCP).
◦ Store result in cache with an appropriate TTL.
◦ Initialize the agent with those instructions.
1. Use session-local fallback
◦ Also save to userdata so repeated accesses inside the same session don’t hit external storage.
Conceptual code sketch (pseudo-Python)
async def on_participant_join(ctx, participant):
key = participant.attributes.get("sip.phoneNumber") or participant.identity
# 1) Try external cache
instructions = await redis.get(key)
if instructions:
instructions = json.loads(instructions)
else:
# 2) Fetch from remote service (or MCP)
instructions = await fetch_instructions_from_remote(key)
await redis.set(key, json.dumps(instructions), ex=3600) # TTL in seconds
# 3) Save to session-local userdata for this session
ctx.proc.userdata["instructions"] = instructions
# 4) Initialize agent/session with instructions...
Notes and best practices
• Normalize the SIP URI/phone number to a stable canonical key so the same participant yields the same cache key.
• Choose TTL based on how frequently instructions change. If instructions are updated by external systems, use a short TTL or add a cache-invalidation webhook from your config server.
• If instructions are highly sensitive, encrypt them at rest and ensure proper access controls to your cache.
• If you already use MCP for dynamic tools/config, you can still cache MCP responses in Redis to avoid repeated remote calls and reduce latency.
If you want, I can:
• Provide a full code example tailored to your runtime (async Python, Node, etc.).
• Suggest a TTL strategy based on how often your instructions change.
Which runtime and caching backend are you using (or planning to use)?
Sources: SIP Lifecycle Management Agent | LiveKit Documentation | Agents and handoffs | LiveKit Documentation | Context Variables | LiveKit Documentation | sip | livekit/livekit_composite | DeepWiki | How can I reduce latency in voice agents using STT, TTS and LLM? | LiveKitacoustic-pharmacist-34178
02/23/2026, 8:35 PMtall-airline-16756
02/23/2026, 8:36 PMproc.userdata at server startup, but this is not cross-process unless you coordinate it with an external store.
Recommended pattern (Redis example) 1) Key design
• Use a canonical key based on the SIP URI or participant.identity. Normalize the value (strip whitespace, lowercase, remove formatting).
• Example key: agent_instructions:sip:+15551234567
2) Cache read/write flow
• On participant join:
◦ Try Redis GET(key). If found, deserialize and use.
◦ If missing, fetch from your remote config service (or MCP), then SET key with JSON-serialized instructions and an appropriate TTL.
• Optionally use Redis' SETNX / Lua scripts to prevent stampedes (only one fetcher writes).
• Use TTL so updates eventually propagate; if you need immediate invalidation, provide an API or webhook from your config service to delete the key.
3) Concurrency and consistency
• Use a short TTL (minutes to hours) for frequently changing instructions.
• For atomic refresh, use a “refresh lock” key (SET key:lock NX PX 30000) or Redis Lua script to ensure only one process refreshes the cache.
• If instructions must change instantly, trigger cache invalidation from the service that updates instructions.
4) Security
• Encrypt sensitive instruction payloads at rest or restrict access to Redis with strong auth, VPC/private network, and RBAC.
• Limit TTLs and rotate keys if instructions contain secrets.
5) Session-local optimization
• After loading from Redis, also store instructions in the session (ctx.proc.userdata or similar) so repeated accesses during the same session avoid Redis calls.
Conceptual async-Python sketch (Redis + TTL + simple lock)
import json
import asyncio
import aioredis
REDIS_URL = "<redis://localhost:6379>"
TTL = 3600 # seconds
async def get_instructions_for_sip(sip_uri):
key = f"agent_instructions:{normalize(sip_uri)}"
async with aioredis.from_url(REDIS_URL) as redis:
cached = await redis.get(key)
if cached:
return json.loads(cached)
lock_key = key + ":lock"
got_lock = await redis.set(lock_key, "1", nx=True, ex=30) # prevent stampede
if got_lock:
try:
instructions = await fetch_instructions_from_remote(sip_uri)
await redis.set(key, json.dumps(instructions), ex=TTL)
return instructions
finally:
await redis.delete(lock_key)
else:
# Another process is fetching — wait and retry
await asyncio.sleep(0.2)
return await get_instructions_for_sip(sip_uri)
def normalize(sip_uri):
return sip_uri.strip().lower()
When to use a DB instead
• Use PostgreSQL or DynamoDB when you need:
◦ Durable history of instruction versions
◦ Complex queries or joins
◦ Transactional updates or strong consistency
• Pattern: write-through or write-back with Redis as front-line cache.
Cache invalidation strategies
• TTL-based expiry (simple and robust).
• Explicit invalidation: webhooks or API calls from your config service that call DEL on the key.
• Versioning: store a version number in the key (e.g., agent_instructions:v42:<sip>) and bump it when instructions change.
MCP-specific note
• If you use MCP for dynamic tools/config, you can cache MCP responses (same Redis pattern) to avoid repeated HTTP/SSE fetches for the same SIP participant.
Next step Which runtime are you using (async Python, Node.js, Go), and which cache backend do you prefer (Redis, PostgreSQL, DynamoDB)? I can provide a full, runnable code example tailored to your stack and include lock/invalidaton details.acoustic-pharmacist-34178
02/23/2026, 8:45 PMtall-airline-16756
02/23/2026, 8:45 PMacoustic-pharmacist-34178
02/23/2026, 8:46 PMtall-airline-16756
02/23/2026, 8:47 PMacoustic-pharmacist-34178
02/23/2026, 8:48 PMtall-airline-16756
02/23/2026, 8:49 PMparticipant.identity).
• On participant join: check the cache first. If present, use cached instructions. If missing, fetch from your remote config (HTTP/MCP), store them in Redis with a TTL, and then use them.
• Also save to session-local storage (proc.userdata / ctx.proc.userdata) so repeated accesses during the same session avoid extra round-trips.
• Prevent cache stampedes with a simple lock (SETNX or short lock key) or let one process refresh while others wait and retry.
• Invalidate or update cache via TTLs, versioned keys, or a config-server webhook that deletes/updates the Redis key.
Concrete async Python example (Redis) 1) Requirements:
• aioredis (or redis-py asyncio)
• An async HTTP fetch function or MCP client to retrieve remote instructions
2) Key design:
• Normalize SIP URI, e.g., remove whitespace, lowercase, E.164 for phone numbers.
• Key example: agent_instructionssip+15551234567
3) Example code (conceptual)
import json
import asyncio
import aioredis
REDIS_URL = "<redis://localhost:6379>"
TTL_SECONDS = 3600 # adjust to your freshness needs
LOCK_TTL = 30 # seconds to hold refresh lock
redis = None
async def get_redis():
global redis
if redis is None:
redis = await aioredis.from_url(REDIS_URL)
return redis
def normalize_key(sip_uri: str) -> str:
return "agent_instructions:" + sip_uri.strip().lower()
async def fetch_instructions_from_remote(sip_uri: str) -> dict:
# Implement your HTTP or MCP fetch logic here
# Example: await http_client.get(f"<https://config.example.com/instructions/{sip_uri}>")
raise NotImplementedError
async def get_instructions(sip_uri: str) -> dict:
key = normalize_key(sip_uri)
r = await get_redis()
cached = await r.get(key)
if cached:
return json.loads(cached)
lock_key = key + ":lock"
got_lock = await r.set(lock_key, "1", ex=LOCK_TTL, nx=True)
if got_lock:
try:
instructions = await fetch_instructions_from_remote(sip_uri)
# store in cache with TTL
await r.set(key, json.dumps(instructions), ex=TTL_SECONDS)
return instructions
finally:
await r.delete(lock_key)
else:
# Another process is fetching — wait and retry a few times
for _ in range(10):
await asyncio.sleep(0.2)
cached = await r.get(key)
if cached:
return json.loads(cached)
# fallback: fetch directly (last resort)
instructions = await fetch_instructions_from_remote(sip_uri)
await r.set(key, json.dumps(instructions), ex=TTL_SECONDS)
return instructions
4) Integrate into agent entrypoint
• Load instructions before connecting the agent so the agent participant is ready when it appears.
async def entrypoint(ctx: JobContext):
# Extract SIP URI from participant attributes or ctx
sip_uri = extract_sip_uri_from_ctx(ctx) # implement extraction
# First, try session-local userdata
instructions = ctx.proc.userdata.get("instructions_for:" + sip_uri)
if not instructions:
instructions = await get_instructions(sip_uri)
# store in session-local userdata for quick reuse within session
ctx.proc.userdata["instructions_for:" + sip_uri] = instructions
# Initialize your agent with these instructions
session = AgentSession(...)
await session.start(
room=ctx.room,
agent=MyAgent(instructions=instructions),
# ...
)
Best practices and additional recommendations
• TTL vs explicit invalidation:
◦ Use reasonable TTLs if instructions change periodically.
◦ If updates must propagate immediately, have your config service call a cache-invalidation endpoint or send a webhook to delete the Redis key.
• Security:
◦ Protect Redis access (VPC, password, TLS). If payloads are sensitive, encrypt them before caching.
• Size and memory:
◦ Store only necessary fields in cache (avoid bloat). If entries are large, prefer external DB + small cache pointers.
• Stampede protection:
◦ Use lock keys (SET NX) or Redis-based algorithms (Redlock/Lua scripts) for atomic refresh.
• Monitoring:
◦ Track cache hit rate, miss rate, average fetch latency, and Redis memory usage.
• Scaling across instances:
◦ External cache lets multiple agent servers share cached instructions without duplicating memory.
When to choose alternatives
• Use Postgres/DynamoDB if you need durability, version history, or transactional updates. Then use Redis as a front-line cache for performance.
• Use Memcached if you need a simple, memory-only cache without persistence and with very high perf.
Next step Tell me:
• Which runtime do you want the example in (async Python, Node.js, Go)?
• Which cache backend do you prefer (Redis, Postgres, DynamoDB)?
I’ll provide a full runnable example (including fetch logic and cache invalidation) tailored to your stack.
Sources: External data and RAG | LiveKit Documentation | Building an Automated IVR Menu Caller | LiveKit Documentation | Server options | LiveKit Documentationacoustic-pharmacist-34178
02/23/2026, 8:52 PMtall-airline-16756
02/23/2026, 8:53 PMacoustic-pharmacist-34178
02/23/2026, 10:27 PMtall-airline-16756
02/23/2026, 10:28 PMimport aioredis
import json
import asyncio
REDIS_URL = "redis://:YOUR_PASSWORD@redis.example.com:6379/0"
TTL_SECONDS = 3600
async def get_redis():
return await aioredis.from_url(REDIS_URL, encoding="utf-8", decode_responses=True)
async def get_instructions(sip_uri):
key = "agent_instructions:" + sip_uri.strip().lower()
r = await get_redis()
cached = await r.get(key)
if cached:
return json.loads(cached)
# fetch remote instructions (HTTP or MCP)
instructions = await fetch_instructions_from_remote(sip_uri)
await r.set(key, json.dumps(instructions), ex=TTL_SECONDS)
return instructions
• For Node.js use ioredis or node-redis with the remote URL and password similarly.
If you cannot open persistent Redis connections (firewall or platform limits)
• Use an HTTP-friendly cache/API:
◦ Upstash has an HTTP REST API for Redis operations (suitable for serverless environments).
◦ Build a thin HTTP cache microservice (hosted on your cloud provider) that your agents call to get/set cached instructions; that service connects to Redis/DB.
• Use a durable DB (DynamoDB, Postgres) directly and add application-level TTL/version checks.
Security & ops reminders
• Use strong auth (password/ACL tokens) and TLS for remote Redis.
• Restrict access: IP allowlists, VPC peering, or private networking where possible.
• Monitor connection counts, memory usage, hit/miss metrics.
• Choose a TTL appropriate for how frequently instructions change; implement explicit invalidation webhooks if you need immediate updates.
Next step I can help with
• Give a concrete runnable example for your runtime (async Python, Node.js, or Go) using a specific managed provider (Upstash, AWS ElastiCache, Redis Cloud).
• Show how to implement connection pooling, retry/backoff, and a simple refresh-lock pattern for stampede protection.
Which runtime and managed cache provider would you like me to use for the sample?acoustic-pharmacist-34178
02/23/2026, 10:34 PMtall-airline-16756
02/23/2026, 10:34 PMctx.proc.userdata) for repeated accesses inside a session to avoid any network call after the first read.
• Prewarm frequently used entries (load hot instructions into the process on startup) to avoid first-request latency.
• Reduce RTTs: batch reads where possible or use small keys/values to reduce payload size.
• Use TTL + proactive refresh: serve cached value while a background refresh updates the cache so clients never block waiting on origin fetch.
• If persistent connections are not possible, consider an HTTP cache layer (a small cache microservice near your agents) that keeps a pooled Redis connection — agents call that local HTTP endpoint with low latency.
Concrete patterns you can use
• Fast path on join:
1) Check session-local userdata (zero network). 2) If missing, GET from Redis (single fast network call over a pooled connection). 3) If Redis miss, trigger remote fetch and write result to Redis (use lock to avoid stampede).
• Background refresh:
◦ On cache hit with TTL nearing expiry, return current value and spawn an async background refresh to update Redis. This keeps agent startup fast.
• Local HTTP cache proxy:
◦ Run a tiny cache service colocated with agents that exposes a simple HTTP API and keeps a single pooled Redis connection. This avoids per-agent Redis connections in constrained environments.
Questions to recommend a concrete setup
• What latency target do you need for instruction retrieval on participant join (e.g., <50 ms, <200 ms)?
• Which runtime are you using (async Python, Node.js, Go)?
• Which managed cache providers can you use / prefer (Upstash, AWS ElastiCache, Redis Cloud)?
• Can you place the cache in the same cloud region as your LiveKit agents?
If you tell me those, I’ll give a specific architecture and a runnable example (client pooling, prewarm + session-local caching, and a background refresh pattern) tuned to your constraints.