openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
400 lines (399 loc) • 19.1 kB
JavaScript
import "./src-CdRlGRnW.mjs";
import { i as asOptionalObjectRecord, t as asNonArrayRecord } from "./record-coerce-TIsquoVR.mjs";
import { n as safeParseJsonRecord } from "./json-coercion-pfKbZnCJ.mjs";
import { t as stableStringify } from "./stable-stringify-Djn3uxNo.mjs";
import { _ as stripLegacyMediaContextFields, d as readPersistedMediaFacts, p as readRuntimePromptMediaFacts } from "./media-facts-CJdQ4R61.mjs";
import { n as markInboundContextLabel } from "./inbound-context-marker-DDxVZisx.mjs";
import { c as hasInterSessionUserProvenance, n as INTER_SESSION_PROMPT_PREFIX_BASE } from "./input-provenance-D1LHo5Qj.mjs";
import "./user-turn-transcript-DGjEQrHM.mjs";
import { t as buildLateMediaAttachedProjection } from "./user-turn-transcript.message-D6tiOY3Z.mjs";
import { t as buildInboundMediaNoteProjection } from "./media-note-n64HYIUu.mjs";
import { r as formatContextJsonBlock } from "./channel-prompt-context-CUFSBN-S.mjs";
import { m as resolveTranscriptPolicy } from "./helpers-DrFdvmEe.mjs";
import { i as hydratePromptMediaMessages } from "./images-W7SPG0Ar.mjs";
import path from "node:path";
//#region src/agents/embedded-agent-runner/run/attempt-tool-call-block-type.ts
function isRunnerToolCallBlock(block) {
const type = asOptionalObjectRecord(block)?.type;
return type === "toolCall" || type === "toolUse" || type === "functionCall";
}
//#endregion
//#region src/agents/embedded-agent-runner/run/attempt-history.ts
const LEADING_TIMESTAMP_ENVELOPE_RE = /^\[[A-Za-z]{3} \d{4}-\d{2}-\d{2} \d{2}:\d{2}[^\]]*\] */;
const CONVERSATION_INFO_LABEL = markInboundContextLabel("Conversation info:");
function splitLeadingTimestampEnvelope(text) {
const envelope = text.match(LEADING_TIMESTAMP_ENVELOPE_RE)?.[0] ?? "";
return {
envelope,
body: envelope ? text.slice(envelope.length) : text
};
}
function readFirstUserText(content) {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return;
return content.find(isUserTextBlock)?.text;
}
function isUserTextBlock(value) {
if (!value || typeof value !== "object") return false;
const block = value;
return block.type === "text" && typeof block.text === "string";
}
function hasNonBlankUserText(content) {
return typeof content === "string" ? Boolean(content.trim()) : Array.isArray(content) && content.some((block) => isUserTextBlock(block) && Boolean(block.text.trim()));
}
function contentMatchesTimestampOverride(content, override) {
const text = readFirstUserText(content);
return text !== void 0 && (text === override.text || text === override.alternateText);
}
function resolveUserTranscriptMessages(messages, contexts, override) {
if (!contexts?.length) return;
const resolved = Array.from({ length: messages.length }, () => void 0);
const unusedContexts = new Set(contexts);
const byRuntimeMessage = /* @__PURE__ */ new Map();
for (const context of unusedContexts) {
const bucket = byRuntimeMessage.get(context.runtimeMessage);
if (bucket) bucket.push(context);
else byRuntimeMessage.set(context.runtimeMessage, [context]);
}
for (const [index, message] of messages.entries()) {
if (message.role !== "user" || message.operatorMessage) continue;
const context = byRuntimeMessage.get(message)?.shift();
if (!context) continue;
resolved[index] = context.transcriptMessage;
unusedContexts.delete(context);
}
if (unusedContexts.size === 0) return resolved;
const byTimestamp = /* @__PURE__ */ new Map();
for (const context of unusedContexts) {
const timestamp = context.runtimeMessage.timestamp;
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) continue;
const bucket = byTimestamp.get(timestamp);
if (bucket) bucket.push(context);
else byTimestamp.set(timestamp, [context]);
}
const activeUserMessageIndex = findActiveUserMessageIndex(messages);
for (const [index, message] of messages.entries()) {
if (message.role !== "user" || message.operatorMessage || resolved[index]) continue;
const timestamp = message.timestamp;
const context = (typeof timestamp === "number" ? byTimestamp.get(timestamp) : void 0)?.find((candidate) => unusedContexts.has(candidate) && userContentMatchesTranscriptContext(message.content, candidate.runtimeMessage.content, index === activeUserMessageIndex || typeof override?.runtimeTimestamp === "number" && override.runtimeTimestamp === timestamp ? override : void 0));
if (!context) continue;
resolved[index] = context.transcriptMessage;
unusedContexts.delete(context);
}
return resolved;
}
function userContentMatchesTranscriptContext(messageContent, runtimeContent, override) {
const messageText = readFirstUserText(messageContent);
const runtimeText = readFirstUserText(runtimeContent);
if (messageText !== void 0 && messageText === runtimeText) return true;
if (messageText === void 0 && runtimeText === void 0 && Array.isArray(messageContent) && Array.isArray(runtimeContent) && stableStringify(messageContent) === stableStringify(runtimeContent)) return true;
return Boolean(override && contentMatchesTimestampOverride(messageContent, override) && contentMatchesTimestampOverride(runtimeContent, override));
}
function normalizePersistedSenderValue(value) {
if (typeof value !== "string") return;
return value.replaceAll("\0", "").trim() || void 0;
}
function readPersistedSender(message) {
const openclaw = Reflect.get(message, "__openclaw");
if (!openclaw || typeof openclaw !== "object" || Array.isArray(openclaw)) return;
const meta = openclaw;
const sender = {
id: normalizePersistedSenderValue(meta["senderId"]),
name: normalizePersistedSenderValue(meta["senderName"]),
username: normalizePersistedSenderValue(meta["senderUsername"])
};
if (Object.values(sender).every((value) => value === void 0)) return;
return sender;
}
function mergeSenderIntoLeadingConversationInfo(text, sender) {
const { body, envelope } = splitLeadingTimestampEnvelope(text);
const jsonPrefix = `${CONVERSATION_INFO_LABEL}\n\`\`\`json\n`;
if (!body.startsWith(jsonPrefix)) return;
const jsonEnd = body.indexOf("\n```", jsonPrefix.length);
if (jsonEnd === -1) return;
const payload = safeParseJsonRecord(body.slice(jsonPrefix.length, jsonEnd));
if (!payload) return;
const suffix = body.slice(jsonEnd + 4);
return `${envelope}${formatContextJsonBlock(CONVERSATION_INFO_LABEL, {
...payload,
sender
})}${suffix}`;
}
function prependContextToUserMessage(message, sender) {
const context = formatContextJsonBlock(CONVERSATION_INFO_LABEL, { sender });
const projectText = (text) => {
const { body, envelope } = splitLeadingTimestampEnvelope(text);
if (body === context || body.startsWith(`${context}\n\n`)) return;
return mergeSenderIntoLeadingConversationInfo(text, sender) ?? `${envelope}${body ? `${context}\n\n${body}` : context}`;
};
const content = message.content;
if (typeof content === "string") {
const text = projectText(content);
return text === void 0 || text === content ? message : {
...message,
content: text
};
}
if (!Array.isArray(content)) return message;
const textIndex = content.findIndex(isUserTextBlock);
if (textIndex === -1) return {
...message,
content: [{
type: "text",
text: context
}, ...content]
};
const textBlock = content[textIndex];
const text = projectText(textBlock.text);
if (text === void 0) return message;
const nextContent = content.slice();
nextContent[textIndex] = {
...textBlock,
text
};
return {
...message,
content: nextContent
};
}
function hasInterSessionPromptPrefix(message) {
const text = readFirstUserText(message.content);
if (text === void 0) return false;
return splitLeadingTimestampEnvelope(text).body.startsWith(INTER_SESSION_PROMPT_PREFIX_BASE);
}
function projectPersistedSenderContext(messages, transcriptMessages) {
let changed = false;
const nextMessages = messages.map((message, index) => {
if (message.role !== "user" || message.operatorMessage) return message;
const transcriptMessage = transcriptMessages?.[index] ?? message;
if (hasInterSessionUserProvenance(message) || hasInterSessionUserProvenance(transcriptMessage) || hasInterSessionPromptPrefix(message) || hasInterSessionPromptPrefix(transcriptMessage)) return message;
const sender = readPersistedSender(transcriptMessage);
if (!sender) return message;
const nextMessage = prependContextToUserMessage(message, sender);
changed ||= nextMessage !== message;
return nextMessage;
});
return changed ? nextMessages : messages;
}
function findActiveUserMessageIndex(messages) {
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (!message) continue;
if (message.role === "user" && !message.operatorMessage) return index;
if (message.role === "assistant" && (!Array.isArray(message.content) || !message.content.some(isRunnerToolCallBlock))) return -1;
}
return -1;
}
/**
* Resolves the transcript policy for an embedded attempt. RuntimePlan owns the
* policy when present; otherwise the older provider/config/env resolver remains
* the compatibility path for callers that have not produced a runtime plan yet.
*/
function resolveAttemptTranscriptPolicy(params) {
return params.runtimePlan?.transcript.resolvePolicy(params.runtimePlanModelContext) ?? resolveTranscriptPolicy({
modelApi: params.runtimePlanModelContext.modelApi,
directApiKey: params.runtimePlanModelContext.directApiKey,
provider: params.provider,
modelId: params.modelId,
config: params.config,
workspaceDir: params.runtimePlanModelContext.workspaceDir,
env: params.env ?? process.env,
model: typeof params.runtimePlanModelContext.model?.id === "string" ? params.runtimePlanModelContext.model : void 0
});
}
//#endregion
//#region src/agents/embedded-agent-runner/run/history-image-prune.ts
/**
* Prunes already-processed image payloads from replayed prompt history.
*/
/** Replacement text for old image blocks that were already available to the model. */
const PRUNED_HISTORY_IMAGE_MARKER = "[image data removed - already processed by model]";
/** Replacement text for fact-owned late-media projections already processed by the model. */
const PRUNED_HISTORY_MEDIA_REFERENCE_MARKER = "[media reference removed - already processed by model]";
const LEGACY_MEDIA_ATTACHED_PATTERN = /\[media attached(?:\s+\d+\/\d+)?:\s*[^\]]+\]/gi;
const LEGACY_IMAGE_SOURCE_PATTERN = /\[Image:\s*source:\s*[^\]]+\]/gi;
const LEGACY_INBOUND_MEDIA_URI_PATTERN = /\bmedia:\/\/inbound\/[^\]\s/\\]+/g;
/**
* Number of most-recent completed turns whose preceding user/toolResult image
* blocks are kept intact. Counts all completed turns, not just image-bearing
* ones, so text-only turns consume the window.
*/
const PRESERVE_RECENT_COMPLETED_TURNS = 3;
function resolvePruneBeforeIndex(messages) {
const completedTurnStarts = [];
let currentTurnStart = -1;
let currentTurnHasAssistantReply = false;
for (let i = 0; i < messages.length; i++) {
const role = messages[i]?.role;
if (role === "user") {
if (currentTurnStart >= 0 && currentTurnHasAssistantReply) {
if (completedTurnStarts.length > PRESERVE_RECENT_COMPLETED_TURNS) completedTurnStarts.shift();
completedTurnStarts.push(currentTurnStart);
}
currentTurnStart = i;
currentTurnHasAssistantReply = false;
continue;
}
if (role === "toolResult") {
if (currentTurnStart < 0) currentTurnStart = i;
continue;
}
if (role === "assistant" && currentTurnStart >= 0) currentTurnHasAssistantReply = true;
}
if (completedTurnStarts.length <= PRESERVE_RECENT_COMPLETED_TURNS) return -1;
return completedTurnStarts.at(-3) ?? -1;
}
function wasStructurallyMediaPruned(message) {
return asNonArrayRecord(Reflect.get(message, "__openclaw")).mediaImagePruned === true;
}
function replaceLegacyFactlessMediaText(text) {
return text.replace(LEGACY_MEDIA_ATTACHED_PATTERN, PRUNED_HISTORY_MEDIA_REFERENCE_MARKER).replace(LEGACY_IMAGE_SOURCE_PATTERN, PRUNED_HISTORY_MEDIA_REFERENCE_MARKER).replace(LEGACY_INBOUND_MEDIA_URI_PATTERN, PRUNED_HISTORY_MEDIA_REFERENCE_MARKER);
}
function normalizeMarkerIdentity(identity) {
return identity.replaceAll("\\", "/");
}
function resolveWorkspaceRelativeMarkerAliases(fact) {
if (!fact.path || !fact.workspaceDir || !path.isAbsolute(fact.path) || !path.isAbsolute(fact.workspaceDir)) return [];
const relativePath = path.relative(fact.workspaceDir, fact.path);
if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) return [];
const normalizedRelativePath = normalizeMarkerIdentity(relativePath);
return [normalizedRelativePath, `./${normalizedRelativePath}`];
}
function factOwnsMarkerIdentity(identity, media) {
const normalizedIdentity = normalizeMarkerIdentity(identity);
return media.some((fact) => {
return [
fact.path,
fact.url,
...resolveWorkspaceRelativeMarkerAliases(fact)
].some((alias) => alias && normalizeMarkerIdentity(alias) === normalizedIdentity);
});
}
function extractMediaAttachedIdentity(marker) {
const content = marker.replace(/^\[media attached(?:\s+\d+\/\d+)?:\s*/i, "").slice(0, -1);
const endIndexes = [content.lastIndexOf(" ("), content.indexOf(" | ")].filter((index) => index >= 0);
const endIndex = endIndexes.length > 0 ? Math.min(...endIndexes) : content.length;
return content.slice(0, endIndex).trim();
}
function replaceOwnedLegacyMediaMarkers(text, media) {
return text.replace(LEGACY_MEDIA_ATTACHED_PATTERN, (marker) => factOwnsMarkerIdentity(extractMediaAttachedIdentity(marker), media) ? PRUNED_HISTORY_MEDIA_REFERENCE_MARKER : marker).replace(LEGACY_IMAGE_SOURCE_PATTERN, (marker) => {
return factOwnsMarkerIdentity(marker.replace(/^\[Image:\s*source:\s*/i, "").slice(0, -1).trim(), media) ? PRUNED_HISTORY_MEDIA_REFERENCE_MARKER : marker;
});
}
function replaceOwnedMediaProjection(text, media) {
if (media.length === 0) return text;
const projectionLines = /* @__PURE__ */ new Set();
for (const facts of [media, ...media.map((fact) => [fact])]) {
const projection = buildInboundMediaNoteProjection({ media: facts }).text;
for (const line of projection?.split("\n") ?? []) if (line) projectionLines.add(line);
}
let redacted = text;
for (const line of projectionLines) redacted = redacted.replaceAll(line, PRUNED_HISTORY_MEDIA_REFERENCE_MARKER);
for (const fact of media) for (const alias of [fact.path, fact.url].filter((value) => Boolean(value))) redacted = redacted.replaceAll(`[Image: source: ${alias}]`, PRUNED_HISTORY_MEDIA_REFERENCE_MARKER).replaceAll(`[media attached: ${alias}]`, PRUNED_HISTORY_MEDIA_REFERENCE_MARKER);
return replaceOwnedLegacyMediaMarkers(redacted, media);
}
function cloneMessageWithContent(message, content, dropMedia = false, dropImageMetadata = dropMedia) {
const clone = {
...message,
content
};
if (dropMedia) {
delete clone.media;
stripLegacyMediaContextFields(clone);
}
if (dropImageMetadata) {
const nextMeta = { ...asNonArrayRecord(clone["__openclaw"]) };
delete nextMeta.mediaImageBlockFactIndexes;
delete nextMeta.mediaImageLayout;
if (dropMedia) {
delete nextMeta.media;
nextMeta.mediaImagePruned = true;
}
if (Object.keys(nextMeta).length > 0) clone["__openclaw"] = nextMeta;
else delete clone["__openclaw"];
}
return clone;
}
/** Prunes old image payloads and references before later LLM-boundary synthesis. */
function pruneProcessedHistoryImages(messages) {
const pruneBeforeIndex = resolvePruneBeforeIndex(messages);
if (pruneBeforeIndex < 0) return null;
let prunedMessages = null;
for (let i = 0; i < pruneBeforeIndex; i++) {
const message = messages[i];
if (!message || message.role !== "user" && message.role !== "toolResult") continue;
const media = message.role === "user" ? readRuntimePromptMediaFacts(message) ?? readPersistedMediaFacts(message) ?? [] : [];
const hasOwnedMedia = media.length > 0;
const structuredMediaWasPruned = wasStructurallyMediaPruned(message);
const pruneText = (text) => hasOwnedMedia ? replaceOwnedMediaProjection(text, media) : structuredMediaWasPruned ? text : replaceLegacyFactlessMediaText(text);
const lateMediaText = (message.role === "user" && !hasNonBlankUserText(message.content) ? buildLateMediaAttachedProjection(message) : void 0)?.media.map(() => PRUNED_HISTORY_MEDIA_REFERENCE_MARKER).join("\n");
const content = lateMediaText ? Array.isArray(message.content) ? [{
type: "text",
text: lateMediaText
}, ...message.content] : lateMediaText : message.content;
if (typeof content === "string") {
const nextText = pruneText(content);
if (nextText !== message.content || hasOwnedMedia) {
prunedMessages ??= messages.slice();
prunedMessages[i] = cloneMessageWithContent(message, nextText, hasOwnedMedia);
}
continue;
}
if (!Array.isArray(content)) continue;
const contentLength = content.length;
let nextContent = hasOwnedMedia || lateMediaText ? content.slice(0, contentLength) : void 0;
let prunedImageBlock = false;
for (let index = 0; index < contentLength; index += 1) {
if (!(index in content)) continue;
const block = content[index];
let nextBlock;
if (block?.type === "text" && typeof block.text === "string") {
const text = pruneText(block.text);
if (text !== block.text) nextBlock = {
...block,
text
};
} else if (block?.type === "image") {
prunedImageBlock = true;
nextBlock = {
type: "text",
text: PRUNED_HISTORY_IMAGE_MARKER
};
}
if (nextBlock !== void 0) {
nextContent ??= content.slice(0, contentLength);
nextContent[index] = nextBlock;
}
}
if (nextContent) {
prunedMessages ??= messages.slice();
prunedMessages[i] = cloneMessageWithContent(message, nextContent, hasOwnedMedia, hasOwnedMedia || prunedImageBlock);
}
}
return prunedMessages;
}
/** Installs an agent context transform that prunes old image/media history before model input. */
function installHistoryImagePruneContextTransform(agent, mediaOptions, onPruned) {
const originalTransformContext = agent.transformContext;
agent.transformContext = async (messages, signal) => {
const pruned = /* @__PURE__ */ new Map();
const prune = (source) => {
const projected = pruneProcessedHistoryImages(source);
projected?.forEach((message, index) => {
const original = source[index];
if (original && message !== original) pruned.set(index, original);
});
return projected ?? source;
};
const prunedInput = prune(messages);
const hydratedInput = mediaOptions ? await hydratePromptMediaMessages(prunedInput, mediaOptions) : prunedInput;
const result = prune(originalTransformContext ? await originalTransformContext.call(agent, hydratedInput, signal) : hydratedInput);
onPruned?.(pruned);
return result;
};
return () => {
agent.transformContext = originalTransformContext;
};
}
//#endregion
export { hasNonBlankUserText as a, resolveAttemptTranscriptPolicy as c, findActiveUserMessageIndex as i, resolveUserTranscriptMessages as l, pruneProcessedHistoryImages as n, isUserTextBlock as o, contentMatchesTimestampOverride as r, projectPersistedSenderContext as s, installHistoryImagePruneContextTransform as t, isRunnerToolCallBlock as u };