UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

277 lines (276 loc) 12.2 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { t as isDiagnosticFlagEnabled } from "./diagnostic-flags-4mY6zjJI.js"; import { r as logVerbose } from "./globals-GTrXU4s9.js"; import { l as mimeTypeFromFilePath } from "./mime-C8mVE2Bw.js"; import { h as stringifyRouteThreadId } from "./channel-route-D7X5briz.js"; import "./message-channel-constants-CgI32lqD.js"; import { i as normalizeMessageChannel } from "./message-channel-normalize-BLLf0ubu.js"; import "./message-channel-BiOeMu0l.js"; import { n as resolveAgentTurnAttachments } from "./agent-turn-attachments-BVNZMNvw.js"; //#region src/shared/silent-reply-policy.ts const DEFAULT_SILENT_REPLY_POLICY = { direct: "disallow", group: "allow", internal: "allow" }; /** Classifies a reply context for silent-reply policy from explicit type, session key, or surface. */ function classifySilentReplyConversationType(params) { if (params.conversationType) return params.conversationType; const normalizedSessionKey = normalizeLowercaseStringOrEmpty(params.sessionKey); if (normalizedSessionKey.includes(":group:") || normalizedSessionKey.includes(":channel:")) return "group"; if (normalizedSessionKey.includes(":direct:") || normalizedSessionKey.includes(":dm:")) return "direct"; if (normalizeLowercaseStringOrEmpty(params.surface) === "webchat") return "direct"; return "internal"; } /** Resolves silent-reply policy with surface overrides while keeping direct replies audible. */ function resolveSilentReplyPolicyFromPolicies(params) { if (params.conversationType === "direct") return "disallow"; return params.surfacePolicy?.[params.conversationType] ?? params.defaultPolicy?.[params.conversationType] ?? DEFAULT_SILENT_REPLY_POLICY[params.conversationType]; } //#endregion //#region src/config/silent-reply.ts function resolveSilentReplyConversationContext(params) { const conversationType = classifySilentReplyConversationType({ sessionKey: params.sessionKey, surface: params.surface, conversationType: params.conversationType }); const normalizedSurface = normalizeLowercaseStringOrEmpty(params.surface); const surface = normalizedSurface ? params.cfg?.surfaces?.[normalizedSurface] : void 0; return { conversationType, defaultPolicy: params.cfg?.agents?.defaults?.silentReply, surfacePolicy: surface?.silentReply }; } /** Resolves the effective silent-reply settings for a routed conversation. */ function resolveSilentReplySettings(params) { return { policy: resolveSilentReplyPolicyFromPolicies(resolveSilentReplyConversationContext(params)) }; } /** Returns just the effective silent-reply policy for callers that do not need metadata. */ function resolveSilentReplyPolicy(params) { return resolveSilentReplySettings(params).policy; } //#endregion //#region src/auto-reply/reply/current-turn-images.ts function isGenericMediaType(mediaType) { if (!mediaType) return true; const normalized = mediaType.split(";")[0]?.trim().toLowerCase(); return normalized === "application/octet-stream" || normalized === "binary/octet-stream"; } /** Resolves image media types from current-turn attachment metadata or filenames. */ function resolveCurrentImageMediaType(pathValue, mediaType) { const mediaPath = normalizeOptionalString(pathValue); if (!mediaPath) return; const normalizedMediaType = normalizeOptionalString(mediaType); if (normalizedMediaType?.startsWith("image/")) return normalizedMediaType; if (!isGenericMediaType(normalizedMediaType)) return; const inferredType = mimeTypeFromFilePath(mediaPath); return inferredType?.startsWith("image/") ? inferredType : void 0; } function collectCurrentImageAttachments(ctx) { const pathsFromArray = Array.isArray(ctx.MediaPaths) ? ctx.MediaPaths : void 0; const paths = pathsFromArray && pathsFromArray.length > 0 ? pathsFromArray : normalizeOptionalString(ctx.MediaPath) ? [ctx.MediaPath] : []; if (paths.length === 0) return []; const types = Array.isArray(ctx.MediaTypes) && ctx.MediaTypes.length === paths.length ? ctx.MediaTypes : void 0; const attachments = []; for (const [index, pathValue] of paths.entries()) { const mediaPath = normalizeOptionalString(pathValue); const mediaType = resolveCurrentImageMediaType(pathValue, types?.[index] ?? ctx.MediaType); if (mediaPath && mediaType) attachments.push({ index, path: mediaPath, mediaType }); } return attachments; } function collectDescribedImageAttachmentIndexes(ctx) { return new Set(ctx.MediaUnderstanding?.filter((output) => output.kind === "image.description").map((output) => output.attachmentIndex) ?? []); } function createUndescribedImageContext(ctx, undescribedAttachments) { const first = undescribedAttachments[0]; return { ...ctx, MediaPath: first?.path, MediaType: first?.mediaType, MediaPaths: undescribedAttachments.map((attachment) => attachment.path), MediaTypes: undescribedAttachments.map((attachment) => attachment.mediaType) }; } /** Resolves current-turn image attachments that were not already described by media understanding. */ async function resolveCurrentTurnImages(params) { if (Array.isArray(params.images) && params.images.length > 0) return { images: params.images, imageOrder: params.imageOrder }; const currentImageAttachments = collectCurrentImageAttachments(params.ctx); if (currentImageAttachments.length === 0) return { images: params.images, imageOrder: params.imageOrder }; const describedImageIndexes = collectDescribedImageAttachmentIndexes(params.ctx); const undescribedImageAttachments = currentImageAttachments.filter((attachment) => !describedImageIndexes.has(attachment.index)); if (undescribedImageAttachments.length === 0) return { images: params.images, imageOrder: params.imageOrder }; try { const images = (await resolveAgentTurnAttachments({ ctx: createUndescribedImageContext(params.ctx, undescribedImageAttachments), cfg: params.cfg, includeRecentHistoryImages: false })).attachments.map((attachment) => ({ type: "image", data: attachment.data, mimeType: attachment.mediaType })); if (images.length < undescribedImageAttachments.length) { logVerbose(`agent-runner: native OpenClaw media resolution produced ${images.length}/${undescribedImageAttachments.length} current image attachment(s); falling back to prompt image refs`); return { images: params.images, imageOrder: params.imageOrder }; } return images.length > 0 ? { images, imageOrder: images.map(() => "inline") } : { images: params.images, imageOrder: params.imageOrder }; } catch (error) { logVerbose(`agent-runner: media attachment image resolution failed, proceeding without native images: ${formatErrorMessage(error)}`); return { images: params.images, imageOrder: params.imageOrder }; } } //#endregion //#region src/auto-reply/reply/reply-timing-tracker.ts const DEFAULT_TIMING_WARN_TOTAL_MS = 1e3; const DEFAULT_TIMING_WARN_STAGE_MS = 500; /** Checks config/env diagnostic flags for reply profiling. */ function isReplyProfilerEnabled(params) { const cfg = params?.config; const env = params?.env ?? process.env; return isDiagnosticFlagEnabled("profiler", cfg, env) || isDiagnosticFlagEnabled("reply.profiler", cfg, env); } /** Creates a lightweight timing tracker for slow reply-stage diagnostics. */ function createReplyTimingTracker(params) { if (!(params.enabled ?? isReplyProfilerEnabled({ config: params.config, env: params.env }))) return { async measure(_name, run) { return await run(); }, measureSync(_name, run) { return run(); }, logIfSlow() {} }; const startedAt = Date.now(); const spans = []; let didLog = false; const totalWarnMs = params.totalWarnMs ?? DEFAULT_TIMING_WARN_TOTAL_MS; const stageWarnMs = params.stageWarnMs ?? DEFAULT_TIMING_WARN_STAGE_MS; const toMs = (value) => Math.max(0, Math.round(value)); const record = (name, spanStartedAt) => { spans.push({ name, durationMs: toMs(Date.now() - spanStartedAt), elapsedMs: toMs(Date.now() - startedAt) }); }; const snapshot = () => ({ totalMs: toMs(Date.now() - startedAt), spans: spans.slice() }); const shouldLog = (summary) => summary.totalMs >= totalWarnMs || summary.spans.some((span) => span.durationMs >= stageWarnMs); const formatSpans = (summary) => summary.spans.length > 0 ? summary.spans.map((span) => `${span.name}:${span.durationMs}ms@${span.elapsedMs}ms`).join(",") : "none"; return { async measure(name, run) { const spanStartedAt = Date.now(); try { return await run(); } finally { record(name, spanStartedAt); } }, measureSync(name, run) { const spanStartedAt = Date.now(); try { return run(); } finally { record(name, spanStartedAt); } }, logIfSlow(logParams) { if (didLog) return; const summary = snapshot(); if (!shouldLog(summary)) return; didLog = true; const suffix = [ `totalMs=${summary.totalMs}`, `stages=${formatSpans(summary)}`, logParams.outcome ? `outcome=${logParams.outcome}` : void 0, logParams.reason ? `reason=${logParams.reason}` : void 0, logParams.error ? `error="${logParams.error}"` : void 0 ].filter(Boolean).join(" "); params.log.warn(`${logParams.message} ${suffix}`, { ...logParams.details, outcome: logParams.outcome, reason: logParams.reason, error: logParams.error, totalMs: summary.totalMs, spans: summary.spans }); } }; } //#endregion //#region src/auto-reply/reply/effective-reply-route.ts /** Returns true for synthetic providers that should not define a user channel route. */ function isSystemEventProvider(provider) { return provider === "heartbeat" || provider === "cron-event" || provider === "exec-event"; } function isSessionsSendInterSessionHandoff(inputProvenance) { return inputProvenance?.kind === "inter_session" && inputProvenance.sourceTool?.toLowerCase() === "sessions_send"; } function resolveTrustedInheritedThreadId(entry) { const deliveryThreadId = entry?.deliveryContext?.threadId; if (deliveryThreadId == null) return; const routeThread = entry?.route?.thread; if (routeThread?.id != null && (routeThread.source === "explicit" || routeThread.source === "target" || routeThread.source === "turn") && stringifyRouteThreadId(routeThread.id) === stringifyRouteThreadId(deliveryThreadId)) return deliveryThreadId; } /** Resolves current, inherited, or persisted reply route for a session turn. */ function resolveEffectiveReplyRoute(params) { const currentSurface = normalizeMessageChannel(params.ctx.Provider) ?? normalizeMessageChannel(params.ctx.Surface) ?? normalizeMessageChannel(params.ctx.OriginatingChannel); const persistedDeliveryContext = params.entry?.deliveryContext; const persistedDeliveryChannel = normalizeMessageChannel(persistedDeliveryContext?.channel); if (isSessionsSendInterSessionHandoff(params.ctx.InputProvenance) && currentSurface === "webchat" && persistedDeliveryChannel && persistedDeliveryChannel !== "webchat" && persistedDeliveryContext?.to) { const inheritedThreadId = resolveTrustedInheritedThreadId(params.entry); return { channel: persistedDeliveryChannel, to: persistedDeliveryContext.to, accountId: persistedDeliveryContext.accountId, ...inheritedThreadId !== void 0 ? { threadId: inheritedThreadId } : {}, inheritedExternalRoute: true }; } if (!isSystemEventProvider(params.ctx.Provider)) return { channel: params.ctx.OriginatingChannel, to: params.ctx.OriginatingTo, accountId: params.ctx.AccountId }; return { channel: params.ctx.OriginatingChannel ?? persistedDeliveryContext?.channel ?? params.entry?.lastChannel, to: params.ctx.OriginatingTo ?? persistedDeliveryContext?.to ?? params.entry?.lastTo, accountId: params.ctx.AccountId ?? persistedDeliveryContext?.accountId ?? params.entry?.lastAccountId }; } //#endregion export { resolveCurrentTurnImages as a, resolveSilentReplyPolicyFromPolicies as c, isReplyProfilerEnabled as i, resolveEffectiveReplyRoute as n, resolveSilentReplyPolicy as o, createReplyTimingTracker as r, resolveSilentReplySettings as s, isSystemEventProvider as t };