flaky-scooter-85113
02/13/2026, 1:46 PMtall-airline-16756
02/13/2026, 1:46 PMtall-airline-16756
02/13/2026, 1:46 PMthinking_sound config, call background_audio.play(...) from inside each tool (or from the wrapper that invokes the tool) and pass the specific audio clip or source you want for that tool. The play call returns a PlayHandle so you can stop or await playback when the tool finishes, giving you precise start/stop control for each tool’s audio.
If helpful, I can:
• Show a short code snippet demonstrating how to call background_audio.play() at tool start and stop it at tool end.
• Explain options for audio sources (built-in clips vs custom files) and how to manage probabilities or fallbacks.
Which would you like next, and do you have a preferred runtime/language?
Sources: External data and RAG | LiveKit Documentation | Agent speech and audio | LiveKit Documentationflaky-scooter-85113
02/13/2026, 1:47 PMtall-airline-16756
02/13/2026, 1:47 PMbackgroundAudio.play(...) with the audio clip for that tool.
• Keep the returned PlayHandle so you can stop or await the playback when the tool finishes or when you want the thinking audio to end.
• Use built-in clips or custom URLs/local files as the source depending on your environment.
2) Minimal TypeScript example
// Assume these come from your agent runtime
import { BackgroundAudioPlayer, BuiltinAudioClip } from 'livekit-audio-sdk'; // adjust import to your SDK
// A simple mapping from tool name to audio clip
const toolThinkingAudio = {
transcription: BuiltinAudioClip.SoftBeep,
search: BuiltinAudioClip.WarmAmbience,
summarize: '<https://example.com/audio/summarize_loop.mp3>', // custom URL
};
async function runTool(toolName: string, toolFn: () => Promise<any>, backgroundAudio: BackgroundAudioPlayer) {
// Choose audio for this tool (fallback to a default)
const audioSource = toolThinkingAudio[toolName] ?? BuiltinAudioClip.DefaultThinking;
// Start thinking audio and keep the handle
const handle = await backgroundAudio.play({ source: audioSource, loop: true });
try {
// Run the tool's logic
const result = await toolFn();
return result;
} finally {
// Stop the thinking audio reliably when tool completes or errors
await handle.stop();
}
}
3) Notes and customization ideas
• Looping: For ongoing tool work, start with loop: true. Stop the handle when the tool ends.
• Timing: You can await handle.finished() if you want to let the clip run to completion instead of stopping it early.
• Probabilities/fallbacks: If you want randomized selection, store an array of audio configs per tool and pick one when starting playback.
• Concurrency: If multiple tools may run concurrently and you want separate audio per tool, keep separate handles and ensure your audio system supports multiple simultaneous players or mix channels.
• Source types: Use built-in clips for consistent UX or host short loopable files for custom sounds. URLs or local assets both work if the player supports them in your runtime.
Would you like a more complete sample showing: 1) randomized selection from multiple clips per tool, or 2) handling concurrent tool runs with separate mixing channels? Tell me which and I’ll provide the TypeScript snippet.flaky-scooter-85113
02/13/2026, 1:48 PMaudio parameter to llm.tool() ! )tall-airline-16756
02/13/2026, 1:48 PMllm.tool() accepted an audio param. Even without that, you can get the same outcome with a small wrapper pattern in TypeScript that starts and stops thinking audio around each tool call. Below I’ll:
• Acknowledge the limitation
• Show a robust TypeScript pattern you can drop into your agent runtime
• Point out options for concurrency and randomization
Acknowledgement It’s understandable to find it messy that tools don’t accept an audio argument directly — wrapping tool execution to control BackgroundAudioPlayer gives you explicit, predictable control and lets you implement per-tool audio reliably.
TypeScript wrapper pattern (robust) 1) Behavior:
• Start a chosen audio clip when the tool begins.
• Keep the returned PlayHandle so you can stop or await it when the tool finishes (success or error).
• Support looping, randomized clip selection, custom URLs, and concurrent tool runs.
2) Example
// Adjust imports to your actual SDK
import { BackgroundAudioPlayer, PlayHandle, BuiltinAudioClip } from 'livekit-audio-sdk';
// Map each tool to either a single source or an array of sources (for random selection)
type AudioSource = string | BuiltinAudioClip;
const toolAudioMap: Record<string, AudioSource | AudioSource[]> = {
transcription: BuiltinAudioClip.SoftBeep,
search: [BuiltinAudioClip.WarmAmbience, BuiltinAudioClip.ToneLoop],
summarize: '<https://example.com/audio/summarize_loop.mp3>',
default: BuiltinAudioClip.DefaultThinking,
};
function pickAudioForTool(toolName: string): AudioSource {
const entry = toolAudioMap[toolName] ?? toolAudioMap.default;
if (Array.isArray(entry)) {
return entry[Math.floor(Math.random() * entry.length)];
}
return entry;
}
// Wrapper that runs a tool with thinking audio
export async function runToolWithAudio<T>(
toolName: string,
toolFn: () => Promise<T>,
backgroundAudio: BackgroundAudioPlayer,
opts?: { loop?: boolean; allowOverlap?: boolean } // allowOverlap lets multiple tools play concurrently
): Promise<T> {
const audioSource = pickAudioForTool(toolName);
// Start playback. Provide options like loop. SDK may accept different shapes; adapt as needed.
const handle: PlayHandle = await backgroundAudio.play({ source: audioSource, loop: opts?.loop ?? true });
try {
const result = await toolFn();
return result;
} finally {
// Ensure we stop audio even on errors
try {
// If you want to let clip finish, use handle.finished() instead of stop()
await handle.stop();
} catch (e) {
// swallow stop errors but optionally log them
console.warn(`Failed to stop audio for ${toolName}`, e);
}
}
}
Concurrency notes
• If multiple tools may run simultaneously and you want separate audio per tool, the player and your environment must support concurrent playback (multiple handles). Keep a handle per tool and stop each independently.
• If you prefer a single global thinking track that changes per tool, you can stop the current handle before starting a new one.
Advanced options (pick any you'd like)
• Fade-in/out: If your audio player supports volume control, fade between clips to avoid abrupt cuts.
• Priority: Add a priority field so high-priority tools interrupt lower-priority audio.
• Channel mixing: If your runtime supports channels, play each tool on separate channels to mix or duck audio automatically.
• Probabilities: Store weighted arrays of clips and sample by weight for varied UX.
Would you like: 1) A version that supports weighted-random selection, 2) An example demonstrating concurrent tool runs and mix/duck behavior, or 3) A snippet integrating this wrapper with your existing agent/tool registration code?
Tell me which and I’ll provide the tailored TypeScript snippet.flaky-scooter-85113
02/13/2026, 1:50 PMtall-airline-16756
02/13/2026, 1:51 PMlivekit-audio-sdk does not exist. That was an error earlier.
What you should use
• Use the BackgroundAudioPlayer from the official @livekit/agents package. That is the correct place for agent audio features in TypeScript/Node.js.
• There is no audio parameter on llm.tool() (or tool()); you trigger thinking/ambient audio manually by calling backgroundAudio.play() inside your tool code or wrapper.
Quick TypeScript example (minimal, correct imports)
import { BackgroundAudioPlayer, voice } from '@livekit/agents';
const { BuiltinAudioClip } = voice;
// Create the player (connect it after agent/room setup)
const backgroundAudio = new BackgroundAudioPlayer();
// Example wrapper that plays per-tool audio
async function runToolWithAudio<T>(
toolName: string,
toolFn: () => Promise<T>,
audioSource: string | typeof BuiltinAudioClip[keyof typeof BuiltinAudioClip]
) {
// Start thinking audio (loop while tool runs)
const handle = await backgroundAudio.play({ source: audioSource, loop: true });
try {
return await toolFn();
} finally {
// stop when the tool finishes or errors
await handle.stop();
}
}
Implementation notes and tips
• Install: npm install @livekit/agents.
• Use built-in clips (e.g., BuiltinAudioClip.KEYBOARD_TYPING) or custom files/URLs.
• For randomized or weighted clips per tool, pick the clip before calling play().
• For concurrent tool runs:
◦ If you want separate audio per tool, keep one handle per tool and ensure your environment/player supports multiple simultaneous handles.
◦ If you want a single global track that changes per tool, stop the current handle before starting the new one.
• In TypeScript the thinking_sound constructor option (available in Python) is not available — manual play() calls are the way to go.
Would you like: 1) A fuller example that wires this into an agent lifecycle (connect/start/stop background audio), or 2) A snippet showing weighted-random selection of clips per tool, or 3) Concurrency examples (separate handles vs global switching)?
Tell me which and I’ll provide the tailored TypeScript snippet.flaky-scooter-85113
02/13/2026, 1:53 PMaudio parameter that defines the audio for that tool.tall-airline-16756
02/13/2026, 1:54 PMllm.tool()-style registration so each tool can declare an audio parameter. The wrapper will:
• Accept an audio option (builtin clip, URL, or undefined).
• Start looping thinking audio when the tool runs.
• Stop the thinking audio when the tool completes or errors.
• Play safely when multiple tools run concurrently (keeps one handle per invocation).
Assumptions:
• You’re using the LiveKit Agents Node SDK: @livekit/agents.
• You have a BackgroundAudioPlayer instance available and started for the agent/session.
• Adjust imports and integration points to your agent runtime as needed.
Wrapper snippet
import { BackgroundAudioPlayer, voice } from '@livekit/agents';
const { BuiltinAudioClip } = voice;
type AudioSource = string | typeof BuiltinAudioClip[keyof typeof BuiltinAudioClip];
type ToolHandler<T = any> = (input: any) => Promise<T>;
type ToolOptions = {
description?: string;
audio?: AudioSource; // new audio param per tool
loop?: boolean; // whether to loop the thinking audio (default true)
};
class ToolRegistry {
private backgroundAudio: BackgroundAudioPlayer;
constructor(backgroundAudio: BackgroundAudioPlayer) {
this.backgroundAudio = backgroundAudio;
}
// Wrapper for registering a tool (mimics llm.tool-style signature)
registerTool<T = any>(name: string, opts: ToolOptions, handler: ToolHandler<T>) {
// Replace this with your actual `llm.tool()` registration call.
// The returned function below is what the runtime will call when invoking the tool.
const wrapped = async (input: any) => {
const audioSource = opts.audio;
let handle: any | null = null;
try {
if (audioSource) {
// Start thinking audio; loop by default unless explicitly disabled
handle = await this.backgroundAudio.play({
source: audioSource,
loop: opts.loop ?? true,
});
}
// Run actual tool logic
return await handler(input);
} finally {
// Ensure we stop/cleanup audio even if handler throws
if (handle) {
try {
await handle.stop();
} catch (e) {
// optional: log but don't throw
console.warn(`Failed to stop thinking audio for tool ${name}`, e);
}
}
}
};
// Example: wire into your agent/tool system here.
// e.g., llm.tool(name, opts.description, wrapped);
// For demonstration we return the wrapped function so caller can register it.
return wrapped;
}
}
Usage example
// assume backgroundAudio is an instance you've created and started
const registry = new ToolRegistry(backgroundAudio);
// Register a tool with a builtin clip
const genJoke = registry.registerTool(
'generate_joke',
{ description: 'Generate a short joke', audio: BuiltinAudioClip.KEYBOARD_TYPING },
async (input) => {
// simulate work
await new Promise((r) => setTimeout(r, 1200));
return { text: 'Why did the chicken cross the road? To get to the other side!' };
}
);
// Register a tool with a custom URL
const summarize = registry.registerTool(
'summarize',
{ description: 'Summarize text', audio: '<https://example.com/audio/summarize_loop.mp3>' },
async (input) => {
/* ... */
}
);
// Now register these wrapped handlers with your agent/llm tooling system.
// e.g., llm.tool('generate_joke', 'Generate a short joke', genJoke);
Notes and tips
• If you want a global default thinking audio, check opts.audio and fall back to a global clip if missing.
• For weighted/random selection, set audio to a helper that picks one before calling play().
• If your runtime does not support multiple concurrent audio streams, consider stopping the global handle before starting a new one instead of allowing overlap.
• If you prefer to let a clip finish rather than stopping it, use handle.finished() instead of handle.stop().
Want this adapted to: 1) automatically call llm.tool(...) for you inside the registry, or 2) include weighted-random selection, or 3) add fade in/out using volume controls if supported?
Tell me which and I’ll extend the snippet.flaky-scooter-85113
02/13/2026, 1:55 PMaudio member.tall-airline-16756
02/13/2026, 1:55 PMllm.tool() signature but accepts an extra optional audio member. It:
• Takes one options object (same shape as llm.tool() parameters + optional audio).
• Starts looping thinking audio when the tool is invoked.
• Stops that audio when the tool finishes or errors.
• Returns the wrapped tool function so you can pass it directly to your llm.tool(...) registration.
Assumptions:
• You have @livekit/agents installed and a running BackgroundAudioPlayer instance (connected/started).
• llm.tool(name, description, handler) is how you register tools in your runtime — replace that call with your actual registration if different.
Wrapper implementation
import { BackgroundAudioPlayer, voice } from '@livekit/agents';
const { BuiltinAudioClip } = voice;
type AudioSource = string | typeof BuiltinAudioClip[keyof typeof BuiltinAudioClip];
// Matches the normal single-argument style for llm.tool(), extended with `audio`
type LlmToolOptions = {
name: string;
description?: string;
// handler is the function that runs when the tool is invoked
handler: (input: any) => Promise<any>;
// optional new audio field
audio?: AudioSource | AudioSource[]; // allow array for random choice
loop?: boolean; // optional per-tool loop override (defaults true)
};
export class LlmToolWrapper {
private backgroundAudio: BackgroundAudioPlayer;
private defaultAudio?: AudioSource;
constructor(backgroundAudio: BackgroundAudioPlayer, opts?: { defaultAudio?: AudioSource }) {
this.backgroundAudio = backgroundAudio;
this.defaultAudio = opts?.defaultAudio;
}
// Single-argument wrapper that returns a handler ready for registration
wrapTool(opts: LlmToolOptions) {
const { name, handler, audio, loop } = opts;
// Helper to pick audio (supports single or array)
const pickAudio = (): AudioSource | undefined => {
if (audio == null) return this.defaultAudio;
if (Array.isArray(audio) && audio.length > 0) {
return audio[Math.floor(Math.random() * audio.length)];
}
return audio as AudioSource;
};
// The function to register with llm.tool(...)
const wrappedHandler = async (input: any) => {
const audioSource = pickAudio();
let playHandle: any | null = null;
try {
if (audioSource) {
// start thinking audio; loop by default unless explicitly false
playHandle = await this.backgroundAudio.play({
source: audioSource,
loop: loop ?? true,
});
}
// run the real tool
return await handler(input);
} finally {
if (playHandle) {
try {
await playHandle.stop();
} catch (e) {
// don't throw if stopping fails; optionally log
console.warn(`Failed to stop thinking audio for tool ${name}`, e);
}
}
}
};
// Return an object matching llm.tool's signature pieces so caller can register
return {
name: opts.name,
description: opts.description,
handler: wrappedHandler,
};
}
}
Example usage
// assume `backgroundAudio` is created and started elsewhere
const wrapper = new LlmToolWrapper(backgroundAudio, { defaultAudio: BuiltinAudioClip.DEFAULT_THINKING });
// Define a tool with per-tool builtin clip
const jokeTool = wrapper.wrapTool({
name: 'generate_joke',
description: 'Return a short joke',
audio: BuiltinAudioClip.KEYBOARD_TYPING,
handler: async (input) => {
// tool logic
await new Promise((r) => setTimeout(r, 1200));
return { text: 'A joke' };
},
});
// Define a tool with randomized selection between clips
const summarizeTool = wrapper.wrapTool({
name: 'summarize',
description: 'Summarize text',
audio: [BuiltinAudioClip.WARM_AMBIENCE, '<https://example.com/summarize_loop.mp3>'],
handler: async (input) => { /* ... */ },
});
// Register with your LLM/tool system (example)
llm.tool(jokeTool.name, jokeTool.description, jokeTool.handler);
llm.tool(summarizeTool.name, summarizeTool.description, summarizeTool.handler);
Notes and tips
• If you want the wrapper to call llm.tool(...) directly, I can change wrapTool to register immediately instead of returning the pieces.
• If you prefer weighted random selection, I can show a small helper to sample by weight.
• For concurrency:
◦ This pattern keeps one play handle per invocation, so concurrent tool runs will play concurrently if the environment supports multiple handles.
◦ If you prefer global switching (one track at a time), modify the wrapper to stop any existing global handle before starting a new one.
Want me to adapt this so the wrapper directly calls llm.tool(...) for you, or add weighted-random selection?