Files
ST-Bionic-Memory-Ecology/prompting/prompt-builder.js
2026-04-15 21:20:08 +08:00

2367 lines
70 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(),
...sanitizePromptBuildDebugSnapshot(snapshot),
};
state.updatedAt = new Date().toISOString();
}
function isVerboseRuntimeDebugEnabled() {
return globalThis.__stBmeVerboseDebug === true;
}
function isLightweightHostModeEnabled() {
return globalThis.__stBmeLightweightHostMode === true;
}
function buildPreviewText(value, maxChars = 240) {
const effectiveMaxChars = isLightweightHostModeEnabled()
? Math.min(maxChars, 160)
: maxChars;
const text = String(value ?? "").replace(/\s+/g, " ").trim();
if (!text) return "";
return text.length > effectiveMaxChars
? `${text.slice(0, effectiveMaxChars)}...`
: text;
}
function summarizeExecutionMessages(messages = []) {
const list = Array.isArray(messages) ? messages : [];
const roles = {};
let totalChars = 0;
const preview = [];
for (const message of list) {
const role = String(message?.role || "system");
const content = String(message?.content || "");
roles[role] = Number(roles[role] || 0) + 1;
totalChars += content.length;
if (preview.length < 3) {
const compact = buildPreviewText(content, 96);
if (compact) {
preview.push(`${role}: ${compact}`);
}
}
}
return {
count: list.length,
roles,
totalChars,
preview,
};
}
function compactExecutionMessages(messages = []) {
const list = Array.isArray(messages) ? messages : [];
return list.slice(0, 6).map((message) => ({
role: String(message?.role || ""),
content: buildPreviewText(message?.content || "", 160),
source: String(message?.source || ""),
blockId: String(message?.blockId || ""),
blockType: String(message?.blockType || ""),
sourceKey: String(message?.sourceKey || ""),
regexSourceType: String(message?.regexSourceType || ""),
}));
}
function summarizeRenderedBlocks(blocks = []) {
const list = Array.isArray(blocks) ? blocks : [];
return {
count: list.length,
preview: list.slice(0, 6).map((block) => ({
id: String(block?.id || ""),
name: String(block?.name || ""),
type: String(block?.type || ""),
role: String(block?.role || ""),
chars: String(block?.content || "").length,
})),
};
}
function summarizeWorldInfoResolution(worldInfoResolution = null) {
const source =
worldInfoResolution &&
typeof worldInfoResolution === "object" &&
!Array.isArray(worldInfoResolution)
? worldInfoResolution
: {};
return {
beforeCount: Array.isArray(source.beforeEntries) ? source.beforeEntries.length : 0,
afterCount: Array.isArray(source.afterEntries) ? source.afterEntries.length : 0,
atDepthCount: Array.isArray(source.atDepthEntries) ? source.atDepthEntries.length : 0,
additionalMessageCount: Array.isArray(source.additionalMessages)
? source.additionalMessages.length
: 0,
activatedEntryNames: Array.isArray(source.activatedEntryNames)
? source.activatedEntryNames.slice(0, 12)
: [],
debug:
source.debug && typeof source.debug === "object" && !Array.isArray(source.debug)
? {
ejsRuntimeStatus: String(source.debug.ejsRuntimeStatus || ""),
cacheHit: Boolean(source.debug.cache?.hit),
loadMs: Number(source.debug.loadMs || 0),
}
: null,
};
}
function sanitizePromptBuildDebugSnapshot(snapshot = {}) {
const cloned = cloneRuntimeDebugValue(snapshot, {});
if (isVerboseRuntimeDebugEnabled()) {
return {
...cloned,
debugMode: "verbose",
};
}
return {
updatedAt: new Date().toISOString(),
debugMode: "summary",
taskType: String(cloned?.taskType || ""),
profileId: String(cloned?.profileId || ""),
profileName: String(cloned?.profileName || ""),
systemPromptPreview: buildPreviewText(cloned?.systemPrompt || ""),
executionMessages: compactExecutionMessages(cloned?.executionMessages),
privateTaskMessages: compactExecutionMessages(cloned?.privateTaskMessages),
renderedBlocks: [],
hostInjections: cloned?.hostInjections
? {
before: Array.isArray(cloned.hostInjections.before)
? cloned.hostInjections.before.length
: 0,
after: Array.isArray(cloned.hostInjections.after)
? cloned.hostInjections.after.length
: 0,
atDepth: Array.isArray(cloned.hostInjections.atDepth)
? cloned.hostInjections.atDepth.length
: 0,
}
: null,
executionMessagesSummary: summarizeExecutionMessages(cloned?.executionMessages),
privateTaskMessagesSummary: summarizeExecutionMessages(cloned?.privateTaskMessages),
renderedBlocksSummary: summarizeRenderedBlocks(cloned?.renderedBlocks),
worldInfoResolutionSummary: summarizeWorldInfoResolution(cloned?.worldInfoResolution),
mvu: cloneRuntimeDebugValue(cloned?.mvu, null),
inputContext: null,
inputContextSummary:
cloned?.inputContext && typeof cloned.inputContext === "object"
? {
keys: Object.keys(cloned.inputContext).slice(0, 16),
}
: null,
regexInput:
cloned?.regexInput && typeof cloned.regexInput === "object"
? {
entryCount: Array.isArray(cloned.regexInput.entries)
? cloned.regexInput.entries.length
: 0,
}
: null,
debug:
cloned?.debug && typeof cloned.debug === "object"
? {
renderedBlockCount: Number(cloned.debug.renderedBlockCount || 0),
executionMessageCount: Number(cloned.debug.executionMessageCount || 0),
hostInjectionCount: Number(cloned.debug.hostInjectionCount || 0),
worldInfoRequested: cloned.debug.worldInfoRequested !== false,
worldInfoCacheHit: Boolean(cloned.debug.worldInfoCacheHit),
ejsRuntimeStatus: String(cloned.debug.ejsRuntimeStatus || ""),
}
: null,
};
}
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,
inputContext:
promptDebug.inputContext && typeof promptDebug.inputContext === "object"
? cloneRuntimeDebugValue(promptDebug.inputContext, {})
: 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 getOptionalFiniteNumber(value) {
if (value === null || value === undefined || value === "") {
return null;
}
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : null;
}
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();
const speaker = String(
value.speaker || value.name || value.displayName || "",
).trim();
return {
content: String(value.content || ""),
role: role === "user" ? "user" : "assistant",
seq: getOptionalFiniteNumber(value.seq),
speaker,
hideSpeakerLabel: value?.hideSpeakerLabel === true,
isContextOnly:
typeof value.isContextOnly === "boolean" ? value.isContextOnly : null,
};
}
if (typeof value.mes === "string") {
const speaker = String(
value.speaker || value.name || value.displayName || "",
).trim();
return {
content: String(value.mes || ""),
role: value.is_user === true ? "user" : "assistant",
seq: getOptionalFiniteNumber(value.seq),
speaker,
hideSpeakerLabel: value?.hideSpeakerLabel === true,
isContextOnly:
typeof value.isContextOnly === "boolean" ? value.isContextOnly : null,
};
}
return null;
}
function isPromptMessageArray(value) {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every((entry) => getPromptMessageLikeDescriptor(entry))
);
}
export const EXTRACTION_CONTEXT_REVIEW_HEADER =
"--- 以下是上下文回顾(已提取过),仅供理解剧情 ---";
export const EXTRACTION_TARGET_CONTENT_HEADER =
"--- 以下是本次需要提取记忆的新对话内容 ---";
export const RECALL_TARGET_CONTENT_HEADER =
"--- 以下是本次需要召回记忆的新对话内容 ---";
function getPromptMessageContextGroup(value) {
const descriptor = getPromptMessageLikeDescriptor(value);
if (!descriptor || typeof descriptor.isContextOnly !== "boolean") {
return null;
}
return descriptor.isContextOnly ? "context" : "target";
}
function getPromptMessageContextHeader(group = "") {
if (group === "context") {
return EXTRACTION_CONTEXT_REVIEW_HEADER;
}
if (group === "target") {
return EXTRACTION_TARGET_CONTENT_HEADER;
}
return "";
}
function formatPromptMessageTranscript(value) {
const entries = Array.isArray(value) ? value : [value];
const hasContextMessages = entries.some(
(entry) => getPromptMessageContextGroup(entry) === "context",
);
const hasTargetMessages = entries.some(
(entry) => getPromptMessageContextGroup(entry) === "target",
);
const lines = [];
let activeGroup = null;
for (let index = 0; index < entries.length; index += 1) {
const entry = entries[index];
const descriptor = getPromptMessageLikeDescriptor(entry);
if (!descriptor) {
continue;
}
const group = getPromptMessageContextGroup(entry);
if (hasContextMessages && hasTargetMessages && group && group !== activeGroup) {
lines.push(getPromptMessageContextHeader(group));
activeGroup = group;
}
const seqLabel =
descriptor.seq != null ? `#${descriptor.seq}` : `#${index + 1}`;
const speakerLabel = !descriptor.hideSpeakerLabel && descriptor.speaker
? `|${descriptor.speaker}`
: "";
lines.push(`${seqLabel} [${descriptor.role}${speakerLabel}]: ${descriptor.content}`);
}
return lines.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);
const messageDescriptor = getPromptMessageLikeDescriptor(value);
if (messageDescriptor) {
const contentKey = typeof value.content === "string"
? "content"
: typeof value.mes === "string"
? "mes"
: "";
const messageRole = messageDescriptor.role === "user"
? "user"
: messageDescriptor.role === "assistant"
? "assistant"
: regexRole;
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [
key,
key === contentKey
? applyLocalRegexToStructuredValue(
entryValue,
regexStage,
messageRole,
seen,
)
: entryValue,
]),
);
}
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;
sanitizedValue = applyLocalRegexToStructuredValue(
sanitizedValue,
regexStage,
regexRole,
);
if (structuredSanitizerInput.renderAsTranscript) {
sanitizedValue = stringifyInterpolatedValue(sanitizedValue);
}
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 sortAtDepthInjectionEntries(entries = []) {
return [...entries].sort((left, right) => {
const depthLeft = Number.isFinite(Number(left?.depth))
? Number(left.depth)
: 0;
const depthRight = Number.isFinite(Number(right?.depth))
? Number(right.depth)
: 0;
const orderLeft = Number.isFinite(Number(left?.order))
? Number(left.order)
: 0;
const orderRight = Number.isFinite(Number(right?.order))
? Number(right.order)
: 0;
const uidLeft = Number.isFinite(Number(left?.uid))
? Number(left.uid)
: Number.NEGATIVE_INFINITY;
const uidRight = Number.isFinite(Number(right?.uid))
? Number(right.uid)
: Number.NEGATIVE_INFINITY;
const indexLeft = Number.isFinite(Number(left?.index))
? Number(left.index)
: 0;
const indexRight = Number.isFinite(Number(right?.index))
? Number(right.index)
: 0;
return (
depthRight - depthLeft ||
orderLeft - orderRight ||
uidRight - uidLeft ||
indexLeft - indexRight
);
});
}
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: sortAtDepthInjectionEntries(plan.atDepth),
};
}
function createInjectedAtDepthChatMessage(message = {}) {
const descriptor = getPromptMessageLikeDescriptor(message);
if (!descriptor) {
return null;
}
return {
...(message && typeof message === "object" ? message : {}),
role: descriptor.role,
content: descriptor.content,
seq: descriptor.seq,
uid: Number.isFinite(Number(message?.uid))
? Number(message.uid)
: null,
index: Number.isFinite(Number(message?.index))
? Number(message.index)
: null,
name: String(message?.name || ""),
sourceName: String(message?.sourceName || ""),
worldbook: String(message?.worldbook || ""),
source: String(message?.source || "worldInfo-atDepth"),
sourceKey: String(message?.sourceKey || "taskAdditionalMessages"),
derivedFromWorldInfo: true,
contentOrigin:
String(message?.contentOrigin || "") ||
PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: message?.sanitizationEligible === true,
regexSourceType: String(message?.regexSourceType || "world_info"),
depth: Number.isFinite(Number(message?.depth))
? Number(message.depth)
: 0,
order: Number.isFinite(Number(message?.order))
? Number(message.order)
: 0,
};
}
function injectAtDepthMessagesIntoChatMessages(
chatMessages = [],
atDepthMessages = [],
) {
const normalizedChatMessages = (Array.isArray(chatMessages) ? chatMessages : [])
.map((message) => {
const descriptor = getPromptMessageLikeDescriptor(message);
if (!descriptor) return null;
return {
...(message && typeof message === "object" ? message : {}),
role: descriptor.role,
content: descriptor.content,
seq: descriptor.seq,
};
})
.filter(Boolean);
if (normalizedChatMessages.length === 0) {
return null;
}
const groupedByDepth = new Map();
for (const message of sortAtDepthInjectionEntries(atDepthMessages)) {
const injectedMessage = createInjectedAtDepthChatMessage(message);
if (!injectedMessage) continue;
const depth = Math.max(0, Number(injectedMessage.depth || 0));
if (!groupedByDepth.has(depth)) {
groupedByDepth.set(depth, []);
}
groupedByDepth.get(depth).push(injectedMessage);
}
if (groupedByDepth.size === 0) {
return normalizedChatMessages;
}
const reversedMessages = [...normalizedChatMessages].reverse();
const sortedDepths = [...groupedByDepth.keys()].sort((left, right) => left - right);
let totalInsertedMessages = 0;
for (const depth of sortedDepths) {
const depthMessages = groupedByDepth.get(depth) || [];
if (depthMessages.length === 0) continue;
const injectIndex = Math.min(
Math.max(0, depth + totalInsertedMessages),
reversedMessages.length,
);
reversedMessages.splice(injectIndex, 0, ...depthMessages);
totalInsertedMessages += depthMessages.length;
}
return reversedMessages.reverse();
}
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 taskInputDebug =
context?.taskInputDebug && typeof context.taskInputDebug === "object"
? cloneRuntimeDebugValue(context.taskInputDebug, {})
: null;
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 = context?.__skipWorldInfo === true
? false
: profileRequiresWorldInfo(profile);
const emptyWorldInfo = buildEmptyWorldInfoContext();
let resolvedWorldInfo = emptyWorldInfo;
let worldInfoRuntimeBlockedContents = [];
let deliveredAtDepthViaChatMessages = false;
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,
};
if (
Array.isArray(sanitizedInputContext.chatMessages) &&
isPromptMessageArray(sanitizedInputContext.chatMessages)
) {
const injectedChatMessages = injectAtDepthMessagesIntoChatMessages(
sanitizedInputContext.chatMessages,
sanitizedWorldInfo.additionalMessages,
);
if (Array.isArray(injectedChatMessages) && injectedChatMessages.length > 0) {
sanitizedInputContext.chatMessages = injectedChatMessages;
if (typeof context.recentMessages === "string") {
sanitizedInputContext.recentMessages =
stringifyInterpolatedValue(injectedChatMessages);
}
if (typeof context.dialogueText === "string") {
sanitizedInputContext.dialogueText =
stringifyInterpolatedValue(injectedChatMessages);
}
deliveredAtDepthViaChatMessages = true;
}
}
}
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,
});
}
}
const atDepthExecutionMessages = (worldInfoResolution.additionalMessages || [])
.map((message) =>
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"),
depth: Number.isFinite(Number(message?.depth))
? Number(message.depth)
: null,
order: Number.isFinite(Number(message?.order))
? Number(message.order)
: 0,
},
),
)
.filter(Boolean);
const finalExecutionMessages = deliveredAtDepthViaChatMessages
? executionMessages
: [...atDepthExecutionMessages, ...executionMessages];
const privateTaskMessages = deliveredAtDepthViaChatMessages
? [...customMessages]
: [...worldInfoResolution.additionalMessages, ...customMessages];
debugLog(
`[ST-BME][prompt-diag] buildTaskPrompt done: ` +
`executionMessages=${finalExecutionMessages.length}, ` +
`userBlocks=${userRoleBlockCount}, systemBlocks=${systemRoleBlockCount}, ` +
`customMessages=${customMessages.length}, ` +
`atDepthMessages=${atDepthExecutionMessages.length}, ` +
`atDepthViaChatMessages=${deliveredAtDepthViaChatMessages}`,
);
const hostInjectionPlan = buildHostInjectionPlan(
renderedBlocks,
worldInfoResolution,
);
const result = {
profile,
hostInjections: worldInfoResolution.injections,
hostInjectionPlan,
privateTaskPrompt: {
systemPrompt,
messages: privateTaskMessages,
},
executionMessages: finalExecutionMessages,
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: finalExecutionMessages.length,
userRoleBlockCount,
assistantRoleBlockCount,
systemRoleBlockCount,
effectiveDelivery: {
profileBlocks: "ordered-private-messages",
worldInfoBeforeAfter: "inline-in-ordered-messages",
worldInfoAtDepth: deliveredAtDepthViaChatMessages
? "inserted-into-chat-messages-by-depth"
: "appended-private-messages-fallback",
},
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 || ""),
},
inputContext: taskInputDebug,
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: finalExecutionMessages,
renderedBlocks,
hostInjections: worldInfoResolution.injections,
hostInjectionPlan,
worldInfoResolution,
mvu: result.debug.mvu,
inputContext: taskInputDebug,
regexInput: result.regexInput,
debug: result.debug,
});
return result;
}
function clonePayloadMessage(message = {}) {
return 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 || ""),
});
}
function splitSectionedTranscriptPayloadMessage(message = {}) {
const normalizedRole = normalizeRole(message?.role);
const sourceKey = String(message?.sourceKey || "").trim();
const content = String(message?.content || "").trim();
const targetSectionHeader = content.includes(RECALL_TARGET_CONTENT_HEADER)
? RECALL_TARGET_CONTENT_HEADER
: content.includes(EXTRACTION_TARGET_CONTENT_HEADER)
? EXTRACTION_TARGET_CONTENT_HEADER
: "";
if (
normalizedRole !== "system" ||
!["recentMessages", "dialogueText"].includes(sourceKey) ||
!content.includes(EXTRACTION_CONTEXT_REVIEW_HEADER) ||
!targetSectionHeader
) {
return [message];
}
const headerMatches = [];
let searchIndex = 0;
while (searchIndex < content.length) {
const contextIndex = content.indexOf(
EXTRACTION_CONTEXT_REVIEW_HEADER,
searchIndex,
);
const targetIndex = targetSectionHeader
? content.indexOf(targetSectionHeader, searchIndex)
: -1;
let nextIndex = -1;
let nextHeader = "";
if (contextIndex >= 0 && (targetIndex < 0 || contextIndex <= targetIndex)) {
nextIndex = contextIndex;
nextHeader = EXTRACTION_CONTEXT_REVIEW_HEADER;
} else if (targetIndex >= 0) {
nextIndex = targetIndex;
nextHeader = targetSectionHeader;
}
if (nextIndex < 0 || !nextHeader) {
break;
}
headerMatches.push({
index: nextIndex,
header: nextHeader,
});
searchIndex = nextIndex + nextHeader.length;
}
if (headerMatches.length < 2 || headerMatches[0].index !== 0) {
return [message];
}
const { role: _role, content: _content, ...sharedMeta } = message;
const splitMessages = [];
for (let index = 0; index < headerMatches.length; index += 1) {
const current = headerMatches[index];
const next = headerMatches[index + 1];
const sectionBody = content
.slice(current.index + current.header.length, next ? next.index : content.length)
.trim();
const transcriptSection =
current.header === EXTRACTION_CONTEXT_REVIEW_HEADER ? "context" : "target";
splitMessages.push(
createExecutionMessage(
"system",
sectionBody ? `${current.header}\n\n${sectionBody}` : current.header,
{
...sharedMeta,
sourceKey,
transcriptSection,
transcriptSectionPart: "section",
},
),
);
}
return splitMessages.filter(Boolean);
}
function expandSectionedTranscriptPayloadMessages(messages = []) {
return (Array.isArray(messages) ? messages : []).flatMap((message) =>
splitSectionedTranscriptPayloadMessage(message),
);
}
function collectPayloadUserMessageTexts(messages = []) {
return (Array.isArray(messages) ? messages : [])
.filter((message) => String(message?.role || "").trim().toLowerCase() === "user")
.map((message) => String(message?.content || "").trim())
.filter(Boolean);
}
function buildSafeFallbackUserPrompt(
settings = {},
taskType,
{
fallbackUserPrompt = "",
blockedContents = [],
rawExecutionMessages = [],
rawPrivateTaskMessages = [],
} = {},
) {
const structuredUserPrompt = [
...collectPayloadUserMessageTexts(rawExecutionMessages),
...collectPayloadUserMessageTexts(rawPrivateTaskMessages),
]
.join("\n\n")
.trim();
const candidates = [
{
source: "structured-user-blocks",
text: structuredUserPrompt,
},
{
source: "fallback-user-prompt",
text: String(fallbackUserPrompt || "").trim(),
},
].filter((candidate) => candidate.text);
for (const candidate of candidates) {
const sanitized = sanitizeInjectionText(settings, taskType, candidate.text, {
mode: "final-injection-safe",
blockedContents,
contentOrigin: PROMPT_CONTENT_ORIGIN.HOST_INJECTED,
sanitizationEligible: true,
role: "user",
applySanitizer: true,
applyHostRegex: false,
path: "payload.fallbackUserPrompt",
stage: "payload-fallback-user-prompt",
});
const text = String(sanitized.text || "").trim();
if (text) {
return {
text,
source: candidate.source,
changed: Boolean(sanitized.changed),
dropped: Boolean(sanitized.dropped),
};
}
}
return {
text: "",
source: candidates[0]?.source || "",
changed: false,
dropped: candidates.length > 0,
};
}
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) => clonePayloadMessage(message))
.filter(Boolean)
: [];
const rawPrivateTaskMessages = Array.isArray(promptBuild?.privateTaskMessages)
? promptBuild.privateTaskMessages
.map((message) => clonePayloadMessage(message))
.filter(Boolean)
: [];
const executionMessages = sanitizePromptMessages(
settings,
taskType,
rawExecutionMessages,
{
blockedContents,
applySanitizer: (message) =>
!(isCustomFilter && messageUsesWorldInfoContent(message)),
},
);
const expandedExecutionMessages = expandSectionedTranscriptPayloadMessages(
executionMessages,
);
const hasUserMessage = expandedExecutionMessages.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=${expandedExecutionMessages.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 =
expandedExecutionMessages.length > 0
? []
: expandSectionedTranscriptPayloadMessages(
sanitizePromptMessages(
settings,
taskType,
rawPrivateTaskMessages,
{
blockedContents,
applySanitizer: (message) =>
!(isCustomFilter && messageUsesWorldInfoContent(message)),
},
),
);
const hasAdditionalUserMessage = additionalMessages.some(
(message) => message.role === "user",
);
const fallbackUserPromptResult =
hasUserMessage || hasAdditionalUserMessage
? {
text: "",
source: hasUserMessage ? "execution-messages" : "additional-messages",
changed: false,
dropped: false,
}
: buildSafeFallbackUserPrompt(settings, taskType, {
fallbackUserPrompt,
blockedContents,
rawExecutionMessages,
rawPrivateTaskMessages,
});
return {
systemPrompt:
expandedExecutionMessages.length > 0
? ""
: String(promptBuild?.systemPrompt || ""),
userPrompt: fallbackUserPromptResult.text,
promptMessages: expandedExecutionMessages,
additionalMessages,
fallbackUserPromptSource: fallbackUserPromptResult.source,
fallbackUserPromptApplied: Boolean(fallbackUserPromptResult.text),
};
}
export function interpolateVariables(template, context = {}) {
return String(template || "").replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => {
return stringifyInterpolatedValue(getByPath(context, key));
});
}