late-glass-71191
12/21/2025, 7:51 PMtall-airline-16756
12/21/2025, 7:51 PMtall-airline-16756
12/21/2025, 7:52 PMlk.transcription text stream topic.
• Transcription works for audio from all sources routed through the LiveKit session, including calls accepted via the SIP/Twilio integration.
• You can capture both user speech (finalized transcripts) and agent TTS transcripts in a Node.js agent.
Quick Node.js examples 1) Listen for user transcripts:
session.on('user_input_transcribed', (event) => {
if (event.is_final) {
console.log('User transcript:', event.transcript);
}
});
2) Capture agent (TTS) transcripts via a `transcriptionNode`:
async transcriptionNode(
text: ReadableStream<string>,
modelSettings: voice.ModelSettings,
): Promise<ReadableStream<string> | null> {
return new ReadableStream({
start(controller) {
const reader = text.getReader();
const pump = async () => {
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
console.log('Agent transcript:', value);
controller.enqueue(value);
pump();
};
pump();
}
});
}
Next steps / questions
• Do you want transcripts saved to a file, sent to a database, or streamed elsewhere in real time?
• Are you using LiveKit’s Agent SDK (Node.js) already, or do you need sample project setup instructions?
Tell me which option you prefer and I’ll provide the exact code/snippets to store or forward transcripts.
Sources: Accepting calls overview | LiveKit docs | Agent session | LiveKit docs | Text and transcriptions | LiveKit docs | Pipeline nodes and hooks | LiveKit docs | Agents (Node.js) | livekit/livekit_composite | DeepWikilate-glass-71191
12/21/2025, 7:53 PMtall-airline-16756
12/21/2025, 7:54 PMuser_input_transcribed events (final transcripts flagged with is_final).
• Agent speech (TTS): available via the agent’s transcriptionNode stream.
Examples
1) Capture finalized user transcripts (log, save, or forward)
// session is your LiveKit Agent session
session.on('user_input_transcribed', (event) => {
if (event.is_final) {
const text = event.transcript;
const metadata = {
participantId: event.participant?.participant_id,
timestamp: Date.now(),
source: 'user',
};
// Example: log or save
console.log('User transcript:', text, metadata);
// saveTranscript(text, metadata);
}
});
2) Capture agent (TTS) transcripts via transcriptionNode
async transcriptionNode(
text: ReadableStream<string>,
modelSettings: voice.ModelSettings,
): Promise<ReadableStream<string> | null> {
return new ReadableStream({
start(controller) {
const reader = text.getReader();
const pump = async () => {
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
const transcript = value;
const metadata = { participantId: 'agent', timestamp: Date.now(), source: 'agent' };
console.log('Agent transcript:', transcript, metadata);
// saveTranscript(transcript, metadata);
controller.enqueue(value);
pump();
};
pump();
}
});
}
Simple storage options
• File: append lines to a file with timestamps.
• Database: insert rows into Postgres/Mongo with participant, timestamp, session_id.
• Webhook/stream: POST each finalized transcript to an external API in real time.
Implementation tips
• Only persist when is_final is true for user transcripts to avoid duplicates/partials.
• Add session/room IDs and participant IDs to your saved records for later lookup.
• If you need ordered conversation logs, store an incrementing turn index or use timestamps and sort on read.
• For compliance/retention, consider redaction or retention policies before saving.
Would you like:
• a complete example that writes transcripts to a file or to Postgres?
• code that forwards transcripts to a webhook in real time?
Tell me which storage option you prefer and I’ll provide the exact snippet.late-glass-71191
12/21/2025, 8:05 PMtall-airline-16756
12/21/2025, 8:05 PMimport { voice } from '@livekit/agents';
export default defineAgent({
entry: async (ctx) => {
const transcriptBuffer: { role: 'user'|'agent'; content: string; timestamp: string }[] = [];
// capture finalized user STT
ctx.session.on('user_input_transcribed', (ev) => {
if (ev.is_final) {
transcriptBuffer.push({
role: 'user',
content: ev.transcript,
timestamp: new Date().toISOString(),
});
}
});
// collect agent replies later from agent.chatContext or capture on conversation events
const agent = new voice.Agent({ instructions: 'You are a helpful assistant.' });
await ctx.session.start({ agent, room: ctx.room });
// shutdown hook runs AFTER session ends — persist here
ctx.addShutdownCallback(async () => {
// pull assistant messages from chatContext
const agentReplies = agent.chatContext.messages
.filter(m => m.role === 'assistant' && !m.type)
.map(m => ({
role: 'agent',
content: Array.isArray(m.content) ? m.content.join(' ') : String(m.content),
timestamp: new Date().toISOString(),
}));
const allTranscripts = [...transcriptBuffer, ...agentReplies];
if (allTranscripts.length === 0) return;
try {
// replace with your DB logic (transactional insert recommended)
await saveTranscriptsToDatabase(allTranscripts);
} catch (err) {
console.error('Failed to save transcripts on shutdown:', err);
// optionally emit to logging/alerting for later retry
}
});
await ctx.connect();
},
});
Key tips
• Only persist user_input_transcribed items when is_final is true to avoid partials.
• Use agent.chatContext at shutdown to get the agent’s final replies (avoids reconstructing streamed pieces).
• Ensure your process has enough memory for buffering; for long calls or high volume, consider a temporary on-disk buffer or short-lived persisted queue.
• Wrap DB writes in try/catch and log failures for retries — shutdown callbacks have limited time to complete.
If you want, I can:
• Provide a full example that writes to Postgres (Prisma/pg) or MongoDB.
• Show how to include session/room/participant IDs and an ordered turn index in the saved records.
Which DB and format would you like the example for?
Sources: Job lifecycle | LiveKit docslate-glass-71191
12/21/2025, 9:38 PMtall-airline-16756
12/21/2025, 9:38 PMvoice.AgentSessionEventTypes.UserInputTranscribed and use voice.AgentSessionEventTypes.Close to persist on session end:
import { voice } from '@livekit/agents';
const buffer: { role: 'user'|'agent'; content: string; timestamp: string }[] = [];
// user STT (only finalize on isFinal)
session.on(
voice.AgentSessionEventTypes.UserInputTranscribed,
(ev) => {
if (ev.isFinal) {
buffer.push({ role: 'user', content: ev.transcript, timestamp: new Date().toISOString() });
}
}
);
// session end — persist buffered transcripts
session.on(
voice.AgentSessionEventTypes.Close,
async () => {
if (buffer.length === 0) return;
try {
await saveTranscriptsToDatabase(buffer); // replace with your DB logic
} catch (err) {
console.error('Failed saving transcripts on Close:', err);
}
}
);
Notes:
• The event constant names are typed; using them prevents the TS error.
• Gather agent replies from agent.chatContext at shutdown if you didn’t buffer agent messages during the session.
• Ensure saveTranscriptsToDatabase is async-safe and handles retries/logging inside the Close handler.
Would you like a full example that also includes pulling assistant messages from agent.chatContext and inserting into Postgres (Prisma) or MongoDB?
Sources: Agents (Node.js) | livekit/livekit_composite | DeepWiki | Agent session | LiveKit docslate-glass-71191
12/21/2025, 10:23 PMtall-airline-16756
12/21/2025, 10:24 PMagent.chatContext.messages and extract assistant messages — this is simplest and avoids streaming reconstruction.
Recommended pattern (concise example)
import { voice } from '@livekit/agents';
const buffer: { role: 'user'|'agent'; content: string; timestamp: string }[] = [];
// User transcripts (final only)
session.on(voice.AgentSessionEventTypes.UserInputTranscribed, (ev) => {
if (ev.isFinal) {
buffer.push({ role: 'user', content: ev.transcript, timestamp: new Date().toISOString() });
}
});
// Create agent
const agent = new voice.Agent({ instructions: 'You are a helpful assistant.' });
await session.start({ agent, room: ctx.room });
// On session end, pull agent replies from chatContext and save all
session.on(voice.AgentSessionEventTypes.Close, async () => {
// extract assistant messages
const agentReplies = agent.chatContext.messages
.filter(m => m.role === 'assistant' && !m.type) // text-only messages
.map(m => ({
role: 'agent',
content: Array.isArray(m.content) ? m.content.join(' ') : String(m.content),
timestamp: new Date().toISOString(),
}));
const allTranscripts = [...buffer, ...agentReplies];
if (allTranscripts.length === 0) return;
try {
await saveTranscriptsToDatabase(allTranscripts); // your DB logic
} catch (err) {
console.error('Failed saving transcripts on Close:', err);
}
});
Notes and best practices
• Using agent.chatContext at shutdown is reliable: it contains finalized assistant messages, so you won’t need to reconstruct streamed fragments.
• If you require immediate ordering, add a turn index when buffering user items and when mapping assistant messages.
• For large sessions or high volume, consider writing buffered items to a temporary store (disk or cache) rather than keeping everything in memory.
• Always guard DB writes in try/catch and log failures for retry.
Would you like a ready-to-run example that writes these combined records into Postgres (Prisma) or MongoDB?late-glass-71191
12/21/2025, 10:25 PMtall-airline-16756
12/21/2025, 10:26 PMAgent doesn't expose chatContext. Use the session's conversation history instead. Session exposes session.history (or session.chatContext on the session object) which contains all conversation items (user and assistant messages). Buffer user STT via the typed events, then on session Close read session.history to get assistant replies and persist everything.
Concise example (TypeScript):
import { voice } from '@livekit/agents';
type TranscriptItem = { role: 'user'|'agent'; content: string; timestamp: string };
const buffer: TranscriptItem[] = [];
// buffer finalized user transcripts
session.on(
voice.AgentSessionEventTypes.UserInputTranscribed,
(ev) => {
if (ev.isFinal) {
buffer.push({ role: 'user', content: ev.transcript, timestamp: new Date().toISOString() });
}
}
);
// start agent as usual
const agent = new voice.Agent({ instructions: 'You are a helpful assistant.' });
await session.start({ agent, room: ctx.room });
// on session end, read session.history and persist
session.on(
voice.AgentSessionEventTypes.Close,
async () => {
// session.history contains conversation items (messages, tool calls, etc.)
const assistantItems = (session.history ?? [])
.filter(item => item.type === 'message' && item.role === 'assistant')
.map(item => ({
role: 'agent',
content: Array.isArray(item.content) ? item.content.join(' ') : String(item.content),
timestamp: (item.created_at ?? new Date().toISOString()) as string,
}));
const all = [...buffer, ...assistantItems];
if (all.length === 0) return;
try {
await saveTranscriptsToDatabase(all); // implement DB insert here
} catch (err) {
console.error('Failed to save transcripts on Close:', err);
}
}
);
Notes:
• Use voice.AgentSessionEventTypes.UserInputTranscribed and Close to avoid TS type errors.
• session.history contains finalized assistant messages; no need for agent.chatContext.
• If session.history is undefined in your SDK version, upgrade to v1.2.0+ or read conversation items via conversation_item_added events during the session and buffer them.
• Add a turn index or timestamps if you need strict ordering.
Want a full example that writes these records to Postgres (Prisma) or MongoDB? Which DB do you use?
Sources: Agent session | LiveKit docs | Agents and handoffs | LiveKit docs | Agents (Node.js) | livekit/livekit_composite | DeepWikilate-glass-71191
12/22/2025, 8:23 AMtall-airline-16756
12/22/2025, 8:24 AMChatContext like an array. session.chatContext (or session.history) is an object that contains a messages array. Use .messages and then filter/map that array.
Corrected TypeScript example (collect users during session, read assistant messages at shutdown):
import { voice } from '@livekit/agents';
type TranscriptItem = { role: 'user'|'agent'; content: string; timestamp: string };
const buffer: TranscriptItem[] = [];
// buffer finalized user transcripts
session.on(
voice.AgentSessionEventTypes.UserInputTranscribed,
(ev) => {
if (ev.isFinal) {
buffer.push({ role: 'user', content: ev.transcript, timestamp: new Date().toISOString() });
}
}
);
// start agent
const agent = new voice.Agent({ instructions: 'You are a helpful assistant.' });
await session.start({ agent, room: ctx.room });
// on session end, read session.chatContext.messages (or session.history) and persist
session.on(
voice.AgentSessionEventTypes.Close,
async () => {
// prefer session.chatContext.messages if available
const messages = (session.chatContext?.messages ?? session.history ?? []) as any[];
const assistantItems = messages
.filter(item => item.role === 'assistant' && item.type === 'message')
.map(item => ({
role: 'agent',
content: Array.isArray(item.content) ? item.content.join(' ') : String(item.content),
timestamp: (item.created_at ?? new Date().toISOString()) as string,
}));
const all = [...buffer, ...assistantItems];
if (all.length === 0) return;
try {
await saveTranscriptsToDatabase(all); // your DB logic
} catch (err) {
console.error('Failed to save transcripts on Close:', err);
}
}
);
Notes:
• Use session.chatContext?.messages — chatContext is an object; messages is the array.
• If your SDK version lacks chatContext, session.history is an alternative that contains conversation items.
• Ensure you check item type === 'message' (some items are tool calls/metadata).
• If you want, tell me which SDK version and DB you’re using and I’ll give a runnable example for Prisma/Postgres or MongoDB.