astonishing-accountant-25155
12/24/2025, 4:29 AMconst {
WorkerOptions,
cli,
defineAgent,
voice,
llm,
initializeLogger,
} = require('@livekit/agents');
const openai = require('@livekit/agents-plugin-openai');
const silero = require('@livekit/agents-plugin-silero');
const { RoomServiceClient } = require('livekit-server-sdk');
// Load environment variables
const livekitUrl = process.env.LIVEKIT_URL || '<ws://localhost:7880>';
const livekitApiKey = process.env.LIVEKIT_API_KEY;
const livekitApiSecret = process.env.LIVEKIT_API_SECRET;
// Verify required environment variables
if (!livekitApiKey || !livekitApiSecret) {
console.error('❌ Missing required environment variables: LIVEKIT_API_KEY, LIVEKIT_API_SECRET');
process.exit(1);
}
// OpenAI API requires OPENAI_API_KEY
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!openaiApiKey) {
console.error('❌ Missing OPENAI_API_KEY - required for OpenAI');
process.exit(1);
}
console.log('✅ OpenAI API key configured');
const roomServiceClient = new RoomServiceClient(
livekitUrl,
livekitApiKey,
livekitApiSecret,
);
// Initialize logger
initializeLogger({
level: process.env.LIVEKIT_LOG_LEVEL || 'warn',
pretty: process.env.NODE_ENV === 'development'
});
// Preload VAD model
let cachedVAD = null;
const loadVAD = async () => {
if (!cachedVAD) {
cachedVAD = await silero.VAD.load();
console.log('✅ VAD model preloaded');
}
return cachedVAD;
};
// Preload VAD at startup
loadVAD().catch(err => {
console.error('⚠️ Failed to preload VAD:', err.message);
});
const SYSTEM_MESSAGE = `You are Tike Mrapp, the original host and creator of 'Um, Actually.'
Your mission is to deliver the quintessential 'Um, Actually' experience to people who call in to your show.
You will provide a series of statements about various aspects of 'nerd' culture—video games, movies, comics, fantasy, sci-fi, etc. Each statement will contain exactly one factual inaccuracy.
The caller must identify and correct that inaccuracy, starting their correction with a resounding 'Um, Actually...' to earn a point. If the caller's response does not begin with 'Um, Actually,' or if they provide an incorrect correction, you, Tike Mrapp, receive the point and the caller cannot attempt to correct that statement again.
Emphasize this rule: 'Um, Actually' is not just a suggestion, it's a requirement!
Maintain your signature enthusiastic, slightly chaotic, and pedantic hosting style.
Embrace tangents, celebrate the minutiae, and deliver dramatic pauses before revealing the correct answers.
Remember to use phrases like 'That's a good correction' or 'Oh, that's a deep cut!'
Keep responses SHORT - this is a voice conversation. One or two sentences at a time.
Rules:
- Do not offer the caller a chance to correct their answer.
- Do not offer the caller another round of the game.
- When asked about points, call pointsStatus function.
- When caller makes correct correction, call userPoints function.
- When caller makes incorrect correction or fails to say 'Um, Actually', call systemPoints function.
- When caller says goodbye or tries to end the call, call gameEnd function.
- When anyone accumulates three points, thank them and call gameEnd function.`;
const agent = defineAgent({
name: 'um-actually-agent',
entry: async (ctx) => {
try {
await ctx.connect();
console.log('✅ Connected to room:', ctx.room?.name);
const vad = await loadVAD();
// Create voice agent with OpenAI
const phoneAgent = new (class extends voice.Agent {
constructor() {
super({ instructions: SYSTEM_MESSAGE });
}
})();
// Use OpenAI for STT, LLM, TTS
// Use recoverable STT wrapper to prevent cleanup errors from killing the session
const stt = new openai.STT({ model: 'whisper-1' });
const llmInstance = new openai.LLM({ model: 'gpt-4o' });
const tts = new openai.TTS({ voice: 'alloy' });
console.log('🔧 Creating AgentSession...');
const session = new voice.AgentSession({
stt: stt,
llm: llmInstance,
tts,
vad,
max_tool_steps: 3
});
session.on('error', (error) => {
// Filter out expected cleanup errors (these occur during normal shutdown)
const errorMessage = error.error?.message || error.message || '';
const errorStack = error.error?.stack || error.stack || '';
const isCleanupError =
errorMessage.includes('Stream is closed') ||
errorMessage.includes('VADStream.endInput') ||
errorStack.includes('VADStream.endInput') ||
errorStack.includes('StreamAdapterWrapper') ||
(error.type === 'stt_error' && errorMessage.includes('closed'));
if (isCleanupError) {
// Silently ignore cleanup errors - these are expected during call termination
return;
}
console.error('❌ Session error:', error);
});
console.log('🚀 Starting session...');
await session.start({ agent: phoneAgent, room: ctx.room });
console.log('✅ Session started successfully');
// Send initial greeting
const greeting = "Welcome to Um, Actually! I'm your host, Tike Mrapp. Here's how this works: I'll give you a statement about pop culture, and it will contain one inaccuracy. Your job is to find it and correct me, but you MUST start your correction with 'Um, Actually.' Ready for your first statement?";
console.log('📢 Sending greeting...');
session.say(greeting, { addToChatCtx: true, allowInterruptions: true });
console.log('✅ Greeting sent');
// Wait for call to end
await new Promise((resolve) => {
let resolved = false;
const cleanup = () => {
if (resolved) return;
resolved = true;
console.log('🔌 Call ended, cleaning up');
resolve();
};
if (ctx.room) {
ctx.room.once('disconnected', (reason) => {
console.log(`🔌 Room disconnected. Reason: ${reason || 'unknown'}`);
cleanup();
});
const checkInterval = setInterval(() => {
if (!ctx.room || !ctx.room.isConnected) {
console.log('🔌 Room no longer connected');
clearInterval(checkInterval);
cleanup();
}
}, 5000);
setTimeout(() => {
console.log('⏰ Call timeout (20m)');
clearInterval(checkInterval);
cleanup();
}, 20 * 60 * 1000);
} else {
cleanup();
}
});
} catch (error) {
console.error('❌ Error in agent entry:', error);
console.error(' Stack:', error.stack);
}
},
});
module.exports = agent;
console.log('🚀 Starting Um Actually agent...');
if (require.main === module) {
process.argv = [process.argv[0], process.argv[1], 'start'];
cli.runApp(new WorkerOptions({ agent: __filename }));
}astonishing-accountant-25155
12/24/2025, 4:32 AMrefined-appointment-81829
12/24/2025, 12:43 PMrefined-appointment-81829
12/24/2025, 12:43 PMastonishing-accountant-25155
12/24/2025, 6:10 PM200 OK to answer the call, so Twilio eventually gives up and cancels."refined-appointment-81829
12/24/2025, 6:18 PMastonishing-accountant-25155
12/24/2025, 6:18 PMastonishing-accountant-25155
12/24/2025, 6:19 PMrefined-appointment-81829
12/24/2025, 6:20 PMastonishing-accountant-25155
12/24/2025, 6:21 PMrefined-appointment-81829
12/24/2025, 6:22 PMastonishing-accountant-25155
12/24/2025, 6:22 PMrefined-appointment-81829
12/24/2025, 6:23 PMastonishing-accountant-25155
12/24/2025, 6:26 PMrefined-appointment-81829
12/24/2025, 6:26 PMrefined-appointment-81829
12/24/2025, 6:28 PMastonishing-accountant-25155
12/24/2025, 6:28 PMrefined-appointment-81829
12/24/2025, 6:30 PMrefined-appointment-81829
12/24/2025, 6:30 PMastonishing-accountant-25155
12/24/2025, 6:31 PMastonishing-accountant-25155
12/24/2025, 6:32 PMrefined-appointment-81829
12/24/2025, 6:33 PMrefined-appointment-81829
12/24/2025, 6:34 PMrefined-appointment-81829
12/24/2025, 6:34 PMastonishing-accountant-25155
12/24/2025, 6:38 PMrefined-appointment-81829
12/24/2025, 6:39 PMrefined-appointment-81829
12/24/2025, 6:44 PMastonishing-accountant-25155
12/24/2025, 6:47 PMastonishing-accountant-25155
12/24/2025, 6:47 PMastonishing-accountant-25155
12/24/2025, 6:49 PMrefined-appointment-81829
12/24/2025, 6:49 PMrefined-appointment-81829
12/24/2025, 6:50 PMrefined-appointment-81829
12/24/2025, 6:50 PMrefined-appointment-81829
12/24/2025, 6:51 PMrefined-appointment-81829
12/24/2025, 6:51 PMastonishing-accountant-25155
12/24/2025, 6:55 PMrefined-appointment-81829
12/24/2025, 6:55 PMastonishing-accountant-25155
12/24/2025, 6:55 PMrefined-appointment-81829
12/24/2025, 6:56 PMastonishing-accountant-25155
12/24/2025, 8:36 PMastonishing-accountant-25155
12/25/2025, 4:19 AMrefined-appointment-81829
12/26/2025, 6:15 PMastonishing-accountant-25155
12/26/2025, 10:06 PMrefined-appointment-81829
12/29/2025, 4:59 PM