UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

175 lines (174 loc) • 8.42 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import "./src-CdRlGRnW.mjs"; import { t as expectDefined } from "./expect-lbe3Hgrh.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { n as safeParseJsonRecord } from "./json-coercion-pfKbZnCJ.mjs"; import { n as HEARTBEAT_TOKEN, r as SILENT_REPLY_TOKEN } from "./tokens-D4ghQG_g.mjs"; import { a as readToolCallName, n as isContractToolCallBlock, r as isContractToolResultBlock, t as collectToolCallIds } from "./tool-block-contract-CgqnHcSD.mjs"; import { a as INTERNAL_WAKE_TRANSCRIPT_PROMPTS, i as HEARTBEAT_RESPONSE_TOOL_PROMPT, l as resolveHeartbeatPromptForResponseTool, o as isHeartbeatAcknowledgementText, r as HEARTBEAT_RESPONSE_TOOL_INSTRUCTIONS } from "./heartbeat-BjLIgeQc.mjs"; import { n as MESSAGE_TOOL_DELIVERY_HINTS } from "./message-tool-delivery-hints-8OSBEg_c.mjs"; import "./delivery-hints-v8S7vGqh.mjs"; import { t as HEARTBEAT_RESPONSE_TOOL_NAME } from "./heartbeat-tool-response-BAjLTEaX.mjs"; //#region src/auto-reply/heartbeat-filter.ts const HEARTBEAT_TASK_PROMPT_PREFIX = "Run the following periodic tasks (only those due based on their intervals):"; const HEARTBEAT_TASK_PROMPT_COMPLETIONS = [ ...[HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN].map((token) => `After completing all due tasks, reply ${token}.`), HEARTBEAT_RESPONSE_TOOL_INSTRUCTIONS, `After completing all due tasks, use ${HEARTBEAT_RESPONSE_TOOL_NAME}` ]; function collectToolResultBlocks(content) { if (!Array.isArray(content)) return []; return content.filter(isContractToolResultBlock); } function isVisibleHeartbeatResponseToolCall(block) { const nested = isRecord(block.function) ? block.function : void 0; const value = block.arguments ?? block.args ?? block.input ?? nested?.arguments ?? nested?.args ?? nested?.input; const args = isRecord(value) ? value : typeof value === "string" ? safeParseJsonRecord(value) : void 0; return args?.notify === true || args?.notify === "true"; } function collectAssistantToolCalls(message) { if (message.role !== "assistant") return []; const toolCalls = message.tool_calls; return [...Array.isArray(toolCalls) ? toolCalls.filter(isRecord) : [], ...Array.isArray(message.content) ? message.content.filter(isContractToolCallBlock) : []]; } function isEmbeddedToolResultOnlyContent(content) { return Array.isArray(content) && content.length > 0 && content.every((block) => isContractToolResultBlock(block)); } function isToolResultMessage(message) { return message.role === "toolResult" || message.role === "tool" || message.role === "user" && isEmbeddedToolResultOnlyContent(message.content); } function isFailedToolResultRecord(record) { return record.isError === true || record.is_error === true || normalizeOptionalString(record.type) === "tool_result_error"; } function matchesHeartbeatPromptText(text, prompt) { const normalized = prompt?.trim(); return Boolean(normalized) && (text === normalized || text.startsWith(`${normalized}\n`)); } function resolveMessageText(content) { if (typeof content === "string") return { text: content, hasNonTextContent: false }; if (!Array.isArray(content)) return { text: "", hasNonTextContent: content != null }; let hasNonTextContent = false; let text = ""; for (const block of content) { if (typeof block !== "object" || block === null || !("type" in block)) { hasNonTextContent = true; continue; } if (block.type === "thinking" || block.type === "reasoning" || block.type === "redacted_thinking") continue; if (block.type !== "text" && block.type !== "input_text" && block.type !== "output_text") { hasNonTextContent = true; continue; } const blockText = block.text; if (typeof blockText !== "string") { hasNonTextContent = true; continue; } text += blockText; } return { text, hasNonTextContent }; } /** Return whether a user message is an internal heartbeat prompt. */ function isHeartbeatUserMessage(message, heartbeatPrompt) { if (message.role !== "user") return false; const { text } = resolveMessageText(message.content); const trimmed = text.trim(); if (!trimmed) return false; const normalizedHeartbeatPrompt = heartbeatPrompt?.trim(); const transcriptPrompts = Object.values(INTERNAL_WAKE_TRANSCRIPT_PROMPTS); if (transcriptPrompts.some((prompt) => trimmed === prompt)) return true; if (MESSAGE_TOOL_DELIVERY_HINTS.some((prefix) => trimmed.startsWith(prefix)) && transcriptPrompts.some((prompt) => trimmed.endsWith(prompt))) return true; if (matchesHeartbeatPromptText(trimmed, normalizedHeartbeatPrompt) || matchesHeartbeatPromptText(trimmed, HEARTBEAT_RESPONSE_TOOL_PROMPT) || normalizedHeartbeatPrompt && matchesHeartbeatPromptText(trimmed, resolveHeartbeatPromptForResponseTool(normalizedHeartbeatPrompt))) return true; return trimmed.startsWith(HEARTBEAT_TASK_PROMPT_PREFIX) && HEARTBEAT_TASK_PROMPT_COMPLETIONS.some((completion) => trimmed.includes(completion)); } /** Return whether an assistant message is only a heartbeat acknowledgement. */ function isHeartbeatOkResponse(message, ackMaxChars) { if (message.role !== "assistant" || collectAssistantToolCalls(message).length > 0) return false; const { text, hasNonTextContent } = resolveMessageText(message.content); return !hasNonTextContent && isHeartbeatAcknowledgementText(text, ackMaxChars); } function advancePastAdjacentToolResults(messages, startIndex) { let index = startIndex; while (index < messages.length) { const message = messages.at(index); if (!message || !isToolResultMessage(message)) break; index++; } return index; } function hasCompletedVisibleHeartbeatResponseToolCall(messages, index, visibleCalls) { const callIds = new Set(visibleCalls.flatMap((call) => collectToolCallIds(call))); for (let resultIndex = index + 1; resultIndex < messages.length; resultIndex++) { const result = expectDefined(messages[resultIndex], "messages entry at resultIndex"); const blocks = collectToolResultBlocks(result.content); if (blocks.length === 0 && !isToolResultMessage(result)) break; if ((blocks.length > 0 ? blocks : [result]).some((record) => !isFailedToolResultRecord(record) && (callIds.size === 0 || collectToolCallIds(record).some((id) => callIds.has(id))))) return true; } return false; } function resolveHeartbeatArtifactSpanEnd(messages, startIndex, ackMaxChars) { let index = startIndex + 1; let sawTerminalHeartbeatArtifact = false; let sawNonTerminalAssistantOutput = false; while (index < messages.length) { const message = messages.at(index); if (!message) break; if (message.role === "user" && !isEmbeddedToolResultOnlyContent(message.content)) break; if (isHeartbeatOkResponse(message, ackMaxChars)) { sawTerminalHeartbeatArtifact = true; index = advancePastAdjacentToolResults(messages, index + 1); continue; } const toolCalls = collectAssistantToolCalls(message); const heartbeatCalls = toolCalls.filter((block) => readToolCallName(block) === HEARTBEAT_RESPONSE_TOOL_NAME); const visibleCalls = heartbeatCalls.filter(isVisibleHeartbeatResponseToolCall); if (visibleCalls.length > 0) { if (hasCompletedVisibleHeartbeatResponseToolCall(messages, index, visibleCalls)) return; index++; continue; } if (heartbeatCalls.length > 0) { sawTerminalHeartbeatArtifact = true; index = advancePastAdjacentToolResults(messages, index + 1); continue; } if (sawTerminalHeartbeatArtifact || isToolResultMessage(message) || toolCalls.length > 0) { index++; continue; } if (message.role === "assistant") { sawNonTerminalAssistantOutput = true; index++; continue; } return; } if (sawNonTerminalAssistantOutput && !sawTerminalHeartbeatArtifact) return; return index; } /** Remove heartbeat-only prompt, ack, and silent tool artifacts from a transcript. */ function filterHeartbeatTranscriptArtifacts(messages, ackMaxChars, heartbeatPrompt) { if (messages.length === 0) return messages; const result = []; let i = 0; while (i < messages.length) { const message = expectDefined(messages[i], "messages entry at i"); const next = isHeartbeatUserMessage(message, heartbeatPrompt) ? resolveHeartbeatArtifactSpanEnd(messages, i, ackMaxChars) : void 0; if (next === void 0) { result.push(message); i++; } else i = next; } return result; } //#endregion export { isHeartbeatOkResponse as n, isHeartbeatUserMessage as r, filterHeartbeatTranscriptArtifacts as t };