silly-petabyte-50668
12/20/2025, 7:50 PMtall-airline-16756
12/20/2025, 7:51 PMtall-airline-16756
12/20/2025, 7:51 PMsession.generate_reply() or session.say() in on_enter, timers, or other hooks).
• Keep your agent instructions focused on replying only to the current user input and not prompting further conversation.
• Trigger responses only from explicit user events (incoming messages, audio input, or your app’s explicit call).
This control is available in both the Python and Node.js SDKs, so you can implement a reactive-only agent as you move from ElevenLabs. If you want, tell me which SDK you plan to use and I’ll give a short code example showing a reactive-only setup.
Sources: Agent speech and audio | LiveKit docssilly-petabyte-50668
12/20/2025, 7:55 PMtall-airline-16756
12/20/2025, 7:55 PMsilly-petabyte-50668
12/20/2025, 7:57 PMtall-airline-16756
12/20/2025, 7:57 PMsilly-petabyte-50668
12/20/2025, 7:58 PMtall-airline-16756
12/20/2025, 7:59 PM// Reactive agent + Speechmatics diarization (Node.js)
// Sketch — adapt imports/paths to your project
import { voice } from '@livekit/agents';
import * as speechmatics from '@livekit/agents-plugin-speechmatics';
// Create STT with diarization enabled
const stt = new speechmatics.STT({
apiKey: process.env.SPEECHMATICS_API_KEY,
enableDiarization: true, // enable diarization
speakerActiveFormat: '<{speaker_id}>{text}<!--{speaker_id}-->',
});
// Create session in manual/reactive mode: the agent will not auto-start replies.
// turnDetection 'auto' still works but we set manual control to ensure no unsolicited speech.
const session = new voice.AgentSession({
stt,
turnDetection: 'manual', // prevents automatic turn-generation by the agent
// ... include your llm, tts, vad, etc.
});
// Prevent agent from speaking unless explicitly triggered
// (No on_enter or timers that call session.say()/generate_reply()).
// Hook: when STT produces a transcription with diarization
session.on('stt.transcript', async (event) => {
// event.text will include speaker tags per speakerActiveFormat, e.g. "<s1>Hello</s1>"
const diarizedText = event.text;
const speakerId = event.speakerId ?? extractSpeakerId(diarizedText); // depends on plugin output
const audioSegment = event.audioSegment; // audio tied to this transcript segment (if provided)
// 1) Optional: call external speaker-recognition to map speakerId -> known identity
// If unknown, store a persistent internal id mapping and keep track of metadata
const identity = await callSpeakerRecognitionService(audioSegment, speakerId);
if (identity) {
// attach identity metadata to session/memory
session.memory?.setSpeakerMeta(speakerId, { identity });
} else {
// assign or update a temporary label/description
const inferredRole = await inferRoleForSpeaker(diarizedText, speakerId);
session.memory?.setSpeakerMeta(speakerId, { role: inferredRole });
}
// 2) Decide whether this segment should trigger the agent to reply.
// Example policy: agent only replies when directly addressed or when a trigger phrase appears.
const addressed = isDirectlyAddressed(diarizedText); // implement this check
if (addressed) {
// Commit user turn and generate reply once — agent will speak only now
session.commitUserTurn({ speakerId, text: diarizedText });
// session.generate_reply() or session.say() will be invoked by commitUserTurn flow
} else {
// Do nothing — agent remains silent
// Optionally store the user turn for context without invoking a reply
session.addContext({ speakerId, text: diarizedText });
}
});
// Utility placeholders — implement per your stack
function extractSpeakerId(text) {
const m = text.match(/<(\w+)>/);
return m ? m[1] : 'unknown';
}
async function callSpeakerRecognitionService(audioSegment, speakerId) {
// send audioSegment to external speaker-ID service
// return identity object or null
return null;
}
async function inferRoleForSpeaker(text, speakerId) {
// call your LLM with diarized conversation snippets to infer role
// return string like "interviewer", "customer", etc.
return 'participant';
}
function isDirectlyAddressed(text) {
// Basic example: check for agent name or direct address phrases
const triggers = ['hey assistant', 'assistant,', 'you okay', 'can you'];
const lower = text.toLowerCase();
return triggers.some(t => lower.includes(t));
}
Key notes
• turnDetection: 'manual' ensures the agent won’t auto-initiate speech; you control commits that trigger replies.
• Use diarized STT output to separate speakers (S1, S2) and collect audio segments per speaker for external speaker-recognition.
• Only call session.commitUserTurn() / session.generate_reply() when you want the agent to respond. Otherwise store context silently.
• Role inference and identity matching must be implemented outside LiveKit (LLM prompts, speaker-recognition APIs). Attach results to speaker metadata for later decisions.
If you’d like, I can:
• Provide a tighter push-to-talk example (user toggles mic).
• Draft an LLM prompt template to infer roles from diarized text.
• Show a sample flow that batches diarized audio and calls Azure/Amazon speaker-recognition. Which would you prefer next?silly-petabyte-50668
12/20/2025, 8:02 PMtall-airline-16756
12/20/2025, 8:03 PMsilly-petabyte-50668
12/20/2025, 8:04 PMtall-airline-16756
12/20/2025, 8:04 PM