import { clamp, sanitizeBotText } from "../utils.ts"; import { buildReplyPrompt, buildSystemPrompt } from "../prompts/index.ts"; import { getMediaPromptCraftGuidance } from "../prompts/promptCore.ts"; import type { ReplyAttemptOptions } from "../bot.ts"; import { REPLY_OUTPUT_JSON_SCHEMA, composeReplyImagePrompt, composeReplyVideoPrompt, embedWebSearchSources, emptyMentionResolution, parseStructuredReplyOutput, pickReplyMediaDirective, resolveMaxMediaPromptLen, normalizeSkipSentinel, splitDiscordMessage, extractUrlsFromText } from "./botHelpers.ts"; import { getLocalTimeZoneLabel } from "./automation.ts"; import { buildReplyToolSet, executeReplyTool } from "../tools/replyTools.ts"; import type { ReplyToolContext, ReplyToolRuntime, ReplyToolDefinition } from "../tools/replyTools.ts"; import { runModelRequestedWebSearch } from "./replyFollowup.ts"; import { buildTextReplyScopeKey } from "../tools/activeReplyRegistry.ts"; import { isAbortError, throwIfAborted } from "../tools/abortError.ts"; import { buildRuntimeDecisionCorrelation } from "../services/runtimeCorrelation.ts"; import { resolveDeterministicMentions } from "./mentions.ts"; import { MAX_MODEL_IMAGE_INPUTS, UNICODE_REACTIONS, appendReplyFollowupPrompt, buildLoggedReplyPrompts, createReplyPerformanceTracker, createReplyPromptCapture, finalizeReplyPerformanceSample } from "./replyPipelineShared.ts"; import { resolveTextAttentionState } from "./replyAdmission.ts"; import { loadConversationContinuityContext } from "./conversationContinuity.ts"; import { loadBehavioralMemoryFacts } from "./memorySlice.ts"; import { getActivitySettings, getAutomationsSettings, getBotName, getDiscoverySettings, getMemorySettings, getReplyPermissions, getVideoContextSettings, getVisionSettings, getVoiceSettings, isDevTaskEnabled, isDevTaskUserAllowed, isMinecraftEnabled } from "../settings/agentStack.ts"; import type { Settings } from "../settings/settingsSchema.ts"; import { buildContextContentBlocks, type ContentBlock, type ContextMessage } from "../llm/serviceShared.ts"; import { SWARM_TOOL_SCHEMAS, VOICE_TOOL_SCHEMAS } from "../tools/sharedToolSchemas.ts"; import type { ReplyPipelineRuntime } from "./botContext.ts"; import { buildMinecraftSessionScopeKey, findReusableMinecraftSession, getMinecraftSessionPromptHint } from "../agents/minecraft/minecraftSessionAccess.ts"; import { isLikelyImageUrl, parseHistoryImageReference } from "./messageHistory.ts"; import { isLikelyDirectVideoUrl, parseVideoTarget, type VideoTarget } from "../video/videoTargets.ts";
type ReplyPipelineAttachment = { url?: string; proxyURL?: string; name?: string; contentType?: string; };
type ReplyPipelineAttachmentCollection = { size?: number; values: () => IterableIterator; };
type ReplyPipelineEmbed = { url?: string; type?: string; video?: { url?: string; proxyURL?: string; } | null; };
type ReplyPipelineGuildMember = { id?: string; displayName?: string; nickname?: string | null; user?: { id?: string; username?: string; globalName?: string | null; } | null; };
type ReplyPipelineGuild = { members?: { cache?: { size?: number; values: () => IterableIterator; }; search?: (options: { query: string; limit: number; }) => Promise<{ values: () => IterableIterator; }>; } | null; };
type ReplyPipelineMentions = { users?: { size: number; has: (id: string | undefined) => boolean; } | null; repliedUser?: { id: string; } | null; };
type ReplyMessagePayloadFile = { attachment: Buffer; name: string; };
type ReplyMessagePayload = Record<string, unknown> & { content: string; files?: ReplyMessagePayloadFile[]; allowedMentions?: { repliedUser?: boolean; }; };
type ReplyPipelineSentMessage = { id: string; createdTimestamp: number; guildId: string | null; channelId: string; content?: string; attachments?: ReplyPipelineAttachmentCollection; embeds?: ReplyPipelineEmbed[]; };
type ReplyPipelineChannel = { sendTyping: () => Promise; send: (payload: ReplyMessagePayload) => Promise; };
type ReplyPipelineMessage = ReplyPipelineSentMessage & { content: string; guild: ReplyPipelineGuild | null; author: { id: string; username?: string; bot?: boolean; }; member?: { displayName?: string | null; } | null; channel: ReplyPipelineChannel; mentions?: ReplyPipelineMentions; reference?: { messageId?: string; } | null; referencedMessage?: { id?: string; } | null; react: (emoji: string) => Promise; reply: (payload: ReplyMessagePayload) => Promise; };
type ReplyAddressSignal = | ReplyAttemptOptions["addressSignal"] | Awaited<ReturnType<ReplyPipelineRuntime["getReplyAddressSignal"]>>; type ReplyRecentMessage = Record<string, unknown>; type ReplyImageInput = Record<string, unknown> & { url?: string; mediaType?: string; contentType?: string; dataBase64?: string; }; type ReplyVideoInput = Record<string, unknown> & { url?: string; filename?: string; contentType?: string; videoRef?: string; }; type ReplyGeneration = { provider?: string; model?: string; usage?: Record<string, unknown> | null; costUsd?: number | null; text: string; rawContent?: unknown; toolCalls?: Array<{ id: string; name: string; input: Record<string, unknown>; }>; }; type ReplyToolExecutionResult = Awaited<ReturnType>; type ReplyPerformance = ReturnType; type ReplyPromptCaptureState = ReturnType; type ReplyPrompts = ReturnType; type ReplyContinuityContext = Awaited<ReturnType>; type ReplyPromptBase = Parameters[0]; type ReplyTrace = { guildId: string | null; channelId: string; userId: string; source: string | null; event: string | null; reason: string | null; messageId: string | null; }; type ReplyDirective = ReturnType; type ReplyMediaDirective = ReturnType; type ReplyMentionResolution = Awaited<ReturnType>; type ReplyReactionResult = Awaited<ReturnType<ReplyPipelineRuntime["maybeApplyReplyReaction"]>>; type ReplyScreenShareOffer = Awaited<ReturnType<ReplyPipelineRuntime["maybeHandleScreenWatchIntent"]>>; type ReplyWebSearchState = ReturnType<ReplyPipelineRuntime["buildWebSearchContext"]> & { summaryText?: string | null; }; type TurnScopedUrlMediaInputs = { imageInputs: ReplyImageInput[]; videoInputs: ReplyVideoInput[]; }; type TurnVisualMediaInspection = { imageInputs: ReplyImageInput[]; promptText: string; };
const MAX_TURN_SCOPED_URL_IMAGES = MAX_MODEL_IMAGE_INPUTS; const MAX_TURN_SCOPED_URL_VIDEOS = 3; const VISUAL_MEDIA_REQUEST_RE = /\b(?:what|what's|whats|happen(?:s|ing)?|main\s+point|point\s+of|describe|break\s+down|look(?:ing)?|watch(?:ing)?|see(?:ing)?|view(?:ed|ing)?|understand(?:s|ing)?|process(?:ed|ing)?)\b/i; const VISUAL_MEDIA_SUBJECT_RE = /\b(?:gif|video|clip|animation|motion|frames?|this|that|it)\b/i;
function isVisualMediaInspectionRequest(text: string) { const normalized = String(text || "").replace(/\s+/g, " ").trim(); if (!normalized) return false; return VISUAL_MEDIA_REQUEST_RE.test(normalized) && VISUAL_MEDIA_SUBJECT_RE.test(normalized); }
function getRecentMessageId(row: Record<string, unknown> | null | undefined) { return String(row?.message_id || row?.messageId || row?.id || "").trim(); }
function getTurnScopedTextEntries({ message, recentMessages, triggerMessageIds }: { message: ReplyPipelineMessage; recentMessages: ReplyRecentMessage[]; triggerMessageIds: string[]; }) { const triggerSet = new Set( [...triggerMessageIds, String(message.id || "")] .map((value) => String(value || "").trim()) .filter(Boolean) ); const rowsById = new Map<string, ReplyRecentMessage>(); for (const row of Array.isArray(recentMessages) ? recentMessages : []) { const id = getRecentMessageId(row); if (!id || !triggerSet.has(id)) continue; rowsById.set(id, row); }
const entries: Array<{ id: string | null; content: string }> = [];
const seen = new Set();
const pushEntry = (id: string | null, content: string) => {
const normalizedContent = String(content || "").trim();
if (!normalizedContent) return;
const key = id ? id:${id} : content:${normalizedContent};
if (seen.has(key)) return;
seen.add(key);
entries.push({ id, content: normalizedContent });
};
for (const id of triggerMessageIds) { const row = rowsById.get(String(id || "").trim()); if (row) pushEntry(getRecentMessageId(row) || null, String(row.content || "")); } pushEntry(String(message.id || "").trim() || null, String(message.content || ""));
return entries; }
function deriveFilenameFromUrl(url: string, fallback = "(linked media)") { const normalized = String(url || "").trim(); if (!normalized) return fallback; try { const parsed = new URL(normalized); const segment = parsed.pathname.split("/").filter(Boolean).at(-1) || ""; return decodeURIComponent(segment).trim() || fallback; } catch { const segment = normalized.split("?")[0].split("/").filter(Boolean).at(-1) || ""; return decodeURIComponent(segment).trim() || fallback; } }
function inferMotionContentTypeFromUrl(url: string) { const filename = deriveFilenameFromUrl(url, "").toLowerCase(); if (filename.endsWith(".gif")) return "image/gif"; if (filename.endsWith(".mp4") || filename.endsWith(".m4v")) return "video/mp4"; if (filename.endsWith(".mov")) return "video/quicktime"; if (filename.endsWith(".webm")) return "video/webm"; if (filename.endsWith(".mkv")) return "video/x-matroska"; if (filename.endsWith(".avi")) return "video/x-msvideo"; if (filename.endsWith(".mpeg") || filename.endsWith(".mpg")) return "video/mpeg"; return ""; }
function isLikelyAnimatedImageUrl(url: string) { const text = String(url || "").trim(); if (!text) return false; try { const parsed = new URL(text); const pathname = String(parsed.pathname || "").toLowerCase(); if (pathname.endsWith(".gif")) return true; return String(parsed.searchParams.get("format") || "").trim().toLowerCase() === "gif"; } catch { return false; } }
function safeHostname(url: string) { try { return new URL(String(url || "")).hostname.toLowerCase(); } catch { return ""; } }
function isLikelyMotionMediaUrl(url: string) { return isLikelyDirectVideoUrl(url) || isLikelyAnimatedImageUrl(url); }
function normalizeVideoInput(input: ReplyVideoInput): ReplyVideoInput | null { const url = String(input?.url || "").trim(); if (!url) return null; return { url, filename: String(input?.filename || deriveFilenameFromUrl(url)).trim() || "(linked media)", contentType: String(input?.contentType || inferMotionContentTypeFromUrl(url)).trim() }; }
function mergeVideoInputs(inputs: ReplyVideoInput[], maxInputs = MAX_TURN_SCOPED_URL_VIDEOS) { const out: ReplyVideoInput[] = []; const seen = new Set(); for (const input of inputs) { if (out.length >= maxInputs) break; const normalized = normalizeVideoInput(input); if (!normalized?.url || seen.has(normalized.url)) continue; seen.add(normalized.url); out.push(normalized); } return out; }
function collectTurnScopedUrlMediaInputs({ message, recentMessages, triggerMessageIds, existingImageUrls, existingVideoUrls }: { message: ReplyPipelineMessage; recentMessages: ReplyRecentMessage[]; triggerMessageIds: string[]; existingImageUrls?: string[]; existingVideoUrls?: string[]; }): TurnScopedUrlMediaInputs { const imageInputs: ReplyImageInput[] = []; const videoInputs: ReplyVideoInput[] = []; const seenImages = new Set((existingImageUrls || []).map((url) => String(url || "").trim()).filter(Boolean)); const seenVideos = new Set((existingVideoUrls || []).map((url) => String(url || "").trim()).filter(Boolean)); const addImageUrl = (url: string) => { const normalizedUrl = String(url || "").trim(); if (!normalizedUrl || seenImages.has(normalizedUrl) || imageInputs.length >= MAX_TURN_SCOPED_URL_IMAGES) return; if (isLikelyAnimatedImageUrl(normalizedUrl) || !isLikelyImageUrl(normalizedUrl)) return; const parsed = parseHistoryImageReference(normalizedUrl); seenImages.add(normalizedUrl); imageInputs.push({ url: normalizedUrl, filename: parsed.filename, contentType: parsed.contentType, mediaType: parsed.contentType }); }; const addVideoUrl = (url: string) => { const normalizedUrl = String(url || "").trim(); if (!normalizedUrl || seenVideos.has(normalizedUrl) || videoInputs.length >= MAX_TURN_SCOPED_URL_VIDEOS) return; seenVideos.add(normalizedUrl); videoInputs.push({ url: normalizedUrl, filename: deriveFilenameFromUrl(normalizedUrl), contentType: inferMotionContentTypeFromUrl(normalizedUrl) }); };
for (const entry of getTurnScopedTextEntries({ message, recentMessages, triggerMessageIds })) { const urls = extractUrlsFromText(entry.content) .map((url) => String(url || "").trim()) .filter(Boolean); if (!urls.length) continue;
const hasDirectMotion = urls.some((url) => isLikelyMotionMediaUrl(url));
for (const url of urls) {
if (isLikelyMotionMediaUrl(url)) {
addVideoUrl(url);
} else {
addImageUrl(url);
}
}
if (hasDirectMotion) continue;
for (const url of urls) {
if (videoInputs.length >= MAX_TURN_SCOPED_URL_VIDEOS) break;
if (parseVideoTarget(url)) addVideoUrl(url);
}
}
return { imageInputs, videoInputs }; }
function buildFallbackVideoTarget(url: string): VideoTarget {
return {
key: generic:${url},
url,
kind: "generic",
videoId: null
};
}
function normalizeVideoDependencyName(value: unknown): string { const name = String(value || "").trim(); return name === "ffmpeg" || name === "yt-dlp" ? name : ""; }
function getVideoMissingDependencies(video: Record<string, unknown> | null | undefined): string[] { if (!video) return []; const dependencies = new Set(); const rawDependencies = Array.isArray(video.missingDependencies) ? video.missingDependencies : []; for (const dependency of rawDependencies) { const normalized = normalizeVideoDependencyName(dependency); if (normalized) dependencies.add(normalized); }
const codes = [video.keyframeErrorCode, video.transcriptErrorCode].map((value) => String(value || "")); if (codes.includes("missing_ffmpeg")) dependencies.add("ffmpeg"); if (codes.includes("missing_yt_dlp")) dependencies.add("yt-dlp");
const errorText = [video.keyframeError, video.transcriptError] .map((value) => String(value || "").toLowerCase()) .join(" "); if (errorText.includes("local runtime dependency missing") || errorText.includes("not installed")) { if (errorText.includes("ffmpeg")) dependencies.add("ffmpeg"); if (errorText.includes("yt-dlp")) dependencies.add("yt-dlp"); }
return Array.from(dependencies); }
function buildVideoDependencyFailureLine(dependencies: string[]): string {
const list = dependencies.join(", ");
const installList = dependencies.length === 1
? dependencies[0]
: ${dependencies.slice(0, -1).join(", ")} and ${dependencies.at(-1)};
return Local runtime dependency missing (${list}). No GIF/video pixels were inspected; tell the user this is an operator setup issue and the bot needs ${installList} installed before visual details are available.;
}
async function inspectTurnVisualMedia({ bot, message, settings, videoInputs, shouldInspect, source }: { bot: ReplyPipelineRuntime; message: ReplyPipelineMessage; settings: Settings; videoInputs: ReplyVideoInput[]; shouldInspect: boolean; source: string; }): Promise { if (!shouldInspect || !videoInputs.length) return { imageInputs: [], promptText: "" };
const videoContextSettings = getVideoContextSettings(settings); if (!videoContextSettings.enabled || !bot.video || typeof bot.video.fetchContexts !== "function") { return { imageInputs: [], promptText: "GIF/video refs are present, but video context extraction is unavailable for this turn. Do not claim visual details from the URL or filename; say you cannot inspect it if visual specifics are needed." }; }
const maxVideos = clamp( Number(videoContextSettings.maxVideosPerMessage) || 1, 1, MAX_TURN_SCOPED_URL_VIDEOS ); const selectedVideos = videoInputs.slice(0, maxVideos); const isGifInspection = /\bgif\b/i.test(String(message.content || "")) || selectedVideos.some((video) => { const url = String(video.url || ""); const type = String(video.contentType || "").toLowerCase(); return type === "image/gif" || isLikelyAnimatedImageUrl(url) || /(?:^|.)giphy.com$/i.test(safeHostname(url)) || /(?:^|.)tenor.com$/i.test(safeHostname(url)); }); const keyframeIntervalSeconds = isGifInspection ? 1 : Number(videoContextSettings.keyframeIntervalSeconds) || 0; const maxKeyframesPerVideo = isGifInspection ? Math.max(4, Number(videoContextSettings.maxKeyframesPerVideo) || 0) : Number(videoContextSettings.maxKeyframesPerVideo) || 0; const targets = selectedVideos.map((video) => { const url = String(video.url || "").trim(); const target = bot.video.extractVideoTargets(url, 1)?.[0] || parseVideoTarget(url) || buildFallbackVideoTarget(url); return target; });
try { const result = await bot.video.fetchContexts({ targets, maxTranscriptChars: Number(videoContextSettings.maxTranscriptChars) || 1200, keyframeIntervalSeconds, maxKeyframesPerVideo, allowAsrFallback: Boolean(videoContextSettings.allowAsrFallback), maxAsrSeconds: Number(videoContextSettings.maxAsrSeconds) || 120, trace: { guildId: message.guildId, channelId: message.channelId, userId: message.author.id, source: "reply_pipeline_visual_media_inspection" } }); const videos = Array.isArray(result?.videos) ? result.videos : []; const imageInputs: ReplyImageInput[] = []; const missingDependencySet = new Set(); let keyframeErrorCount = 0; const lines = [ "GIF/video visual inspection was attempted for this visual question. Only attached keyframe images are visual evidence; metadata and error lines are not pixels." ];
selectedVideos.forEach((input, index) => {
const ref = String(input.videoRef || `VID ${index + 1}`).trim();
const video = videos[index] as Record<string, unknown> | undefined;
if (!video) {
lines.push(`${ref}: inspection returned no usable context.`);
return;
}
const title = String(video.title || input.filename || "linked media").trim();
const frameImages = Array.isArray(video.frameImages) ? video.frameImages as ReplyImageInput[] : [];
imageInputs.push(...frameImages);
const transcript = String(video.transcript || "").trim();
const keyframeError = String(video.keyframeError || "").trim();
const missingDependencies = getVideoMissingDependencies(video);
for (const dependency of missingDependencies) missingDependencySet.add(dependency);
if (keyframeError) keyframeErrorCount += 1;
lines.push(`${ref}: ${title}; keyframes attached: ${frameImages.length}.`);
if (transcript) lines.push(`${ref} transcript: ${transcript.slice(0, 500)}`);
if (keyframeError) lines.push(`${ref} keyframe error: ${keyframeError.slice(0, 240)}`);
if (missingDependencies.length && !frameImages.length) {
lines.push(`${ref}: ${buildVideoDependencyFailureLine(missingDependencies)}`);
}
});
const errorCount = Array.isArray(result?.errors) ? result.errors.length : 0;
const missingDependencies = Array.from(missingDependencySet);
if (!imageInputs.length) {
lines.push("No keyframe pixels were attached. Do not describe visual specifics unless metadata/transcript above supports them.");
if (missingDependencies.length) {
lines.push(buildVideoDependencyFailureLine(missingDependencies));
}
}
bot.store.logAction({
kind: "runtime",
guildId: message.guildId,
channelId: message.channelId,
messageId: message.id,
userId: message.author.id,
content: "turn_visual_media_inspection",
metadata: {
source,
videoRefs: selectedVideos.map((video, index) => String(video.videoRef || `VID ${index + 1}`)),
targetCount: targets.length,
keyframeCount: imageInputs.length,
keyframeErrorCount,
missingDependencies,
errorCount
}
});
return {
imageInputs,
promptText: lines.join("
")
};
} catch (error) {
const errorMessage = String(error instanceof Error ? error.message : error);
bot.store.logAction({
kind: "bot_error",
guildId: message.guildId,
channelId: message.channelId,
messageId: message.id,
userId: message.author.id,
content: "turn_visual_media_inspection_failed",
metadata: {
source,
error: errorMessage.slice(0, 500)
}
});
return {
imageInputs: [],
promptText: GIF/video inspection failed: ${errorMessage.slice(0, 300)}. Do not claim visual details from the URL or filename; say you cannot inspect it if visual specifics are needed.
};
}
}
type ReplyPipelineContext = { shouldRun: true; signal?: AbortSignal; recentMessages: ReplyRecentMessage[]; addressSignal: ReplyAddressSignal; triggerMessageIds: string[]; addressed: boolean; reactivity: number; isReplyChannel: boolean; ambientReplyEagerness: number; responseWindowEagerness: number; recentReplyWindowActive: boolean; reactionEmojiOptions: string[]; source: string; performance: ReplyPerformance; memorySlice: ReplyContinuityContext["memorySlice"]; replyMediaMemoryFacts: ReturnType<ReplyPipelineRuntime["buildMediaMemoryFacts"]>; attachmentImageInputs: ReplyImageInput[]; attachmentVideoInputs: ReplyVideoInput[]; videoLookupRefs: Record<string, string>; visualMediaContext: string; imageBudget: ReturnType<ReplyPipelineRuntime["getImageBudgetState"]>; videoBudget: ReturnType<ReplyPipelineRuntime["getVideoGenerationBudgetState"]>; mediaCapabilities: ReturnType<ReplyPipelineRuntime["getMediaGenerationCapabilities"]>; simpleImageCapabilityReady: boolean; complexImageCapabilityReady: boolean; imageCapabilityReady: boolean; videoCapabilityReady: boolean; gifBudget: ReturnType<ReplyPipelineRuntime["getGifBudgetState"]>; gifsConfigured: boolean; webSearch: ReplyWebSearchState; browserBrowse: ReturnType<ReplyPipelineRuntime["buildBrowserBrowseContext"]>; recentConversationHistory: ReplyContinuityContext["recentConversationHistory"]; recentVoiceSessionContext: Array<{ sessionId?: string | null; guildId?: string | null; channelId?: string | null; endedAt?: string | null; ageMinutes?: number | null; summaryText?: string | null; }>; memoryLookup: ReturnType<ReplyPipelineRuntime["buildMemoryLookupContext"]>; modelImageInputs: ReplyImageInput[]; imageLookup: ReturnType<ReplyPipelineRuntime["buildImageLookupContext"]>; subAgentSessions: ReturnType<ReplyPipelineRuntime["buildSubAgentSessionsRuntime"]>; replyTrace: ReplyTrace; screenShareCapability: ReturnType<ReplyPipelineRuntime["getVoiceScreenWatchCapability"]>; activeVoiceSession: ReturnType<ReplyPipelineRuntime["voiceSessionManager"]["getSession"]> | null; inVoiceChannelNow: boolean; activeVoiceParticipantRoster: string[]; musicState: ReturnType<ReplyPipelineRuntime["voiceSessionManager"]["getMusicPromptContext"]> | null; musicDisambiguation: ReturnType<ReplyPipelineRuntime["voiceSessionManager"]["getMusicDisambiguationPromptContext"]> | null; systemPrompt: string; replyPromptBase: ReplyPromptBase; initialUserPrompt: string; replyPromptCapture: ReplyPromptCaptureState; replyPrompts: ReplyPrompts; };
type ReplyIntentHandledResult = { handledByIntent: true; };
type ReplyActionableLlmResult = { handledByIntent: false; generation: ReplyGeneration; typingDelayMs: number; usedToolLoop: boolean; usedWebSearchFollowup: boolean; usedBrowserBrowseFollowup: boolean; usedMemoryLookupFollowup: boolean; usedImageLookupFollowup: boolean; mediaPromptLimit: number; replyDirective: ReplyDirective; webSearch: ReplyWebSearchState; browserBrowse: ReturnType<ReplyPipelineRuntime["buildBrowserBrowseContext"]>; memoryLookup: ReturnType<ReplyPipelineRuntime["buildMemoryLookupContext"]>; imageLookup: ReturnType<ReplyPipelineRuntime["buildImageLookupContext"]>; modelImageInputs: ReplyImageInput[]; toolResultImageInputs: ReplyImageInput[]; replyPrompts: ReplyPrompts; };
type ReplyLlmResult = ReplyIntentHandledResult | ReplyActionableLlmResult;
type ReplySkippedActionResult = { skipped: true; };
type ReplySendableActionResult = { skipped: false; reaction: ReplyReactionResult; mediaDirective: ReplyMediaDirective; finalText: string; mentionResolution: ReplyMentionResolution; screenShareOffer: ReplyScreenShareOffer; allowMediaOnlyReply: boolean; modelProducedSkip: boolean; modelProducedEmpty: boolean; payload: ReplyMessagePayload; toolImagesUsed: boolean; imageUsed: boolean; imageBudgetBlocked: boolean; imageCapabilityBlocked: boolean; imageVariantUsed: string | null; videoUsed: boolean; videoBudgetBlocked: boolean; videoCapabilityBlocked: boolean; gifUsed: boolean; gifBudgetBlocked: boolean; gifConfigBlocked: boolean; imagePrompt: ReplyDirective["imagePrompt"]; complexImagePrompt: ReplyDirective["complexImagePrompt"]; videoPrompt: ReplyDirective["videoPrompt"]; gifQuery: ReplyDirective["gifQuery"]; };
type ReplyActionResult = ReplySkippedActionResult | ReplySendableActionResult;
function isReplyActionableLlmResult(result: ReplyLlmResult): result is ReplyActionableLlmResult { return result.handledByIntent === false; }
function isReplySendableActionResult(result: ReplyActionResult): result is ReplySendableActionResult { return result.skipped === false; }
const TOOL_NARRATION_PATTERN = /^(?:I\s+(?:searched|looked|found|checked|browsed|fetched|retrieved|queried|ran|used|called|executed|performed|attempted|tried)\b|(?:After|Based on|From)\s+(?:searching|looking|browsing|checking|running|using|calling)\b)/i;
function looksLikeToolNarration(text: string): boolean { const trimmed = text.trim(); if (!trimmed) return false; return TOOL_NARRATION_PATTERN.test(trimmed); }
function logReplyPipelineGate( bot: ReplyPipelineRuntime, { message, settings, options, allow, reason, sendBudgetAllowed = null, talkNowAllowed = null, ctx = null }: { message: ReplyPipelineMessage; settings: Settings; options: ReplyAttemptOptions; allow: boolean; reason: string; sendBudgetAllowed?: boolean | null; talkNowAllowed?: boolean | null; ctx?: ReplyPipelineContext | null; } ) { const triggerMessageIds = [ ...new Set( [...(Array.isArray(options.triggerMessageIds) ? options.triggerMessageIds : []), message.id] .map((value) => String(value || "").trim()) .filter(Boolean) ) ]; const source = String(options.source || "message_event").trim() || "message_event"; const triggerMessageId = triggerMessageIds[0] || String(message.id || "").trim() || null; const isReplyChannel = bot.isReplyChannel(settings, message.channelId); const addressSignal = options.addressSignal && typeof options.addressSignal === "object" ? options.addressSignal : null;
bot.store.logAction({ kind: "text_runtime", guildId: message.guildId, channelId: message.channelId, messageId: message.id, userId: message.author?.id || null, content: "reply_pipeline_gate", metadata: { ...buildRuntimeDecisionCorrelation({ botId: bot.client.user?.id || null, triggerMessageId, source, stage: "pipeline", allow, reason }), triggerMessageIds, forceRespond: Boolean(options.forceRespond), forceDecisionLoop: Boolean(options.forceDecisionLoop), isReplyChannel, sendBudgetAllowed, talkNowAllowed, ctxBuilt: Boolean(ctx), ctxShouldRun: ctx ? Boolean(ctx.shouldRun) : null, addressed: ctx ? Boolean(ctx.addressed) : null, addressSignal: addressSignal ? { direct: Boolean(addressSignal.direct), inferred: Boolean(addressSignal.inferred), triggered: Boolean(addressSignal.triggered), reason: String(addressSignal.reason || "llm_decides"), confidence: Math.max(0, Math.min(1, Number(addressSignal.confidence) || 0)), threshold: Math.max(0.4, Math.min(0.95, Number(addressSignal.threshold) || 0.62)), confidenceSource: String(addressSignal.confidenceSource || "fallback") } : null } }); }
function buildReplyToolAvailabilityState( settings: Settings, { webSearch, browserBrowse, imageLookup, userId }: Pick<ReplyPipelineContext, "webSearch" | "browserBrowse" | "imageLookup"> & { userId: string | null | undefined } ): { tools: ReplyToolDefinition[]; capabilities: { webSearchAvailable?: boolean; webScrapeAvailable?: boolean; browserBrowseAvailable?: boolean; memoryAvailable?: boolean; imageLookupAvailable?: boolean; swarmToolsAvailable?: boolean; voiceToolsAvailable?: boolean; }; includedTools: string[]; excludedTools: Array<{ name: string; reason: string }>; } { const memoryEnabled = Boolean(getMemorySettings(settings).enabled); const voiceEnabled = Boolean(getVoiceSettings(settings).enabled); const codeAgentEnabled = isDevTaskEnabled(settings) && isDevTaskUserAllowed(settings, userId); const minecraftEnabled = isMinecraftEnabled(settings);
const webSearchReason = !webSearch?.enabled ? "settings_disabled" : !webSearch?.configured ? "provider_unconfigured" : webSearch?.optedOutByUser ? "opted_out_by_user" : webSearch?.blockedByBudget ? "budget_blocked" : webSearch?.budget?.canSearch === false ? "budget_exhausted" : "available"; const browserBrowseReason = !browserBrowse?.enabled ? "settings_disabled" : !browserBrowse?.configured ? "runtime_unavailable" : browserBrowse?.blockedByBudget ? "budget_blocked" : browserBrowse?.budget?.canBrowse === false ? "budget_exhausted" : "available"; const imageLookupReason = imageLookup?.enabled ? "available" : imageLookup?.error ? "lookup_error" : "no_history_images"; const memoryReason = memoryEnabled ? "available" : "settings_disabled"; const swarmToolsReason = codeAgentEnabled ? "available" : "settings_disabled"; const voiceToolReason = voiceEnabled ? "available" : "settings_disabled";
const videoContextEnabled = Boolean(getVideoContextSettings(settings).enabled); const videoContextReason = videoContextEnabled ? "available" : "settings_disabled";
const capabilities = { webSearchAvailable: webSearchReason === "available", webScrapeAvailable: webSearchReason === "available", browserBrowseAvailable: browserBrowseReason === "available", memoryAvailable: memoryReason === "available", imageLookupAvailable: imageLookupReason === "available", videoContextAvailable: videoContextReason === "available", swarmToolsAvailable: swarmToolsReason === "available", voiceToolsAvailable: voiceToolReason === "available", minecraftAvailable: minecraftEnabled }; const tools = buildReplyToolSet(settings, capabilities); const includedSet = new Set(tools.map((tool) => String(tool.name || "").trim()).filter(Boolean)); const candidates: Array<{ name: string; reason: string }> = [ { name: "web_search", reason: webSearchReason }, { name: "web_scrape", reason: webSearchReason }, { name: "video_context", reason: videoContextReason }, { name: "browser_browse", reason: browserBrowseReason }, { name: "memory_search", reason: memoryReason }, { name: "memory_write", reason: memoryReason }, { name: "conversation_search", reason: "available" }, { name: "image_lookup", reason: imageLookupReason }, { name: "spawn_code_worker", reason: swarmToolsReason }, ...SWARM_TOOL_SCHEMAS.map((schema) => ({ name: schema.name, reason: swarmToolsReason })), { name: "minecraft_task", reason: minecraftEnabled ? "available" : "settings_disabled" }, ...VOICE_TOOL_SCHEMAS.map((schema) => ({ name: schema.name, reason: voiceToolReason })) ];
return { tools, capabilities, includedTools: candidates .map((candidate) => candidate.name) .filter((name, index, values) => values.indexOf(name) === index && includedSet.has(name)), excludedTools: candidates .filter((candidate, index, values) => values.findIndex((entry) => entry.name === candidate.name) === index && !includedSet.has(candidate.name) ) .map((candidate) => ({ name: candidate.name, reason: candidate.reason })) }; }
function logReplyToolAvailability( bot: ReplyPipelineRuntime, { message, options, includedTools, excludedTools, capabilities }: { message: ReplyPipelineMessage; options: ReplyAttemptOptions; includedTools: string[]; excludedTools: Array<{ name: string; reason: string }>; capabilities: { webSearchAvailable?: boolean; webScrapeAvailable?: boolean; browserBrowseAvailable?: boolean; memoryAvailable?: boolean; imageLookupAvailable?: boolean; swarmToolsAvailable?: boolean; voiceToolsAvailable?: boolean; }; } ) { const triggerMessageIds = [ ...new Set( [...(Array.isArray(options.triggerMessageIds) ? options.triggerMessageIds : []), message.id] .map((value) => String(value || "").trim()) .filter(Boolean) ) ]; const source = String(options.source || "message_event").trim() || "message_event"; const triggerMessageId = triggerMessageIds[0] || String(message.id || "").trim() || null; bot.store.logAction({ kind: "text_runtime", guildId: message.guildId, channelId: message.channelId, messageId: message.id, userId: message.author?.id || null, content: "reply_tool_availability", metadata: { ...buildRuntimeDecisionCorrelation({ botId: bot.client.user?.id || null, triggerMessageId, source, stage: "tool_availability", allow: includedTools.length > 0, reason: includedTools.length > 0 ? "tools_available" : "no_tools_available" }), triggerMessageIds, includedToolCount: includedTools.length, excludedToolCount: excludedTools.length, includedTools, excludedTools, capabilities } }); }
async function buildReplyContext( bot: ReplyPipelineRuntime, message: ReplyPipelineMessage, settings: Settings, options: ReplyAttemptOptions ): Promise<ReplyPipelineContext | false> { const memorySettings = getMemorySettings(settings); const activity = getActivitySettings(settings); const automationsSettings = getAutomationsSettings(settings); const discovery = getDiscoverySettings(settings); const voiceSettings = getVoiceSettings(settings); const visionSettings = getVisionSettings(settings); const recentMessages = Array.isArray(options.recentMessages) ? options.recentMessages : bot.store.getRecentMessages(message.channelId, memorySettings.promptSlice.maxRecentMessages); const addressSignal = options.addressSignal || await bot.getReplyAddressSignal(settings, message, recentMessages); const triggerMessageIds = [ ...new Set( [...(Array.isArray(options.triggerMessageIds) ? options.triggerMessageIds : []), message.id] .map((value) => String(value || "").trim()) .filter(Boolean) ) ]; const addressed = Boolean(addressSignal?.triggered); const reactivity = clamp(Number(activity.reactivity) || 0, 0, 100); const isReplyChannel = bot.isReplyChannel(settings, message.channelId); const ambientReplyEagerness = clamp( Number(activity.ambientReplyEagerness) || 0, 0, 100 ); const responseWindowEagerness = clamp( Number(activity.responseWindowEagerness) || 0, 0, 100 ); const textAttentionState = resolveTextAttentionState({ botUserId: bot.client.user?.id || null, settings, message, recentMessages, addressSignal, triggerMessageId: message.id, triggerAuthorId: message.author?.id || null, triggerReferenceMessageId: message.reference?.messageId || message.referencedMessage?.id || null }); const recentReplyWindowActive = textAttentionState.recentReplyWindowActive; const reactionEmojiOptions = [ ...new Set([...bot.getReactionEmojiOptions(message.guild), ...UNICODE_REACTIONS]) ];
const shouldRunDecisionLoop = bot.shouldAttemptReplyDecision({ settings, message, recentMessages, addressSignal, isReplyChannel, forceRespond: Boolean(options.forceRespond), forceDecisionLoop: Boolean(options.forceDecisionLoop), triggerMessageId: message.id, triggerAuthorId: message.author?.id || null, triggerReferenceMessageId: message.reference?.messageId || message.referencedMessage?.id || null }); if (!shouldRunDecisionLoop) return false;
const source = String(options.source || "message_event"); const performance = createReplyPerformanceTracker({ messageCreatedAtMs: message?.createdTimestamp, source, seed: options.performanceSeed });
const memorySliceStartedAtMs = Date.now();
const continuity = await loadConversationContinuityContext({
settings,
userId: message.author.id,
guildId: message.guildId,
channelId: message.channelId,
queryText: message.content,
trace: {
guildId: message.guildId,
channelId: message.channelId,
userId: message.author.id
},
source,
recentMessages,
loadFactProfile: (payload) =>
bot.loadFactProfile({
settings: payload.settings,
userId: payload.userId,
guildId: payload.guildId ?? message.guildId ?? null,
channelId: payload.channelId,
queryText: payload.queryText,
trace: payload.trace,
source: payload.source
}),
loadRecentConversationHistory: (payload) => bot.getConversationHistoryForPrompt(payload)
});
const memorySlice = continuity.memorySlice;
const behavioralFacts = await loadBehavioralMemoryFacts(bot, {
settings,
guildId: message.guildId,
channelId: message.channelId,
queryText: message.content,
participantIds: Array.isArray(memorySlice?.participantProfiles)
? memorySlice.participantProfiles
.map((entry) => String((entry as Record<string, unknown>)?.userId || "").trim())
.filter(Boolean)
: [],
trace: {
guildId: message.guildId,
channelId: message.channelId,
userId: message.author.id,
source: "reply_pipeline_behavioral_memory"
},
limit: 8
});
performance.memorySliceMs = Math.max(0, Date.now() - memorySliceStartedAtMs);
const replyMediaMemoryFacts = bot.buildMediaMemoryFacts({
userFacts: memorySlice.userFacts,
relevantFacts: memorySlice.relevantFacts
});
const directAttachmentImageInputs: ReplyImageInput[] = bot.getImageInputs(message);
const directAttachmentVideoInputs: ReplyVideoInput[] = bot.getVideoInputs(message) || [];
const turnScopedUrlMedia = collectTurnScopedUrlMediaInputs({
message,
recentMessages,
triggerMessageIds,
existingImageUrls: directAttachmentImageInputs.map((image) => String(image?.url || "").trim()),
existingVideoUrls: directAttachmentVideoInputs.map((video) => String(video?.url || "").trim())
});
const attachmentImageInputs: ReplyImageInput[] = bot.mergeImageInputs({
baseInputs: directAttachmentImageInputs,
extraInputs: turnScopedUrlMedia.imageInputs,
maxInputs: MAX_MODEL_IMAGE_INPUTS
});
const attachmentVideoInputs: ReplyVideoInput[] = mergeVideoInputs([
...directAttachmentVideoInputs,
...turnScopedUrlMedia.videoInputs
])
.map((video, index) => ({
...video,
videoRef: VID ${index + 1}
}));
const videoLookupRefs = Object.fromEntries(
attachmentVideoInputs
.map((video) => [
String(video.videoRef || "").trim(),
String(video.url || "").trim()
])
.filter(([videoRef, url]) => Boolean(videoRef && url))
);
const imageBudget = bot.getImageBudgetState(settings);
const videoBudget = bot.getVideoGenerationBudgetState(settings);
const mediaCapabilities = bot.getMediaGenerationCapabilities(settings);
const simpleImageCapabilityReady = mediaCapabilities.simpleImageReady;
const complexImageCapabilityReady = mediaCapabilities.complexImageReady;
const imageCapabilityReady = simpleImageCapabilityReady || complexImageCapabilityReady;
const videoCapabilityReady = mediaCapabilities.videoReady;
const gifBudget = bot.getGifBudgetState(settings);
const gifsConfigured = Boolean(bot.gifs?.isConfigured?.());
const webSearch: ReplyWebSearchState = bot.buildWebSearchContext(settings, message.content);
const browserBrowse = bot.buildBrowserBrowseContext(settings);
const recentConversationHistory = continuity.recentConversationHistory;
const recentVoiceSessionContext =
memorySettings.enabled && typeof bot.memory?.getRecentVoiceSessionSummariesForPrompt === "function"
? bot.memory.getRecentVoiceSessionSummariesForPrompt({
guildId: message.guildId,
channelId: message.channelId,
referenceAtMs: message.createdTimestamp
})
: [];
const memoryLookup = bot.buildMemoryLookupContext({ settings });
const visualMediaContext = await inspectTurnVisualMedia({
bot,
message,
settings,
videoInputs: attachmentVideoInputs,
shouldInspect: isVisualMediaInspectionRequest(message.content),
source
});
const modelImageInputs: ReplyImageInput[] = bot.mergeImageInputs({
baseInputs: attachmentImageInputs,
extraInputs: visualMediaContext.imageInputs,
maxInputs: MAX_MODEL_IMAGE_INPUTS
});
const imageLookup = bot.buildImageLookupContext({
recentMessages,
excludedUrls: modelImageInputs.map((image) => String(image?.url || "").trim())
});
if (Boolean(visionSettings.enabled) && imageLookup.candidates?.length) { // Fire-and-forget: caption uncaptioned images in background for future text matching. // Historical images stay out of direct model vision context until the model explicitly asks. bot.captionRecentHistoryImages({ candidates: imageLookup.candidates, settings, trace: { guildId: message.guildId, channelId: message.channelId, userId: message.author.id, source: "reply_pipeline_auto_caption" } }); } const replyTrace = { guildId: message.guildId, channelId: message.channelId, userId: message.author.id, source, event: null, reason: null, messageId: message.id }; const screenShareCapability = bot.getVoiceScreenWatchCapability({ settings, guildId: message.guildId, channelId: message.channelId, requesterUserId: message.author?.id || null }); const activeVoiceSession = typeof bot.voiceSessionManager?.getSession === "function" ? bot.voiceSessionManager.getSession(message.guildId) : null; const inVoiceChannelNow = Boolean(activeVoiceSession && !activeVoiceSession.ending); const activeVoiceParticipantRoster = inVoiceChannelNow && typeof bot.voiceSessionManager?.getVoiceChannelParticipants === "function" ? bot.voiceSessionManager .getVoiceChannelParticipants(activeVoiceSession) .map((entry) => String(entry?.displayName || "").trim()) .filter(Boolean) : []; const musicDisambiguation = inVoiceChannelNow && typeof bot.voiceSessionManager?.getMusicDisambiguationPromptContext === "function" ? bot.voiceSessionManager.getMusicDisambiguationPromptContext(activeVoiceSession) : null; const musicState = inVoiceChannelNow && typeof bot.voiceSessionManager?.getMusicPromptContext === "function" ? bot.voiceSessionManager.getMusicPromptContext(activeVoiceSession) : null; const subAgentSessions = bot.buildSubAgentSessionsRuntime(); const activeMinecraftSession = findReusableMinecraftSession(subAgentSessions.manager, { ownerUserId: message.author.id, scopeKey: buildMinecraftSessionScopeKey({ guildId: message.guildId, channelId: message.channelId }) }); const minecraftSessionHint = getMinecraftSessionPromptHint(activeMinecraftSession);
const systemPrompt = buildSystemPrompt(settings); const replyPromptBase: ReplyPromptBase = { message: { authorName: message.member?.displayName || message.author.username, content: message.content }, triggerMessageIds, imageInputs: modelImageInputs, recentMessages, participantProfiles: memorySlice.participantProfiles, selfFacts: memorySlice.selfFacts, loreFacts: memorySlice.loreFacts, guidanceFacts: Array.isArray(memorySlice.guidanceFacts) ? memorySlice.guidanceFacts : [], behavioralFacts, userFacts: memorySlice.userFacts, relevantFacts: memorySlice.relevantFacts, emojiHints: bot.getEmojiHints(message.guild), reactionEmojiOptions, allowReplySimpleImages: discovery.allowReplyImages && simpleImageCapabilityReady && imageBudget.canGenerate, allowReplyComplexImages: discovery.allowReplyImages && complexImageCapabilityReady && imageBudget.canGenerate, remainingReplyImages: imageBudget.remaining, allowReplyVideos: discovery.allowReplyVideos && videoCapabilityReady && videoBudget.canGenerate, remainingReplyVideos: videoBudget.remaining, allowReplyGifs: discovery.allowReplyGifs && gifsConfigured && gifBudget.canFetch, remainingReplyGifs: gifBudget.remaining, gifRepliesEnabled: discovery.allowReplyGifs, gifsConfigured, ambientReplyEagerness, responseWindowEagerness, recentReplyWindowActive, textAttentionMode: textAttentionState.mode, textAttentionReason: textAttentionState.reason, reactivity, addressing: { directlyAddressed: addressed, directAddressConfidence: Number(addressSignal?.confidence) || 0, directAddressThreshold: Number(addressSignal?.threshold) || 0.62, mentionsOtherUsers: Boolean( !addressed && message.mentions?.users?.size > 0 && !message.mentions.users.has(bot.client.user?.id) ), repliesToOtherUser: Boolean( !addressed && message.mentions?.repliedUser && message.mentions.repliedUser.id !== bot.client.user?.id ) }, allowMemoryDirective: memorySettings.enabled, allowAutomationDirective: Boolean(automationsSettings.enabled), automationTimeZoneLabel: getLocalTimeZoneLabel(), voiceMode: { enabled: Boolean(voiceSettings.enabled), activeSession: inVoiceChannelNow, participantRoster: activeVoiceParticipantRoster, musicState, musicDisambiguation }, recentConversationHistory, recentVoiceSessionContext, minecraftSessionHint, screenShare: screenShareCapability, visualMediaContext: visualMediaContext.promptText, channelMode: isReplyChannel ? "reply_channel" : bot.isDiscoveryChannel(settings, message.channelId) ? "discovery_channel" : "other_channel", maxMediaPromptChars: resolveMaxMediaPromptLen(settings), mediaPromptCraftGuidance: getMediaPromptCraftGuidance(settings) }; const initialUserPrompt = buildReplyPrompt({ ...replyPromptBase, imageInputs: modelImageInputs, videoInputs: attachmentVideoInputs, webSearch, browserBrowse, memoryLookup, imageLookup }); const replyPromptCapture = createReplyPromptCapture({ systemPrompt, initialUserPrompt }); const replyPrompts = buildLoggedReplyPrompts(replyPromptCapture, 0);
return { shouldRun: true, recentMessages, addressSignal, triggerMessageIds, addressed, reactivity, isReplyChannel, ambientReplyEagerness, responseWindowEagerness, recentReplyWindowActive, reactionEmojiOptions, source, performance, memorySlice, replyMediaMemoryFacts, attachmentImageInputs, attachmentVideoInputs, videoLookupRefs, visualMediaContext: visualMediaContext.promptText, imageBudget, videoBudget, mediaCapabilities, simpleImageCapabilityReady, complexImageCapabilityReady, imageCapabilityReady, videoCapabilityReady, gifBudget, gifsConfigured, webSearch, browserBrowse, recentConversationHistory, recentVoiceSessionContext, memoryLookup, modelImageInputs, imageLookup, subAgentSessions, replyTrace, screenShareCapability, activeVoiceSession, inVoiceChannelNow, activeVoiceParticipantRoster, musicState, musicDisambiguation, systemPrompt, replyPromptBase, initialUserPrompt, replyPromptCapture, replyPrompts }; }
async function executeReplyLlm( bot: ReplyPipelineRuntime, message: ReplyPipelineMessage, settings: Settings, options: ReplyAttemptOptions, ctx: ReplyPipelineContext ): Promise { const { addressSignal, triggerMessageIds, source, performance, signal, replyTrace, systemPrompt, initialUserPrompt, replyPromptCapture, activeVoiceSession, inVoiceChannelNow, videoLookupRefs } = ctx; const { memoryLookup } = ctx; let { webSearch, browserBrowse, modelImageInputs, imageLookup, replyPrompts } = ctx; const { subAgentSessions } = ctx;
const replyToolAvailability = buildReplyToolAvailabilityState(settings, { webSearch, browserBrowse, imageLookup, userId: message.author.id }); const replyTools = replyToolAvailability.tools; logReplyToolAvailability(bot, { message, options, includedTools: replyToolAvailability.includedTools, excludedTools: replyToolAvailability.excludedTools, capabilities: replyToolAvailability.capabilities });
const activeVoiceCallbacks = inVoiceChannelNow && activeVoiceSession ? bot.voiceSessionManager.buildVoiceToolCallbacks({ session: activeVoiceSession, settings }) : null;
const replyToolRuntime: ReplyToolRuntime = {
search: bot.search,
video: bot.video ? {
fetchContext: async ({ url, settings: toolSettings, trace }) => {
const videoContextSettings = getVideoContextSettings(toolSettings);
const targets = bot.video.extractVideoTargets(url, 1);
if (!targets.length) {
// URL didn't match a known video host — fall through with a generic target
targets.push({
key: generic:${url},
url,
kind: "generic",
provider: "generic",
videoId: null
});
}
const result = await bot.video.fetchContexts({
targets,
maxTranscriptChars: Number(videoContextSettings.maxTranscriptChars) || 1200,
keyframeIntervalSeconds: Number(videoContextSettings.keyframeIntervalSeconds) || 0,
maxKeyframesPerVideo: Number(videoContextSettings.maxKeyframesPerVideo) || 0,
allowAsrFallback: Boolean(videoContextSettings.allowAsrFallback),
maxAsrSeconds: Number(videoContextSettings.maxAsrSeconds) || 120,
trace
});
if (result.errors?.length && !result.videos?.length) {
return {
text: Video context extraction failed for ${url}: ${result.errors[0]?.error || "unknown error"}. Try web_scrape or browser_browse as fallback.,
isError: true
};
}
const video = result.videos?.[0];
if (!video) {
return {
text: No video metadata could be extracted from ${url}. Try web_scrape or browser_browse instead.,
isError: true
};
}
const lines: string[] = [];
lines.push(Provider: ${video.provider || "unknown"});
lines.push(Title: ${video.title || "untitled"});
lines.push(Channel: ${video.channel || "unknown"});
if (video.url) lines.push(URL: ${video.url});
if (video.publishedAt) lines.push(Published: ${video.publishedAt});
if (video.durationSeconds) lines.push(Duration: ${video.durationSeconds}s);
if (video.viewCount) lines.push(Views: ${video.viewCount});
if (video.description) lines.push(Description: ${video.description});
if (video.transcript) lines.push(Transcript (${video.transcriptSource || "unknown source"}): ${video.transcript});
if (video.transcriptError) lines.push(Transcript error: ${video.transcriptError});
if (video.keyframeError) lines.push(Keyframe error: ${video.keyframeError});
const frameImages = video.frameImages || [];
const missingDependencies = getVideoMissingDependencies(video as Record<string, unknown>);
if (missingDependencies.length && !frameImages.length) {
lines.push(buildVideoDependencyFailureLine(missingDependencies));
}
if (frameImages.length) lines.push(Keyframes: ${frameImages.length} frame(s) attached);
const toolResultText = lines.join("
");
const frameImageBytes = frameImages.reduce((sum, img) => sum + (img.dataBase64 ? Math.ceil(img.dataBase64.length * 3 / 4) : 0), 0);
bot.store.logAction({
kind: "runtime",
guildId: trace.guildId,
channelId: trace.channelId,
userId: trace.userId,
content: "video_context_tool_result",
metadata: {
url,
provider: video.provider || "unknown",
title: video.title || "untitled",
durationSeconds: video.durationSeconds || null,
hasTranscript: Boolean(video.transcript),
transcriptChars: video.transcript?.length || 0,
keyframeCount: frameImages.length,
keyframePayloadBytes: frameImageBytes,
keyframeError: video.keyframeError || null,
keyframeErrorCode: video.keyframeErrorCode || null,
transcriptErrorCode: video.transcriptErrorCode || null,
missingDependencies,
toolResultChars: toolResultText.length,
toolResultPreview: toolResultText.slice(0, 300)
}
});
return {
text: toolResultText,
imageInputs: frameImages.length ? frameImages : undefined
};
}
} : undefined,
browser: {
browse: async ({ settings: toolSettings, query, guildId, channelId, userId, source }) => {
browserBrowse = await bot.runModelRequestedBrowserBrowse({
settings: toolSettings,
browserBrowse,
query,
guildId,
channelId,
userId,
source,
signal
});
return browserBrowse;
}
},
memory: bot.memory,
store: bot.store,
swarm: {
peerManager: bot.swarmPeerManager,
reservationKeeper: bot.swarmReservationKeeper,
activityBridge: bot.swarmActivityBridge
},
subAgentSessions,
voiceSession: activeVoiceCallbacks || undefined,
voiceJoin: Boolean(getVoiceSettings(settings).enabled) && bot.voiceSessionManager
? async () => {
try {
const joined = await bot.voiceSessionManager.requestJoin({
message,
settings,
intentConfidence: 1
});
if (!joined) {
return { ok: false, reason: "join_not_handled" };
}
const session = bot.voiceSessionManager.getSession(message.guildId);
if (!session || session.ending) {
return { ok: false, reason: "session_not_available_after_join" };
}
const callbacks = bot.voiceSessionManager.buildVoiceToolCallbacks({ session, settings });
const voiceChannelId = String(session.voiceChannelId || "");
const guild = bot.client.guilds?.cache?.get(message.guildId);
const voiceChannel = voiceChannelId && guild?.channels?.cache?.get(voiceChannelId);
const voiceChannelName = String(voiceChannel?.name || voiceChannelId || "voice channel");
return { ok: true, voiceSession: callbacks, voiceChannelName };
} catch (error) {
return { ok: false, reason: String((error as Error)?.message || error) };
}
}
: undefined
};
const replyToolContext: ReplyToolContext = {
settings,
guildId: message.guildId,
channelId: message.channelId,
userId: message.author.id,
sourceMessageId: message.id,
sourceText: message.content,
botUserId: bot.client.user?.id || undefined,
actorName: message.member?.displayName || message.author?.username || undefined,
trace: {
...replyTrace,
source
},
videoLookup: {
refs: videoLookupRefs
},
signal
};
let replyContextMessages: ContextMessage[] = [];
throwIfAborted(signal, "Reply cancelled"); const typingStartedAtMs = Date.now(); await message.channel.sendTyping(); const typingDelayMs = Math.max(0, Date.now() - typingStartedAtMs); const llm1StartedAtMs = Date.now(); let generation: ReplyGeneration = await bot.llm.generate({ settings, systemPrompt, userPrompt: initialUserPrompt, imageInputs: modelImageInputs, contextMessages: replyContextMessages, jsonSchema: REPLY_OUTPUT_JSON_SCHEMA, tools: replyTools, trace: replyTrace, signal }); performance.llm1Ms = Math.max(0, Date.now() - llm1StartedAtMs); let usedWebSearchFollowup = false; let usedBrowserBrowseFollowup = false; let usedMemoryLookupFollowup = false; let usedImageLookupFollowup = false; let toolResultImageInputs: ReplyImageInput[] = []; const REPLY_TOOL_LOOP_MAX_STEPS = 2; const REPLY_TOOL_LOOP_MAX_CALLS = 3; let replyToolLoopSteps = 0; let replyTotalToolCalls = 0; const followupStartedAtMs = Date.now();
while ( generation.toolCalls?.length > 0 && replyToolLoopSteps < REPLY_TOOL_LOOP_MAX_STEPS && replyTotalToolCalls < REPLY_TOOL_LOOP_MAX_CALLS ) { throwIfAborted(signal, "Reply cancelled"); const assistantContent = buildContextContentBlocks(generation.rawContent, generation.text); // On the first iteration, seed the context with the original user prompt. // On subsequent iterations the prompt is already in the history — don't duplicate it. if (replyContextMessages.length === 0) { replyContextMessages = [ { role: "user", content: initialUserPrompt }, { role: "assistant", content: assistantContent } ]; } else { replyContextMessages = [ ...replyContextMessages, { role: "assistant", content: assistantContent } ]; }
const toolResultMessages: ContentBlock[] = [];
let toolResultImageInputsAdded = false;
const mergeToolResultImages = (extraInputs: ReplyImageInput[] | undefined) => {
if (!Array.isArray(extraInputs) || extraInputs.length === 0 || typeof bot.mergeImageInputs !== "function") {
return false;
}
modelImageInputs = bot.mergeImageInputs({
baseInputs: modelImageInputs,
extraInputs,
maxInputs: MAX_MODEL_IMAGE_INPUTS
});
toolResultImageInputs = bot.mergeImageInputs({
baseInputs: toolResultImageInputs,
extraInputs,
maxInputs: MAX_MODEL_IMAGE_INPUTS
});
return true;
};
// Separate sub-agent tools (can run concurrently) from sequential tools
const CONCURRENT_TOOL_NAMES = new Set(["spawn_code_worker", "browser_browse"]);
const eligibleToolCalls = generation.toolCalls.slice(
0,
Math.max(0, REPLY_TOOL_LOOP_MAX_CALLS - replyTotalToolCalls)
);
replyTotalToolCalls += eligibleToolCalls.length;
const concurrentCalls = eligibleToolCalls.filter((tc) => CONCURRENT_TOOL_NAMES.has(tc.name));
const sequentialCalls = eligibleToolCalls.filter((tc) => !CONCURRENT_TOOL_NAMES.has(tc.name));
// Run concurrent sub-agent calls in parallel
const concurrentResults = new Map<string, ReplyToolExecutionResult>();
if (concurrentCalls.length > 0) {
const settledCalls = await Promise.allSettled(concurrentCalls.map(async (toolCall) => {
throwIfAborted(signal, "Reply cancelled");
const toolInput = toolCall.input;
const result = await executeReplyTool(toolCall.name, toolInput, replyToolRuntime, replyToolContext);
if (mergeToolResultImages(result?.imageInputs)) {
toolResultImageInputsAdded = true;
}
concurrentResults.set(toolCall.id, result);
}));
settledCalls.forEach((settled, index) => {
if (settled.status === "fulfilled") return;
const toolCall = concurrentCalls[index];
const errorMessage =
settled.reason instanceof Error
? settled.reason.message
: String(settled.reason || "unknown_error");
concurrentResults.set(toolCall.id, {
content: `${toolCall.name} failed: ${errorMessage}`,
isError: true
});
bot.store?.logAction?.({
kind: "bot_error",
guildId: message.guildId,
channelId: message.channelId,
messageId: message.id,
userId: message.author?.id || null,
content: `reply_tool_concurrent_failure:${toolCall.name}`,
metadata: {
source,
toolCallId: toolCall.id,
error: errorMessage
}
});
});
}
// Run sequential tools in order
const sequentialResults = new Map<string, ReplyToolExecutionResult>();
for (const toolCall of sequentialCalls) {
throwIfAborted(signal, "Reply cancelled");
const toolInput = toolCall.input;
let result: ReplyToolExecutionResult;
if (toolCall.name === "web_search") {
const toolQuery = String(toolInput.query || "");
webSearch = await runModelRequestedWebSearch(
{ llm: bot.llm, search: bot.search, memory: bot.memory },
{
settings,
webSearch,
query: toolQuery,
trace: {
...replyTrace,
source
},
signal
}
);
usedWebSearchFollowup = Boolean(webSearch?.used);
const rows = Array.isArray(webSearch?.results) ? webSearch.results : [];
result = {
isError: Boolean(webSearch?.error),
content: webSearch?.error
? `Web search failed: ${String(webSearch.error)}`
: rows.length || String(webSearch?.summaryText || "").trim()
? `Web results for "${String(webSearch?.query || toolQuery)}":
${[
String(webSearch?.summaryText || "").trim()
? Summary: ${String(webSearch?.summaryText || "").trim()}
: "",
rows
.map((item, index) => {
const title = String(item?.title || "untitled").trim();
const url = String(item?.url || "").trim();
const domain = String(item?.domain || "").trim();
const snippet = String(item?.snippet || "").trim();
const pageSummary = String(item?.pageSummary || "").trim();
const domainLabel = domain ? (${domain}) : "";
const snippetLine = snippet ? Snippet: ${snippet} : "";
const pageLine = pageSummary ? Page: ${pageSummary} : "";
return [${index + 1}] ${title}${domainLabel} URL: ${url}${snippetLine}${pageLine};
})
.join("
") ] .filter(Boolean) .join("
")} :No results found for: "${toolQuery}"`
};
} else {
result = await executeReplyTool(
toolCall.name,
toolInput,
replyToolRuntime,
replyToolContext
);
}
if (mergeToolResultImages(result?.imageInputs)) {
toolResultImageInputsAdded = true;
}
if (toolCall.name === "memory_search" && !result.isError) {
usedMemoryLookupFollowup = true;
} else if (toolCall.name === "image_lookup" && !result.isError) {
const imageLookupRequest = String(toolInput.imageId || toolInput.query || "");
imageLookup = await bot.runModelRequestedImageLookup({
imageLookup,
query: imageLookupRequest
});
if (mergeToolResultImages(imageLookup.selectedImageInputs || [])) {
toolResultImageInputsAdded = true;
}
usedImageLookupFollowup = Boolean(imageLookup?.used);
}
sequentialResults.set(toolCall.id, result);
}
// Collect results in original order
for (const toolCall of eligibleToolCalls) {
const result = concurrentResults.get(toolCall.id) || sequentialResults.get(toolCall.id);
if (result) {
if (toolCall.name === "browser_browse" && !result.isError) {
usedBrowserBrowseFollowup = Boolean(browserBrowse?.used);
}
toolResultMessages.push({
type: "tool_result",
tool_use_id: toolCall.id,
content: result.content
});
}
}
replyContextMessages = [
...replyContextMessages,
{ role: "user", content: toolResultMessages }
];
throwIfAborted(signal, "Reply cancelled");
const toolLoopUserPrompt = toolResultImageInputsAdded
? "Attached are images returned by the previous tool call. Use them if they help. If you want to include those exact images in the final Discord reply, set media to {\"type\":\"tool_images\",\"prompt\":null}."
: "";
appendReplyFollowupPrompt(replyPromptCapture, toolLoopUserPrompt);
generation = await bot.llm.generate({
settings,
systemPrompt,
userPrompt: toolLoopUserPrompt,
imageInputs: modelImageInputs,
contextMessages: replyContextMessages,
jsonSchema: REPLY_OUTPUT_JSON_SCHEMA,
tools: replyTools,
trace: {
...replyTrace,
event: `reply_tool_loop:${replyToolLoopSteps + 1}`
},
signal
});
replyToolLoopSteps += 1;
}
const mediaPromptLimit = resolveMaxMediaPromptLen(settings); const replyDirective = parseStructuredReplyOutput(generation.text, mediaPromptLimit); replyPrompts = buildLoggedReplyPrompts(replyPromptCapture, replyToolLoopSteps);
const automationIntentHandled = await bot.maybeHandleStructuredAutomationIntent({ message, settings, replyDirective, generation, source, triggerMessageIds, addressing: addressSignal, performance, replyPrompts }); if (automationIntentHandled) return { handledByIntent: true };
if ( replyToolLoopSteps > 0 || usedWebSearchFollowup || usedBrowserBrowseFollowup || usedMemoryLookupFollowup || usedImageLookupFollowup ) { performance.followupMs = Math.max(0, Date.now() - followupStartedAtMs); }
return { handledByIntent: false, generation, typingDelayMs, usedToolLoop: replyToolLoopSteps > 0, usedWebSearchFollowup, usedBrowserBrowseFollowup, usedMemoryLookupFollowup, usedImageLookupFollowup, mediaPromptLimit, replyDirective, webSearch, browserBrowse, memoryLookup, imageLookup, modelImageInputs, toolResultImageInputs, replyPrompts }; }
async function dispatchReplyActions( bot: ReplyPipelineRuntime, message: ReplyPipelineMessage, settings: Settings, _options: ReplyAttemptOptions, ctx: ReplyPipelineContext, llmResult: ReplyActionableLlmResult ): Promise { const discovery = getDiscoverySettings(settings); const { addressSignal, triggerMessageIds, reactionEmojiOptions, source, performance, replyMediaMemoryFacts } = ctx; const { generation, usedToolLoop, usedWebSearchFollowup, mediaPromptLimit, replyDirective, webSearch, toolResultImageInputs, replyPrompts } = llmResult; if (replyDirective.parseState === "unstructured") { if (usedToolLoop && looksLikeToolNarration(String(generation.text || ""))) { bot.logSkippedReply({ message, source, triggerMessageIds, addressSignal, generation, usedWebSearchFollowup, reason: "invalid_structured_output_after_tool_loop", reaction: null, screenShareOffer: null, performance, prompts: replyPrompts, extraMetadata: { rawTextPreview: sanitizeBotText(String(generation.text || ""), 280) || null } }); return { skipped: true }; } const recoveredText = sanitizeBotText(String(generation.text || "")); if (recoveredText) { replyDirective.text = recoveredText; bot.store.logAction({ kind: "bot_warning", guildId: message.guildId, channelId: message.channelId, messageId: message.id, userId: bot.client.user?.id || null, content: "structured_output_recovered_as_prose", metadata: { source, parseState: replyDirective.parseState } }); } else { bot.logSkippedReply({ message, source, triggerMessageIds, addressSignal, generation, usedWebSearchFollowup, reason: "invalid_structured_output", reaction: null, screenShareOffer: null, performance, prompts: replyPrompts, extraMetadata: { rawTextPreview: sanitizeBotText(String(generation.text || ""), 280) || null } }); return { skipped: true }; } }
const reaction = await bot.maybeApplyReplyReaction({ message, settings, emojiOptions: reactionEmojiOptions, emojiToken: replyDirective.reactionEmoji, generation, source, triggerMessageId: message.id, triggerMessageIds, addressing: addressSignal });
const mediaDirective = pickReplyMediaDirective(replyDirective); let finalText = sanitizeBotText(replyDirective.text || ""); let mentionResolution = emptyMentionResolution(); finalText = normalizeSkipSentinel(finalText); const screenShareOffer = await bot.maybeHandleScreenWatchIntent({ message, replyDirective, source }); if (screenShareOffer.appendText) { const textParts = []; if (finalText && finalText !== "[SKIP]") textParts.push(finalText); textParts.push(screenShareOffer.appendText); finalText = sanitizeBotText(textParts.join(" "), 1700); } const allowMediaOnlyReply = !finalText && Boolean(mediaDirective); const modelProducedSkip = finalText === "[SKIP]"; const modelProducedEmpty = !finalText; if (modelProducedEmpty && !allowMediaOnlyReply) { bot.store.logAction({ kind: "bot_error", guildId: message.guildId, channelId: message.channelId, messageId: message.id, userId: bot.client.user?.id || null, content: "reply_model_output_empty", metadata: { source, triggerMessageIds, addressed: Boolean(addressSignal?.triggered) } }); } if (finalText === "[SKIP]" || (!finalText && !allowMediaOnlyReply)) { bot.logSkippedReply({ message, source, triggerMessageIds, addressSignal, generation, usedWebSearchFollowup, reason: modelProducedSkip ? "llm_skip" : "empty_reply", reaction, screenShareOffer, performance, prompts: replyPrompts }); return { skipped: true }; }
mentionResolution = await resolveDeterministicMentions( { store: bot.store }, { text: finalText, guild: message.guild, guildId: message.guildId } ); finalText = mentionResolution.text; finalText = embedWebSearchSources(finalText, webSearch);
let payload: ReplyMessagePayload = { content: finalText }; let toolImagesUsed = false; let imageUsed = false; let imageBudgetBlocked = false; let imageCapabilityBlocked = false; let imageVariantUsed = null; let videoUsed = false; let videoBudgetBlocked = false; let videoCapabilityBlocked = false; let gifUsed = false; let gifBudgetBlocked = false; let gifConfigBlocked = false; const imagePrompt = replyDirective.imagePrompt; const complexImagePrompt = replyDirective.complexImagePrompt; const videoPrompt = replyDirective.videoPrompt; const gifQuery = replyDirective.gifQuery; const mediaAttachment = await bot.resolveMediaAttachment({ settings, text: finalText, directive: { type: mediaDirective?.type ?? null, gifQuery, imagePrompt: mediaDirective?.type === "image_simple" && discovery.allowReplyImages && imagePrompt ? composeReplyImagePrompt( imagePrompt, finalText, mediaPromptLimit, replyMediaMemoryFacts ) : null, complexImagePrompt: mediaDirective?.type === "image_complex" && discovery.allowReplyImages && complexImagePrompt ? composeReplyImagePrompt( complexImagePrompt, finalText, mediaPromptLimit, replyMediaMemoryFacts ) : null, videoPrompt: mediaDirective?.type === "video" && discovery.allowReplyVideos && videoPrompt ? composeReplyVideoPrompt( videoPrompt, finalText, mediaPromptLimit, replyMediaMemoryFacts ) : null }, toolImageInputs: toolResultImageInputs, trace: { guildId: message.guildId, channelId: message.channelId, userId: message.author.id, source: "reply_message" } }); payload = mediaAttachment.payload; toolImagesUsed = mediaAttachment.toolImagesUsed; imageUsed = mediaAttachment.imageUsed; imageBudgetBlocked = mediaAttachment.imageBudgetBlocked; imageCapabilityBlocked = mediaAttachment.imageCapabilityBlocked; imageVariantUsed = mediaAttachment.imageVariantUsed; videoUsed = mediaAttachment.videoUsed; videoBudgetBlocked = mediaAttachment.videoBudgetBlocked; videoCapabilityBlocked = mediaAttachment.videoCapabilityBlocked; gifUsed = mediaAttachment.gifUsed; gifBudgetBlocked = mediaAttachment.gifBudgetBlocked; gifConfigBlocked = mediaAttachment.gifConfigBlocked;
if (!finalText && !toolImagesUsed && !imageUsed && !videoUsed && !gifUsed) { bot.store.logAction({ kind: "bot_error", guildId: message.guildId, channelId: message.channelId, messageId: message.id, userId: bot.client.user?.id || null, content: "reply_model_output_empty_after_media", metadata: { source, triggerMessageIds, addressed: Boolean(addressSignal?.triggered) } }); bot.logSkippedReply({ message, source, triggerMessageIds, addressSignal, generation, usedWebSearchFollowup, reason: "empty_reply_after_media", reaction, screenShareOffer, performance, prompts: replyPrompts }); return { skipped: true }; }
return { skipped: false, reaction, mediaDirective, finalText, mentionResolution, screenShareOffer, allowMediaOnlyReply, modelProducedSkip, modelProducedEmpty, payload, toolImagesUsed, imageUsed, imageBudgetBlocked, imageCapabilityBlocked, imageVariantUsed, videoUsed, videoBudgetBlocked, videoCapabilityBlocked, gifUsed, gifBudgetBlocked, gifConfigBlocked, imagePrompt, complexImagePrompt, videoPrompt, gifQuery }; }
async function sendReplyMessage( bot: ReplyPipelineRuntime, message: ReplyPipelineMessage, settings: Settings, options: ReplyAttemptOptions, ctx: ReplyPipelineContext, llmResult: ReplyActionableLlmResult, actionResult: ReplySendableActionResult ): Promise { const botName = getBotName(settings); const { addressSignal, triggerMessageIds, addressed, isReplyChannel, source, performance, imageBudget, videoBudget, simpleImageCapabilityReady, complexImageCapabilityReady, imageCapabilityReady, videoCapabilityReady, gifBudget } = ctx; const { generation, typingDelayMs, usedWebSearchFollowup, usedMemoryLookupFollowup, usedImageLookupFollowup, toolResultImageInputs, webSearch, imageLookup, memoryLookup, replyPrompts } = llmResult; const { reaction, mediaDirective, finalText, mentionResolution, screenShareOffer, payload, toolImagesUsed, imageUsed, imageBudgetBlocked, imageCapabilityBlocked, imageVariantUsed, videoUsed, videoBudgetBlocked, videoCapabilityBlocked, gifUsed, gifBudgetBlocked, gifConfigBlocked, imagePrompt, complexImagePrompt, videoPrompt, gifQuery } = actionResult;
const shouldThreadReply = addressed; const isDiscovery = bot.isDiscoveryChannel(settings, message.channelId); const canStandalonePost = isReplyChannel || isDiscovery || !shouldThreadReply; const sendAsReply = bot.shouldSendAsReply({ isReplyChannel, shouldThreadReply, replyText: finalText }); const sendStartedAtMs = Date.now(); const textChunks = splitDiscordMessage(payload.content); const firstPayload = { ...payload, content: textChunks[0] }; const sent = sendAsReply ? await message.reply({ ...firstPayload, allowedMentions: { repliedUser: false } }) : await message.channel.send(firstPayload); for (let i = 1; i < textChunks.length; i++) { await message.channel.send({ content: textChunks[i] }); } const sendMs = Math.max(0, Date.now() - sendStartedAtMs); const actionKind = sendAsReply ? "sent_reply" : "sent_message"; const referencedMessageId = sendAsReply ? message.id : null; const memorySettings = getMemorySettings(settings);
bot.markSpoke();
bot.store.recordMessage({
messageId: sent.id,
createdAt: sent.createdTimestamp,
guildId: sent.guildId,
channelId: sent.channelId,
authorId: bot.client.user.id,
authorName: botName,
isBot: true,
content: bot.composeMessageContentForHistory(sent, finalText),
referencedMessageId
});
if (memorySettings.enabled && typeof bot.memory?.ingestMessage === "function") {
void bot.memory.ingestMessage({
messageId: sent.id,
authorId: bot.client.user.id,
authorName: botName,
content: finalText,
isBot: true,
settings,
trace: {
guildId: sent.guildId,
channelId: sent.channelId,
userId: bot.client.user.id,
source: "text_reply_memory_ingest"
}
}).catch((error) => {
bot.store.logAction({
kind: "bot_error",
guildId: sent.guildId,
channelId: sent.channelId,
messageId: sent.id,
userId: bot.client.user.id,
content: memory_text_reply_ingest: ${String(error?.message || error)}
});
});
}
bot.store.logAction({
kind: actionKind,
guildId: sent.guildId,
channelId: sent.channelId,
messageId: sent.id,
userId: bot.client.user.id,
content: finalText,
metadata: {
triggerMessageId: message.id,
triggerMessageIds,
source,
addressing: addressSignal,
replyPrompts,
sendAsReply,
canStandalonePost,
image: {
requestedByModel: Boolean(imagePrompt || complexImagePrompt),
requestedSimpleByModel: Boolean(imagePrompt),
requestedComplexByModel: Boolean(complexImagePrompt),
selectedVariant: imageVariantUsed,
used: imageUsed,
blockedByDailyCap: imageBudgetBlocked,
blockedByCapability: imageCapabilityBlocked,
maxPerDay: imageBudget.maxPerDay,
remainingAtPromptTime: imageBudget.remaining,
simpleCapabilityReadyAtPromptTime: simpleImageCapabilityReady,
complexCapabilityReadyAtPromptTime: complexImageCapabilityReady,
capabilityReadyAtPromptTime: imageCapabilityReady
},
videoGeneration: {
requestedByModel: Boolean(videoPrompt),
used: videoUsed,
blockedByDailyCap: videoBudgetBlocked,
blockedByCapability: videoCapabilityBlocked,
maxPerDay: videoBudget.maxPerDay,
remainingAtPromptTime: videoBudget.remaining,
capabilityReadyAtPromptTime: videoCapabilityReady
},
gif: {
requestedByModel: Boolean(gifQuery),
used: gifUsed,
blockedByDailyCap: gifBudgetBlocked,
blockedByConfiguration: gifConfigBlocked,
maxPerDay: gifBudget.maxPerDay,
remainingAtPromptTime: gifBudget.remaining
},
toolImages: {
requestedByModel: mediaDirective?.type === "tool_images",
availableFromTools: toolResultImageInputs.length,
used: toolImagesUsed
},
memory: {
toolCallsUsed: usedMemoryLookupFollowup,
query: memoryLookup?.query || null,
results: (memoryLookup?.results || []).map((r: Record<string, unknown>) => ({
fact: r.fact,
fact_type: r.fact_type,
subject: r.subject,
confidence: r.confidence
}))
},
imageLookup: {
requested: imageLookup.requested,
used: imageLookup.used,
query: imageLookup.query,
candidateCount: imageLookup.candidates?.length || 0,
resultCount: imageLookup.results?.length || 0,
error: imageLookup.error || null,
results: (imageLookup.results || []).map((r: Record<string, unknown>) => ({
filename: r.filename,
authorName: r.authorName,
url: r.url,
matchReason: r.matchReason
}))
},
mentions: mentionResolution,
reaction,
screenShareOffer,
webSearch: {
requested: webSearch.requested,
used: webSearch.used,
query: webSearch.query,
resultCount: webSearch.results?.length || 0,
results: (webSearch.results || []).map((r) => ({
title: r.title,
url: r.url,
domain: r.domain
})),
fetchedPages: webSearch.fetchedPages || 0,
providerUsed: webSearch.providerUsed || null,
providerFallbackUsed: Boolean(webSearch.providerFallbackUsed),
blockedByHourlyCap: webSearch.blockedByBudget,
maxPerHour: webSearch.budget?.maxPerHour ?? null,
remainingAtPromptTime: webSearch.budget?.remaining ?? null,
configured: webSearch.configured,
optedOutByUser: webSearch.optedOutByUser,
error: webSearch.error || null
},
video: {
mode: "agent_tool"
},
llm: {
provider: generation.provider,
model: generation.model,
usage: generation.usage,
costUsd: generation.costUsd,
usedWebSearchFollowup,
usedMemoryLookupFollowup,
usedImageLookupFollowup
},
performance: finalizeReplyPerformanceSample({
performance,
actionKind,
typingDelayMs,
sendMs
})
}
});
return true; }
export async function maybeReplyToMessagePipeline( bot: ReplyPipelineRuntime, message: ReplyPipelineMessage, settings: Settings, options: ReplyAttemptOptions = {} ): Promise { const permissions = getReplyPermissions(settings); if (!permissions.allowReplies) { logReplyPipelineGate(bot, { message, settings, options, allow: false, reason: "replies_disabled", sendBudgetAllowed: null, talkNowAllowed: null }); return false; } const sendBudgetAllowed = bot.canSendMessage(permissions.maxMessagesPerHour); if (!sendBudgetAllowed) { logReplyPipelineGate(bot, { message, settings, options, allow: false, reason: "send_budget_blocked", sendBudgetAllowed, talkNowAllowed: null }); return false; } const talkNowAllowed = bot.canTalkNow(settings); if (!talkNowAllowed) { logReplyPipelineGate(bot, { message, settings, options, allow: false, reason: "talk_now_blocked", sendBudgetAllowed, talkNowAllowed }); return false; }
const replyScopeKey = buildTextReplyScopeKey({ guildId: message.guildId, channelId: message.channelId }); const activeReply = bot.activeReplies.begin(replyScopeKey, "text-reply"); const signal = activeReply.abortController.signal;
try { throwIfAborted(signal, "Reply cancelled"); const ctx = await buildReplyContext(bot, message, settings, options); if (!ctx || !ctx.shouldRun) { logReplyPipelineGate(bot, { message, settings, options, allow: false, reason: "context_unavailable", sendBudgetAllowed, talkNowAllowed, ctx: ctx || null }); return false; } ctx.signal = signal; logReplyPipelineGate(bot, { message, settings, options, allow: true, reason: "ready", sendBudgetAllowed, talkNowAllowed, ctx });
throwIfAborted(signal, "Reply cancelled");
const llmResult = await executeReplyLlm(bot, message, settings, options, ctx);
if (!isReplyActionableLlmResult(llmResult)) return true;
const actionableLlmResult = llmResult;
throwIfAborted(signal, "Reply cancelled");
const actionResult = await dispatchReplyActions(bot, message, settings, options, ctx, actionableLlmResult);
if (!isReplySendableActionResult(actionResult)) return false;
const sendableActionResult = actionResult;
throwIfAborted(signal, "Reply cancelled");
return await sendReplyMessage(
bot,
message,
settings,
options,
ctx,
actionableLlmResult,
sendableActionResult
);
} catch (error) { if (isAbortError(error) || signal.aborted) { // Return true ("reply handled") so the caller does not retry or fall back // to another reply path after the user explicitly cancelled this turn. return true; } throw error; } finally { bot.activeReplies.clear(activeReply); } }
