Files
ST-Bionic-Memory-Ecology/prompt-builder.js
2026-04-05 16:40:20 +08:00

1527 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ST-BME: Prompt Builder
// 统一负责任务预设块排序、变量渲染,以及世界书/EJS 上下文接入。
import { getActiveTaskProfile, getLegacyPromptForTask } from "./prompt-profiles.js";
import { sanitizeMvuContent, MVU_SANITIZE_MODES } from "./mvu-compat.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",
];
/**
* 字段族 → sanitize mode 映射表。
*
* PASSIVE用户原文字段对话、角色描述、摘要、候选节点等——只剥离 MVU 容器/宏,
* 不整段 drop。这些字段不可能"整段就是一条 MVU 世界书条目"。
* AGGRESSIVE默认保留现有行为用于世界书条目路径sanitizeWorldInfoEntries
*
* 未列入此表的字段走 AGGRESSIVE与改动前行为一致。
*/
const INPUT_CONTEXT_FIELD_MODE = {
userMessage: MVU_SANITIZE_MODES.PASSIVE,
recentMessages: MVU_SANITIZE_MODES.PASSIVE,
chatMessages: MVU_SANITIZE_MODES.PASSIVE,
dialogueText: MVU_SANITIZE_MODES.PASSIVE,
charDescription: MVU_SANITIZE_MODES.PASSIVE,
userPersona: MVU_SANITIZE_MODES.PASSIVE,
candidateText: MVU_SANITIZE_MODES.PASSIVE,
candidateNodes: MVU_SANITIZE_MODES.PASSIVE,
nodeContent: MVU_SANITIZE_MODES.PASSIVE,
eventSummary: MVU_SANITIZE_MODES.PASSIVE,
characterSummary: MVU_SANITIZE_MODES.PASSIVE,
threadSummary: MVU_SANITIZE_MODES.PASSIVE,
contradictionSummary: MVU_SANITIZE_MODES.PASSIVE,
};
/** 这些字段被清空时必须 warn兜底告警。 */
const CRITICAL_INPUT_FIELDS = new Set([
"recentMessages",
"dialogueText",
"chatMessages",
"charDescription",
"userPersona",
"candidateNodes",
]);
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",
};
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 stringifyInterpolatedValue(value) {
if (value == null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") {
return String(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 {
sanitizedFieldCount: 0,
sanitizedFields: [],
finalMessageStripCount: 0,
worldInfoBlockedContentHits: 0,
};
}
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 = "aggressive",
blockedContents = [],
regexStage = "",
role = "system",
regexCollector = null,
applyMvu = true,
} = {},
) {
const originalText = typeof text === "string" ? text : "";
const mvuResult = applyMvu
? sanitizeMvuContent(originalText, {
mode,
blockedContents,
})
: {
text: originalText,
changed: false,
dropped: false,
reasons: [],
blockedHitCount: 0,
artifactRemovedCount: 0,
};
const afterMvu = String(mvuResult.text || "");
const finalText = regexStage
? applyTaskRegex(
settings,
taskType,
regexStage,
afterMvu,
regexCollector,
role,
)
: afterMvu;
return {
text: finalText,
changed: finalText !== originalText,
dropped: Boolean(mvuResult.dropped),
reasons: Array.isArray(mvuResult.reasons) ? mvuResult.reasons : [],
blockedHitCount: Number(mvuResult.blockedHitCount || 0),
artifactRemovedCount: Number(mvuResult.artifactRemovedCount || 0),
};
}
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 mergeSanitizeReasons(...reasonLists) {
const merged = new Set();
for (const list of reasonLists) {
for (const reason of Array.isArray(list) ? list : []) {
if (reason) {
merged.add(String(reason));
}
}
}
return [...merged];
}
function summarizeSanitizePreview(value, maxLength = 200) {
const rendered = stringifyInterpolatedValue(value)
.replace(/\s+/g, " ")
.trim();
if (!rendered) {
return "";
}
return rendered.length > maxLength
? `${rendered.slice(0, maxLength)}...`
: rendered;
}
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,
dropped: Boolean(sanitized.dropped),
reasons: Array.isArray(sanitized.reasons) ? [...sanitized.reasons] : [],
blockedHitCount: Number(sanitized.blockedHitCount || 0),
artifactRemovedCount: Number(sanitized.artifactRemovedCount || 0),
rawPreview: summarizeSanitizePreview(value),
sanitizedPreview: summarizeSanitizePreview(sanitized.text),
};
}
if (Array.isArray(value)) {
const sanitizedArray = [];
let changed = false;
let dropped = false;
let blockedHitCount = 0;
let artifactRemovedCount = 0;
let reasons = [];
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;
dropped = dropped || Boolean(childResult.dropped);
blockedHitCount += Number(childResult.blockedHitCount || 0);
artifactRemovedCount += Number(childResult.artifactRemovedCount || 0);
reasons = mergeSanitizeReasons(reasons, childResult.reasons);
continue;
}
sanitizedArray.push(childResult.value);
if (childResult.changed) {
changed = true;
}
dropped = dropped || Boolean(childResult.dropped);
blockedHitCount += Number(childResult.blockedHitCount || 0);
artifactRemovedCount += Number(childResult.artifactRemovedCount || 0);
reasons = mergeSanitizeReasons(reasons, childResult.reasons);
}
return {
value: sanitizedArray,
changed: changed || sanitizedArray.length !== value.length,
omit: value.length > 0 && sanitizedArray.length === 0,
dropped,
reasons,
blockedHitCount,
artifactRemovedCount,
rawPreview: summarizeSanitizePreview(value),
sanitizedPreview: summarizeSanitizePreview(sanitizedArray),
};
}
if (value && typeof value === "object") {
if (seen.has(value)) {
return {
value,
changed: false,
omit: false,
dropped: false,
reasons: [],
blockedHitCount: 0,
artifactRemovedCount: 0,
rawPreview: summarizeSanitizePreview(value),
sanitizedPreview: summarizeSanitizePreview(value),
};
}
seen.add(value);
const originalLooksMvuContainer = looksLikeMvuStateContainer(value);
const sanitizedObject = {};
let changed = false;
let keptEntries = 0;
let dropped = false;
let blockedHitCount = 0;
let artifactRemovedCount = 0;
let reasons = [];
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,
});
dropped = true;
reasons = mergeSanitizeReasons(reasons, [stripReason]);
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;
dropped = dropped || Boolean(childResult.dropped);
blockedHitCount += Number(childResult.blockedHitCount || 0);
artifactRemovedCount += Number(childResult.artifactRemovedCount || 0);
reasons = mergeSanitizeReasons(reasons, childResult.reasons);
continue;
}
sanitizedObject[key] = childResult.value;
keptEntries += 1;
if (childResult.changed) {
changed = true;
}
dropped = dropped || Boolean(childResult.dropped);
blockedHitCount += Number(childResult.blockedHitCount || 0);
artifactRemovedCount += Number(childResult.artifactRemovedCount || 0);
reasons = mergeSanitizeReasons(reasons, childResult.reasons);
}
return {
value: sanitizedObject,
changed,
omit: originalLooksMvuContainer && keptEntries === 0,
dropped,
reasons,
blockedHitCount,
artifactRemovedCount,
rawPreview: summarizeSanitizePreview(value),
sanitizedPreview: summarizeSanitizePreview(sanitizedObject),
};
}
return {
value,
changed: false,
omit: false,
dropped: false,
reasons: [],
blockedHitCount: 0,
artifactRemovedCount: 0,
rawPreview: summarizeSanitizePreview(value),
sanitizedPreview: summarizeSanitizePreview(value),
};
}
function sanitizePromptMessages(
settings = {},
taskType,
messages = [],
{
blockedContents = [],
regexStage = "",
debugState = null,
regexCollector = null,
} = {},
) {
return (Array.isArray(messages) ? messages : [])
.map((message, index) => {
const sanitized = sanitizeStructuredPromptValue(
settings,
taskType,
message,
{
fieldName: "message",
path: `message[${index}]`,
mode: "final-safe",
blockedContents,
regexStage,
role: message?.role || "system",
debugState,
regexCollector,
},
);
if (debugState && (sanitized.changed || sanitized.omit)) {
debugState.finalMessageStripCount += 1;
}
if (sanitized.omit) {
return null;
}
const executionMessage = createExecutionMessage(
sanitized.value?.role || message?.role,
sanitized.value?.content,
{
source: String(sanitized.value?.source || message?.source || ""),
blockId: String(sanitized.value?.blockId || message?.blockId || ""),
blockName: String(
sanitized.value?.blockName || message?.blockName || "",
),
blockType: String(
sanitized.value?.blockType || message?.blockType || "",
),
sourceKey: String(
sanitized.value?.sourceKey || message?.sourceKey || "",
),
injectionMode: String(
sanitized.value?.injectionMode || message?.injectionMode || "",
),
},
);
return executionMessage;
})
.filter(Boolean);
}
function sanitizePromptContextInputs(
settings = {},
taskType,
context = {},
debugState = null,
regexCollector = null,
options = {},
) {
const sanitizedContext = {
...context,
};
const {
applyMvu = true,
stripMvuContainers = applyMvu,
} = options || {};
for (const fieldName of INPUT_CONTEXT_MVU_FIELDS) {
if (!(fieldName in sanitizedContext)) {
continue;
}
const value = sanitizedContext[fieldName];
const regexStage = INPUT_REGEX_STAGE_BY_FIELD[fieldName] || "";
const regexRole = INPUT_REGEX_ROLE_BY_FIELD[fieldName] || "system";
const fieldMode = INPUT_CONTEXT_FIELD_MODE[fieldName] || MVU_SANITIZE_MODES.AGGRESSIVE;
const sanitized = sanitizeStructuredPromptValue(
settings,
taskType,
value,
{
fieldName,
path: fieldName,
mode: fieldMode,
regexStage,
role: regexRole,
debugState,
regexCollector,
applyMvu,
stripMvuContainers,
},
);
if (sanitized.omit && CRITICAL_INPUT_FIELDS.has(fieldName)) {
const rawLength = typeof value === "string" ? value.length : -1;
console.warn(
"[ST-BME] 关键任务输入字段被 MVU 策略清空",
{
taskType,
fieldName,
mode: fieldMode,
rawLength,
dropped: Boolean(sanitized.dropped),
reasons: Array.isArray(sanitized.reasons) ? sanitized.reasons : [],
artifactRemovedCount: Number(sanitized.artifactRemovedCount || 0),
blockedHitCount: Number(sanitized.blockedHitCount || 0),
rawPreview: String(sanitized.rawPreview || ""),
sanitizedPreview: String(sanitized.sanitizedPreview || ""),
},
);
}
sanitizedContext[fieldName] = sanitized.omit
? Array.isArray(value)
? []
: typeof value === "string"
? ""
: null
: sanitized.value;
}
return sanitizedContext;
}
function sanitizeWorldInfoEntries(
settings = {},
taskType,
entries = [],
blockedContents = [],
debugState = null,
regexCollector = null,
) {
return (Array.isArray(entries) ? entries : [])
.map((entry, index) => {
const sanitized = sanitizeTaskPromptText(
settings,
taskType,
String(entry?.content || ""),
{
mode: "aggressive",
blockedContents,
regexStage: "",
role: entry?.role || "system",
regexCollector,
},
);
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 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,
);
const afterEntries = sanitizeWorldInfoEntries(
settings,
taskType,
worldInfo?.afterEntries,
runtimeBlockedContents,
debugState,
regexCollector,
);
const atDepthEntries = sanitizeWorldInfoEntries(
settings,
taskType,
worldInfo?.atDepthEntries,
runtimeBlockedContents,
debugState,
regexCollector,
);
const additionalMessages = (Array.isArray(worldInfo?.additionalMessages)
? worldInfo.additionalMessages
: []
)
.map((message) => {
const sanitized = sanitizeTaskPromptText(
settings,
taskType,
String(message?.content || ""),
{
mode: "aggressive",
blockedContents: runtimeBlockedContents,
regexStage: "",
role: message?.role || "system",
regexCollector,
},
);
debugState.worldInfoBlockedContentHits += sanitized.blockedHitCount;
if (sanitized.changed || sanitized.dropped) {
debugState.finalMessageStripCount += 1;
}
if (!sanitized.text.trim()) {
return null;
}
return {
...message,
content: sanitized.text,
};
})
.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 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) {
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;
}
}
return false;
}
function extractWorldInfoChatMessages(context = {}) {
if (Array.isArray(context.chatMessages)) {
return context.chatMessages;
}
return [];
}
export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
const profile = getActiveTaskProfile(settings, taskType);
const legacyPrompt = getLegacyPromptForTask(settings, taskType);
const promptRegexInput = { entries: [] };
const worldInfoRegexInput = { entries: [] };
const mvuPromptDebug = createEmptyMvuPromptDebug();
const worldInfoInputContext = sanitizePromptContextInputs(
settings,
taskType,
context,
null,
worldInfoRegexInput,
{
applyMvu: false,
stripMvuContainers: false,
},
);
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;
for (const block of blocks) {
if (!block || block.enabled === false) continue;
const role = normalizeRole(block.role);
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);
}
const sanitizedBlockContent = sanitizeTaskPromptText(
settings,
taskType,
content,
{
mode: "final-safe",
blockedContents: worldInfoRuntimeBlockedContents,
regexStage: "",
role,
regexCollector: promptRegexInput,
},
);
mvuPromptDebug.worldInfoBlockedContentHits +=
sanitizedBlockContent.blockedHitCount;
if (sanitizedBlockContent.changed || sanitizedBlockContent.dropped) {
mvuPromptDebug.finalMessageStripCount += 1;
}
content = sanitizedBlockContent.text;
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,
injectionMode: mode,
delivery: resolveBlockDelivery(block),
effectiveDelivery: resolveBlockDelivery(block),
diagnosticInjectionPosition: getBlockDiagnosticInjectionPosition(block),
});
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,
});
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 });
} else {
customMessages.push({ role, content });
}
}
for (const message of worldInfoResolution.additionalMessages || []) {
const executionMessage = createExecutionMessage(
message.role,
message.content,
{
source: "worldInfo-atDepth",
},
);
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, worldInfoRegexInput),
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,
},
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 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 || ""),
}),
)
.filter(Boolean)
: [];
const executionMessages = sanitizePromptMessages(
{},
taskType,
rawExecutionMessages,
{
blockedContents,
regexStage: "",
},
);
const hasUserMessage = executionMessages.some(
(message) => message.role === "user",
);
const sanitizedFallbackUserPrompt = sanitizeTaskPromptText(
{},
promptBuild?.debug?.taskType || "",
String(fallbackUserPrompt || ""),
{
mode: "final-safe",
blockedContents,
regexStage: "",
},
).text;
const additionalMessages =
executionMessages.length > 0
? []
: sanitizePromptMessages(
{},
taskType,
Array.isArray(promptBuild?.privateTaskMessages)
? promptBuild.privateTaskMessages
: [],
{
blockedContents,
regexStage: "",
},
);
return {
systemPrompt:
executionMessages.length > 0 ? "" : String(promptBuild?.systemPrompt || ""),
userPrompt: hasUserMessage ? "" : sanitizedFallbackUserPrompt,
promptMessages: executionMessages,
additionalMessages,
};
}
export function interpolateVariables(template, context = {}) {
return String(template || "").replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => {
return stringifyInterpolatedValue(getByPath(context, key));
});
}