import { clamp } from "../utils.ts"; import { getMusicResumeStateSnapshot, hasKnownMusicResumeState, noteMusicResumeRequest, setKnownMusicQueuePausedState } from "./musicResumeState.ts"; import { normalizeInlineText } from "./voiceSessionHelpers.ts"; import { ensureSessionToolRuntimeState } from "./voiceToolCallToolRegistry.ts"; import { clearMusicDisambiguationState, clearBotSpeechMusicUnduckTimer, clearPendingMusicReplyHandoff, findPendingMusicSelectionById, getMusicPhase, getMusicDisambiguationPromptContext, releaseBotSpeechMusicDuck, setMusicPhase, setPendingMusicReplyHandoff, setMusicDisambiguationState } from "./voiceMusicPlayback.ts"; import { throwIfAborted } from "../tools/abortError.ts"; import { musicPhaseCanResume, musicPhaseIsActive } from "./voiceSessionTypes.ts"; import type { MusicSelectionResult, VoiceRealtimeToolSettings, VoiceSession, VoiceToolRuntimeSessionLike } from "./voiceSessionTypes.ts"; import type { VoiceToolCallArgs, VoiceToolCallManager } from "./voiceToolCallTypes.ts";
type ToolRuntimeSession = VoiceSession | VoiceToolRuntimeSessionLike;
function hasFullVoiceSessionShape(session: ToolRuntimeSession | null | undefined): session is VoiceSession { return Boolean( session && typeof session === "object" && typeof session.id === "string" && typeof session.guildId === "string" && typeof session.voiceChannelId === "string" && typeof session.ending === "boolean" ); }
type VoiceMusicToolOptions = { session?: ToolRuntimeSession | null; settings?: VoiceRealtimeToolSettings | null; args?: VoiceToolCallArgs; signal?: AbortSignal; }; type MusicQueueTrack = { id: string; title: string; artist: string; durationMs: number | null; source: "yt" | "sc"; streamUrl: string | null; platform: MusicSelectionResult["platform"]; externalUrl: string | null; };
type PlaybackStartOverrides = { requestReason?: string; failureLogContent?: string; resultFieldName?: "track" | "video"; };
function isMusicSelectionResult(value: unknown): value is MusicSelectionResult { if (!value || typeof value !== "object") return false; const candidate = value as Record<string, unknown>; return ( typeof candidate.id === "string" && typeof candidate.title === "string" && typeof candidate.artist === "string" && (candidate.platform === "youtube" || candidate.platform === "soundcloud" || candidate.platform === "discord" || candidate.platform === "auto") ); } function toMusicQueueTrack(track: MusicSelectionResult): MusicQueueTrack { return { id: track.id, title: track.title, artist: track.artist, durationMs: Number.isFinite(Number(track.durationSeconds)) ? Math.max(0, Math.round(Number(track.durationSeconds) * 1000)) : null, source: track.platform === "soundcloud" ? "sc" : "yt", streamUrl: track.externalUrl || null, platform: track.platform, externalUrl: track.externalUrl }; } function resolveMusicCatalogTracks(catalog: Map<string, unknown>, trackIds: string[]) { return trackIds .map((trackId) => { const track = catalog.get(trackId); return isMusicSelectionResult(track) ? toMusicQueueTrack(track) : null; }) .filter((entry): entry is MusicQueueTrack => Boolean(entry)); }
function logMusicResumeUnavailable( manager: VoiceToolCallManager, session: ToolRuntimeSession | null | undefined, source: string, phase: string ) { const snapshot = getMusicResumeStateSnapshot(session); manager.store.logAction({ kind: "voice_runtime", guildId: session?.guildId, channelId: session?.textChannelId, userId: session?.lastRealtimeToolCallerUserId || manager.client.user?.id || null, content: "voice_music_resume_unavailable", metadata: { sessionId: session?.id || null, source, phase, hasQueuedTrack: snapshot.hasQueuedTrack, hasRememberedTrack: snapshot.hasRememberedTrack, queueNowPlayingIndex: snapshot.queueNowPlayingIndex, queueTrackId: snapshot.queueTrackId, rememberedTrackId: snapshot.rememberedTrackId, rememberedTrackUrl: snapshot.rememberedTrackUrl } }); }
function clearUnavailableMusicResumeState( manager: VoiceToolCallManager, session: ToolRuntimeSession | null | undefined, source: string, phase: string ) { if (session) { setMusicPhase(manager, session, "idle"); clearPendingMusicReplyHandoff(manager, session); setKnownMusicQueuePausedState(session, false); } logMusicResumeUnavailable(manager, session, source, phase); return { ok: false as const, error: "media_resume_unavailable" as const, phase: session ? getMusicPhase(manager, session) : "idle", queue_state: manager.buildVoiceQueueStatePayload(session) }; }
function getToolMusicCatalog(manager: VoiceToolCallManager, session: ToolRuntimeSession | null | undefined) { const runtimeSession = ensureSessionToolRuntimeState(manager, session); const catalog = runtimeSession?.toolMusicTrackCatalog instanceof Map ? runtimeSession.toolMusicTrackCatalog : new Map<string, unknown>(); if (runtimeSession && !(runtimeSession.toolMusicTrackCatalog instanceof Map)) { runtimeSession.toolMusicTrackCatalog = catalog; } return { runtimeSession, catalog }; }
function buildMusicToolTrackResult(track: MusicSelectionResult | MusicQueueTrack) { const durationMs = "durationSeconds" in track ? Number.isFinite(Number(track.durationSeconds)) ? Math.max(0, Math.round(Number(track.durationSeconds) * 1000)) : null : track.durationMs; const platform = "platform" in track ? track.platform : "youtube"; const externalUrl = "externalUrl" in track ? track.externalUrl : null; return { id: track.id, title: track.title, artist: track.artist, durationMs, source: platform === "soundcloud" ? "sc" : "yt", platform, streamUrl: externalUrl || null }; }
function buildMusicToolOptionResult(track: MusicSelectionResult) { const base = buildMusicToolTrackResult(track); return { selection_id: track.id, ...base }; }
function searchVoiceMusicCatalog( manager: VoiceToolCallManager, { session, query, platform, maxResults }: { session?: ToolRuntimeSession | null; query: string; platform: "youtube" | "soundcloud" | "auto"; maxResults: number; } ) { const { catalog } = getToolMusicCatalog(manager, session); return manager.musicSearch.search(query, { platform, limit: maxResults }).then((searchResponse) => { const results = (Array.isArray(searchResponse?.results) ? searchResponse.results : []) .slice(0, maxResults) .map((row) => normalizeMusicSearchResult(manager, row)) .filter((entry): entry is MusicSelectionResult => Boolean(entry)); for (const result of results) { catalog.set(result.id, result); } return { catalog, results }; }); }
function normalizeMusicSearchResult( manager: VoiceToolCallManager, row: { id: string; title: string; artist: string; platform: string; externalUrl?: string | null; durationSeconds?: number | null; } ) { return manager.normalizeMusicSelectionResult({ id: row.id, title: row.title, artist: row.artist, platform: row.platform, externalUrl: row.externalUrl, durationSeconds: row.durationSeconds }); }
function resolveMusicPlaySelection( manager: VoiceToolCallManager, session: ToolRuntimeSession | null | undefined, selectionId: string, catalog: Map<string, unknown> ) { const pendingSelection = findPendingMusicSelectionById(manager, session, selectionId); if (pendingSelection) return pendingSelection; const catalogSelection = catalog.get(selectionId); if (isMusicSelectionResult(catalogSelection)) return catalogSelection;
const queueState = manager.ensureToolMusicQueueState(session); const queuedTrack = Array.isArray(queueState?.tracks) ? queueState.tracks.find((track) => String(track?.id || "").trim() === selectionId) || null : null; if (queuedTrack?.id && queuedTrack.title) { return manager.normalizeMusicSelectionResult({ id: queuedTrack.id, title: queuedTrack.title, artist: queuedTrack.artist || "", platform: queuedTrack.platform || "youtube", externalUrl: queuedTrack.externalUrl || null, durationSeconds: Number.isFinite(Number(queuedTrack.durationMs)) ? Math.max(0, Math.round(Number(queuedTrack.durationMs) / 1000)) : null }); }
const musicState = manager.ensureSessionMusicState(session); if (musicState?.lastTrackId === selectionId && musicState.lastTrackTitle) { return manager.normalizeMusicSelectionResult({ id: musicState.lastTrackId, title: musicState.lastTrackTitle, artist: Array.isArray(musicState.lastTrackArtists) ? musicState.lastTrackArtists.join(", ") : "", platform: String(musicState.provider || "").trim().toLowerCase() === "soundcloud" ? "soundcloud" : "youtube", externalUrl: musicState.lastTrackUrl || null, durationSeconds: null }); }
return null; }
function normalizeMusicDisambiguationQueryToken(value: unknown) { return String(value || "") .trim() .toLowerCase() .replace(/&/g, " and ") .replace(/&/g, " and ") .replace(/[^a-z0-9]+/g, " ") .replace(/\s+/g, " ") .trim(); }
function resolvePendingMusicSelectionFromQuery( manager: VoiceToolCallManager, session: ToolRuntimeSession | null | undefined, query: string ) { const disambiguation = getMusicDisambiguationPromptContext(manager, session); const options = Array.isArray(disambiguation?.options) ? disambiguation.options : []; if (!disambiguation?.active || !options.length) return null;
const rawQuery = String(query || "").trim().toLowerCase(); if (!rawQuery) return null; const queryIdMatch = options.find((entry) => String(entry?.id || "").trim().toLowerCase() === rawQuery); if (queryIdMatch) return queryIdMatch;
const normalizedQuery = normalizeMusicDisambiguationQueryToken(rawQuery); if (!normalizedQuery) return null;
const exactTitleMatches = options.filter((entry) => { const normalizedTitle = normalizeMusicDisambiguationQueryToken(entry?.title); return Boolean(normalizedTitle) && normalizedTitle === normalizedQuery; }); if (exactTitleMatches.length === 1) return exactTitleMatches[0];
const containedTitleMatches = options.filter((entry) => { const normalizedTitle = normalizeMusicDisambiguationQueryToken(entry?.title); if (!normalizedTitle) return false; return ( (normalizedQuery.length >= 10 && normalizedTitle.includes(normalizedQuery)) || (normalizedTitle.length >= 10 && normalizedQuery.includes(normalizedTitle)) ); }); if (containedTitleMatches.length === 1) return containedTitleMatches[0];
return null; }
function resolvePendingSelectionQueryPlayback( manager: VoiceToolCallManager, { session, settings, query, catalog, resolvedLogContent, playbackOverrides, onBeforePlaybackStart }: { session: ToolRuntimeSession | null | undefined; settings?: VoiceRealtimeToolSettings | null; query: string; catalog: Map<string, unknown>; resolvedLogContent: string; playbackOverrides?: PlaybackStartOverrides; onBeforePlaybackStart?: () => void; } ) { const queryResolvedSelection = resolvePendingMusicSelectionFromQuery(manager, session, query); if (!queryResolvedSelection) return null; catalog.set(queryResolvedSelection.id, queryResolvedSelection); manager.store.logAction({ kind: "voice_runtime", guildId: String(session?.guildId || "").trim() || null, channelId: String(session?.textChannelId || "").trim() || null, userId: String(session?.lastRealtimeToolCallerUserId || "").trim() || null, content: resolvedLogContent, metadata: { sessionId: String(session?.id || "").trim() || null, query, selectionId: queryResolvedSelection.id } }); onBeforePlaybackStart?.(); return startVoicePlaybackRequest(manager, { session, settings, query, selectedTrack: queryResolvedSelection, ...playbackOverrides }); }
function startVoicePlaybackRequest(
manager: VoiceToolCallManager,
{
session,
settings,
query,
selectedTrack,
requestReason = "voice_tool_music_play",
failureLogContent = "voice_tool_music_play_failed",
resultFieldName = "track"
}: {
session: ToolRuntimeSession | null | undefined;
settings?: VoiceRealtimeToolSettings | null;
query?: string | null;
selectedTrack: MusicSelectionResult;
requestReason?: string;
failureLogContent?: string;
resultFieldName?: "track" | "video";
}
) {
const queueState = manager.ensureToolMusicQueueState(session);
if (!queueState) return { ok: false, error: "queue_unavailable" };
const replacementTrack = toMusicQueueTrack(selectedTrack);
const trailingTracks = queueState.nowPlayingIndex == null
? []
: queueState.tracks.slice(Math.max(0, queueState.nowPlayingIndex + 1));
queueState.tracks = [replacementTrack, ...trailingTracks];
queueState.nowPlayingIndex = 0;
queueState.isPaused = false;
const playbackQuery =
normalizeInlineText(query || ${selectedTrack.title} ${selectedTrack.artist || ""}, 120) ||
normalizeInlineText(${selectedTrack.title} ${selectedTrack.artist || ""}, 120);
if (session) { clearMusicDisambiguationState(manager, session); manager.clearVoiceCommandSession(session); manager.setMusicPhase(session, "loading"); }
manager.requestPlayMusic({
guildId: session?.guildId,
channelId: session?.textChannelId,
requestedByUserId: session?.lastRealtimeToolCallerUserId || null,
settings,
query: playbackQuery,
trackId: selectedTrack.id,
searchResults: [selectedTrack],
reason: requestReason,
source: "voice_tool_call",
mustNotify: false
}).catch((error: unknown) => {
manager.store.logAction({
kind: "voice_error",
guildId: String(session?.guildId || "").trim() || null,
channelId: String(session?.textChannelId || "").trim() || null,
userId: manager.client.user?.id || null,
content: ${failureLogContent}: ${String(error instanceof Error ? error.message : error)},
metadata: {
sessionId: String(session?.id || "").trim() || null,
trackId: selectedTrack.id
}
});
});
const resultItem = buildMusicToolTrackResult(replacementTrack); return { ok: true, status: "loading", query: playbackQuery || null, [resultFieldName]: resultItem, queue_state: manager.buildVoiceQueueStatePayload(session) }; }
export async function executeVoiceMusicSearchTool( manager: VoiceToolCallManager, { session, args, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music search cancelled"); const query = normalizeInlineText(args?.query, 180); if (!query) return { ok: false, tracks: [], error: "query_required" }; const maxResults = clamp(Math.floor(Number(args?.max_results || 5)), 1, 10); const { results } = await searchVoiceMusicCatalog(manager, { session, query, platform: "auto", maxResults }); const tracks = results .map((normalized) => buildMusicToolTrackResult(normalized)) .filter(Boolean);
return { ok: true, query, tracks }; }
export async function executeVoiceVideoSearchTool( manager: VoiceToolCallManager, { session, args, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice video search cancelled"); const query = normalizeInlineText(args?.query, 180); if (!query) return { ok: false, videos: [], error: "query_required" }; const maxResults = clamp(Math.floor(Number(args?.max_results || 5)), 1, 10); const { results } = await searchVoiceMusicCatalog(manager, { session, query, platform: "youtube", maxResults }); const videos = results .map((normalized) => buildMusicToolTrackResult(normalized)) .filter(Boolean);
return { ok: true, query, videos }; }
async function resolveVoiceMusicQueueToolTracks( manager: VoiceToolCallManager, { session, args, action }: { session?: ToolRuntimeSession | null; args?: VoiceToolCallArgs; action: "queue_next" | "queue_add"; } ): Promise< | { ok: true; query: string | null; resolvedTracks: MusicQueueTrack[]; } | { ok: false; response: Record<string, unknown>; }
{ const queueState = manager.ensureToolMusicQueueState(session); const runtimeSession = ensureSessionToolRuntimeState(manager, session); if (!queueState || !runtimeSession) { return { ok: false, response: { ok: false, queue_length: 0, added: [], error: "queue_unavailable" } }; }
const queueLength = queueState.tracks.length; const requestedTrackIds = Array.isArray(args?.tracks) ? args.tracks.map((entry) => normalizeInlineText(entry, 180)).filter(Boolean).slice(0, 12) : []; const query = normalizeInlineText(args?.query, 180); const selectionId = normalizeInlineText(args?.selection_id, 180); const platformToken = normalizeInlineText(args?.platform, 32)?.toLowerCase(); const platform = platformToken === "youtube" || platformToken === "soundcloud" || platformToken === "auto" ? platformToken : "auto"; const maxResults = clamp(Math.floor(Number(args?.max_results || 5)), 1, 10); const catalog = runtimeSession.toolMusicTrackCatalog instanceof Map ? runtimeSession.toolMusicTrackCatalog : new Map<string, unknown>(); if (!(runtimeSession.toolMusicTrackCatalog instanceof Map)) { runtimeSession.toolMusicTrackCatalog = catalog; }
if (requestedTrackIds.length > 0) { const resolvedTracks = resolveMusicCatalogTracks(catalog, requestedTrackIds); if (!resolvedTracks.length) { return { ok: false, response: { ok: false, queue_length: queueLength, added: [], error: "unknown_track_ids" } }; } return { ok: true, query, resolvedTracks }; }
if (selectionId) { const selectedTrack = resolveMusicPlaySelection(manager, session, selectionId, catalog); if (selectedTrack) { catalog.set(selectedTrack.id, selectedTrack); return { ok: true, query, resolvedTracks: [toMusicQueueTrack(selectedTrack)] }; } if (!query) { return { ok: false, response: { ok: false, queue_length: queueLength, added: [], error: "unknown_selection_id" } }; } manager.store.logAction({ kind: "voice_runtime", guildId: String(session?.guildId || "").trim() || null, channelId: String(session?.textChannelId || "").trim() || null, userId: String(session?.lastRealtimeToolCallerUserId || "").trim() || null, content: "voice_tool_music_queue_selection_fallback", metadata: { sessionId: String(session?.id || "").trim() || null, action, selectionId, query, reason: "unknown_selection_id" } }); }
if (!query) { return { ok: false, response: { ok: false, queue_length: queueLength, added: [], error: "tracks_or_query_required" } }; }
const canSearch = Boolean(manager.musicSearch?.isConfigured?.()) && typeof manager.musicSearch?.search === "function"; if (!canSearch) { return { ok: false, response: { ok: false, queue_length: queueLength, added: [], query, error: "search_unavailable" } }; }
const searchOutcome = await searchVoiceMusicCatalog(manager, { session, query, platform, maxResults }); const results = searchOutcome.results;
if (!results.length) { return { ok: false, response: { ok: false, status: "not_found", query, queue_length: queueLength, added: [], error: "no_results" } }; }
if (results.length > 1) { const requestedByUserId = session?.lastRealtimeToolCallerUserId || null; setMusicDisambiguationState(manager, { session, query, platform, action, results, requestedByUserId }); if (requestedByUserId) { manager.beginVoiceCommandSession({ session, userId: requestedByUserId, domain: "music", intent: "music_disambiguation" }); } const disambiguation = getMusicDisambiguationPromptContext(manager, session); const options = Array.isArray(disambiguation?.options) && disambiguation.options.length > 0 ? disambiguation.options : results; return { ok: false, response: { ok: true, status: "needs_disambiguation", query, queue_length: queueLength, added: [], options: options.map((entry) => buildMusicToolOptionResult(entry)), queue_state: manager.buildVoiceQueueStatePayload(session), instruction: "Ask the user which option they want, then call this tool again with selection_id set to the exact id of their choice. Do not re-search." } }; }
return { ok: true, query, resolvedTracks: [toMusicQueueTrack(results[0])] }; }
export async function executeVoiceMusicQueueAddTool(
manager: VoiceToolCallManager,
{ session, settings, args, signal }: VoiceMusicToolOptions
) {
throwIfAborted(signal, "Voice music queue add cancelled");
const queueState = manager.ensureToolMusicQueueState(session);
if (!queueState) return { ok: false, queue_length: 0, added: [], error: "queue_unavailable" };
const resolved = await resolveVoiceMusicQueueToolTracks(manager, {
session,
args,
action: "queue_add"
});
if (resolved.ok === false) return resolved.response;
const { query, resolvedTracks } = resolved;
const wasEmpty = queueState.tracks.length === 0;
const rawPos = args?.position;
const parsedPos = rawPos === "end"
? undefined
: typeof rawPos === "string" && /^\d+$/.test(rawPos)
? parseInt(rawPos, 10)
: typeof rawPos === "number"
? rawPos
: undefined;
const insertAt = parsedPos != null
? clamp(Math.floor(parsedPos), 0, queueState.tracks.length)
: queueState.tracks.length;
queueState.tracks.splice(insertAt, 0, ...resolvedTracks);
if (queueState.nowPlayingIndex == null && queueState.tracks.length > 0) {
queueState.nowPlayingIndex = 0;
}
const shouldAutoPlay = wasEmpty && !manager.isMusicPlaybackActive(session) && !queueState.isPaused;
if (shouldAutoPlay && settings) {
const playIndex = queueState.nowPlayingIndex ?? 0;
manager.playVoiceQueueTrackByIndex({ session, settings, index: playIndex }).catch((error) => {
manager.store.logAction({
kind: "voice_error",
guildId: String(session?.guildId || "").trim() || null,
channelId: String(session?.textChannelId || "").trim() || null,
userId: manager.client.user?.id || null,
content: voice_music_queue_autoplay_failed: ${String(error?.message || error)},
metadata: {
sessionId: String(session?.id || "").trim() || null,
playIndex,
queueLength: queueState.tracks.length
}
});
});
}
return { ok: true, status: "queued", query, queue_length: queueState.tracks.length, added: resolvedTracks.map((entry) => entry.id), auto_playing: shouldAutoPlay, queue_state: { tracks: queueState.tracks.map((entry) => ({ id: entry.id, title: entry.title, artist: entry.artist, source: entry.source })), nowPlayingIndex: queueState.nowPlayingIndex, isPaused: queueState.isPaused } }; }
export async function executeVoiceMusicQueueNextTool( manager: VoiceToolCallManager, { session, settings, args, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music queue next cancelled"); const queueState = manager.ensureToolMusicQueueState(session); if (!queueState) return { ok: false, queue_length: 0, added: [], error: "queue_unavailable" }; const resolved = await resolveVoiceMusicQueueToolTracks(manager, { session, args, action: "queue_next" }); if (resolved.ok === false) return resolved.response; const { query, resolvedTracks } = resolved; const insertAt = queueState.nowPlayingIndex == null ? queueState.tracks.length : clamp(queueState.nowPlayingIndex + 1, 0, queueState.tracks.length); queueState.tracks.splice(insertAt, 0, ...resolvedTracks); if (queueState.nowPlayingIndex == null && queueState.tracks.length > 0) { queueState.nowPlayingIndex = 0; } const shouldAutoPlay = !manager.isMusicPlaybackActive(session) && !queueState.isPaused; if (shouldAutoPlay && settings) { await manager.playVoiceQueueTrackByIndex({ session, settings, index: queueState.nowPlayingIndex ?? 0 }); }
return { ok: true, status: "queued_next", query, queue_length: queueState.tracks.length, added: resolvedTracks.map((entry) => entry.id), inserted_after_index: queueState.nowPlayingIndex, auto_playing: shouldAutoPlay, queue_state: manager.buildVoiceQueueStatePayload(session) }; }
export async function executeVoiceMusicPlayTool( manager: VoiceToolCallManager, { session, settings, args, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music play cancelled"); const query = normalizeInlineText(args?.query, 180); const selectionId = normalizeInlineText(args?.selection_id, 180); const platformToken = normalizeInlineText(args?.platform, 32)?.toLowerCase(); const platform = platformToken === "youtube" || platformToken === "soundcloud" || platformToken === "auto" ? platformToken : "auto"; const maxResults = clamp(Math.floor(Number(args?.max_results || 5)), 1, 10); if (!query && !selectionId) return { ok: false, error: "query_or_selection_id_required" };
const { catalog } = getToolMusicCatalog(manager, session); if (selectionId) { const selectedTrack = resolveMusicPlaySelection(manager, session, selectionId, catalog); if (selectedTrack) { catalog.set(selectedTrack.id, selectedTrack); return startVoicePlaybackRequest(manager, { session, settings, query, selectedTrack }); } if (!query) return { ok: false, error: "unknown_selection_id" }; manager.store.logAction({ kind: "voice_runtime", guildId: String(session?.guildId || "").trim() || null, channelId: String(session?.textChannelId || "").trim() || null, userId: String(session?.lastRealtimeToolCallerUserId || "").trim() || null, content: "voice_tool_music_play_selection_fallback", metadata: { sessionId: String(session?.id || "").trim() || null, selectionId, query, reason: "unknown_selection_id" } }); }
if (!selectionId && query) { const queryResolvedPlayback = resolvePendingSelectionQueryPlayback(manager, { session, settings, query, catalog, resolvedLogContent: "voice_tool_music_play_query_resolved_pending_selection" }); if (queryResolvedPlayback) return queryResolvedPlayback; }
const canSearch = Boolean(manager.musicSearch?.isConfigured?.()) && typeof manager.musicSearch?.search === "function";
if (!canSearch) {
if (session) {
manager.setMusicPhase(session, "loading");
}
manager.requestPlayMusic({
guildId: session?.guildId,
channelId: session?.textChannelId,
requestedByUserId: session?.lastRealtimeToolCallerUserId || null,
settings,
query,
reason: "voice_tool_music_play",
source: "voice_tool_call",
mustNotify: false
}).catch((error: unknown) => {
manager.store.logAction({
kind: "voice_error",
guildId: String(session?.guildId || "").trim() || null,
channelId: String(session?.textChannelId || "").trim() || null,
userId: manager.client.user?.id || null,
content: voice_tool_music_play_failed: ${String(error instanceof Error ? error.message : error)},
metadata: {
sessionId: String(session?.id || "").trim() || null,
query
}
});
});
return {
ok: true,
status: "loading",
query,
queue_state: manager.buildVoiceQueueStatePayload(session)
};
}
const { results } = await searchVoiceMusicCatalog(manager, { session, query, platform, maxResults });
if (!results.length) { return { ok: false, status: "not_found", query, error: "no_results" }; }
if (results.length > 1) { const requestedByUserId = session?.lastRealtimeToolCallerUserId || null; setMusicDisambiguationState(manager, { session, query, platform, action: "play_now", results, requestedByUserId }); if (requestedByUserId) { manager.beginVoiceCommandSession({ session, userId: requestedByUserId, domain: "music", intent: "music_disambiguation" }); } const disambiguation = getMusicDisambiguationPromptContext(manager, session); const options = Array.isArray(disambiguation?.options) && disambiguation.options.length > 0 ? disambiguation.options : results; return { ok: true, status: "needs_disambiguation", query, options: options.map((entry) => buildMusicToolOptionResult(entry)), instruction: "Ask the user which option they want, then call this tool again with selection_id set to the exact id of their choice. Do not re-search." }; }
return startVoicePlaybackRequest(manager, { session, settings, query, selectedTrack: results[0] }); }
export async function executeVoiceVideoPlayTool( manager: VoiceToolCallManager, { session, settings, args, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice video play cancelled"); const query = normalizeInlineText(args?.query, 180); const selectionId = normalizeInlineText(args?.selection_id, 180); const maxResults = clamp(Math.floor(Number(args?.max_results || 5)), 1, 10); if (!query && !selectionId) return { ok: false, error: "query_or_selection_id_required" };
const { catalog } = getToolMusicCatalog(manager, session); if (selectionId) { const selectedTrack = resolveMusicPlaySelection(manager, session, selectionId, catalog); if (selectedTrack) { catalog.set(selectedTrack.id, selectedTrack); if (hasFullVoiceSessionShape(session)) { session.streamPublishIntent = { mode: "video" }; } return startVoicePlaybackRequest(manager, { session, settings, query, selectedTrack, requestReason: "voice_tool_video_play", failureLogContent: "voice_tool_video_play_failed", resultFieldName: "video" }); } if (!query) return { ok: false, error: "unknown_selection_id" }; manager.store.logAction({ kind: "voice_runtime", guildId: String(session?.guildId || "").trim() || null, channelId: String(session?.textChannelId || "").trim() || null, userId: String(session?.lastRealtimeToolCallerUserId || "").trim() || null, content: "voice_tool_video_play_selection_fallback", metadata: { sessionId: String(session?.id || "").trim() || null, selectionId, query, reason: "unknown_selection_id" } }); }
if (!selectionId && query) { const queryResolvedPlayback = resolvePendingSelectionQueryPlayback(manager, { session, settings, query, catalog, resolvedLogContent: "voice_tool_video_play_query_resolved_pending_selection", playbackOverrides: { requestReason: "voice_tool_video_play", failureLogContent: "voice_tool_video_play_failed", resultFieldName: "video" }, onBeforePlaybackStart: () => { if (hasFullVoiceSessionShape(session)) { session.streamPublishIntent = { mode: "video" }; } } }); if (queryResolvedPlayback) return queryResolvedPlayback; }
const canSearch = Boolean(manager.musicSearch?.isConfigured?.()) && typeof manager.musicSearch?.search === "function";
if (!canSearch) {
if (session) {
manager.setMusicPhase(session, "loading");
}
if (hasFullVoiceSessionShape(session)) {
session.streamPublishIntent = { mode: "video" };
}
manager.requestPlayMusic({
guildId: session?.guildId,
channelId: session?.textChannelId,
requestedByUserId: session?.lastRealtimeToolCallerUserId || null,
settings,
query,
reason: "voice_tool_video_play",
source: "voice_tool_call",
mustNotify: false
}).catch((error: unknown) => {
manager.store.logAction({
kind: "voice_error",
guildId: String(session?.guildId || "").trim() || null,
channelId: String(session?.textChannelId || "").trim() || null,
userId: manager.client.user?.id || null,
content: voice_tool_video_play_failed: ${String(error instanceof Error ? error.message : error)},
metadata: {
sessionId: String(session?.id || "").trim() || null,
query
}
});
});
return {
ok: true,
status: "loading",
query,
queue_state: manager.buildVoiceQueueStatePayload(session)
};
}
const { results } = await searchVoiceMusicCatalog(manager, { session, query, platform: "youtube", maxResults });
if (!results.length) { return { ok: false, status: "not_found", query, error: "no_results" }; }
if (results.length > 1) { const requestedByUserId = session?.lastRealtimeToolCallerUserId || null; setMusicDisambiguationState(manager, { session, query, platform: "youtube", action: "play_now", results, requestedByUserId }); if (requestedByUserId) { manager.beginVoiceCommandSession({ session, userId: requestedByUserId, domain: "music", intent: "music_disambiguation" }); } const disambiguation = getMusicDisambiguationPromptContext(manager, session); const options = Array.isArray(disambiguation?.options) && disambiguation.options.length > 0 ? disambiguation.options : results; return { ok: true, status: "needs_disambiguation", query, options: options.map((entry) => buildMusicToolOptionResult(entry)), instruction: "Ask the user which option they want, then call this tool again with selection_id set to the exact id of their choice. Do not re-search." }; }
if (hasFullVoiceSessionShape(session)) { session.streamPublishIntent = { mode: "video" }; } return startVoicePlaybackRequest(manager, { session, settings, query, selectedTrack: results[0], requestReason: "voice_tool_video_play", failureLogContent: "voice_tool_video_play_failed", resultFieldName: "video" }); }
export async function executeVoiceMusicStopTool( manager: VoiceToolCallManager, { session, settings, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music stop cancelled"); await manager.requestStopMusic({ guildId: session?.guildId, channelId: session?.textChannelId, requestedByUserId: session?.lastRealtimeToolCallerUserId || null, settings, reason: "voice_tool_media_stop", source: "voice_tool_call", clearQueue: true, mustNotify: false }); return { ok: true, queue_state: manager.buildVoiceQueueStatePayload(session) }; }
export async function executeVoiceMusicPauseTool( manager: VoiceToolCallManager, { session, settings, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music pause cancelled"); await manager.requestPauseMusic({ guildId: session?.guildId, channelId: session?.textChannelId, requestedByUserId: session?.lastRealtimeToolCallerUserId || null, settings, reason: "voice_tool_media_pause", source: "voice_tool_call", mustNotify: false }); const queueState = manager.ensureToolMusicQueueState(session); if (queueState) queueState.isPaused = true; return { ok: true, queue_state: manager.buildVoiceQueueStatePayload(session) }; }
export async function executeVoiceMusicResumeTool( manager: VoiceToolCallManager, { session, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music resume cancelled"); const currentPhase = session ? getMusicPhase(manager, session) : "idle"; if (!musicPhaseCanResume(currentPhase)) { return { ok: false, error: "music_not_paused", phase: currentPhase, queue_state: manager.buildVoiceQueueStatePayload(session) }; } if (!hasKnownMusicResumeState(session)) { return clearUnavailableMusicResumeState( manager, session, "voice_tool_media_resume", currentPhase ); } noteMusicResumeRequest(session, "voice_tool_media_resume"); manager.musicPlayer?.resume?.(); return { ok: true, status: "resume_requested", phase: session ? getMusicPhase(manager, session) : currentPhase, queue_state: manager.buildVoiceQueueStatePayload(session) }; }
export async function executeVoiceMusicReplyHandoffTool( manager: VoiceToolCallManager, { session, settings, args, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music reply handoff cancelled"); if (!session) { return { ok: false, error: "voice_session_unavailable" }; } const modeToken = normalizeInlineText(args?.mode, 32)?.toLowerCase() || ""; const mode = modeToken === "pause" || modeToken === "duck" || modeToken === "none" ? modeToken : null; if (!mode) { return { ok: false, error: "mode_required" }; }
const currentPhase = getMusicPhase(manager, session); const requestedByUserId = session.lastRealtimeToolCallerUserId || null;
if (mode === "none") { clearPendingMusicReplyHandoff(manager, session); return { ok: true, mode, applied: true, phase: getMusicPhase(manager, session), queue_state: manager.buildVoiceQueueStatePayload(session) }; }
if (mode === "pause") { const canApplyPause = currentPhase === "playing" || currentPhase === "loading" || currentPhase === "paused_wake_word"; if (!canApplyPause) { return { ok: true, mode, applied: false, reason: "music_not_audible", phase: currentPhase, queue_state: manager.buildVoiceQueueStatePayload(session) }; } if (currentPhase === "playing" || currentPhase === "loading") { clearBotSpeechMusicUnduckTimer(manager, session); if (hasFullVoiceSessionShape(session)) { await releaseBotSpeechMusicDuck(manager, session, settings, { force: true }); } manager.musicPlayer?.pause?.(); setMusicPhase(manager, session, "paused_wake_word", "wake_word"); const queueState = manager.ensureToolMusicQueueState(session); if (queueState) queueState.isPaused = true; } setPendingMusicReplyHandoff(manager, session, { mode: "pause", requestedByUserId, source: "voice_tool_media_reply_handoff" }); manager.replyManager.schedulePausedReplyMusicResume(session, 200); return { ok: true, mode, applied: true, autoRestore: "resume", phase: getMusicPhase(manager, session), queue_state: manager.buildVoiceQueueStatePayload(session) }; }
const canApplyDuck = currentPhase === "playing" || currentPhase === "loading" || currentPhase === "paused_wake_word"; if (!canApplyDuck) { return { ok: true, mode, applied: false, reason: "music_not_audible", phase: currentPhase, queue_state: manager.buildVoiceQueueStatePayload(session) }; } if (currentPhase === "paused_wake_word" && musicPhaseCanResume(currentPhase)) { if (!hasKnownMusicResumeState(session)) { const unavailable = clearUnavailableMusicResumeState( manager, session, "voice_tool_media_reply_handoff_duck", currentPhase ); return { ...unavailable, mode, applied: false, reason: "media_resume_unavailable" }; } noteMusicResumeRequest(session, "media_resumed_reply_handoff_duck"); manager.musicPlayer?.resume?.(); } setPendingMusicReplyHandoff(manager, session, { mode: "duck", requestedByUserId, source: "voice_tool_media_reply_handoff" }); return { ok: true, mode, applied: true, autoRestore: "unduck", phase: getMusicPhase(manager, session), queue_state: manager.buildVoiceQueueStatePayload(session) }; }
export async function executeVoiceMusicSkipTool( manager: VoiceToolCallManager, { session, settings, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music skip cancelled"); const queueState = manager.ensureToolMusicQueueState(session); if (!queueState || queueState.nowPlayingIndex == null) { await manager.requestStopMusic({ guildId: session?.guildId, channelId: session?.textChannelId, requestedByUserId: session?.lastRealtimeToolCallerUserId || null, settings, reason: "voice_tool_music_skip_without_queue", source: "voice_tool_call", mustNotify: false }); return { ok: true, queue_state: manager.buildVoiceQueueStatePayload(session) }; } const nextIndex = queueState.nowPlayingIndex + 1; await manager.requestStopMusic({ guildId: session?.guildId, channelId: session?.textChannelId, requestedByUserId: session?.lastRealtimeToolCallerUserId || null, settings, reason: "voice_tool_music_skip", source: "voice_tool_call", mustNotify: false }); if (nextIndex < queueState.tracks.length) { return manager.playVoiceQueueTrackByIndex({ session, settings, index: nextIndex }); } queueState.nowPlayingIndex = null; queueState.isPaused = false; return { ok: true, queue_state: manager.buildVoiceQueueStatePayload(session) }; }
export async function executeVoiceMusicNowPlayingTool( manager: VoiceToolCallManager, { session, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice music now playing cancelled"); const queueState = manager.ensureToolMusicQueueState(session); const nowTrack = queueState && queueState.nowPlayingIndex != null ? queueState.tracks[queueState.nowPlayingIndex] || null : null; const musicState = manager.ensureSessionMusicState(session); return { ok: true, now_playing: nowTrack ? { ...nowTrack } : musicState?.lastTrackTitle ? { id: musicState.lastTrackId || null, title: musicState.lastTrackTitle, artist: Array.isArray(musicState.lastTrackArtists) ? musicState.lastTrackArtists.join(", ") : null, source: String(musicState.provider || "").trim().toLowerCase() === "discord" ? "yt" : "yt", streamUrl: musicState.lastTrackUrl || null } : null, queue_state: manager.buildVoiceQueueStatePayload(session) }; }
export async function executeVoiceStreamVisualizerTool( manager: VoiceToolCallManager, { session, args, signal }: VoiceMusicToolOptions ) { throwIfAborted(signal, "Voice stream visualizer cancelled"); const currentPhase = session ? getMusicPhase(manager, session) : "idle"; if (!musicPhaseIsActive(currentPhase)) { return { ok: false, error: "music_not_active", reason: "stream_visualizer requires active music playback", phase: currentPhase }; }
const modeArg = normalizeInlineText(args?.mode, 32)?.toLowerCase() || null; const result = manager.startVisualizerStreamPublish({ guildId: String(session?.guildId || "").trim(), visualizerMode: modeArg, source: "stream_visualizer_tool" });
return { ok: Boolean(result?.ok), mode: modeArg || "default", reason: String(result?.reason || "").trim() || null }; }
