openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
237 lines (236 loc) • 13.5 kB
JavaScript
import { t as escapeRegExp } from "./regexp-BZyMFTlj.mjs";
//#region src/agents/internal-runtime-context.ts
/**
* Internal runtime-context delimiter and stripping helpers.
* Protects runtime-generated prompt blocks from user text and removes old
* context formats before replaying or comparing messages.
*/
/** Opening delimiter for protected OpenClaw runtime context blocks. */
const INTERNAL_RUNTIME_CONTEXT_BEGIN = "<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>";
/** Closing delimiter for protected OpenClaw runtime context blocks. */
const INTERNAL_RUNTIME_CONTEXT_END = "<<<END_OPENCLAW_INTERNAL_CONTEXT>>>";
const ESCAPED_INTERNAL_RUNTIME_CONTEXT_BEGIN = "[[OPENCLAW_INTERNAL_CONTEXT_BEGIN]]";
const ESCAPED_INTERNAL_RUNTIME_CONTEXT_END = "[[OPENCLAW_INTERNAL_CONTEXT_END]]";
/** Notice inserted into runtime-generated context blocks. */
const OPENCLAW_RUNTIME_CONTEXT_NOTICE = "This context is runtime-generated, not user-authored. Keep internal details private.";
/** Custom message type used for structured runtime-context messages. */
const OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE = "openclaw.runtime-context";
const LEGACY_INTERNAL_CONTEXT_HEADER = [
"OpenClaw runtime context (internal):",
OPENCLAW_RUNTIME_CONTEXT_NOTICE,
""
].join("\n") + "\n";
const LEGACY_INTERNAL_EVENT_MARKER = "[Internal task completion event]";
const LEGACY_INTERNAL_EVENT_SEPARATOR = "\n\n---\n\n";
const LEGACY_UNTRUSTED_RESULT_BEGIN = "<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>";
const LEGACY_UNTRUSTED_RESULT_END = "<<<END_UNTRUSTED_CHILD_RESULT>>>";
/** Escape protected context delimiters before embedding untrusted text. */
function escapeInternalRuntimeContextDelimiters(value) {
return value.replaceAll(INTERNAL_RUNTIME_CONTEXT_BEGIN, ESCAPED_INTERNAL_RUNTIME_CONTEXT_BEGIN).replaceAll(INTERNAL_RUNTIME_CONTEXT_END, ESCAPED_INTERNAL_RUNTIME_CONTEXT_END);
}
function createDelimitedToken(token) {
return {
token,
pattern: new RegExp(`(?:^|\\r?\\n)[ \\t]*${escapeRegExp(token)}[ \\t]*(?=\\r?\\n|$)`, "g")
};
}
const BEGIN_DELIMITER = createDelimitedToken(INTERNAL_RUNTIME_CONTEXT_BEGIN);
const END_DELIMITER = createDelimitedToken(INTERNAL_RUNTIME_CONTEXT_END);
function findDelimitedTokenIndex(text, delimiter, from) {
delimiter.pattern.lastIndex = Math.max(0, from);
const match = delimiter.pattern.exec(text);
if (!match) return -1;
return match.index + match[0].indexOf(delimiter.token);
}
function findDelimitedTokenLinePrefixStart(text, tokenIndex) {
const lineStart = text.lastIndexOf("\n", tokenIndex - 1) + 1;
if (lineStart === 0) return 0;
return text[lineStart - 2] === "\r" ? lineStart - 2 : lineStart - 1;
}
function stripDelimitedBlocks(text, options = {}) {
const begin = BEGIN_DELIMITER;
const end = END_DELIMITER;
let next = text;
for (;;) {
const start = findDelimitedTokenIndex(next, begin, 0);
if (start === -1) return next;
let cursor = start + begin.token.length;
let depth = 1;
let finish = -1;
while (depth > 0) {
const nextBegin = findDelimitedTokenIndex(next, begin, cursor);
const nextEnd = findDelimitedTokenIndex(next, end, cursor);
if (nextEnd === -1) break;
if (nextBegin !== -1 && nextBegin < nextEnd) {
depth += 1;
cursor = nextBegin + begin.token.length;
continue;
}
depth -= 1;
finish = nextEnd;
cursor = nextEnd + end.token.length;
}
const blockStart = options.preserveSurroundingWhitespace ? findDelimitedTokenLinePrefixStart(next, start) : start;
const before = options.preserveSurroundingWhitespace ? next.slice(0, blockStart) : next.slice(0, start).trimEnd();
if (finish === -1 || depth !== 0) return before;
let blockEnd = finish + end.token.length;
while (next[blockEnd] === " " || next[blockEnd] === " ") blockEnd += 1;
const after = options.preserveSurroundingWhitespace ? next.slice(blockEnd) : next.slice(blockEnd).trimStart();
next = !options.preserveSurroundingWhitespace && before && after ? `${before}${options.separator ?? "\n\n"}${after}` : `${before}${after}`;
}
}
function findLegacyInternalEventEnd(text, start) {
if (!text.startsWith(LEGACY_INTERNAL_EVENT_MARKER, start)) return null;
const resultBegin = text.indexOf(LEGACY_UNTRUSTED_RESULT_BEGIN, start + 32);
if (resultBegin === -1) return null;
const resultEnd = text.indexOf(LEGACY_UNTRUSTED_RESULT_END, resultBegin + 34);
if (resultEnd === -1) return null;
const actionIndex = text.indexOf("\n\nAction:\n", resultEnd + 32);
if (actionIndex === -1) return null;
const afterAction = actionIndex + 10;
const nextEvent = text.indexOf(`${LEGACY_INTERNAL_EVENT_SEPARATOR}${LEGACY_INTERNAL_EVENT_MARKER}`, afterAction);
if (nextEvent !== -1) return nextEvent;
const nextParagraph = text.indexOf("\n\n", afterAction);
return nextParagraph === -1 ? text.length : nextParagraph;
}
function stripLegacyInternalRuntimeContext(text) {
let next = text;
let searchFrom = 0;
for (;;) {
const headerStart = next.indexOf(LEGACY_INTERNAL_CONTEXT_HEADER, searchFrom);
if (headerStart === -1) return next;
const eventStart = headerStart + LEGACY_INTERNAL_CONTEXT_HEADER.length;
if (!next.startsWith(LEGACY_INTERNAL_EVENT_MARKER, eventStart)) {
searchFrom = eventStart;
continue;
}
let blockEnd = findLegacyInternalEventEnd(next, eventStart);
if (blockEnd == null) {
const nextParagraph = next.indexOf("\n\n", eventStart + 32);
blockEnd = nextParagraph === -1 ? next.length : nextParagraph;
} else while (next.startsWith(`${LEGACY_INTERNAL_EVENT_SEPARATOR}${LEGACY_INTERNAL_EVENT_MARKER}`, blockEnd)) {
const nextEventStart = blockEnd + 7;
const nextEventEnd = findLegacyInternalEventEnd(next, nextEventStart);
if (nextEventEnd == null) break;
blockEnd = nextEventEnd;
}
const before = next.slice(0, headerStart).trimEnd();
const after = next.slice(blockEnd).trimStart();
next = before && after ? `${before}\n\n${after}` : `${before}${after}`;
searchFrom = Math.max(0, before.length - 1);
}
}
const RUNTIME_CONTEXT_PROMPT_HEADERS = [
"OpenClaw runtime context for the active user request in this turn. Do not reply to or describe this context. Use it to continue answering the active user request now. Do not wait for another message.",
"OpenClaw runtime context for the immediately preceding user message.",
"OpenClaw runtime event."
];
const RUNTIME_CONTEXT_CARRIER_PREFIX_PATTERN = new RegExp(RUNTIME_CONTEXT_PROMPT_HEADERS.flatMap((header) => {
const sentences = header.split(". ");
return sentences.map((_, index) => sentences.slice(index).join(". "));
}).map((prefix) => prefix.split(/\s+/).map(escapeRegExp).join("\\s+")).join("|"));
const RUNTIME_CONTEXT_NOTICE_PATTERN = new RegExp(OPENCLAW_RUNTIME_CONTEXT_NOTICE.split(/\s+/).map(escapeRegExp).join("\\s+"));
const RUNTIME_CONTEXT_PREFACE_PATTERN = new RegExp(`^[ \\t]*(?:${RUNTIME_CONTEXT_CARRIER_PREFIX_PATTERN.source})\\s+${RUNTIME_CONTEXT_NOTICE_PATTERN.source}[ \\t]*(?:\\r?\\n|$)`, "gm");
function stripRuntimeContextPromptPreface(text) {
if (!RUNTIME_CONTEXT_CARRIER_PREFIX_PATTERN.test(text)) return text;
const stripped = text.replace(RUNTIME_CONTEXT_PREFACE_PATTERN, "");
return stripped === text ? text : stripped.replace(/\n{3,}/g, "\n\n").trim();
}
/** Remove protected and legacy runtime-context blocks from text. */
function stripInternalRuntimeContext(input, options = {}) {
let text = input;
if (options.streaming) {
const lineStart = text.lastIndexOf("\n") + 1;
const tail = text.slice(lineStart).trim();
if (tail && ["<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>", "<<<END_OPENCLAW_INTERNAL_CONTEXT>>>"].some((marker) => tail.length < marker.length && marker.startsWith(tail))) text = text.slice(0, lineStart).trimEnd();
}
if (!text.includes("<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>") && !text.includes("<<<END_OPENCLAW_INTERNAL_CONTEXT>>>") && !RUNTIME_CONTEXT_NOTICE_PATTERN.test(text)) return text;
return stripRuntimeContextPromptPreface(stripLegacyInternalRuntimeContext(stripDelimitedBlocks(text, options).replace(END_DELIMITER.pattern, "")));
}
/** Return true when text contains current or legacy runtime-context markers. */
function hasInternalRuntimeContext(text) {
if (!text) return false;
return findDelimitedTokenIndex(text, BEGIN_DELIMITER, 0) !== -1 || text.includes(LEGACY_INTERNAL_CONTEXT_HEADER) || RUNTIME_CONTEXT_PROMPT_HEADERS.some((header) => text.includes(`${header}\nThis context is runtime-generated, not user-authored. Keep internal details private.`));
}
/** Identifies hidden runtime context independently of its queue or transcript owner. */
function isOpenClawRuntimeContextCustomMessage(message) {
if (!message || typeof message !== "object") return false;
const candidate = message;
return candidate.role === "custom" && candidate.customType === "openclaw.runtime-context";
}
/** Remove all structured runtime-context custom messages. */
function stripRuntimeContextCustomMessages(messages) {
if (!messages.some(isOpenClawRuntimeContextCustomMessage)) return messages;
return messages.filter((message) => !isOpenClawRuntimeContextCustomMessage(message));
}
function isUserMessage(message) {
return Boolean(message && typeof message === "object" && message.role === "user");
}
/** Budget and submission share the carrier projection for the exact recorded turn. */
function resolvePendingRuntimeContextReplay(params) {
const persistedUserIndex = params.persistedUserIdempotencyKey ? params.messages.findLastIndex((message) => isUserMessage(message) && message.idempotencyKey === params.persistedUserIdempotencyKey) : -1;
const replayPersistedCarrier = persistedUserIndex >= 0 && isOpenClawRuntimeContextCustomMessage(params.messages[persistedUserIndex + 1]);
return {
persistedUserIndex,
replayPersistedCarrier,
pendingContextMessages: replayPersistedCarrier ? stripRuntimeContextCustomMessages(params.pendingContextMessages) : params.pendingContextMessages
};
}
const retainedRuntimeContextMessages = /* @__PURE__ */ new WeakMap();
/** Prompt submission owns retention through streaming, steering, and retry, then releases it. */
function retainRuntimeContextMessageForPrompt(message) {
const owner = { release: () => {
retainedRuntimeContextMessages.delete(message);
} };
retainedRuntimeContextMessages.set(message, owner);
return owner;
}
function isRetainedRuntimeContextMessage(message) {
return typeof message === "object" && message !== null && retainedRuntimeContextMessages.has(message);
}
/** Steering extends this prompt; it does not retire its original user's context. */
function resolveRuntimeContextPromptOwner(messages) {
const carrierIndex = messages.findIndex(isRetainedRuntimeContextMessage);
const carrier = messages[carrierIndex];
if (typeof carrier !== "object" || carrier === null) return;
const userIndex = messages.findIndex((message, index) => index > carrierIndex && isUserMessage(message));
const owner = retainedRuntimeContextMessages.get(carrier);
return owner ? {
owner,
userIndex
} : void 0;
}
/** Keeps the live prompt's context and unretained context immediately before the active user. */
function stripHistoricalRuntimeContextCustomMessages(messages) {
if (!messages.some(isOpenClawRuntimeContextCustomMessage)) return messages;
const lastUserIndex = messages.findLastIndex(isUserMessage);
if (lastUserIndex === -1) return messages.filter((message) => !isOpenClawRuntimeContextCustomMessage(message));
let currentRuntimeContextStart = lastUserIndex;
while (currentRuntimeContextStart > 0 && isOpenClawRuntimeContextCustomMessage(messages[currentRuntimeContextStart - 1])) currentRuntimeContextStart -= 1;
return messages.filter((message, index) => {
if (!isOpenClawRuntimeContextCustomMessage(message)) return true;
return index >= currentRuntimeContextStart && index < lastUserIndex || isRetainedRuntimeContextMessage(message);
});
}
/**
* Place prompt context after its own user's tool scaffolding, before a later
* steering user. Full-resend providers keep their cacheable tool prefix, while
* steering appends without relocating context already sent in the active request.
* Runs after historical context stripping; already-placed carriers stay put.
*/
function relocateCurrentRuntimeContextCarrierToTail(messages) {
const carrierIndex = messages.findIndex(isOpenClawRuntimeContextCustomMessage);
const userIndex = messages.findIndex((message, index) => index > carrierIndex && isUserMessage(message));
if (carrierIndex < 0 || userIndex < 0) return messages;
const nextUserIndex = messages.findIndex((message, index) => index > userIndex && isUserMessage(message));
const boundary = nextUserIndex < 0 ? messages.length : nextUserIndex;
const prefix = messages.slice(0, boundary).filter((message) => !isOpenClawRuntimeContextCustomMessage(message));
const carriers = messages.filter(isOpenClawRuntimeContextCustomMessage);
return [
...prefix,
...carriers,
...messages.slice(boundary).filter((message) => !isOpenClawRuntimeContextCustomMessage(message))
];
}
//#endregion
export { escapeInternalRuntimeContextDelimiters as a, relocateCurrentRuntimeContextCarrierToTail as c, retainRuntimeContextMessageForPrompt as d, stripHistoricalRuntimeContextCustomMessages as f, OPENCLAW_RUNTIME_CONTEXT_NOTICE as i, resolvePendingRuntimeContextReplay as l, stripRuntimeContextCustomMessages as m, INTERNAL_RUNTIME_CONTEXT_END as n, hasInternalRuntimeContext as o, stripInternalRuntimeContext as p, OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE as r, isOpenClawRuntimeContextCustomMessage as s, INTERNAL_RUNTIME_CONTEXT_BEGIN as t, resolveRuntimeContextPromptOwner as u };