bulky-afternoon-12213
02/17/2026, 4:35 PMtall-airline-16756
02/17/2026, 4:35 PMtall-airline-16756
02/17/2026, 4:36 PMBVC() or BVCTelephony() depending on participant type) for agent processing when you want the agent to receive cleaned audio before STT.
• Self-hosted
◦ Agents run on your infrastructure (Kubernetes, Docker, etc.). You provide the runtime resources.
◦ You must manage the Dockerfile, scaling, environment variables, monitoring, and resource sizing to accommodate CPU used by BVC.
◦ Use BVCTelephony() for SIP/phone participants and BVC() for WebRTC participants to get optimal results for each audio type.
◦ Agents register with LiveKit over WebSocket — no inbound ports required for the server.
• Hybrid (frontend vs agent-side processing)
◦ Frontend (trunk) cancellation: LiveKit can run Krisp-based noise cancellation on inbound SIP trunks (krisp_enabled: true) so callers’ audio is cleaned before being forwarded to room participants.
◦ Important: Do not run Krisp frontend cancellation and agent-side BVC at the same time. Double-processing can degrade audio quality and cause STT errors.
◦ Recommendation: Choose one strategy:
▪︎ If you need to remove background conversations (non-primary speakers) for STT/agent logic, prefer agent-side `BVC()`/`BVCTelephony()`.
▪︎ If you only need general noise suppression on caller audio and want it applied as early as possible, use Krisp on the trunk and keep agent-side cancellation off.
Configuration & compatibility notes
• Participant matching:
◦ Use BVC() for WebRTC participants.
◦ Use BVCTelephony() for SIP/telephony participants (optimized for 8kHz).
• How it’s enabled:
◦ Filters are set per participant in agent session setup (e.g., via `RoomOptions`/session configuration) so you can dynamically select filters depending on participant type.
• Avoid double-processing:
◦ If you enable frontend krisp_enabled for trunk inbound, disable agent-side BVC to prevent audio artifacts and STT inaccuracies.
If you want next steps
• Tell me whether your agents receive audio from SIP callers, WebRTC users, or both.
• Tell me which deployment you plan to use (self-hosted, hybrid, or LiveKit Cloud).
• I can then give:
◦ A short code snippet showing how to apply BVC() vs BVCTelephony() per participant.
◦ Recommendations for CPU sizing and monitoring for agent hosts.
◦ Example session/RoomOptions settings for enabling noise cancellation.
Would you like an example configuration for SIP agents, WebRTC agents, or a hybrid flow?
Sources: Choosing the right noise cancellation for your agent | LiveKit | Agent deployment overview | LiveKit Documentation | Voice AI quickstart | LiveKit Documentation | Self-hosted deployments | LiveKit Documentation | sip | livekit/livekit_composite | DeepWikibulky-afternoon-12213
02/17/2026, 4:37 PM[Agent] Prewarmed in ${Date.now() - t0}ms — VAD model cached, MongoDB connected);
},
entry: async (ctx: JobContext) => {
// Wait for the agent to be connected to the room
await ctx.connect();
// Extract metadata from the dispatch/job (embedded in token's RoomAgentDispatch)
const rawMetadata = ctx.job.metadata ?? '{}';
let metadata: DispatchMetadata;
try {
metadata = JSON.parse(rawMetadata);
} catch {
console.error('[Agent] Failed to parse dispatch metadata:', rawMetadata);
metadata = {
channel: 'web',
user_id: 'unknown',
session_id: session_fallback_${Date.now()},
};
}
const { channel, user_id, session_id } = metadata;
const roomName = ctx.room.name ?? 'unknown_room';
console.log(
[Agent] Joined room: ${roomName} | Channel: ${channel} | User: ${user_id} | Session: ${session_id}
);
// Fetch instructions and config fresh from MongoDB every session (parallel)
let instructions = DEFAULT_INSTRUCTIONS;
const [dbInstructions, agentConfig] = await Promise.all([
getAgentInstructions().catch(() => null),
getAgentConfig(),
]);
if (dbInstructions) instructions = dbInstructions;
const { stt, llm, tts, voice: voiceCfg } = agentConfig;
console.log(
[Agent] Config: STT=${stt.model} | LLM=${llm.model} | TTS=${tts.provider}/${tts.model} | VAD silence=${voiceCfg.minSilenceDuration}ms | Endpointing=${voiceCfg.minEndpointingDelay}-${voiceCfg.maxEndpointingDelay}ms
);
const sessionContext = createSessionContext(session_id, roomName, channel, user_id);
// Create session document in MongoDB (fire-and-forget)
const modelsUsed = {
stt: stt.model,
llm: llm.model,
tts: ${tts.provider}/${tts.model},
};
initSession(sessionContext, { models_used: modelsUsed }).catch((err) =>
console.error('[Logging] Failed to create session:', err)
);
// Build VAD per-session with configured minSilenceDuration (ONNX model already cached from prewarm)
const sessionVad = await silero.VAD.load({
minSilenceDuration: voiceCfg.minSilenceDuration,
});
const agent = new voice.Agent({
instructions,
// tools: { searchKnowledgeBase }, // temporarily disabled
});
const session = new voice.AgentSession({
vad: sessionVad,
stt: stt.model,
llm: llm.model,
tts: buildTts(tts),
turnDetection: voiceCfg.turnDetectorEnabled
? new livekit.turnDetector.MultilingualModel()
: undefined,
voiceOptions: {
preemptiveGeneration: voiceCfg.preemptiveGeneration,
minEndpointingDelay: voiceCfg.minEndpointingDelay,
maxEndpointingDelay: voiceCfg.maxEndpointingDelay,
allowInterruptions: voiceCfg.allowInterruptions,
minInterruptionDuration: voiceCfg.minInterruptionDuration,
minInterruptionWords: voiceCfg.minInterruptionWords,
},
});
session.on(voice.AgentSessionEventTypes.UserInputTranscribed, (ev) => {
if (ev.isFinal && ev.transcript) {
logUserSpeech(sessionContext, ev.transcript).catch((err) =>
console.error('[Logging] Failed to log user speech:', err)
);
}
});
session.on(voice.AgentSessionEventTypes.ConversationItemAdded, (ev) => {
const item = ev.item;
if ('role' in item && item.role === 'assistant') {
const text = item.textContent;
if (text) {
logAgentResponse(sessionContext, text).catch((err) =>
console.error('[Logging] Failed to log agent response:', err)
);
}
}
});
// Track agent state → 'speaking' for total TTFV measurement
session.on(voice.AgentSessionEventTypes.AgentStateChanged, (ev) => {
if (ev.newState === 'speaking') {
markAgentSpeaking(sessionContext);
}
});
// Collect per-component latency metrics (EOU, LLM, TTS)
session.on(voice.AgentSessionEventTypes.MetricsCollected, (ev) => {
collectMetrics(sessionContext, ev.metrics);
});
session.on(voice.AgentSessionEventTypes.Close, () => {
console.log([Agent] Session closed: ${session_id});
logSessionEnd(sessionContext).catch((err) =>
console.error('[Logging] Failed to log session end:', err)
);
});
// Start the voice agent session with background voice cancellation
await session.start({
agent,
room: ctx.room,
inputOptions: {
noiseCancellation: BackgroundVoiceCancellation(),
},
});
// Greet the user
session.generateReply({
instructions: 'Greet the user warmly and ask how you can help them today. Do not use any tools for this greeting.',
});
console.log([Agent] Session started, greeting sent for session: ${session_id});
},
});
// Register with explicit agent name and start the CLI
cli.runApp(
new WorkerOptions({
agent: fileURLToPath(import.meta.url),
agentName: process.env.AGENT_NAME || 'customer-service',
})
);
is this snippet correct?
We have agent worker service deployed on railwaytall-airline-16756
02/17/2026, 4:38 PMdotenv and file URL utilities — that approach is fine for local and most containerized environments, as long as the .env path exists in Railway build/runtime.
• Prewarm and session setup: You prewarm the VAD and MongoDB connection, then create per-session VAD with configured minSilenceDuration — that’s correct and efficient.
• Agent/Session construction: You create voice.Agent and voice.AgentSession with STT/LLM/TTS config and VAD, plus event handlers for transcription, conversation items, metrics, and session lifecycle — this follows expected patterns and will surface useful logs.
• Background voice cancellation: You enable it via inputOptions: { noiseCancellation: BackgroundVoiceCancellation() } when starting the session — that is the correct place to apply agent-side BVC so audio is filtered before STT/agent processing.
• Per-channel logic and TTS builder: The buildTts function and dynamic selection of models/providers looks consistent with how the agent should initialize TTS for sessions.
• Telephony vs WebRTC: You already plan to use telephony-optimized filters (BVCTelephony()) when appropriate — good to match filter type to audio bandwidth.
Small issues & things to verify
• Escaped identifiers in your pasted snippet:
◦ I see DEFAULTINSTRUCTIONS, DEFAULTINSTRUCTIONS backslashes, and DEFAULTINSTRUCTIONS style escaping in the snippet you pasted. Ensure in your actual source files the identifier names are plain (e.g., DEFAULT_INSTRUCTIONS) and not escaped by copy-paste/markdown artifacts.
• __dirname declaration:
◦ You recreate __dirname as const __dirname = dirname(fileURLToPath(import.meta.url)); — that’s correct for ESM, but ensure you’re using Node with ESM and not mixing CommonJS.
• BackgroundVoiceCancellation import and use:
◦ You import BackgroundVoiceCancellation from @LiveKit/noise-cancellation-node and call it with no args. Confirm that BackgroundVoiceCancellation() is the intended factory function and that any optional config (if supported) is not required in your environment.
• Tools commented out:
◦ tools: { searchKnowledgeBase } is commented out. If you plan to enable the knowledge-base tool later, remember to re-add it and ensure permission/credentials are available.
• Error handling around getAgentConfig() and `getAgentInstructions()`:
◦ You catch getAgentInstructions() failure but not getAgentConfig() — consider wrapping getAgentConfig() with a catch and default fallback to avoid runtime throws.
• MongoDB connection prewarm:
◦ You await connectToMongoDB() as part of the Promise.all but only destructure [vad]. This still calls the function, but if you intended to capture the DB client, destructure both values or rely on connectToMongoDB() side effects. Current code is valid if connection is handled internally and stored globally.
• Railway specifics — filesystem and environment:
◦ Railway may not persist a local .env file in production. Ensure you set env variables via Railway’s environment variable UI instead of relying on .env at runtime.
◦ Railway ephemeral filesystem: if your prewarm caches models on disk, verify the path and persistence expectations — cached files are ephemeral across restarts.
• CPU / resource sizing:
◦ BVC and VAD consume CPU. On Railway, ensure the selected plan/container has sufficient CPU for real-time processing; otherwise you may see increased latency or dropped audio.
• Logging and sensitive data:
◦ Avoid logging secrets. The code logs model names — that’s OK — but ensure environment keys (API keys, DB URIs) are not logged anywhere.
Practical checks to run now
1. Start locally with the same NODE_ENV and env variables Railway will provide to confirm the code runs and BVC initializes without error.
2. On Railway:
◦ Add required env vars in Railway’s project settings (DB URI, API keys, AGENT_NAME, etc.).
◦ Ensure your service has adequate CPU/memory for real-time audio processing.
1. Watch logs for these startup signs:
◦ "[Agent] Prewarmed..." (VAD cached, MongoDB connected)
◦ "[Agent] Joined room: ..." after token/dispatch
◦ "[Agent] Session started, greeting sent..." after session.start and generateReply
1. If you see BVC-specific errors, capture the full error text and we can debug config vs runtime issues.
Questions to help me confirm fully
• Are you seeing any specific runtime errors on Railway (startup failures, missing module errors, or runtime exceptions)? If yes, paste the exact log lines.
• Are your participants primarily SIP (telephone) or WebRTC? I can confirm whether to call BVCTelephony() instead or change selection logic.
• Do you want a small example snippet showing how to use BVCTelephony() vs BVC() dynamically per participant type?
• Do you cache model files to disk during prewarm, or do you rely purely on in-memory downloads? (Railway ephemeral storage matters if you expect persisted caches.)
If you share any Railway logs or confirm the participant types, I’ll give a targeted checklist or a short patch for anything that needs changing.bulky-afternoon-12213
02/17/2026, 4:38 PMtall-airline-16756
02/17/2026, 4:39 PMBackgroundVoiceCancellation() is created:
▪︎ "BVC initialized: BackgroundVoiceCancellation() called"
◦ If the factory returns an object, log its type or properties.
• Place a try/catch around the creation so you log any error immediately:
◦ On success: log the instance and the session/room id.
◦ On failure: log the full error stack so you can see missing modules or permission issues.
2) Observe runtime logs during session.start
• Confirm these lines appear in your logs:
◦ "[Agent] Prewarmed..." (VAD cached, DB connected)
◦ "[Agent] Session started, greeting sent..." (session started)
• Add an extra log where you pass inputOptions so you see a line like:
◦ "Starting session with noiseCancellation=BackgroundVoiceCancellation"
• If you see initialization logs and no errors, BVC was at least created and attached to the session.
3) Monitor CPU usage and process metrics
• BVC runs on agent CPU. While it’s low-latency, enabling it increases CPU during audio processing.
• Compare CPU while processing test audio with BVC enabled vs disabled. A measurable increase during voice activity indicates the filter is running.
• On Railway:
◦ Ensure the container shows non-idle CPU during calls.
◦ If you have app-level metrics, log CPU at session start and during active voice frames.
4) Use VAD / pipeline metrics to confirm audio flows through the pipeline
• You already wire VAD and have a MetricsCollected handler. Use those events to ensure audio frames are being processed.
• Track counts/timestamps for:
◦ VAD voice frames detected
◦ STT requests sent
◦ Any per-frame latency
• If frames are processed and STT calls are being made only for primary speech, that’s evidence BVC is filtering as expected.
5) Behavioral audio tests (most reliable functional check)
• Prepare a controlled test with a primary speaker + background speaker/noise.
• Run 3 runs:
a. No cancellation (disable BVC) — observe STT transcripts; background voice should appear.
b. With BVC enabled — background voice should be suppressed from transcript.
c. With frontend Krisp + agent-BVC both enabled — you may see degraded audio; this confirms double-processing issues.
• If transcripts show background speech in (1) but not in (2), BVC is working.
6) Add short diagnostic audio capture (optional, for deeper debugging)
• Capture a short sample of the pre- and post-BVC audio buffers (small, encrypted, and stored securely) to compare what the STT receives vs original input.
• Use only in tests, and ensure you follow data protection rules.
7) Avoid double-processing
• Confirm krisp_enabled is false on SIP trunks if using agent-side BVC. Double-processing can reduce quality and produce wrong STT results.
Practical code snippets to add
• Add an init log and safe creation:
let noiseFilter;
try {
noiseFilter = BackgroundVoiceCancellation();
console.log('[Agent] BVC initialized', { filter: !!noiseFilter });
} catch (err) {
console.error('[Agent] Failed to init BVC', err);
}
...
await session.start({
agent,
room: ctx.room,
inputOptions: {
noiseCancellation: noiseFilter,
},
});
console.log('[Agent] session.start called with BVC:', !!noiseFilter);
What to check in Railway logs / runtime
• Look for the BVC init log and session.start logs.
• Check for runtime errors related to the noise-cancellation module.
• Watch CPU consumption during active calls and compare to a baseline without BVC.
If you want, I can:
• Provide a short test script/instructions you can run to generate the three audio runs (no BVC, BVC, double-processed) and compare STT output.
• Show a minimal snippet that captures pre/post audio frames for a one-second test (securely) so you can audibly confirm filtering.
Which would help most: a test script for STT comparison, or the minimal pre/post capture snippet? And are your participants SIP, WebRTC, or both?
Sources: Choosing the right noise cancellation for your agent | LiveKit | VAD Metrics | LiveKit Documentation