Files
ST-Bionic-Memory-Ecology/prompting/prompt-builder.js
2026-04-09 00:28:29 +08:00

1763 lines
51 KiB
JavaScript

// ST-BME: Prompt Builder
// 统一负责任务预设块排序、变量渲染,以及世界书/EJS 上下文接入。
import { debugLog, debugWarn } from "../runtime/debug-logging.js";
import { getActiveTaskProfile, getLegacyPromptForTask } from "./prompt-profiles.js";
import {
createEmptyInjectionSanitizerDebug,
PROMPT_CONTENT_ORIGIN,
sanitizeInjectionText,
sanitizeInjectionMessages,
sanitizeInjectionStructuredValue,
} from "./injection-sanitizer.js";
import { resolveTaskWorldInfo } from "./task-worldinfo.js";
import { applyTaskRegex } from "./task-regex.js";
const WORLD_INFO_VARIABLE_KEYS = [
"worldInfoBefore",
"worldInfoAfter",
"worldInfoBeforeEntries",
"worldInfoAfterEntries",
"worldInfoAtDepthEntries",
"activatedWorldInfoNames",
"taskAdditionalMessages",
];
const INPUT_CONTEXT_MVU_FIELDS = [
"userMessage",
"recentMessages",
"chatMessages",
"dialogueText",
"candidateText",
"candidateNodes",
"nodeContent",
"eventSummary",
"characterSummary",
"threadSummary",
"contradictionSummary",
"charDescription",
"userPersona",
];
const INPUT_REGEX_STAGE_BY_FIELD = {
userMessage: "input.userMessage",
recentMessages: "input.recentMessages",
chatMessages: "input.recentMessages",
dialogueText: "input.recentMessages",
candidateText: "input.candidateText",
candidateNodes: "input.candidateText",
nodeContent: "input.candidateText",
eventSummary: "input.candidateText",
characterSummary: "input.candidateText",
threadSummary: "input.candidateText",
contradictionSummary: "input.candidateText",
};
const INPUT_REGEX_ROLE_BY_FIELD = {
userMessage: "user",
recentMessages: "mixed",
chatMessages: "mixed",
dialogueText: "mixed",
};
const INPUT_HOST_REGEX_SOURCE_BY_FIELD = {
userMessage: "user_input",
recentMessages: "ai_output",
chatMessages: "ai_output",
dialogueText: "ai_output",
candidateText: "ai_output",
candidateNodes: "ai_output",
nodeContent: "ai_output",
eventSummary: "ai_output",
characterSummary: "ai_output",
threadSummary: "ai_output",
contradictionSummary: "ai_output",
charDescription: "ai_output",
userPersona: "user_input",
};
function cloneRuntimeDebugValue(value, fallback = null) {
if (value == null) {
return fallback;
}
try {
return JSON.parse(JSON.stringify(value));
} catch {
return fallback ?? value;
}
}
function getRuntimeDebugState() {
const stateKey = "__stBmeRuntimeDebugState";
if (
!globalThis[stateKey] ||
typeof globalThis[stateKey] !== "object"
) {
globalThis[stateKey] = {
hostCapabilities: null,
taskPromptBuilds: {},
taskLlmRequests: {},
injections: {},
updatedAt: "",
};
}
return globalThis[stateKey];
}
function recordTaskPromptBuild(taskType, snapshot = {}) {
const normalizedTaskType = String(taskType || "").trim() || "unknown";
const state = getRuntimeDebugState();
state.taskPromptBuilds[normalizedTaskType] = {
updatedAt: new Date().toISOString(),
...cloneRuntimeDebugValue(snapshot, {}),
};
state.updatedAt = new Date().toISOString();
}
function mergeRegexCollectors(...collectors) {
const mergedEntries = [];
for (const collector of collectors) {
if (!Array.isArray(collector?.entries)) {
continue;
}
mergedEntries.push(...collector.entries);
}
return {
entries: mergedEntries,
};
}
export function buildTaskExecutionDebugContext(
promptBuild = null,
options = {},
) {
const promptDebug = promptBuild?.debug || {};
const worldInfoDebug =
promptBuild?.worldInfo?.debug || promptBuild?.worldInfoResolution?.debug || {};
const worldInfoHit =
Number(promptDebug.worldInfoBeforeCount || 0) +
Number(promptDebug.worldInfoAfterCount || 0) +
Number(promptDebug.worldInfoAtDepthCount || 0) >
0;
return {
promptAssembly: {
mode: "ordered-private-messages",
hostInjectionPlanMode:
promptDebug.hostInjectionPlanMode || "diagnostic-plan-only",
privateTaskMessageCount: Number(
promptDebug.executionMessageCount ??
promptBuild?.executionMessages?.length ??
promptDebug.privateTaskMessageCount ??
promptBuild?.privateTaskMessages?.length ??
0,
),
},
promptBuild: {
taskType: String(promptDebug.taskType || ""),
profileId: String(promptDebug.profileId || ""),
profileName: String(promptDebug.profileName || ""),
renderedBlockCount: Number(promptDebug.renderedBlockCount || 0),
privateTaskMessageCount: Number(promptDebug.privateTaskMessageCount || 0),
},
effectiveDelivery:
promptDebug.effectiveDelivery && typeof promptDebug.effectiveDelivery === "object"
? cloneRuntimeDebugValue(promptDebug.effectiveDelivery, {})
: null,
ejsRuntimeStatus: String(
promptDebug.ejsRuntimeStatus || worldInfoDebug.ejsRuntimeStatus || "",
),
worldInfo: {
requested: promptDebug.worldInfoRequested !== false,
hit: worldInfoHit,
cacheHit: Boolean(promptDebug.worldInfoCacheHit),
beforeCount: Number(promptDebug.worldInfoBeforeCount || 0),
afterCount: Number(promptDebug.worldInfoAfterCount || 0),
atDepthCount: Number(promptDebug.worldInfoAtDepthCount || 0),
loadMs: Number(worldInfoDebug.loadMs || 0),
},
mvu:
promptDebug.mvu && typeof promptDebug.mvu === "object"
? cloneRuntimeDebugValue(promptDebug.mvu, {})
: null,
regexInput:
(() => {
const merged = mergeRegexCollectors(
promptBuild?.regexInput,
options.regexInput,
);
return Array.isArray(merged.entries) && merged.entries.length > 0
? cloneRuntimeDebugValue(merged, {})
: null;
})(),
};
}
function getByPath(target, path) {
return String(path || "")
.split(".")
.filter(Boolean)
.reduce((acc, key) => (acc == null ? undefined : acc[key]), target);
}
function normalizeRole(role) {
const value = String(role || "system").toLowerCase();
if (["system", "user", "assistant"].includes(value)) {
return value;
}
return "system";
}
function normalizeInjectionMode(mode) {
const value = String(mode || "append").toLowerCase();
if (["prepend", "append", "relative"].includes(value)) {
return value;
}
return "append";
}
function createExecutionMessage(
role,
content,
extra = {},
) {
const trimmedContent = String(content || "").trim();
if (!trimmedContent) {
return null;
}
return {
role: normalizeRole(role),
content: trimmedContent,
...extra,
};
}
function isCustomWorldInfoFilterEnabled(settings = {}) {
return String(settings?.worldInfoFilterMode || "default").trim() === "custom";
}
function usesWorldInfoSourceKey(sourceKey = "") {
return [
"worldInfoBefore",
"worldInfoAfter",
"worldInfoBeforeEntries",
"worldInfoAfterEntries",
"worldInfoAtDepthEntries",
"activatedWorldInfoNames",
"taskAdditionalMessages",
].includes(String(sourceKey || ""));
}
function blockUsesWorldInfoContent(block = {}) {
if (usesWorldInfoSourceKey(block?.sourceKey)) {
return true;
}
const content = String(block?.content || "");
return /\{\{\s*(worldInfoBefore|worldInfoAfter|worldInfoBeforeEntries|worldInfoAfterEntries|worldInfoAtDepthEntries|activatedWorldInfoNames|taskAdditionalMessages)\s*\}\}/.test(
content,
);
}
function messageUsesWorldInfoContent(message = {}) {
if (message?.derivedFromWorldInfo === true) {
return true;
}
if (usesWorldInfoSourceKey(message?.sourceKey)) {
return true;
}
return String(message?.source || "") === "worldInfo-atDepth";
}
function getPromptMessageLikeDescriptor(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
if (typeof value.content === "string") {
const role = String(value.role || "assistant").trim().toLowerCase();
return {
content: String(value.content || ""),
role: role === "user" ? "user" : "assistant",
seq: Number.isFinite(Number(value.seq)) ? Number(value.seq) : null,
};
}
if (typeof value.mes === "string") {
return {
content: String(value.mes || ""),
role: value.is_user === true ? "user" : "assistant",
seq: Number.isFinite(Number(value.seq)) ? Number(value.seq) : null,
};
}
return null;
}
function isPromptMessageArray(value) {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every((entry) => getPromptMessageLikeDescriptor(entry))
);
}
function formatPromptMessageTranscript(value) {
const entries = Array.isArray(value) ? value : [value];
return entries
.map((entry, index) => {
const descriptor = getPromptMessageLikeDescriptor(entry);
if (!descriptor) {
return "";
}
const seqLabel =
descriptor.seq != null ? `#${descriptor.seq}` : `#${index + 1}`;
return `${seqLabel} [${descriptor.role}]: ${descriptor.content}`;
})
.filter(Boolean)
.join("\n\n");
}
function stringifyInterpolatedValue(value) {
if (value == null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (getPromptMessageLikeDescriptor(value)) {
return formatPromptMessageTranscript(value);
}
if (isPromptMessageArray(value)) {
return formatPromptMessageTranscript(value);
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function buildEmptyWorldInfoContext() {
return {
worldInfoBefore: "",
worldInfoAfter: "",
worldInfoBeforeEntries: [],
worldInfoAfterEntries: [],
worldInfoAtDepthEntries: [],
activatedWorldInfoNames: [],
taskAdditionalMessages: [],
worldInfoDebug: null,
};
}
function createEmptyMvuPromptDebug() {
return createEmptyInjectionSanitizerDebug();
}
function pushMvuPromptDebugEntry(debugState, entry = {}) {
if (!debugState || !entry || (!entry.changed && !entry.dropped)) {
return;
}
debugState.sanitizedFields.push({
name: String(entry.name || ""),
stage: String(entry.stage || ""),
changed: Boolean(entry.changed),
dropped: Boolean(entry.dropped),
reasons: Array.isArray(entry.reasons) ? [...entry.reasons] : [],
blockedHitCount: Number(entry.blockedHitCount || 0),
});
debugState.sanitizedFieldCount = debugState.sanitizedFields.length;
}
function sanitizeTaskPromptText(
settings = {},
taskType,
text,
{
mode = "injection-safe",
blockedContents = [],
regexStage = "",
role = "system",
regexCollector = null,
applyMvu = true,
contentOrigin = PROMPT_CONTENT_ORIGIN.HOST_INJECTED,
sanitizationEligible = true,
regexSourceType = "",
} = {},
) {
const sanitized = sanitizeInjectionText(settings, taskType, text, {
mode:
String(mode || "").trim() === "final-safe"
? "final-injection-safe"
: "injection-safe",
blockedContents,
contentOrigin,
sanitizationEligible,
regexSourceType,
role,
regexCollector,
applySanitizer: applyMvu,
applyHostRegex: Boolean(regexSourceType),
});
const finalText = regexStage
? applyTaskRegex(
settings,
taskType,
regexStage,
sanitized.text,
regexCollector,
role,
)
: sanitized.text;
return {
...sanitized,
text: finalText,
changed: finalText !== String(text || ""),
};
}
function joinStructuredPath(basePath = "", segment = "") {
const normalizedSegment = String(segment || "");
if (!normalizedSegment) {
return basePath;
}
if (!basePath) {
return normalizedSegment.startsWith("[")
? normalizedSegment.slice(1, -1)
: normalizedSegment;
}
return normalizedSegment.startsWith("[")
? `${basePath}${normalizedSegment}`
: `${basePath}.${normalizedSegment}`;
}
function looksLikeMvuStateContainer(value, seen = new WeakSet()) {
if (!value || typeof value !== "object") {
return false;
}
if (seen.has(value)) {
return false;
}
seen.add(value);
if (Array.isArray(value)) {
return value.some((item) => looksLikeMvuStateContainer(item, seen));
}
const keys = Object.keys(value).map((key) =>
String(key || "").trim().toLowerCase(),
);
if (
keys.some((key) =>
["stat_data", "display_data", "delta_data", "$internal"].includes(key),
)
) {
return true;
}
return Object.values(value).some((item) =>
looksLikeMvuStateContainer(item, seen),
);
}
function getMvuObjectKeyStripReason(key, value) {
const normalizedKey = String(key || "").trim().toLowerCase();
if (
["stat_data", "display_data", "delta_data", "$internal"].includes(
normalizedKey,
)
) {
return "mvu_state_key_removed";
}
if (
["variables", "message_variables", "chat_variables"].includes(normalizedKey) &&
looksLikeMvuStateContainer(value)
) {
return "mvu_variables_container_removed";
}
return "";
}
function sanitizeStructuredPromptValue(
settings = {},
taskType,
value,
{
fieldName = "",
path = fieldName,
mode = "aggressive",
blockedContents = [],
regexStage = "",
role = "system",
debugState = null,
regexCollector = null,
applyMvu = true,
stripMvuContainers = true,
seen = new WeakSet(),
} = {},
) {
if (typeof value === "string") {
const sanitized = sanitizeTaskPromptText(settings, taskType, value, {
mode,
blockedContents,
regexStage,
role,
regexCollector,
applyMvu,
});
pushMvuPromptDebugEntry(debugState, {
name: path || fieldName,
stage: regexStage,
...sanitized,
});
return {
value: sanitized.text,
changed: Boolean(sanitized.changed || sanitized.dropped),
omit:
!String(sanitized.text || "").trim() &&
String(value || "").trim().length > 0,
};
}
if (Array.isArray(value)) {
const sanitizedArray = [];
let changed = false;
for (let index = 0; index < value.length; index += 1) {
const childResult = sanitizeStructuredPromptValue(
settings,
taskType,
value[index],
{
fieldName,
path: joinStructuredPath(path, `[${index}]`),
mode,
blockedContents,
regexStage,
role,
debugState,
regexCollector,
applyMvu,
stripMvuContainers,
seen,
},
);
if (childResult.omit) {
changed = true;
continue;
}
sanitizedArray.push(childResult.value);
if (childResult.changed) {
changed = true;
}
}
return {
value: sanitizedArray,
changed: changed || sanitizedArray.length !== value.length,
omit: value.length > 0 && sanitizedArray.length === 0,
};
}
if (value && typeof value === "object") {
if (seen.has(value)) {
return {
value,
changed: false,
omit: false,
};
}
seen.add(value);
const originalLooksMvuContainer = looksLikeMvuStateContainer(value);
const sanitizedObject = {};
let changed = false;
let keptEntries = 0;
for (const [key, entryValue] of Object.entries(value)) {
const stripReason = stripMvuContainers
? getMvuObjectKeyStripReason(key, entryValue)
: "";
if (stripReason) {
changed = true;
pushMvuPromptDebugEntry(debugState, {
name: joinStructuredPath(path, key),
stage: regexStage,
changed: true,
dropped: true,
reasons: [stripReason],
blockedHitCount: 0,
});
continue;
}
const childResult = sanitizeStructuredPromptValue(
settings,
taskType,
entryValue,
{
fieldName,
path: joinStructuredPath(path, key),
mode,
blockedContents,
regexStage,
role,
debugState,
regexCollector,
applyMvu,
stripMvuContainers,
seen,
},
);
if (childResult.omit) {
changed = true;
continue;
}
sanitizedObject[key] = childResult.value;
keptEntries += 1;
if (childResult.changed) {
changed = true;
}
}
return {
value: sanitizedObject,
changed,
omit: originalLooksMvuContainer && keptEntries === 0,
};
}
return {
value,
changed: false,
omit: false,
};
}
function sanitizePromptMessages(
settings = {},
taskType,
messages = [],
{
blockedContents = [],
debugState = null,
regexCollector = null,
applySanitizer = null,
} = {},
) {
const preparedMessages = (Array.isArray(messages) ? messages : []).map(
(message, index) => {
if (!message || typeof message !== "object") {
return message;
}
const shouldSanitize =
typeof applySanitizer === "function"
? applySanitizer(message, index)
: applySanitizer;
if (shouldSanitize === false) {
return {
...message,
sanitizationEligible: false,
};
}
return message;
},
);
return sanitizeInjectionMessages(settings, taskType, preparedMessages, {
blockedContents,
debugState,
regexCollector,
})
.map((message) =>
createExecutionMessage(message.role, message.content, {
source: String(message?.source || ""),
blockId: String(message?.blockId || ""),
blockName: String(message?.blockName || ""),
blockType: String(message?.blockType || ""),
sourceKey: String(message?.sourceKey || ""),
injectionMode: String(message?.injectionMode || ""),
derivedFromWorldInfo: message?.derivedFromWorldInfo === true,
contentOrigin: String(message?.contentOrigin || ""),
sanitizationEligible: message?.sanitizationEligible === true,
regexSourceType: String(message?.regexSourceType || ""),
}),
)
.filter(Boolean);
}
function resolveStructuredMessageSanitizerInput(fieldName = "", context = {}, value) {
const normalizedFieldName = String(fieldName || "").trim();
if (!["recentMessages", "dialogueText"].includes(normalizedFieldName)) {
return {
value,
renderAsTranscript: false,
};
}
if (
typeof value === "string" &&
Array.isArray(context?.chatMessages) &&
isPromptMessageArray(context.chatMessages)
) {
return {
value: context.chatMessages,
renderAsTranscript: true,
};
}
return {
value,
renderAsTranscript: false,
};
}
function sanitizePromptContextInputs(
settings = {},
taskType,
context = {},
debugState = null,
regexCollector = null,
options = {},
) {
const sanitizedContext = {
...context,
};
const {
applyMvu = true,
stripMvuContainers = applyMvu,
} = options || {};
const applyLocalRegexToStructuredValue = (
value,
regexStage,
regexRole,
seen = new WeakSet(),
) => {
if (!regexStage) {
return value;
}
if (typeof value === "string") {
return applyTaskRegex(
settings,
taskType,
regexStage,
value,
regexCollector,
regexRole,
);
}
if (Array.isArray(value)) {
return value.map((item) =>
applyLocalRegexToStructuredValue(item, regexStage, regexRole, seen),
);
}
if (value && typeof value === "object") {
if (seen.has(value)) {
return value;
}
seen.add(value);
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [
key,
applyLocalRegexToStructuredValue(
entryValue,
regexStage,
regexRole,
seen,
),
]),
);
}
return value;
};
for (const fieldName of INPUT_CONTEXT_MVU_FIELDS) {
if (!(fieldName in sanitizedContext)) {
continue;
}
const value = sanitizedContext[fieldName];
const structuredSanitizerInput = resolveStructuredMessageSanitizerInput(
fieldName,
context,
value,
);
const valueForSanitizer = structuredSanitizerInput.value;
const regexStage = INPUT_REGEX_STAGE_BY_FIELD[fieldName] || "";
const regexRole = INPUT_REGEX_ROLE_BY_FIELD[fieldName] || "system";
const regexSourceType = INPUT_HOST_REGEX_SOURCE_BY_FIELD[fieldName] || "";
const sanitized = sanitizeInjectionStructuredValue(
settings,
taskType,
valueForSanitizer,
{
fieldName,
path: fieldName,
mode: "injection-safe",
contentOrigin: PROMPT_CONTENT_ORIGIN.HOST_INJECTED,
sanitizationEligible: true,
regexSourceType,
role: regexRole,
debugState,
regexCollector,
applySanitizer: applyMvu,
applyHostRegex: Boolean(regexSourceType),
stripMvuContainers,
},
);
let sanitizedValue = sanitized.omit
? Array.isArray(valueForSanitizer)
? []
: typeof valueForSanitizer === "string"
? ""
: null
: sanitized.value;
if (structuredSanitizerInput.renderAsTranscript) {
sanitizedValue = stringifyInterpolatedValue(sanitizedValue);
}
sanitizedValue = applyLocalRegexToStructuredValue(
sanitizedValue,
regexStage,
regexRole,
);
sanitizedContext[fieldName] = sanitizedValue;
}
return sanitizedContext;
}
function sanitizeWorldInfoEntries(
settings = {},
taskType,
entries = [],
blockedContents = [],
debugState = null,
regexCollector = null,
{ applyMvu = true } = {},
) {
return (Array.isArray(entries) ? entries : [])
.map((entry, index) => {
const sanitized = sanitizeInjectionText(
settings,
taskType,
String(entry?.content || ""),
{
mode: "injection-safe",
blockedContents,
contentOrigin: PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: true,
regexSourceType: "world_info",
role: entry?.role || "system",
regexCollector,
applySanitizer: applyMvu,
applyHostRegex: true,
path: `worldInfo[${index}]`,
stage: "world-info-rendered",
},
);
debugState.worldInfoBlockedContentHits += sanitized.blockedHitCount;
if (sanitized.changed || sanitized.dropped) {
debugState.finalMessageStripCount += 1;
}
if (!sanitized.text.trim()) {
return null;
}
return {
...entry,
content: sanitized.text,
index:
Number.isFinite(Number(entry?.index))
? Number(entry.index)
: index,
};
})
.filter(Boolean);
}
function sanitizeWorldInfoContext(
settings = {},
taskType,
worldInfo = null,
debugState = null,
regexCollector = null,
) {
const isCustomFilter = isCustomWorldInfoFilterEnabled(settings);
const rawDebug =
worldInfo?.debug && typeof worldInfo.debug === "object"
? worldInfo.debug
: null;
const blockedContentsCount = Number(rawDebug?.mvu?.blockedContentsCount || 0);
const blockedContents = [];
if (blockedContentsCount > 0 && Array.isArray(rawDebug?.mvu?.filteredEntries)) {
// Use only the structural count for debug; blocked content strings stay internal
// on the world info object via the non-enumerable runtime property below.
}
const runtimeBlockedContents = Array.isArray(worldInfo?.__mvuBlockedContents)
? worldInfo.__mvuBlockedContents
: [];
const beforeEntries = sanitizeWorldInfoEntries(
settings,
taskType,
worldInfo?.beforeEntries,
runtimeBlockedContents,
debugState,
regexCollector,
{ applyMvu: !isCustomFilter },
);
const afterEntries = sanitizeWorldInfoEntries(
settings,
taskType,
worldInfo?.afterEntries,
runtimeBlockedContents,
debugState,
regexCollector,
{ applyMvu: !isCustomFilter },
);
const atDepthEntries = sanitizeWorldInfoEntries(
settings,
taskType,
worldInfo?.atDepthEntries,
runtimeBlockedContents,
debugState,
regexCollector,
{ applyMvu: !isCustomFilter },
);
const additionalMessages = (Array.isArray(worldInfo?.additionalMessages)
? worldInfo.additionalMessages
: []
)
.map((message) => {
const sanitized = sanitizeInjectionText(
settings,
taskType,
String(message?.content || ""),
{
mode: "injection-safe",
blockedContents: runtimeBlockedContents,
contentOrigin: PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: true,
regexSourceType: "world_info",
role: message?.role || "system",
regexCollector,
applySanitizer: !isCustomFilter,
applyHostRegex: true,
path: "taskAdditionalMessages",
stage: "world-info-rendered",
},
);
debugState.worldInfoBlockedContentHits += sanitized.blockedHitCount;
if (sanitized.changed || sanitized.dropped) {
debugState.finalMessageStripCount += 1;
}
if (!sanitized.text.trim()) {
return null;
}
return {
...message,
content: sanitized.text,
source: String(message?.source || "worldInfo-atDepth"),
sourceKey: String(message?.sourceKey || "taskAdditionalMessages"),
contentOrigin: PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: true,
regexSourceType: "world_info",
};
})
.filter(Boolean);
const beforeText = beforeEntries.map((entry) => entry.content).join("\n\n");
const afterText = afterEntries.map((entry) => entry.content).join("\n\n");
const activatedEntryNames = [
...beforeEntries.map((entry) => entry.name),
...afterEntries.map((entry) => entry.name),
...atDepthEntries.map((entry) => entry.name),
].filter(Boolean);
const sanitizedWorldInfo = {
beforeEntries,
afterEntries,
atDepthEntries,
beforeText,
afterText,
additionalMessages,
activatedEntryNames: [...new Set(activatedEntryNames)],
debug: rawDebug,
};
Object.defineProperty(sanitizedWorldInfo, "__mvuBlockedContents", {
value: [...runtimeBlockedContents],
configurable: true,
enumerable: false,
writable: false,
});
return sanitizedWorldInfo;
}
function createHostInjectionEntry(
entry = {},
position = "after",
source = "worldInfo",
) {
return {
source,
position,
role: normalizeRole(entry.role),
content: String(entry.content || "").trim(),
name: String(entry.name || ""),
sourceName: String(entry.sourceName || entry.name || ""),
worldbook: String(entry.worldbook || ""),
depth:
position === "atDepth" && Number.isFinite(Number(entry.depth))
? Number(entry.depth)
: null,
order: Number.isFinite(Number(entry.order)) ? Number(entry.order) : 0,
};
}
function buildWorldInfoResolution(worldInfoContext = {}) {
const beforeEntries = Array.isArray(worldInfoContext.worldInfoBeforeEntries)
? worldInfoContext.worldInfoBeforeEntries
: [];
const afterEntries = Array.isArray(worldInfoContext.worldInfoAfterEntries)
? worldInfoContext.worldInfoAfterEntries
: [];
const atDepthEntries = Array.isArray(worldInfoContext.worldInfoAtDepthEntries)
? worldInfoContext.worldInfoAtDepthEntries
: [];
const additionalMessages = Array.isArray(worldInfoContext.taskAdditionalMessages)
? worldInfoContext.taskAdditionalMessages
: [];
return {
beforeText: String(worldInfoContext.worldInfoBefore || ""),
afterText: String(worldInfoContext.worldInfoAfter || ""),
beforeEntries,
afterEntries,
atDepthEntries,
activatedEntryNames: Array.isArray(worldInfoContext.activatedWorldInfoNames)
? worldInfoContext.activatedWorldInfoNames
: [],
additionalMessages,
debug:
worldInfoContext.worldInfoDebug &&
typeof worldInfoContext.worldInfoDebug === "object"
? worldInfoContext.worldInfoDebug
: null,
injections: {
before: beforeEntries
.map((entry) => createHostInjectionEntry(entry, "before"))
.filter((entry) => entry.content),
after: afterEntries
.map((entry) => createHostInjectionEntry(entry, "after"))
.filter((entry) => entry.content),
atDepth: atDepthEntries
.map((entry) => createHostInjectionEntry(entry, "atDepth"))
.filter((entry) => entry.content),
},
};
}
function sortInjectionEntries(entries = []) {
return [...entries].sort((left, right) => {
const orderLeft = Number.isFinite(Number(left?.order))
? Number(left.order)
: 0;
const orderRight = Number.isFinite(Number(right?.order))
? Number(right.order)
: 0;
return orderLeft - orderRight;
});
}
function createHostInjectionPlanEntry(block = {}, position, extra = {}) {
return {
source: "block",
origin: "profile-block",
position,
role: normalizeRole(block.role),
content: String(block.content || "").trim(),
blockId: String(block.id || ""),
blockName: String(block.name || ""),
sourceKey: String(block.sourceKey || ""),
injectionMode: normalizeInjectionMode(block.injectionMode),
order: Number.isFinite(Number(block.order)) ? Number(block.order) : 0,
...extra,
};
}
function buildHostInjectionPlan(renderedBlocks = [], worldInfoResolution = {}) {
const beforeEntryNames = (
Array.isArray(worldInfoResolution.beforeEntries)
? worldInfoResolution.beforeEntries
: []
)
.map((entry) => String(entry?.name || entry?.sourceName || "").trim())
.filter(Boolean);
const afterEntryNames = (
Array.isArray(worldInfoResolution.afterEntries)
? worldInfoResolution.afterEntries
: []
)
.map((entry) => String(entry?.name || entry?.sourceName || "").trim())
.filter(Boolean);
const atDepthEntries = Array.isArray(worldInfoResolution.injections?.atDepth)
? worldInfoResolution.injections.atDepth
: [];
const plan = {
before: [],
after: [],
atDepth: [],
};
for (const block of renderedBlocks) {
if (!block?.content) continue;
if (
block.type === "builtin" &&
String(block.sourceKey || "") === "worldInfoBefore"
) {
plan.before.push(
createHostInjectionPlanEntry(block, "before", {
entryNames: beforeEntryNames,
entryCount: beforeEntryNames.length,
}),
);
continue;
}
if (
block.type === "builtin" &&
String(block.sourceKey || "") === "worldInfoAfter"
) {
plan.after.push(
createHostInjectionPlanEntry(block, "after", {
entryNames: afterEntryNames,
entryCount: afterEntryNames.length,
}),
);
}
}
for (const entry of atDepthEntries) {
if (!entry?.content) continue;
plan.atDepth.push({
...entry,
origin: "worldInfo-entry",
entryName: String(entry.name || entry.sourceName || "").trim(),
});
}
return {
before: sortInjectionEntries(plan.before),
after: sortInjectionEntries(plan.after),
atDepth: sortInjectionEntries(plan.atDepth),
};
}
function getPromptFieldContentOrigin(sourceKey = "") {
const normalizedSourceKey = String(sourceKey || "").trim();
if (!normalizedSourceKey) {
return PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
}
if (WORLD_INFO_VARIABLE_KEYS.includes(normalizedSourceKey)) {
return PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED;
}
if (INPUT_CONTEXT_MVU_FIELDS.includes(normalizedSourceKey)) {
return PROMPT_CONTENT_ORIGIN.HOST_INJECTED;
}
return PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
}
function getPromptFieldRegexSourceType(sourceKey = "") {
const normalizedSourceKey = String(sourceKey || "").trim();
if (!normalizedSourceKey) {
return "";
}
if (WORLD_INFO_VARIABLE_KEYS.includes(normalizedSourceKey)) {
return "world_info";
}
return INPUT_HOST_REGEX_SOURCE_BY_FIELD[normalizedSourceKey] || "";
}
function blockIsPureInjectedContent(block = {}) {
return (
block?.type === "builtin" &&
!String(block?.content || "").trim() &&
String(block?.sourceKey || "").trim().length > 0
);
}
function describeBlockContentOwnership(block = {}) {
const contentOrigin = blockIsPureInjectedContent(block)
? getPromptFieldContentOrigin(block.sourceKey)
: PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
return {
contentOrigin,
sanitizationEligible:
contentOrigin !== PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED,
regexSourceType:
contentOrigin === PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED
? ""
: getPromptFieldRegexSourceType(block.sourceKey),
};
}
function resolveBlockDelivery(block = {}) {
return normalizeRole(block.role) === "system"
? "private.system"
: "private.message";
}
function getBlockDiagnosticInjectionPosition(block = {}) {
if (
block.type === "builtin" &&
String(block.sourceKey || "") === "worldInfoBefore"
) {
return "before";
}
if (
block.type === "builtin" &&
String(block.sourceKey || "") === "worldInfoAfter"
) {
return "after";
}
return "";
}
function profileRequiresWorldInfo(profile) {
if (
profile?.worldInfo === false ||
profile?.metadata?.disableWorldInfo === true
) {
return false;
}
const blocks = Array.isArray(profile?.blocks) ? profile.blocks : [];
for (const block of blocks) {
if (!block || block.enabled === false) continue;
if (
block.type === "builtin" &&
["worldInfoBefore", "worldInfoAfter"].includes(String(block.sourceKey || ""))
) {
return true;
}
const rawContent = String(block.content || "");
if (!rawContent.includes("{{")) continue;
if (
WORLD_INFO_VARIABLE_KEYS.some((key) =>
rawContent.includes(`{{${key}}}`) ||
rawContent.includes(`{{ ${key} }}`),
)
) {
return true;
}
}
// atDepth world info is implicit in the final message chain, so profiles
// without explicit before/after placeholders should still resolve lore.
return blocks.some((block) => block && block.enabled !== false);
}
function extractWorldInfoChatMessages(context = {}) {
if (Array.isArray(context.chatMessages)) {
return context.chatMessages;
}
return [];
}
export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
const isCustomFilter = isCustomWorldInfoFilterEnabled(settings);
const profile = getActiveTaskProfile(settings, taskType);
const legacyPrompt = getLegacyPromptForTask(settings, taskType);
const promptRegexInput = { entries: [] };
const mvuPromptDebug = createEmptyMvuPromptDebug();
const worldInfoInputContext = {
...context,
};
const sanitizedInputContext = sanitizePromptContextInputs(
settings,
taskType,
context,
mvuPromptDebug,
promptRegexInput,
);
const rawBlocks = Array.isArray(profile?.blocks) ? profile.blocks : [];
const blocks = rawBlocks
.map((block, index) => ({ ...block, _orderIndex: index }))
.sort((a, b) => {
const orderA = Number.isFinite(Number(a.order))
? Number(a.order)
: a._orderIndex;
const orderB = Number.isFinite(Number(b.order))
? Number(b.order)
: b._orderIndex;
return orderA - orderB;
});
const worldInfoRequested = profileRequiresWorldInfo(profile);
const emptyWorldInfo = buildEmptyWorldInfoContext();
let resolvedWorldInfo = emptyWorldInfo;
let worldInfoRuntimeBlockedContents = [];
if (worldInfoRequested) {
const worldInfo = await resolveTaskWorldInfo({
settings,
chatMessages: extractWorldInfoChatMessages(worldInfoInputContext),
userMessage: String(worldInfoInputContext.userMessage || ""),
templateContext: worldInfoInputContext,
});
const sanitizedWorldInfo = sanitizeWorldInfoContext(
settings,
taskType,
worldInfo,
mvuPromptDebug,
promptRegexInput,
);
worldInfoRuntimeBlockedContents = Array.isArray(
sanitizedWorldInfo.__mvuBlockedContents,
)
? sanitizedWorldInfo.__mvuBlockedContents
: [];
resolvedWorldInfo = {
worldInfoBefore: sanitizedWorldInfo.beforeText || "",
worldInfoAfter: sanitizedWorldInfo.afterText || "",
worldInfoBeforeEntries: sanitizedWorldInfo.beforeEntries || [],
worldInfoAfterEntries: sanitizedWorldInfo.afterEntries || [],
worldInfoAtDepthEntries: sanitizedWorldInfo.atDepthEntries || [],
activatedWorldInfoNames: sanitizedWorldInfo.activatedEntryNames || [],
taskAdditionalMessages: sanitizedWorldInfo.additionalMessages || [],
worldInfoDebug: sanitizedWorldInfo.debug || null,
};
}
const resolvedContext = {
...sanitizedInputContext,
...emptyWorldInfo,
...resolvedWorldInfo,
};
const worldInfoResolution = buildWorldInfoResolution(resolvedContext);
let systemPrompt = "";
const customMessages = [];
const executionMessages = [];
const renderedBlocks = [];
let userRoleBlockCount = 0;
let assistantRoleBlockCount = 0;
let systemRoleBlockCount = 0;
debugLog(
`[ST-BME][prompt-diag] buildTaskPrompt: taskType=${taskType}, ` +
`total blocks=${blocks.length}, ` +
`block roles=[${blocks.map((b) => `${b.name}(${b.role},${b.enabled !== false ? "on" : "off"})`).join(", ")}]`,
);
for (const block of blocks) {
if (!block || block.enabled === false) continue;
const role = normalizeRole(block.role);
const blockDerivedFromWorldInfo = blockUsesWorldInfoContent(block);
const blockOwnership = describeBlockContentOwnership(block);
let content = "";
if (block.type === "legacyPrompt") {
content = legacyPrompt || block.content || "";
} else if (block.type === "builtin") {
if (block.content) {
content = interpolateVariables(block.content, resolvedContext);
} else if (block.sourceKey) {
content = stringifyInterpolatedValue(
getByPath(resolvedContext, block.sourceKey),
);
}
} else if (block.type === "custom") {
content = interpolateVariables(block.content || "", resolvedContext);
}
if (role === "user") {
debugLog(
`[ST-BME][prompt-diag] user block "${block.name || block.id}": ` +
`type=${block.type}, contentLen=${String(content || "").length}, ` +
`rawContentLen=${String(block.content || "").length}, ` +
`blockedContentsCount=${worldInfoRuntimeBlockedContents.length}`,
);
}
if (!String(content || "").trim()) {
continue;
}
const mode = normalizeInjectionMode(block.injectionMode);
renderedBlocks.push({
id: String(block.id || ""),
name: String(block.name || ""),
type: String(block.type || "custom"),
role,
sourceKey: String(block.sourceKey || ""),
sourceField: String(block.sourceField || ""),
content,
order: Number.isFinite(Number(block.order))
? Number(block.order)
: block._orderIndex,
derivedFromWorldInfo: blockDerivedFromWorldInfo,
injectionMode: mode,
delivery: resolveBlockDelivery(block),
effectiveDelivery: resolveBlockDelivery(block),
diagnosticInjectionPosition: getBlockDiagnosticInjectionPosition(block),
contentOrigin: blockOwnership.contentOrigin,
sanitizationEligible: blockOwnership.sanitizationEligible,
regexSourceType: blockOwnership.regexSourceType,
});
const executionMessage = createExecutionMessage(role, content, {
source: "profile-block",
blockId: String(block.id || ""),
blockName: String(block.name || ""),
blockType: String(block.type || "custom"),
sourceKey: String(block.sourceKey || ""),
injectionMode: mode,
derivedFromWorldInfo: blockDerivedFromWorldInfo,
contentOrigin: blockOwnership.contentOrigin,
sanitizationEligible: blockOwnership.sanitizationEligible,
regexSourceType: blockOwnership.regexSourceType,
});
if (executionMessage) {
executionMessages.push(executionMessage);
}
if (role === "system") {
systemRoleBlockCount += 1;
if (!systemPrompt) {
systemPrompt = content;
} else if (mode === "prepend") {
systemPrompt = `${content}\n\n${systemPrompt}`;
} else {
systemPrompt = `${systemPrompt}\n\n${content}`;
}
continue;
}
if (role === "user") {
userRoleBlockCount += 1;
} else if (role === "assistant") {
assistantRoleBlockCount += 1;
}
if (mode === "prepend") {
customMessages.unshift({
role,
content,
source: "profile-block",
blockId: String(block.id || ""),
blockName: String(block.name || ""),
blockType: String(block.type || "custom"),
sourceKey: String(block.sourceKey || ""),
injectionMode: mode,
derivedFromWorldInfo: blockDerivedFromWorldInfo,
contentOrigin: blockOwnership.contentOrigin,
sanitizationEligible: blockOwnership.sanitizationEligible,
regexSourceType: blockOwnership.regexSourceType,
});
} else {
customMessages.push({
role,
content,
source: "profile-block",
blockId: String(block.id || ""),
blockName: String(block.name || ""),
blockType: String(block.type || "custom"),
sourceKey: String(block.sourceKey || ""),
injectionMode: mode,
derivedFromWorldInfo: blockDerivedFromWorldInfo,
contentOrigin: blockOwnership.contentOrigin,
sanitizationEligible: blockOwnership.sanitizationEligible,
regexSourceType: blockOwnership.regexSourceType,
});
}
}
debugLog(
`[ST-BME][prompt-diag] buildTaskPrompt done: ` +
`executionMessages=${executionMessages.length}, ` +
`userBlocks=${userRoleBlockCount}, systemBlocks=${systemRoleBlockCount}, ` +
`customMessages=${customMessages.length}`,
);
for (const message of worldInfoResolution.additionalMessages || []) {
const executionMessage = createExecutionMessage(
message.role,
message.content,
{
source: "worldInfo-atDepth",
sourceKey: "taskAdditionalMessages",
contentOrigin:
String(message.contentOrigin || "") ||
PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: message.sanitizationEligible === true,
regexSourceType: String(message.regexSourceType || "world_info"),
},
);
if (executionMessage) {
executionMessages.push(executionMessage);
}
}
const privateTaskMessages = [
...customMessages,
...worldInfoResolution.additionalMessages,
];
const hostInjectionPlan = buildHostInjectionPlan(
renderedBlocks,
worldInfoResolution,
);
const result = {
profile,
hostInjections: worldInfoResolution.injections,
hostInjectionPlan,
privateTaskPrompt: {
systemPrompt,
messages: privateTaskMessages,
},
executionMessages,
privateTaskMessages,
renderedBlocks,
regexInput: mergeRegexCollectors(promptRegexInput),
worldInfoResolution,
systemPrompt,
customMessages,
additionalMessages: worldInfoResolution.additionalMessages,
worldInfo: {
beforeText: worldInfoResolution.beforeText,
afterText: worldInfoResolution.afterText,
beforeEntries: worldInfoResolution.beforeEntries,
afterEntries: worldInfoResolution.afterEntries,
atDepthEntries: worldInfoResolution.atDepthEntries,
activatedEntryNames: worldInfoResolution.activatedEntryNames,
debug: worldInfoResolution.debug,
},
debug: {
taskType,
profileId: profile?.id || "",
profileName: profile?.name || "",
usedLegacyPrompt: Boolean(legacyPrompt),
blockCount: blocks.length,
renderedBlockCount: renderedBlocks.length,
worldInfoRequested,
worldInfoBeforeCount: worldInfoResolution.beforeEntries.length,
worldInfoAfterCount: worldInfoResolution.afterEntries.length,
worldInfoAtDepthCount: worldInfoResolution.atDepthEntries.length,
hostInjectionCount:
worldInfoResolution.injections.before.length +
worldInfoResolution.injections.after.length +
worldInfoResolution.injections.atDepth.length,
hostInjectionPlanCount:
hostInjectionPlan.before.length +
hostInjectionPlan.after.length +
hostInjectionPlan.atDepth.length,
hostInjectionPlanMode: "diagnostic-plan-only",
customMessageCount: customMessages.length,
additionalMessageCount: worldInfoResolution.additionalMessages.length,
privateTaskMessageCount: privateTaskMessages.length,
executionMessageCount: executionMessages.length,
userRoleBlockCount,
assistantRoleBlockCount,
systemRoleBlockCount,
effectiveDelivery: {
profileBlocks: "ordered-private-messages",
worldInfoBeforeAfter: "inline-in-ordered-messages",
worldInfoAtDepth: "appended-private-messages",
},
worldInfoCacheHit: Boolean(worldInfoResolution.debug?.cache?.hit),
ejsRuntimeStatus: worldInfoResolution.debug?.ejsRuntimeStatus || "",
mvu: {
sanitizedFieldCount: mvuPromptDebug.sanitizedFieldCount,
sanitizedFields: cloneRuntimeDebugValue(
mvuPromptDebug.sanitizedFields,
[],
),
finalMessageStripCount: mvuPromptDebug.finalMessageStripCount,
worldInfoBlockedContentHits: mvuPromptDebug.worldInfoBlockedContentHits,
sanitizerAppliedFields: cloneRuntimeDebugValue(
mvuPromptDebug.sanitizerAppliedFields,
[],
),
sanitizerHitKinds: cloneRuntimeDebugValue(
mvuPromptDebug.sanitizerHitKinds,
[],
),
hostReuseAppliedFields: cloneRuntimeDebugValue(
mvuPromptDebug.hostReuseAppliedFields,
[],
),
hostReuseSkippedDisplayOnlyRules: Number(
mvuPromptDebug.hostReuseSkippedDisplayOnlyRules || 0,
),
regexExecutionMode: String(
mvuPromptDebug.regexExecutionMode || "host-unavailable",
),
hostFormatterAvailable: Boolean(
mvuPromptDebug.hostFormatterAvailable,
),
hostFormatterSource: String(
mvuPromptDebug.hostFormatterSource || "",
),
fallbackReason: String(mvuPromptDebug.fallbackReason || ""),
},
effectivePath: {
promptAssembly: "ordered-private-messages",
hostInjectionPlan: "diagnostic-plan-only",
worldInfoInputContext: "raw-context-for-trigger-and-ejs",
ejs:
worldInfoResolution.debug?.ejsRuntimeStatus ||
"unknown",
worldInfo:
worldInfoRequested !== false
? worldInfoResolution.activatedEntryNames.length > 0
? "matched"
: "requested-but-missed"
: "disabled",
},
},
};
Object.defineProperty(result, "__mvuRuntime", {
value: {
blockedContents: [...worldInfoRuntimeBlockedContents],
},
configurable: true,
enumerable: false,
writable: false,
});
recordTaskPromptBuild(taskType, {
taskType,
profileId: profile?.id || "",
profileName: profile?.name || "",
systemPrompt,
privateTaskMessages,
executionMessages,
renderedBlocks,
hostInjections: worldInfoResolution.injections,
hostInjectionPlan,
worldInfoResolution,
mvu: result.debug.mvu,
regexInput: result.regexInput,
debug: result.debug,
});
return result;
}
export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "") {
const runtimeMvu = promptBuild?.__mvuRuntime || {};
const taskType = String(promptBuild?.debug?.taskType || "");
const settings = {};
const isCustomFilter =
String(
promptBuild?.worldInfo?.debug?.customFilter?.mode ||
promptBuild?.worldInfoResolution?.debug?.customFilter?.mode ||
"default",
).trim() === "custom";
const blockedContents = Array.isArray(runtimeMvu?.blockedContents)
? runtimeMvu.blockedContents
: [];
const rawExecutionMessages = Array.isArray(promptBuild?.executionMessages)
? promptBuild.executionMessages
.map((message) =>
createExecutionMessage(message.role, message.content, {
source: String(message.source || ""),
blockId: String(message.blockId || ""),
blockName: String(message.blockName || ""),
blockType: String(message.blockType || ""),
sourceKey: String(message.sourceKey || ""),
injectionMode: String(message.injectionMode || ""),
derivedFromWorldInfo: message.derivedFromWorldInfo === true,
contentOrigin: String(message.contentOrigin || ""),
sanitizationEligible: message.sanitizationEligible === true,
regexSourceType: String(message.regexSourceType || ""),
}),
)
.filter(Boolean)
: [];
const executionMessages = sanitizePromptMessages(
settings,
taskType,
rawExecutionMessages,
{
blockedContents,
applySanitizer: (message) =>
!(isCustomFilter && messageUsesWorldInfoContent(message)),
},
);
const hasUserMessage = executionMessages.some(
(message) => message.role === "user",
);
if (!hasUserMessage && rawExecutionMessages.length > 0) {
const userBlocksBefore = (promptBuild?.executionMessages || []).filter(
(m) => m?.role === "user",
);
const userBlocksAfterRaw = rawExecutionMessages.filter(
(m) => m?.role === "user",
);
const userBlocksAfterSanitize = executionMessages.filter(
(m) => m?.role === "user",
);
debugWarn(
`[ST-BME] buildTaskLlmPayload fallback triggered: ` +
`user blocks in promptBuild=${userBlocksBefore.length}, ` +
`after recreate=${userBlocksAfterRaw.length}, ` +
`after sanitize=${userBlocksAfterSanitize.length}, ` +
`blockedContents count=${blockedContents.length}, ` +
`total executionMessages=${executionMessages.length}`,
);
if (userBlocksBefore.length > 0) {
for (const block of userBlocksBefore) {
debugWarn(
`[ST-BME] user block "${block.blockName || block.blockId}": ` +
`content length=${String(block.content || "").length}, ` +
`content preview="${String(block.content || "").slice(0, 80)}..."`,
);
}
}
if (blockedContents.length > 0) {
debugWarn(
`[ST-BME] blockedContents lengths: [${blockedContents.map((c) => String(c || "").length).join(", ")}]`,
);
}
}
const additionalMessages =
executionMessages.length > 0
? []
: sanitizePromptMessages(
settings,
taskType,
Array.isArray(promptBuild?.privateTaskMessages)
? promptBuild.privateTaskMessages
: [],
{
blockedContents,
applySanitizer: (message) =>
!(isCustomFilter && messageUsesWorldInfoContent(message)),
},
);
return {
systemPrompt:
executionMessages.length > 0 ? "" : String(promptBuild?.systemPrompt || ""),
userPrompt: hasUserMessage ? "" : String(fallbackUserPrompt || ""),
promptMessages: executionMessages,
additionalMessages,
};
}
export function interpolateVariables(template, context = {}) {
return String(template || "").replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => {
return stringifyInterpolatedValue(getByPath(context, key));
});
}