openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
204 lines (203 loc) • 8.57 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { n as SILENT_REPLY_TOKEN, o as isSilentReplyText } from "./tokens-DbeAFqg7.js";
import { r as logVerbose } from "./globals-CTaGxEqj.js";
import { a as normalizeAttachments, n as isImageAttachment } from "./attachments.normalize-BdfpE9Qn.js";
import { t as stripExtractedFileImageMetadata } from "./extracted-file-images-CdmNdoIK.js";
import { r as resolveAgentTurnAttachments, t as collectDescribedImageAttachmentIndexes } from "./agent-turn-attachments-DPqIEbM0.js";
//#region src/auto-reply/reply/queue-policy.ts
/** Resolves whether an active session should run, queue, or drop a new inbound turn. */
function resolveActiveRunQueueAction(params) {
if (!params.isActive && (!params.queueAdmissionState || params.queueAdmissionState === "empty")) return "run-now";
if (params.isHeartbeat) return "drop";
if (params.resetTriggered) return "run-now";
if (params.queueAdmissionState && params.queueAdmissionState !== "empty") return "enqueue-followup";
if (params.shouldFollowup) return "enqueue-followup";
return "run-now";
}
//#endregion
//#region src/auto-reply/reply/typing-mode.ts
/** Group chats default to message-triggered typing to avoid noisy indicators. */
const DEFAULT_GROUP_TYPING_MODE = "message";
/** Resolves the effective typing mode for the current auto-reply turn. */
function resolveTypingMode({ configured, isGroupChat, wasMentioned, isHeartbeat, typingPolicy, suppressTyping, sourceReplyDeliveryMode }) {
if (isHeartbeat || typingPolicy === "heartbeat" || typingPolicy === "system_event" || typingPolicy === "internal_webchat" || suppressTyping) return "never";
if (configured) return configured;
if (sourceReplyDeliveryMode === "message_tool_only") return "instant";
if (!isGroupChat || wasMentioned) return "instant";
return DEFAULT_GROUP_TYPING_MODE;
}
/** Creates a typing signaler that starts or refreshes typing from stream events. */
function createTypingSignaler(params) {
const { typing, mode, isHeartbeat } = params;
const shouldStartImmediately = mode === "instant";
const shouldStartOnMessageStart = mode === "message";
const shouldStartOnText = mode === "message" || mode === "instant";
const shouldStartOnReasoning = mode === "thinking";
const disabled = isHeartbeat || mode === "never";
let hasRenderableText = false;
const isRenderableText = (text) => {
const trimmed = normalizeOptionalString(text);
if (!trimmed) return false;
return !isSilentReplyText(trimmed, SILENT_REPLY_TOKEN);
};
const signalRunStart = async () => {
if (disabled || !shouldStartImmediately) return;
await typing.startTypingLoop();
};
const signalMessageStart = async () => {
if (disabled || !shouldStartOnMessageStart) return;
if (!hasRenderableText) return;
await typing.startTypingLoop();
};
const signalTextDelta = async (text) => {
if (disabled) return;
if (isRenderableText(text)) hasRenderableText = true;
else if (normalizeOptionalString(text)) return;
else return;
if (shouldStartOnText) {
await typing.startTypingOnText(text);
return;
}
if (shouldStartOnReasoning) {
if (!typing.isActive()) await typing.startTypingLoop();
typing.refreshTypingTtl();
}
};
const signalReasoningDelta = async () => {
if (disabled || !shouldStartOnReasoning) return;
await typing.startTypingLoop();
typing.refreshTypingTtl();
};
const signalToolStart = async () => {
if (disabled) return;
if (!typing.isActive()) {
if (shouldStartOnMessageStart && !hasRenderableText) return;
await typing.startTypingLoop();
typing.refreshTypingTtl();
return;
}
typing.refreshTypingTtl();
};
const signalExecutionActivity = async () => {
if (disabled) return;
if (!typing.isActive()) await typing.startTypingLoop();
typing.refreshTypingTtl();
};
return {
mode,
shouldStartImmediately,
shouldStartOnMessageStart,
shouldStartOnText,
shouldStartOnReasoning,
signalRunStart,
signalMessageStart,
signalTextDelta,
signalReasoningDelta,
signalToolStart,
signalExecutionActivity
};
}
//#endregion
//#region src/auto-reply/reply/current-turn-images.ts
function collectCurrentImageAttachments(ctx) {
return normalizeAttachments(ctx).flatMap((attachment) => {
const mediaPath = normalizeOptionalString(attachment.path);
return mediaPath && isImageAttachment(attachment) ? [{
...attachment,
path: mediaPath
}] : [];
});
}
function appendOrderedImages(params) {
const images = params.images ?? [];
if (!params.imageOrder || params.imageOrder.length === 0) {
for (const image of images) params.entries.push({
image,
imageOrder: "inline",
sourceIndex: params.sourceIndex,
sequence: params.entries.length
});
return;
}
let inlineIndex = 0;
for (const imageOrder of params.imageOrder) params.entries.push({
image: imageOrder === "inline" ? images[inlineIndex++] : void 0,
imageOrder,
sourceIndex: params.sourceIndex,
sequence: params.entries.length
});
while (inlineIndex < images.length) params.entries.push({
image: images[inlineIndex++],
imageOrder: "inline",
sourceIndex: params.sourceIndex,
sequence: params.entries.length
});
}
function resolveMergedTurnImages(entries) {
if (entries.length === 0) return {};
const merged = entries.toSorted((left, right) => {
if (left.sourceIndex !== void 0 && right.sourceIndex !== void 0) return left.sourceIndex - right.sourceIndex || left.sequence - right.sequence;
return left.sequence - right.sequence;
});
const images = merged.flatMap((entry) => entry.image ? [entry.image] : []);
const result = {
...images.length > 0 ? { images } : {},
imageOrder: merged.map((entry) => entry.imageOrder)
};
Object.defineProperty(result, "imageSourceIndexes", { value: merged.map((entry) => entry.sourceIndex) });
return result;
}
/** Resolves current-turn image attachments that were not already described by media understanding. */
async function resolveCurrentTurnImages(params) {
const entries = [];
appendOrderedImages({
entries,
images: params.images,
imageOrder: params.imageOrder
});
for (const image of params.extractedFileImages ?? []) appendOrderedImages({
entries,
images: [stripExtractedFileImageMetadata(image)],
sourceIndex: image.attachmentIndex
});
const currentImageAttachments = collectCurrentImageAttachments(params.ctx);
if (currentImageAttachments.length === 0) return resolveMergedTurnImages(entries);
const describedImageIndexes = collectDescribedImageAttachmentIndexes(params.ctx);
const undescribedImageAttachments = currentImageAttachments.filter((attachment) => !describedImageIndexes.has(attachment.index));
if (undescribedImageAttachments.length === 0) return resolveMergedTurnImages(entries);
try {
const resolved = await resolveAgentTurnAttachments({
ctx: params.ctx,
cfg: params.cfg,
includeRecentHistoryImages: false,
includeAttachmentIndexes: true
});
const images = resolved.attachments.map((attachment) => ({
type: "image",
data: attachment.data,
mimeType: attachment.mediaType
}));
const resolvedIndexes = resolved.attachmentIndexes ?? [];
if (images.length < undescribedImageAttachments.length) logVerbose(`agent-runner: native OpenClaw media resolution produced ${images.length}/${undescribedImageAttachments.length} current image attachment(s); retaining resolved images`);
const imageByResolvedIndex = new Map(resolvedIndexes.map((resolvedIndex, imageIndex) => [resolvedIndex, images[imageIndex]]));
const unresolvedSourceIndexes = [];
for (const attachment of undescribedImageAttachments) {
const image = imageByResolvedIndex.get(attachment.index);
if (image) appendOrderedImages({
entries,
images: [image],
sourceIndex: attachment.index
});
else unresolvedSourceIndexes.push(attachment.index);
}
const merged = resolveMergedTurnImages(entries);
return unresolvedSourceIndexes.length > 0 ? Object.assign(merged, { unresolvedSourceIndexes }) : merged;
} catch (error) {
logVerbose(`agent-runner: media attachment image resolution failed, proceeding without native images: ${formatErrorMessage(error)}`);
const merged = resolveMergedTurnImages(entries);
return undescribedImageAttachments.length > 0 ? Object.assign(merged, { unresolvedSourceIndexes: undescribedImageAttachments.map((attachment) => attachment.index) }) : merged;
}
}
//#endregion
export { resolveActiveRunQueueAction as i, createTypingSignaler as n, resolveTypingMode as r, resolveCurrentTurnImages as t };