UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,020 lines (1,019 loc) 43.2 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { t as createLazyImportLoader } from "./lazy-promise-BONnzNfb.js"; import { l as isDiagnosticsEnabled } from "./diagnostic-events-Chmz2PSy.js"; import "./agent-scope-MrLta7Pq.js"; import { f as resolveAgentIdFromSessionKey } from "./session-key-B_NoIfpX.js"; import { a as resolveAgentDir, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js"; import { r as logVerbose } from "./globals-GTrXU4s9.js"; import { _ as resolveConfiguredTtsMode, b as shouldCleanTtsDirectiveText } from "./gateway-startup-plugin-ids-3X_G47Qt.js"; import { s as isReplyPayloadStatusNotice, u as markReplyPayloadAsTtsSupplement } from "./reply-payload-CZJ2cgZc.js"; import { i as emitAgentEvent } from "./agent-events-C1B8VhOg.js"; import { a as toAcpRuntimeError, n as AcpRuntimeError } from "./errors-B_W4aC5J.js"; import { t as formatAcpRuntimeErrorText } from "./error-text-CDyDbZLi.js"; import { o as isSessionIdentityPending, u as resolveSessionIdentityFromMeta } from "./session-identity-_q5okeQC.js"; import { a as resolveAcpThreadSessionDetailLines } from "./session-identifiers-Yz4V9hoo.js"; import "./errors-xz3P9oAm.js"; import { _ as markDiagnosticSessionProgress } from "./diagnostic-TmaJRyPq.js"; import { s as hasOutboundReplyContent } from "./reply-payload-D-VMfpYI.js"; import { a as resolveAcpDispatchPolicyError, i as resolveAcpAgentPolicyError } from "./policy-D-GnwOcj.js"; import { a as generateSecureUuid } from "./secure-random-Ds4AFLgz.js"; import { n as formatToolSummary, r as resolveToolDisplay } from "./tool-display-CX5DV8AV.js"; import { r as prefixSystemMessage } from "./system-message-Dltw0_t9.js"; import { t as createTtsDirectiveTextStreamCleaner } from "./directives-BaSUaNZN.js"; import { t as EmbeddedBlockChunker } from "./embedded-agent-block-chunker-DH4NVUQq.js"; import { n as hasInboundMedia } from "./inbound-media-Bidp906t.js"; import { i as appendRecentHistoryImageContext, n as resolveAgentTurnAttachments, r as resolveInlineAgentImageAttachments, t as loadAgentTurnMediaRuntime } from "./agent-turn-attachments-BVNZMNvw.js"; import { r as createBlockReplyPipeline } from "./block-reply-pipeline-DJITIDPa.js"; import { i as waitForReplyDispatcherIdle, t as readDispatcherFailedCounts } from "./reply-dispatcher.types-CKL81tAi.js"; import { t as resolveRoutedDeliveryThreadId } from "./routed-delivery-thread-CohUtbwY.js"; import { r as resolveFirstContextText } from "./context-text-BegMwqu-.js"; import { n as resolveAcpProjectionSettings, r as resolveAcpStreamingConfig, t as isAcpTagVisible } from "./acp-stream-settings-912H0-bc.js"; import { t as resolveStatusTtsSnapshot } from "./status-config-T30kTKuI.js"; //#region src/auto-reply/reply/acp-projector.ts const ACP_BLOCK_REPLY_TIMEOUT_MS = 15e3; const ACP_LIVE_IDLE_FLUSH_FLOOR_MS = 750; const ACP_LIVE_IDLE_MIN_CHARS = 80; const ACP_LIVE_SOFT_FLUSH_CHARS = 220; const ACP_LIVE_HARD_FLUSH_CHARS = 480; const TERMINAL_TOOL_STATUSES = new Set([ "completed", "failed", "cancelled", "done", "error" ]); const HIDDEN_BOUNDARY_TAGS = new Set(["tool_call", "tool_call_update"]); function truncateText(input, maxChars) { if (input.length <= maxChars) return input; if (maxChars <= 1) return input.slice(0, maxChars); return `${input.slice(0, maxChars - 1)}…`; } function hashText(text) { return text.trim(); } function normalizeToolStatus(status) { return normalizeOptionalLowercaseString(status) || void 0; } function resolveHiddenBoundarySeparatorText(mode) { if (mode === "space") return " "; if (mode === "newline") return "\n"; if (mode === "paragraph") return "\n\n"; return ""; } function shouldInsertSeparator(params) { if (!params.separator) return false; if (!params.nextText) return false; const firstChar = params.nextText[0]; if (typeof firstChar === "string" && /\s/.test(firstChar)) return false; const tail = params.previousTail ?? ""; if (!tail) return false; if (params.separator === " " && /\s$/.test(tail)) return false; if ((params.separator === "\n" || params.separator === "\n\n") && tail.endsWith("\n")) return false; return true; } function shouldFlushLiveBufferOnBoundary(text) { if (!text) return false; if (text.length >= ACP_LIVE_HARD_FLUSH_CHARS) return true; if (text.endsWith("\n\n")) return true; if (/[.!?][)"'`]*\s$/.test(text)) return true; if (text.length >= ACP_LIVE_SOFT_FLUSH_CHARS && /\s$/.test(text)) return true; return false; } function shouldFlushLiveBufferOnIdle(text) { if (!text) return false; if (text.length >= ACP_LIVE_IDLE_MIN_CHARS) return true; if (/[.!?][)"'`]*$/.test(text.trimEnd())) return true; if (text.includes("\n")) return true; return false; } function renderToolSummaryText(event) { const detailParts = []; const title = normalizeOptionalString(event.title); if (title) detailParts.push(title); const status = normalizeOptionalString(event.status); if (status) detailParts.push(`status=${status}`); const fallback = normalizeOptionalString(event.text); if (detailParts.length === 0 && fallback) detailParts.push(fallback); return formatToolSummary(resolveToolDisplay({ name: "tool_call", meta: detailParts.join(" · ") || "tool call" })); } function createAcpReplyProjector(params) { const settings = resolveAcpProjectionSettings(params.cfg); const streaming = resolveAcpStreamingConfig({ cfg: params.cfg, provider: params.provider, accountId: params.accountId, deliveryMode: settings.deliveryMode }); const createTurnBlockReplyPipeline = () => createBlockReplyPipeline({ onBlockReply: async (payload) => { await params.deliver("block", payload); }, timeoutMs: ACP_BLOCK_REPLY_TIMEOUT_MS, coalescing: settings.deliveryMode === "live" ? void 0 : streaming.coalescing }); let blockReplyPipeline = createTurnBlockReplyPipeline(); const chunker = new EmbeddedBlockChunker(streaming.chunking); const liveIdleFlushMs = Math.max(streaming.coalescing.idleMs, ACP_LIVE_IDLE_FLUSH_FLOOR_MS); let emittedOutputChars = 0; let truncationNoticeEmitted = false; let lastStatusHash; let lastToolHash; let lastUsageTuple; let lastVisibleOutputTail; let pendingHiddenBoundary = false; let liveBufferText = ""; let finalOnlyOutputText = ""; let liveIdleTimer; const pendingToolDeliveries = []; const toolLifecycleById = /* @__PURE__ */ new Map(); const shouldSendToolSummaries = () => params.shouldSendToolSummariesNow?.() ?? params.shouldSendToolSummaries; const clearLiveIdleTimer = () => { if (!liveIdleTimer) return; clearTimeout(liveIdleTimer); liveIdleTimer = void 0; }; const drainChunker = (force) => { if (settings.deliveryMode === "final_only" && !force) return; chunker.drain({ force, emit: (chunk) => { blockReplyPipeline.enqueue({ text: chunk }); } }); }; const flushLiveBuffer = (opts) => { if (settings.deliveryMode !== "live") return; if (!liveBufferText) return; if (opts?.idle && !shouldFlushLiveBufferOnIdle(liveBufferText)) return; const text = liveBufferText; liveBufferText = ""; chunker.append(text); drainChunker(opts?.force === true); }; const scheduleLiveIdleFlush = () => { if (settings.deliveryMode !== "live") return; if (liveIdleFlushMs <= 0 || !liveBufferText) return; clearLiveIdleTimer(); liveIdleTimer = setTimeout(() => { flushLiveBuffer({ force: true, idle: true }); if (liveBufferText) scheduleLiveIdleFlush(); }, liveIdleFlushMs); }; const resetTurnState = () => { clearLiveIdleTimer(); blockReplyPipeline.stop(); blockReplyPipeline = createTurnBlockReplyPipeline(); emittedOutputChars = 0; truncationNoticeEmitted = false; lastStatusHash = void 0; lastToolHash = void 0; lastUsageTuple = void 0; lastVisibleOutputTail = void 0; pendingHiddenBoundary = false; liveBufferText = ""; finalOnlyOutputText = ""; pendingToolDeliveries.length = 0; toolLifecycleById.clear(); }; const flushBufferedToolDeliveries = async (force) => { if (!(settings.deliveryMode === "final_only" && force)) return; if (!shouldSendToolSummaries()) { pendingToolDeliveries.length = 0; return; } for (const entry of pendingToolDeliveries.splice(0)) await params.deliver("tool", entry.payload, entry.meta); }; const flush = async (force = false) => { if (settings.deliveryMode === "live") { clearLiveIdleTimer(); flushLiveBuffer({ force: true }); } await flushBufferedToolDeliveries(force); if (settings.deliveryMode === "final_only") { if (force && finalOnlyOutputText.trim().length > 0) { const text = finalOnlyOutputText; finalOnlyOutputText = ""; await params.deliver("final", { text }); } } else drainChunker(force); await blockReplyPipeline.flush({ force }); }; const emitSystemStatus = async (text, meta, opts) => { if (!shouldSendToolSummaries()) return; const bounded = truncateText(text.trim(), settings.maxSessionUpdateChars); if (!bounded) return; const formatted = prefixSystemMessage(bounded); const hash = hashText(formatted); if (settings.repeatSuppression && opts?.dedupe !== false && lastStatusHash === hash) return; if (settings.deliveryMode === "final_only") pendingToolDeliveries.push({ payload: { text: formatted }, meta }); else { await flush(true); await params.deliver("tool", { text: formatted }, meta); } lastStatusHash = hash; }; const markHiddenToolBoundary = (event) => { if (!event.tag || !HIDDEN_BOUNDARY_TAGS.has(event.tag)) return; const status = normalizeToolStatus(event.status); const isTerminal = status ? TERMINAL_TOOL_STATUSES.has(status) : false; pendingHiddenBoundary = pendingHiddenBoundary || event.tag === "tool_call" || isTerminal; }; const emitToolSummary = async (event) => { if (!shouldSendToolSummaries()) { markHiddenToolBoundary(event); return; } if (!isAcpTagVisible(settings, event.tag)) return; const renderedToolSummary = renderToolSummaryText(event); const toolSummary = truncateText(renderedToolSummary, settings.maxSessionUpdateChars); const hash = hashText(renderedToolSummary); const toolCallId = normalizeOptionalString(event.toolCallId); const status = normalizeToolStatus(event.status); const isTerminal = status ? TERMINAL_TOOL_STATUSES.has(status) : false; const isStart = status === "in_progress" || event.tag === "tool_call"; if (settings.repeatSuppression) { if (toolCallId) { const state = toolLifecycleById.get(toolCallId) ?? { started: false, terminal: false }; if (isTerminal && state.terminal) return; if (isStart && state.started) return; if (state.lastRenderedHash === hash) return; if (isStart) state.started = true; if (isTerminal) state.terminal = true; state.lastRenderedHash = hash; toolLifecycleById.set(toolCallId, state); } else if (lastToolHash === hash) return; } const deliveryMeta = { ...event.tag ? { tag: event.tag } : {}, ...toolCallId ? { toolCallId } : {}, ...status ? { toolStatus: status } : {}, allowEdit: Boolean(toolCallId && event.tag === "tool_call_update") }; if (settings.deliveryMode === "final_only") { pendingToolDeliveries.push({ payload: { text: toolSummary }, meta: deliveryMeta }); markHiddenToolBoundary(event); } else { await flush(true); await params.deliver("tool", { text: toolSummary }, deliveryMeta); } lastToolHash = hash; }; const emitTruncationNotice = async () => { if (truncationNoticeEmitted) return; truncationNoticeEmitted = true; await emitSystemStatus("output truncated", { tag: "session_info_update" }, { dedupe: false }); }; const onEvent = async (event) => { params.onProgress?.(); if (event.type === "text_delta") { if (event.stream && event.stream !== "output") return; if (!isAcpTagVisible(settings, event.tag)) return; let text = event.text; if (!text) return; if (pendingHiddenBoundary && shouldInsertSeparator({ separator: resolveHiddenBoundarySeparatorText(settings.hiddenBoundarySeparator), previousTail: lastVisibleOutputTail, nextText: text })) text = `${resolveHiddenBoundarySeparatorText(settings.hiddenBoundarySeparator)}${text}`; pendingHiddenBoundary = false; if (emittedOutputChars >= settings.maxOutputChars) { await emitTruncationNotice(); return; } const remaining = settings.maxOutputChars - emittedOutputChars; const accepted = remaining < text.length ? text.slice(0, remaining) : text; if (accepted.length > 0) { emittedOutputChars += accepted.length; lastVisibleOutputTail = accepted.slice(-1); if (settings.deliveryMode === "live") { liveBufferText += accepted; if (shouldFlushLiveBufferOnBoundary(liveBufferText)) { clearLiveIdleTimer(); flushLiveBuffer({ force: true }); } else scheduleLiveIdleFlush(); } else finalOnlyOutputText += accepted; } if (accepted.length < text.length) await emitTruncationNotice(); return; } if (event.type === "status") { if (!isAcpTagVisible(settings, event.tag)) return; if (event.tag === "usage_update" && settings.repeatSuppression) { const usageTuple = typeof event.used === "number" && typeof event.size === "number" ? `${event.used}/${event.size}` : hashText(event.text); if (usageTuple === lastUsageTuple) return; lastUsageTuple = usageTuple; } await emitSystemStatus(event.text, event.tag ? { tag: event.tag } : void 0, { dedupe: true }); return; } if (event.type === "tool_call") { if (!isAcpTagVisible(settings, event.tag)) { markHiddenToolBoundary(event); return; } await emitToolSummary(event); return; } if (event.type === "done" || event.type === "error") { await flush(true); resetTurnState(); } }; return { onEvent, flush }; } //#endregion //#region src/auto-reply/reply/dispatch-acp-delivery.ts const routeReplyRuntimeLoader = createLazyImportLoader(() => import("./route-reply.runtime.js")); const dispatchAcpTtsRuntimeLoader$1 = createLazyImportLoader(() => import("./dispatch-acp-tts.runtime.js")); const channelPluginRuntimeLoader = createLazyImportLoader(() => import("./plugins-DktKuCnn.js")); const messageActionRuntimeLoader = createLazyImportLoader(() => import("./message-action-runner-Tfh9H2ZB.js")); function loadRouteReplyRuntime() { return routeReplyRuntimeLoader.load(); } function loadDispatchAcpTtsRuntime$1() { return dispatchAcpTtsRuntimeLoader$1.load(); } function loadChannelPluginRuntime() { return channelPluginRuntimeLoader.load(); } function loadMessageActionRuntime() { return messageActionRuntimeLoader.load(); } async function shouldTreatDeliveredTextAsVisible(params) { if (!normalizeOptionalString(params.text)) return false; if (params.kind === "final") return true; const channelId = normalizeOptionalLowercaseString(params.channel); if (!channelId) return false; const { getChannelPlugin } = await loadChannelPluginRuntime(); const outbound = getChannelPlugin(channelId)?.outbound; const visibilityOverride = outbound?.shouldTreatDeliveredTextAsVisible ?? outbound?.shouldTreatRoutedTextAsVisible; if (visibilityOverride) return visibilityOverride({ kind: params.kind, text: params.text }); return false; } async function maybeApplyAcpTts(params) { if (params.skipTts) return params.payload; if (isReplyPayloadStatusNotice(params.payload)) return params.payload; const ttsStatus = resolveStatusTtsSnapshot({ cfg: params.cfg, sessionAuto: params.ttsAuto, agentId: params.agentId, channelId: params.channel, accountId: params.accountId }); if (!ttsStatus) return params.payload; if (ttsStatus.autoMode === "inbound" && !params.inboundAudio) return params.payload; if (params.kind !== "final" && resolveConfiguredTtsMode(params.cfg, { agentId: params.agentId, channelId: params.channel, accountId: params.accountId }) === "final") return params.payload; const { maybeApplyTtsToPayload } = await loadDispatchAcpTtsRuntime$1(); return await maybeApplyTtsToPayload({ payload: params.payload, cfg: params.cfg, channel: params.channel, kind: params.kind, inboundAudio: params.inboundAudio, ttsAuto: params.ttsAuto, agentId: params.agentId, accountId: params.accountId }); } function createAcpDispatchDeliveryCoordinator(params) { const directChannel = normalizeOptionalLowercaseString(params.ctx.Provider ?? params.ctx.Surface); const routedChannel = normalizeOptionalLowercaseString(params.originatingChannel); const deliverySessionKey = normalizeOptionalString(params.sessionKey) ?? params.ctx.SessionKey; const resolvedAccountId = normalizeOptionalString(params.originatingAccountId) ?? normalizeOptionalString(params.ctx.AccountId) ?? normalizeOptionalString(params.cfg.channels?.[routedChannel ?? directChannel ?? ""]?.defaultAccount); const state = { startedReplyLifecycle: false, accumulatedBlockText: "", accumulatedVisibleBlockText: "", accumulatedBlockTtsText: "", accumulatedFinalText: "", cleanBlockTtsDirectiveText: shouldCleanTtsDirectiveText({ cfg: params.cfg, ttsAuto: params.sessionTtsAuto, agentId: params.agentId, channelId: params.ttsChannel, accountId: resolvedAccountId }) ? createTtsDirectiveTextStreamCleaner() : void 0, blockCount: 0, deliveredFinalReply: false, deliveredVisibleText: false, failedVisibleTextDelivery: false, queuedDirectVisibleTextDeliveries: 0, settledDirectVisibleText: false, routedCounts: { tool: 0, block: 0, final: 0 }, toolMessageByCallId: /* @__PURE__ */ new Map() }; let hasPendingDirectBlockReplyDelivery = false; const waitForPendingDirectBlockReplyDelivery = async () => { if (!hasPendingDirectBlockReplyDelivery) return; hasPendingDirectBlockReplyDelivery = false; await waitForReplyDispatcherIdle(params.dispatcher, params.abortSignal); }; const settleDirectVisibleText = async () => { if (state.settledDirectVisibleText || state.queuedDirectVisibleTextDeliveries === 0) return; state.settledDirectVisibleText = true; hasPendingDirectBlockReplyDelivery = false; await params.dispatcher.waitForIdle(); const failedCounts = readDispatcherFailedCounts(params.dispatcher); const failedVisibleCount = failedCounts.block + failedCounts.final; if (failedVisibleCount > 0) state.failedVisibleTextDelivery = true; if (state.queuedDirectVisibleTextDeliveries > failedVisibleCount) state.deliveredVisibleText = true; }; const startReplyLifecycleOnce = async () => { if (state.startedReplyLifecycle) return; state.startedReplyLifecycle = true; if (params.suppressReplyLifecycle) return; Promise.resolve(params.onReplyStart?.()).catch((error) => { logVerbose(`dispatch-acp: reply lifecycle start failed: ${error instanceof Error ? error.message : String(error)}`); }); }; const tryEditToolMessage = async (payload, toolCallId) => { if (!params.shouldRouteToOriginating || !params.originatingChannel || !params.originatingTo) return false; const handle = state.toolMessageByCallId.get(toolCallId); if (!handle?.messageId) return false; const message = normalizeOptionalString(payload.text); if (!message) return false; try { const { runMessageAction } = await loadMessageActionRuntime(); await runMessageAction({ cfg: params.cfg, action: "edit", params: { channel: handle.channel, accountId: handle.accountId, to: handle.to, threadId: handle.threadId, messageId: handle.messageId, message }, sessionKey: params.ctx.SessionKey, requesterAccountId: params.ctx.AccountId }); state.routedCounts.tool += 1; return true; } catch (error) { logVerbose(`dispatch-acp: tool message edit failed for ${toolCallId}: ${formatErrorMessage(error)}`); return false; } }; const deliver = async (kind, payload, meta) => { let visiblePayload = payload; const rawBlockText = kind === "block" ? normalizeOptionalString(payload.text) : void 0; if (rawBlockText) { const isStatusNotice = isReplyPayloadStatusNotice(payload); const joinsBufferedTtsDirective = state.cleanBlockTtsDirectiveText?.hasBufferedDirectiveText() === true; if (!isStatusNotice) { if (state.accumulatedBlockText.length > 0) state.accumulatedBlockText += "\n"; state.accumulatedBlockText += rawBlockText; if (state.accumulatedBlockTtsText.length > 0 && !joinsBufferedTtsDirective) state.accumulatedBlockTtsText += "\n"; state.accumulatedBlockTtsText += rawBlockText; state.blockCount += 1; } if (state.cleanBlockTtsDirectiveText && !isStatusNotice) { const text = state.cleanBlockTtsDirectiveText.push(rawBlockText); visiblePayload = { ...payload, text: text.trim() ? text : void 0 }; } if (visiblePayload.text) { if (state.accumulatedVisibleBlockText.length > 0) state.accumulatedVisibleBlockText += "\n"; state.accumulatedVisibleBlockText += visiblePayload.text; } } const isStatusNotice = isReplyPayloadStatusNotice(payload); const rawFinalText = kind === "final" && !isStatusNotice ? normalizeOptionalString(payload.text) : void 0; if (rawFinalText) { if (state.accumulatedFinalText.length > 0) state.accumulatedFinalText += "\n"; state.accumulatedFinalText += rawFinalText; } if (hasOutboundReplyContent(visiblePayload, { trimText: true })) await startReplyLifecycleOnce(); else return false; if (params.suppressUserDelivery) return false; const ttsPayload = await maybeApplyAcpTts({ payload: visiblePayload, cfg: params.cfg, agentId: params.agentId, channel: params.ttsChannel, accountId: resolvedAccountId, kind, inboundAudio: params.inboundAudio, ttsAuto: params.sessionTtsAuto, skipTts: meta?.skipTts }); if (params.shouldRouteToOriginating && params.originatingChannel && params.originatingTo) { const toolCallId = normalizeOptionalString(meta?.toolCallId); if (kind === "tool" && meta?.allowEdit === true && toolCallId) { if (await tryEditToolMessage(ttsPayload, toolCallId)) return true; } const tracksVisibleText = await shouldTreatDeliveredTextAsVisible({ channel: routedChannel, kind, text: ttsPayload.text, routed: true }); const { routeReply } = await loadRouteReplyRuntime(); const threadId = params.originatingThreadId ?? resolveRoutedDeliveryThreadId({ ctx: params.ctx, sessionKey: deliverySessionKey }); const result = await routeReply({ payload: ttsPayload, channel: params.originatingChannel, to: params.originatingTo, sessionKey: deliverySessionKey, ...deliverySessionKey !== params.ctx.SessionKey ? { policySessionKey: params.ctx.SessionKey } : {}, accountId: resolvedAccountId, requesterSenderId: params.ctx.SenderId, requesterSenderName: params.ctx.SenderName, requesterSenderUsername: params.ctx.SenderUsername, requesterSenderE164: params.ctx.SenderE164, threadId, cfg: params.cfg, mirror: false, replyKind: kind, runId: params.runId }); if (!result.ok) { if (tracksVisibleText) state.failedVisibleTextDelivery = true; logVerbose(`dispatch-acp: route-reply (acp/${kind}) failed: ${result.error ?? "unknown error"}`); return false; } if (result.suppressed) { if (kind === "final") state.deliveredFinalReply = true; if (tracksVisibleText) state.deliveredVisibleText = true; return true; } if (kind === "tool" && meta?.toolCallId && result.messageId) state.toolMessageByCallId.set(meta.toolCallId, { channel: params.originatingChannel, accountId: resolvedAccountId, to: params.originatingTo, ...threadId != null ? { threadId } : {}, messageId: result.messageId }); if (kind === "final") state.deliveredFinalReply = true; if (tracksVisibleText) state.deliveredVisibleText = true; state.routedCounts[kind] += 1; return true; } if (kind === "tool") await waitForPendingDirectBlockReplyDelivery(); const tracksVisibleText = await shouldTreatDeliveredTextAsVisible({ channel: directChannel, kind, text: ttsPayload.text, routed: false }); const delivered = kind === "tool" ? params.dispatcher.sendToolResult(ttsPayload) : kind === "block" ? params.dispatcher.sendBlockReply(ttsPayload) : params.dispatcher.sendFinalReply(ttsPayload); if (kind === "final" && delivered) state.deliveredFinalReply = true; if (delivered && tracksVisibleText) { state.queuedDirectVisibleTextDeliveries += 1; state.settledDirectVisibleText = false; } else if (!delivered && tracksVisibleText) state.failedVisibleTextDelivery = true; if (kind === "block" && delivered) hasPendingDirectBlockReplyDelivery = true; return delivered; }; return { startReplyLifecycle: startReplyLifecycleOnce, deliver, getBlockCount: () => state.blockCount, getAccumulatedBlockText: () => state.accumulatedBlockText, getAccumulatedVisibleBlockText: () => state.accumulatedVisibleBlockText, getAccumulatedBlockTtsText: () => state.accumulatedBlockTtsText, getAccumulatedFinalText: () => state.accumulatedFinalText, settleVisibleText: settleDirectVisibleText, hasDeliveredFinalReply: () => state.deliveredFinalReply, hasDeliveredVisibleText: () => state.deliveredVisibleText, hasFailedVisibleTextDelivery: () => state.failedVisibleTextDelivery, getRoutedCounts: () => ({ ...state.routedCounts }), applyRoutedCounts: (counts) => { counts.tool += state.routedCounts.tool; counts.block += state.routedCounts.block; counts.final += state.routedCounts.final; } }; } //#endregion //#region src/auto-reply/reply/dispatch-acp.ts const dispatchAcpManagerRuntimeLoader = createLazyImportLoader(() => import("./dispatch-acp-manager.runtime.js")); const dispatchAcpSessionRuntimeLoader = createLazyImportLoader(() => import("./dispatch-acp-session.runtime.js")); const dispatchAcpTtsRuntimeLoader = createLazyImportLoader(() => import("./dispatch-acp-tts.runtime.js")); const dispatchAcpTranscriptRuntimeLoader = createLazyImportLoader(() => import("./dispatch-acp-transcript.runtime.js")); function loadDispatchAcpManagerRuntime() { return dispatchAcpManagerRuntimeLoader.load(); } function loadDispatchAcpSessionRuntime() { return dispatchAcpSessionRuntimeLoader.load(); } function loadDispatchAcpTtsRuntime() { return dispatchAcpTtsRuntimeLoader.load(); } function loadDispatchAcpTranscriptRuntime() { return dispatchAcpTranscriptRuntimeLoader.load(); } function resolveAcpPromptText(ctx) { return resolveFirstContextText(ctx, [ "BodyForAgent", "BodyForCommands", "CommandBody", "RawBody", "Body" ]).trim(); } function resolveAcpRequestId(ctx) { const id = ctx.MessageSidFull ?? ctx.MessageSid ?? ctx.MessageSidFirst ?? ctx.MessageSidLast; if (typeof id === "string") { const normalizedId = normalizeOptionalString(id); if (normalizedId) return normalizedId; } if (typeof id === "number" || typeof id === "bigint") return String(id); return generateSecureUuid(); } function resolveAcpTurnText(params) { if (params.sourceReplyDeliveryMode !== "message_tool_only") return params.promptText; const guidance = prefixSystemMessage([ "Source channel delivery is private by default for this turn.", "Normal ACP final output will not be automatically posted to the source channel.", "To send visible output, use message(action=send). The target defaults to the current source channel." ].join(" ")); return params.promptText ? `${guidance}\n\n${params.promptText}` : guidance; } function isRestrictiveRuntimeToolsAllow(toolsAllow) { if (toolsAllow === void 0) return false; return !toolsAllow.some((entry) => normalizeLowercaseStringOrEmpty(entry) === "*"); } async function hasBoundConversationForSession(params) { const channel = normalizeOptionalLowercaseString(params.channelRaw) ?? ""; if (!channel) return false; const accountId = normalizeOptionalLowercaseString(params.accountIdRaw) ?? ""; const configuredDefaultAccountId = params.cfg.channels?.[channel]?.defaultAccount; const normalizedAccountId = accountId || normalizeOptionalLowercaseString(configuredDefaultAccountId) || "default"; const { getSessionBindingService } = await loadDispatchAcpManagerRuntime(); return getSessionBindingService().listBySession(params.sessionKey).some((binding) => { const bindingChannel = normalizeOptionalLowercaseString(binding.conversation.channel) ?? ""; const bindingAccountId = normalizeOptionalLowercaseString(binding.conversation.accountId) ?? ""; const conversationId = normalizeOptionalString(binding.conversation.conversationId) ?? ""; return bindingChannel === channel && (bindingAccountId || "default") === normalizedAccountId && conversationId.length > 0; }); } function finishAcpDispatchAttempt(params) { const counts = params.dispatcher.getQueuedCounts(); params.delivery.applyRoutedCounts(counts); const acpStats = params.getStats(); const runId = normalizeOptionalString(params.runId); if (runId && params.lifecyclePhase) emitAgentEvent({ runId, sessionKey: params.sessionKey, stream: "lifecycle", data: { phase: params.lifecyclePhase, startedAt: params.startedAt, endedAt: Date.now(), ...params.outcome.kind === "error" ? { error: params.outcome.error.message } : {} } }); if (params.outcome.kind === "ok") { logVerbose(`acp-dispatch: session=${params.sessionKey} outcome=ok latencyMs=${Date.now() - params.startedAt} queueDepth=${acpStats.turns.queueDepth} activeRuntimes=${acpStats.runtimeCache.activeSessions}`); params.recordProcessed("completed", { reason: "acp_dispatch" }); } else { logVerbose(`acp-dispatch: session=${params.sessionKey} outcome=error code=${params.outcome.error.code} latencyMs=${Date.now() - params.startedAt} queueDepth=${acpStats.turns.queueDepth} activeRuntimes=${acpStats.runtimeCache.activeSessions}`); params.recordProcessed("completed", { reason: `acp_error:${normalizeLowercaseStringOrEmpty(params.outcome.error.code)}` }); } params.markIdle("message_completed"); return { queuedFinal: params.queuedFinal, counts }; } const ACP_STALE_BINDING_UNBIND_REASON = "acp-session-init-failed"; function isStaleSessionInitError(params) { if (params.code !== "ACP_SESSION_INIT_FAILED") return false; return /(ACP (session )?metadata is missing|missing ACP metadata|Session is not ACP-enabled|Resource not found)/i.test(params.message); } async function maybeUnbindStaleBoundConversations(params) { if (!isStaleSessionInitError(params.error)) return; try { const { getSessionBindingService } = await loadDispatchAcpManagerRuntime(); const removed = await getSessionBindingService().unbind({ targetSessionKey: params.targetSessionKey, reason: ACP_STALE_BINDING_UNBIND_REASON }); if (removed.length > 0) logVerbose(`dispatch-acp: removed ${removed.length} stale bound conversation(s) for ${params.targetSessionKey} after ${params.error.code}: ${params.error.message}`); } catch (error) { logVerbose(`dispatch-acp: failed to unbind stale bound conversations for ${params.targetSessionKey}: ${formatErrorMessage(error)}`); } } async function finalizeAcpTurnOutput(params) { await params.delivery.settleVisibleText(); let queuedFinal = params.delivery.hasDeliveredVisibleText() && !params.delivery.hasFailedVisibleTextDelivery(); const ttsMode = resolveConfiguredTtsMode(params.cfg, { agentId: params.agentId, channelId: params.ttsChannel, accountId: params.ttsAccountId }); const accumulatedVisibleBlockText = params.delivery.getAccumulatedVisibleBlockText(); const accumulatedBlockTtsText = params.delivery.getAccumulatedBlockTtsText(); const hasAccumulatedBlockText = accumulatedBlockTtsText.trim().length > 0; const ttsStatus = resolveStatusTtsSnapshot({ cfg: params.cfg, sessionAuto: params.sessionTtsAuto, agentId: params.agentId, channelId: params.ttsChannel, accountId: params.ttsAccountId }); const canAttemptFinalTts = ttsStatus != null && !(ttsStatus.autoMode === "inbound" && !params.inboundAudio); let finalMediaDelivered = false; if (ttsMode === "final" && hasAccumulatedBlockText && canAttemptFinalTts) try { const { maybeApplyTtsToPayload } = await loadDispatchAcpTtsRuntime(); const ttsSyntheticReply = await maybeApplyTtsToPayload({ payload: { text: accumulatedBlockTtsText }, cfg: params.cfg, channel: params.ttsChannel, kind: "final", inboundAudio: params.inboundAudio, ttsAuto: params.sessionTtsAuto, agentId: params.agentId, accountId: params.ttsAccountId }); if (ttsSyntheticReply.mediaUrl) { const delivered = await params.delivery.deliver("final", markReplyPayloadAsTtsSupplement({ mediaUrl: ttsSyntheticReply.mediaUrl, audioAsVoice: ttsSyntheticReply.audioAsVoice, spokenText: accumulatedBlockTtsText, trustedLocalMedia: true }, accumulatedBlockTtsText, { visibleTextAlreadyDelivered: true })); queuedFinal = queuedFinal || delivered; finalMediaDelivered = delivered; } } catch (err) { logVerbose(`dispatch-acp: accumulated ACP block TTS failed: ${formatErrorMessage(err)}`); } if (ttsMode !== "all" && accumulatedVisibleBlockText.trim().length > 0 && !finalMediaDelivered && !params.delivery.hasDeliveredFinalReply() && (!params.delivery.hasDeliveredVisibleText() || params.delivery.hasFailedVisibleTextDelivery())) { const delivered = await params.delivery.deliver("final", { text: accumulatedVisibleBlockText }, { skipTts: true }); queuedFinal = queuedFinal || delivered; } if (params.shouldEmitResolvedIdentityNotice) { const { readAcpSessionEntry } = await loadDispatchAcpSessionRuntime(); const currentMeta = readAcpSessionEntry({ cfg: params.cfg, sessionKey: params.sessionKey })?.acp; if (!isSessionIdentityPending(resolveSessionIdentityFromMeta(currentMeta))) { const resolvedDetails = resolveAcpThreadSessionDetailLines({ sessionKey: params.sessionKey, meta: currentMeta }); if (resolvedDetails.length > 0) { const delivered = await params.delivery.deliver("final", { text: prefixSystemMessage(["Session ids resolved.", ...resolvedDetails].join("\n")) }); queuedFinal = queuedFinal || delivered; } } } return queuedFinal; } async function tryDispatchAcpReply(params) { const sessionKey = normalizeOptionalString(params.sessionKey); if (!sessionKey || params.bypassForCommand) return null; const { getAcpSessionManager } = await loadDispatchAcpManagerRuntime(); const acpManager = getAcpSessionManager(); const acpResolution = acpManager.resolveSession({ cfg: params.cfg, sessionKey }); if (acpResolution.kind === "none") return null; const canonicalSessionKey = acpResolution.sessionKey; const acpAgentId = resolveAgentIdFromSessionKey(canonicalSessionKey); const progressSessionKeys = isDiagnosticsEnabled(params.cfg) ? Array.from(new Set([ params.ctx.SessionKey, sessionKey, canonicalSessionKey ].map((key) => normalizeOptionalString(key)).filter((key) => Boolean(key)))) : []; const markAcpProgress = progressSessionKeys.length > 0 ? () => { for (const key of progressSessionKeys) markDiagnosticSessionProgress({ sessionKey: key }); } : void 0; let queuedFinal = false; const delivery = createAcpDispatchDeliveryCoordinator({ cfg: params.cfg, agentId: acpAgentId, ctx: params.ctx, dispatcher: params.dispatcher, inboundAudio: params.inboundAudio, sessionKey: canonicalSessionKey, sessionTtsAuto: params.sessionTtsAuto, ttsChannel: params.ttsChannel, suppressUserDelivery: params.suppressUserDelivery, suppressReplyLifecycle: params.suppressReplyLifecycle, shouldRouteToOriginating: params.shouldRouteToOriginating, originatingChannel: params.originatingChannel, originatingTo: params.originatingTo, originatingAccountId: params.originatingAccountId, originatingThreadId: params.originatingThreadId, onReplyStart: params.onReplyStart, abortSignal: params.abortSignal, runId: params.runId }); const identityPendingBeforeTurn = isSessionIdentityPending(resolveSessionIdentityFromMeta(acpResolution.kind === "ready" ? acpResolution.meta : void 0)); const shouldEmitResolvedIdentityNotice = !params.suppressUserDelivery && identityPendingBeforeTurn && (Boolean(params.ctx.MessageThreadId != null && (normalizeOptionalString(String(params.ctx.MessageThreadId)) ?? "")) || await hasBoundConversationForSession({ cfg: params.cfg, sessionKey: canonicalSessionKey, channelRaw: params.ctx.OriginatingChannel ?? params.ctx.Surface ?? params.ctx.Provider, accountIdRaw: params.ctx.AccountId })); const resolvedAcpAgent = acpResolution.kind === "ready" ? normalizeOptionalString(acpResolution.meta.agent) ?? normalizeOptionalString(params.cfg.acp?.defaultAgent) ?? resolveAgentIdFromSessionKey(canonicalSessionKey) : resolveAgentIdFromSessionKey(canonicalSessionKey); const normalizedDispatchChannel = normalizeOptionalLowercaseString(params.ctx.OriginatingChannel ?? params.ctx.Surface ?? params.ctx.Provider); const explicitDispatchAccountId = normalizeOptionalString(params.ctx.AccountId); const dispatchChannels = params.cfg.channels; const defaultDispatchAccount = normalizedDispatchChannel == null ? void 0 : dispatchChannels?.[normalizedDispatchChannel]?.defaultAccount; const effectiveDispatchAccountId = explicitDispatchAccountId ?? normalizeOptionalString(defaultDispatchAccount); const projector = createAcpReplyProjector({ cfg: params.cfg, shouldSendToolSummaries: params.shouldSendToolSummaries, shouldSendToolSummariesNow: params.shouldSendToolSummariesNow, deliver: delivery.deliver, onProgress: markAcpProgress, provider: params.ctx.Surface ?? params.ctx.Provider, accountId: effectiveDispatchAccountId }); const acpDispatchStartedAt = Date.now(); const finishAttempt = (options) => finishAcpDispatchAttempt({ ...options, dispatcher: params.dispatcher, delivery, getStats: () => acpManager.getObservabilitySnapshot(params.cfg), sessionKey, runId: params.runId, startedAt: acpDispatchStartedAt, recordProcessed: params.recordProcessed, markIdle: params.markIdle }); try { const dispatchPolicyError = resolveAcpDispatchPolicyError(params.cfg); if (dispatchPolicyError) throw dispatchPolicyError; if (isRestrictiveRuntimeToolsAllow(params.toolsAllow)) throw new AcpRuntimeError("ACP_DISPATCH_DISABLED", "ACP dispatch cannot enforce runtime toolsAllow for this session; use an embedded runtime for restricted tool policy."); if (acpResolution.kind === "stale") { await maybeUnbindStaleBoundConversations({ targetSessionKey: canonicalSessionKey, error: acpResolution.error }); return finishAttempt({ queuedFinal: await delivery.deliver("final", { text: formatAcpRuntimeErrorText(acpResolution.error), isError: true }), outcome: { kind: "error", error: acpResolution.error } }); } const agentPolicyError = resolveAcpAgentPolicyError(params.cfg, resolvedAcpAgent); if (agentPolicyError) throw agentPolicyError; if (hasInboundMedia(params.ctx) && !params.ctx.MediaUnderstanding?.length) try { const { applyMediaUnderstanding } = await loadAgentTurnMediaRuntime(); await applyMediaUnderstanding({ ctx: params.ctx, cfg: params.cfg, agentId: acpAgentId, agentDir: resolveAgentDir(params.cfg, acpAgentId), workspaceDir: resolveAgentWorkspaceDir(params.cfg, acpAgentId) }); } catch (err) { logVerbose(`dispatch-acp: media understanding failed, proceeding with raw content: ${formatErrorMessage(err)}`); } const promptText = resolveAcpPromptText(params.ctx); const resolvedTurnAttachments = await resolveAgentTurnAttachments({ ctx: params.ctx, cfg: params.cfg }); const mediaAttachments = resolvedTurnAttachments.attachments; const inlineAttachments = resolveInlineAgentImageAttachments(params.images); const mediaAttachmentsAreOnlyRecentHistory = mediaAttachments.length > 0 && mediaAttachments.length === resolvedTurnAttachments.recentHistoryImages.length; const attachments = mediaAttachments.length > 0 && !(mediaAttachmentsAreOnlyRecentHistory && inlineAttachments.length > 0) ? mediaAttachments : inlineAttachments; const turnPromptText = attachments === mediaAttachments ? appendRecentHistoryImageContext({ promptText, images: resolvedTurnAttachments.recentHistoryImages }) : promptText; if (!turnPromptText && attachments.length === 0) { const counts = params.dispatcher.getQueuedCounts(); delivery.applyRoutedCounts(counts); params.recordProcessed("completed", { reason: "acp_empty_prompt" }); params.markIdle("message_completed"); return { queuedFinal: false, counts }; } try { await delivery.startReplyLifecycle(); } catch (error) { logVerbose(`dispatch-acp: start reply lifecycle failed: ${formatErrorMessage(error)}`); } await acpManager.runTurn({ cfg: params.cfg, sessionKey: canonicalSessionKey, text: resolveAcpTurnText({ promptText: turnPromptText, sourceReplyDeliveryMode: params.sourceReplyDeliveryMode }), attachments: attachments.length > 0 ? attachments : void 0, mode: "prompt", requestId: resolveAcpRequestId(params.ctx), ...params.abortSignal ? { signal: params.abortSignal } : {}, onEvent: async (event) => await projector.onEvent(event) }); await projector.flush(true); if (params.abortSignal?.aborted) { const counts = params.dispatcher.getQueuedCounts(); delivery.applyRoutedCounts(counts); params.recordProcessed("completed", { reason: "acp_aborted" }); params.markIdle("message_aborted"); return { queuedFinal, counts }; } try { const { persistAcpDispatchTranscript } = await loadDispatchAcpTranscriptRuntime(); await persistAcpDispatchTranscript({ cfg: params.cfg, sessionKey: canonicalSessionKey, promptText: turnPromptText, finalText: delivery.getAccumulatedFinalText() || delivery.getAccumulatedBlockText(), meta: acpResolution.meta, threadId: params.ctx.MessageThreadId }); } catch (error) { logVerbose(`dispatch-acp: transcript persistence failed for ${canonicalSessionKey}: ${formatErrorMessage(error)}`); } queuedFinal = await finalizeAcpTurnOutput({ cfg: params.cfg, sessionKey: canonicalSessionKey, agentId: acpAgentId, delivery, inboundAudio: params.inboundAudio, sessionTtsAuto: params.sessionTtsAuto, ttsChannel: params.ttsChannel, ttsAccountId: effectiveDispatchAccountId, shouldEmitResolvedIdentityNotice }) || queuedFinal; return finishAttempt({ queuedFinal, outcome: { kind: "ok" }, lifecyclePhase: "end" }); } catch (err) { await projector.flush(true); const acpError = toAcpRuntimeError({ error: err, fallbackCode: "ACP_TURN_FAILED", fallbackMessage: "ACP turn failed before completion." }); await maybeUnbindStaleBoundConversations({ targetSessionKey: canonicalSessionKey, error: acpError }); const delivered = await delivery.deliver("final", { text: formatAcpRuntimeErrorText(acpError), isError: true }); queuedFinal = queuedFinal || delivered; return finishAttempt({ queuedFinal, outcome: { kind: "error", error: acpError }, lifecyclePhase: "error" }); } } //#endregion export { tryDispatchAcpReply };