openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
232 lines (231 loc) • 10.5 kB
JavaScript
import { a as normalizeAssistantPhase, o as parseAssistantTextSignature } from "./chat-message-content-C-d0FKNG.mjs";
import { c as trimTextPreservingCode, i as createTextProjection, s as trimTextFilter, t as applyTextFilters } from "./text-projection-DmHct7rN.mjs";
import { n as assistantVisibleTextFilters } from "./assistant-visible-text-CWq-s28E.mjs";
import { M as userFacingTextFilters, U as extractTextFromChatContent, j as sanitizeUserFacingText, n as renderUserFacingText } from "./user-facing-text-BCPDY-39.mjs";
import { stripCompactionReplayCheckpointInPlace } from "@openclaw/ai/transports";
//#region src/agents/embedded-agent-utils.ts
function isAssistantMessage(msg) {
return msg?.role === "assistant";
}
function sanitizeAssistantText(text, phase, streaming = false, options) {
return applyTextFilters(text, assistantVisibleTextFilters(phase === "final_answer" ? "final-answer-delivery" : "delivery", streaming && phase === "final_answer", options));
}
function isAssistantTextContentBlockType(value) {
return value === "text" || value === "input_text" || value === "output_text";
}
function sanitizeAssistantVisibleStreamText(text, phase, options) {
return sanitizeUserFacingText(sanitizeAssistantText(text, phase, true, options), {
errorContext: false,
streaming: true
});
}
function createAssistantVisibleStreamText(phase) {
return createTextProjection([
...assistantVisibleTextFilters(phase === "final_answer" ? "final-answer-delivery" : "delivery", phase === "final_answer"),
...userFacingTextFilters(false, true),
trimTextFilter("both", { preserveCodeIndentation: true })
]);
}
function finalizeAssistantExtraction(errorContext, extracted) {
return errorContext ? renderUserFacingText(extracted, { errorContext: true }) : sanitizeUserFacingText(extracted);
}
function prepareEmbeddedAssistantTextForPhase(msg, requestedPhase, prepareText) {
const prepareRender = (selectedPhase, renderText) => {
const errorContext = msg.stopReason === "error";
return () => {
const extracted = finalizeAssistantExtraction(errorContext, renderText());
return selectedPhase === "final_answer" && !extracted.trim() ? "" : extracted;
};
};
const messagePhase = normalizeAssistantPhase(msg.phase);
if (typeof msg.content === "string") {
const selectedPhase = requestedPhase === "final_answer" && messagePhase !== "final_answer" ? void 0 : requestedPhase;
if (messagePhase !== selectedPhase) return () => "";
const preparedText = prepareText ? prepareText(msg.content, true, messagePhase) : msg.content;
return prepareRender(selectedPhase, () => sanitizeAssistantText(preparedText, messagePhase));
}
if (!Array.isArray(msg.content)) return () => "";
let hasExplicitPhasedTextBlocks = false;
let hasFinalAnswerText = false;
for (const block of msg.content) {
if (!block || typeof block !== "object") continue;
const record = block;
if (!isAssistantTextContentBlockType(record.type)) continue;
const phase = parseAssistantTextSignature(record)?.phase;
hasExplicitPhasedTextBlocks ||= Boolean(phase);
hasFinalAnswerText ||= phase === "final_answer" && typeof record.text === "string";
if (hasExplicitPhasedTextBlocks && (requestedPhase === "commentary" || hasFinalAnswerText)) break;
}
const selectedPhase = requestedPhase === "final_answer" && !hasFinalAnswerText && (hasExplicitPhasedTextBlocks || messagePhase !== "final_answer") ? void 0 : requestedPhase;
const parts = [];
for (const [contentIndex, block] of msg.content.entries()) {
if (!block || typeof block !== "object") continue;
const record = block;
if (!isAssistantTextContentBlockType(record.type) || typeof record.text !== "string") continue;
const signature = parseAssistantTextSignature(record);
const resolvedPhase = signature?.phase ?? (hasExplicitPhasedTextBlocks ? void 0 : messagePhase);
if (resolvedPhase !== selectedPhase) continue;
const sanitizerPhase = resolvedPhase ?? (requestedPhase === "final_answer" && signature?.id ? "final_answer" : void 0);
parts.push({
text: record.text,
phase: sanitizerPhase,
contentIndex
});
}
if (prepareText) for (const [index, part] of parts.entries()) part.text = prepareText(part.text, index === parts.length - 1, part.phase, part.contentIndex);
return prepareRender(selectedPhase, () => parts.map(({ text, phase }) => sanitizeAssistantText(text, phase)).filter((text) => text.trim()).join("\n").trimEnd());
}
/** Prepare selected source parts now; render their visible text only when requested. */
function prepareAssistantVisibleText(msg, prepareText) {
return prepareEmbeddedAssistantTextForPhase(msg, "final_answer", prepareText);
}
/** Extract text intended for users, preferring explicit final-answer phase blocks. */
function extractAssistantVisibleText(msg, prepareText) {
return prepareAssistantVisibleText(msg, prepareText)();
}
function extractAssistantCommentaryText(msg) {
return prepareEmbeddedAssistantTextForPhase(msg, "commentary")();
}
function extractEmbeddedAssistantText(msg) {
const extracted = extractTextFromChatContent(msg.content, {
sanitizeText: (text) => sanitizeAssistantText(text),
joinWith: "\n",
normalizeText: (text) => text.trimEnd()
}) ?? "";
return finalizeAssistantExtraction(msg.stopReason === "error", extracted);
}
/** Extract native thinking block text; signature-only blocks (no summary) surface nothing. */
function extractAssistantThinking(msg) {
if (!Array.isArray(msg.content)) return "";
const blocks = [];
for (const block of msg.content) {
if (!block || typeof block !== "object") continue;
const type = Reflect.get(block, "type");
const rawThinking = Reflect.get(block, "thinking");
if (type === "thinking" && typeof rawThinking === "string") {
const thinking = rawThinking.trim();
if (thinking) blocks.push(thinking);
}
}
return blocks.join("\n");
}
function formatReasoningMessage(text) {
const trimmed = text.trim();
if (!trimmed) return "";
return `Thinking\n\n${trimmed.split("\n").map((line) => line ? `_${line}_` : line).join("\n")}`;
}
const THINKING_TAG_NAME_PATTERN = String.raw`(?:(?:antml:|mm:)?(?:think(?:ing)?|thought)|antthinking)`;
const THINKING_TAG_OPEN_RE = new RegExp(String.raw`<\s*${THINKING_TAG_NAME_PATTERN}\s*>`, "i");
const THINKING_TAG_CLOSE_RE = new RegExp(String.raw`<\s*\/\s*${THINKING_TAG_NAME_PATTERN}\s*>`, "i");
const THINKING_TAG_SCAN_RE = new RegExp(String.raw`<\s*(\/?)\s*${THINKING_TAG_NAME_PATTERN}\s*>`, "gi");
const THINKING_TAG_EXACT_RE = new RegExp(String.raw`^<\s*(\/?)\s*${THINKING_TAG_NAME_PATTERN}\s*>$`, "i");
function createThinkingTagStreamState() {
return {
scannedOffset: 0,
inThinking: false,
extracted: "",
lastMatchEnd: 0
};
}
function splitThinkingTaggedText(text) {
const trimmedStart = text.trimStart();
if (!trimmedStart.startsWith("<") || !THINKING_TAG_OPEN_RE.test(trimmedStart) || !THINKING_TAG_CLOSE_RE.test(text)) return null;
let inThinking = false;
let cursor = 0;
const blocks = [];
const pushBlock = (type, value) => {
const content = type === "thinking" ? value.trim() : value;
if (content) blocks.push(type === "thinking" ? {
type,
thinking: content
} : {
type,
text: content
});
};
for (const match of text.matchAll(THINKING_TAG_SCAN_RE)) {
const index = match.index ?? 0;
const isClose = match[1]?.includes("/") ?? false;
if (isClose !== inThinking) continue;
pushBlock(inThinking ? "thinking" : "text", text.slice(cursor, index));
cursor = index + match[0].length;
inThinking = !isClose;
}
if (inThinking) return null;
pushBlock("text", text.slice(cursor));
return blocks.some((block) => block.type === "thinking") ? blocks : null;
}
function promoteThinkingTagsToBlocks(message) {
if (!Array.isArray(message.content) || message.content.some((block) => block && typeof block === "object" && block.type === "thinking")) return;
const next = [];
let changed = false;
for (const block of message.content) {
if (!block || typeof block !== "object" || !("type" in block) || block.type !== "text") {
next.push(block);
continue;
}
const split = splitThinkingTaggedText(block.text);
if (!split) {
next.push(block);
continue;
}
changed = true;
for (const part of split) if (part.type === "thinking") next.push({
type: "thinking",
thinking: part.thinking
});
else if (part.type === "text") {
const cleaned = trimTextPreservingCode(part.text, "start");
if (cleaned) next.push({
type: "text",
text: cleaned
});
}
}
if (!changed) return;
message.content = next;
stripCompactionReplayCheckpointInPlace(message);
}
function extractThinkingFromTaggedText(text) {
if (!text) return "";
let result = "";
let lastIndex = 0;
let inThinking = false;
for (const match of text.matchAll(THINKING_TAG_SCAN_RE)) {
const idx = match.index ?? 0;
if (inThinking) result += text.slice(lastIndex, idx);
inThinking = !(match[1] === "/");
lastIndex = idx + match[0].length;
}
return result.trim();
}
function extractThinkingFromTaggedStream(text, state, delta) {
const unscanned = text.length - state.scannedOffset === delta.length ? delta : text.slice(state.scannedOffset);
for (let offset = 0; offset < unscanned.length; offset += 1) {
const index = state.scannedOffset + offset;
const char = unscanned[offset];
if (char === "<") {
state.pendingTagStart = index;
continue;
}
if (char !== ">" || state.pendingTagStart === void 0) continue;
const start = state.pendingTagStart;
state.pendingTagStart = void 0;
const match = THINKING_TAG_EXACT_RE.exec(text.slice(start, index + 1));
if (!match) continue;
if (state.inThinking) state.extracted += text.slice(state.lastMatchEnd, start);
const isClose = match[1] === "/";
state.inThinking = !isClose;
state.lastMatchEnd = index + 1;
state.lastTag = {
type: isClose ? "close" : "open",
end: index + 1
};
}
state.scannedOffset = text.length;
const closed = state.extracted.trim();
if (closed || state.lastTag?.type !== "open") return closed;
return text.slice(state.lastTag.end).trim();
}
//#endregion
export { extractAssistantVisibleText as a, extractThinkingFromTaggedText as c, prepareAssistantVisibleText as d, promoteThinkingTagsToBlocks as f, extractAssistantThinking as i, formatReasoningMessage as l, createThinkingTagStreamState as n, extractEmbeddedAssistantText as o, sanitizeAssistantVisibleStreamText as p, extractAssistantCommentaryText as r, extractThinkingFromTaggedStream as s, createAssistantVisibleStreamText as t, isAssistantMessage as u };