openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
221 lines (220 loc) • 7.58 kB
JavaScript
import { _ as sleep } from "./utils-CCC-BEJH.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { i as getReplyPayloadMetadata, r as copyReplyPayloadMetadata } from "./reply-payload-CZJ2cgZc.js";
import { n as SILENT_REPLY_TOKEN, o as isSilentReplyText } from "./tokens-U6o7_k27.js";
import { r as generateSecureInt } from "./secure-random-Ds4AFLgz.js";
import { t as normalizeReplyPayload } from "./normalize-reply-BJvr7poh.js";
import { n as registerDispatcher } from "./dispatcher-registry-CaTZukRA.js";
//#region src/auto-reply/reply/reply-dispatcher.ts
const DEFAULT_HUMAN_DELAY_MIN_MS = 800;
const DEFAULT_HUMAN_DELAY_MAX_MS = 2500;
const silentReplyLogger = createSubsystemLogger("silent-reply/dispatcher");
function buildReplyDispatchRuntimeInfo(payload, kind) {
const assistantMessageIndex = getReplyPayloadMetadata(payload)?.assistantMessageIndex;
return {
kind,
...assistantMessageIndex !== void 0 ? { assistantMessageIndex } : {}
};
}
/** Generate a random delay within the configured range. */
function getHumanDelay(config) {
const mode = config?.mode ?? "off";
if (mode === "off") return 0;
const min = mode === "custom" ? config?.minMs ?? DEFAULT_HUMAN_DELAY_MIN_MS : DEFAULT_HUMAN_DELAY_MIN_MS;
const max = mode === "custom" ? config?.maxMs ?? DEFAULT_HUMAN_DELAY_MAX_MS : DEFAULT_HUMAN_DELAY_MAX_MS;
if (max <= min) return min;
return min + generateSecureInt(max - min + 1);
}
function normalizeReplyPayloadInternal(payload, opts) {
const prefixContext = opts.responsePrefixContextProvider?.() ?? opts.responsePrefixContext;
return normalizeReplyPayload(payload, {
responsePrefix: opts.responsePrefix,
responsePrefixContext: prefixContext,
onHeartbeatStrip: opts.onHeartbeatStrip,
transformReplyPayload: opts.transformReplyPayload,
onSkip: opts.onSkip
});
}
function createReplyDispatcher(options) {
let beforeDeliver = options.beforeDeliver;
let sendChain = Promise.resolve();
let pending = 1;
let completeCalled = false;
let sentFirstBlock = false;
const queuedCounts = {
tool: 0,
block: 0,
final: 0
};
const failedCounts = {
tool: 0,
block: 0,
final: 0
};
const cancelledCounts = {
tool: 0,
block: 0,
final: 0
};
const { unregister } = registerDispatcher({
pending: () => pending,
waitForIdle: () => sendChain
});
const enqueue = (kind, payload) => {
const originalWasExactSilent = isSilentReplyText(payload.text, SILENT_REPLY_TOKEN);
const normalized = normalizeReplyPayloadInternal(payload, {
responsePrefix: options.responsePrefix,
responsePrefixContext: options.responsePrefixContext,
responsePrefixContextProvider: options.responsePrefixContextProvider,
transformReplyPayload: options.transformReplyPayload,
onHeartbeatStrip: options.onHeartbeatStrip,
onSkip: (reason) => options.onSkip?.(payload, {
...buildReplyDispatchRuntimeInfo(payload, kind),
reason
})
});
if (!normalized) {
if (kind === "final" && originalWasExactSilent) silentReplyLogger.debug("exact NO_REPLY final payload was skipped before delivery", {
hasSessionKey: Boolean(options.silentReplyContext?.sessionKey),
surface: options.silentReplyContext?.surface,
conversationType: options.silentReplyContext?.conversationType
});
return false;
}
queuedCounts[kind] += 1;
pending += 1;
const shouldDelay = kind === "block" && sentFirstBlock;
if (kind === "block") sentFirstBlock = true;
sendChain = sendChain.then(async () => {
if (shouldDelay) {
const delayMs = getHumanDelay(options.humanDelay);
if (delayMs > 0) await sleep(delayMs);
}
const dispatchInfo = buildReplyDispatchRuntimeInfo(normalized, kind);
let deliverPayload = normalized;
if (beforeDeliver) {
try {
deliverPayload = await beforeDeliver(normalized, dispatchInfo);
} catch (err) {
try {
await options.onBeforeDeliverCancelled?.(normalized, dispatchInfo);
} catch (cancelErr) {
options.onError?.(cancelErr, dispatchInfo);
}
throw err;
}
if (!deliverPayload) {
cancelledCounts[kind] += 1;
try {
await options.onBeforeDeliverCancelled?.(normalized, dispatchInfo);
} catch (err) {
options.onError?.(err, dispatchInfo);
}
return;
}
deliverPayload = copyReplyPayloadMetadata(normalized, deliverPayload);
}
await options.deliver(deliverPayload, dispatchInfo);
}).catch((err) => {
failedCounts[kind] += 1;
options.onError?.(err, buildReplyDispatchRuntimeInfo(normalized, kind));
}).finally(() => {
pending -= 1;
if (pending === 1 && completeCalled) pending -= 1;
if (pending === 0) {
unregister();
options.onIdle?.();
}
});
return true;
};
const markComplete = () => {
if (completeCalled) return;
completeCalled = true;
Promise.resolve().then(() => {
if (pending === 1 && completeCalled) {
pending -= 1;
if (pending === 0) {
unregister();
options.onIdle?.();
}
}
});
};
return {
sendToolResult: (payload) => enqueue("tool", payload),
sendBlockReply: (payload) => enqueue("block", payload),
sendFinalReply: (payload) => enqueue("final", payload),
appendBeforeDeliver: (hook) => {
const previousBeforeDeliver = beforeDeliver;
beforeDeliver = previousBeforeDeliver ? async (payload, info) => {
const previousPayload = await previousBeforeDeliver(payload, info);
return previousPayload ? hook(copyReplyPayloadMetadata(payload, previousPayload), info) : null;
} : hook;
},
waitForIdle: () => sendChain,
getQueuedCounts: () => ({ ...queuedCounts }),
getCancelledCounts: () => ({ ...cancelledCounts }),
getFailedCounts: () => ({ ...failedCounts }),
markComplete
};
}
async function waitForReplyDispatcherIdle(dispatcher, abortSignal) {
if (!abortSignal) {
await dispatcher.waitForIdle();
return;
}
if (abortSignal.aborted) return;
let removeAbortListener;
const aborted = new Promise((resolve) => {
const onAbort = () => resolve();
abortSignal.addEventListener("abort", onAbort, { once: true });
removeAbortListener = () => abortSignal.removeEventListener("abort", onAbort);
});
try {
await Promise.race([dispatcher.waitForIdle(), aborted]);
} finally {
removeAbortListener?.();
}
}
function createReplyDispatcherWithTyping(options) {
const { typingCallbacks, onReplyStart, onIdle, onSettled: _onSettled, onFreshSettledDelivery: _onFreshSettledDelivery, onCleanup, ...dispatcherOptions } = options;
const resolvedOnReplyStart = onReplyStart ?? typingCallbacks?.onReplyStart;
const resolvedOnIdle = onIdle ?? typingCallbacks?.onIdle;
const resolvedOnCleanup = onCleanup ?? typingCallbacks?.onCleanup;
let typingController;
return {
dispatcher: createReplyDispatcher({
...dispatcherOptions,
onIdle: () => {
typingController?.markDispatchIdle();
return resolvedOnIdle?.();
}
}),
replyOptions: {
onReplyStart: resolvedOnReplyStart,
onTypingCleanup: resolvedOnCleanup,
onTypingController: (typing) => {
typingController = typing;
}
},
markDispatchIdle: () => {
typingController?.markDispatchIdle();
resolvedOnIdle?.();
},
markRunComplete: () => {
typingController?.markRunComplete();
}
};
}
//#endregion
//#region src/auto-reply/reply/reply-dispatcher.types.ts
function readDispatcherFailedCounts(dispatcher) {
return dispatcher.getFailedCounts?.() ?? {
tool: 0,
block: 0,
final: 0
};
}
//#endregion
export { waitForReplyDispatcherIdle as i, createReplyDispatcher as n, createReplyDispatcherWithTyping as r, readDispatcherFailedCounts as t };