import assert from "node:assert/strict"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import vm from "node:vm"; async function loadRetrieve(stubs) { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const retrieverPath = path.resolve(__dirname, "../retrieval/retriever.js"); const source = await fs.readFile(retrieverPath, "utf8"); const transformed = `${source .replace(/^import[\s\S]*?from\s+["'][^"']+["'];\r?\n/gm, "") .replace("export async function retrieve", "async function retrieve")} this.retrieve = retrieve; `; const context = vm.createContext({ console: { log() {}, error() {}, warn() {} }, debugLog() {}, ...stubs, }); new vm.Script(transformed).runInContext(context); return context.retrieve; } function createGraph() { const nodes = [ { id: "rule-1", type: "rule", importance: 9, createdTime: 1, archived: false, fields: { title: "规则一" }, seqRange: [1, 1], }, { id: "rule-2", type: "rule", importance: 7, createdTime: 2, archived: false, fields: { title: "规则二" }, seqRange: [2, 2], }, { id: "rule-3", type: "rule", importance: 3, createdTime: 3, archived: false, fields: { title: "规则三" }, seqRange: [3, 3], }, ]; return { nodes, edges: [] }; } function createGraphHelpers(graph) { return { getActiveNodes(target, type = null) { const source = target?.nodes || graph.nodes; return source.filter( (node) => !node.archived && (!type || node.type === type), ); }, getNode(target, id) { return (target?.nodes || graph.nodes).find((node) => node.id === id) || null; }, getNodeEdges(target, nodeId) { return (target?.edges || graph.edges).filter( (edge) => edge.fromId === nodeId || edge.toId === nodeId, ); }, buildTemporalAdjacencyMap() { return new Map(); }, }; } function getPromptNodeLabel(node = {}, { maxLength = 32 } = {}) { const raw = String( node?.fields?.title || node?.fields?.name || node?.fields?.summary || node?.fields?.insight || node?.fields?.belief || node?.id || "—", ) .replace(/\s+/g, " ") .trim(); if (!raw) return "—"; if (!Number.isFinite(maxLength) || maxLength < 2 || raw.length <= maxLength) { return raw; } return `${raw.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`; } function createPromptNodeReferenceMap(entries = [], { prefix = "N", buildMeta = null } = {}) { const keyToNodeId = {}; const keyToMeta = {}; const nodeIdToKey = {}; const references = []; for (const [index, entry] of (Array.isArray(entries) ? entries : []).entries()) { const node = entry?.node || entry || {}; const nodeId = String(entry?.nodeId || node?.id || "").trim(); if (!nodeId || nodeIdToKey[nodeId]) continue; const key = `${String(prefix || "N").trim() || "N"}${references.length + 1}`; keyToNodeId[key] = nodeId; nodeIdToKey[nodeId] = key; keyToMeta[key] = { nodeId, type: String(node?.type || ""), label: getPromptNodeLabel(node), ...((typeof buildMeta === "function" ? buildMeta({ entry, node, nodeId, key, index, label: getPromptNodeLabel(node) }) : {}) || {}), }; references.push({ key, nodeId, node, meta: keyToMeta[key] }); } return { keyToNodeId, keyToMeta, nodeIdToKey, references, }; } function normalizeQueryText(value, maxLength = 400) { const normalized = String(value ?? "") .replace(/\r\n/g, "\n") .replace(/\s+/g, " ") .trim(); if (!normalized) return ""; return normalized.slice(0, Math.max(1, maxLength)); } function splitIntentSegments(text, { maxSegments = 4, minLength = 1 } = {}) { const raw = String(text || "").trim(); if (!raw) return []; const segments = raw .split(/[,,。.;;!!??\n]+|(?:和|顺便|另外|还有|对了|然后|而且|并且|同时)/) .map((item) => item.trim()) .filter((item) => item.length >= minLength); return uniqueStrings(segments).slice(0, Math.max(1, maxSegments)); } function uniqueStrings(values = [], maxLength = 400) { const result = []; const seen = new Set(); for (const value of values) { const text = normalizeQueryText(value, maxLength); const key = text.toLowerCase(); if (!text || seen.has(key)) continue; seen.add(key); result.push(text); } return result; } function mergeVectorResults(groups, limit) { const merged = new Map(); let rawHitCount = 0; for (const group of groups) { for (const item of group) { rawHitCount += 1; const existing = merged.get(item.nodeId); if (!existing || item.score > existing.score) { merged.set(item.nodeId, item); } } } return { rawHitCount, results: [...merged.values()].slice(0, limit), }; } function parseContextLine(line = "") { const raw = String(line ?? "").trim(); if (!raw) return null; const bracketMatch = raw.match(/^\[(user|assistant)\]\s*:\s*([\s\S]*)$/i); if (bracketMatch) { const role = String(bracketMatch[1] || "").toLowerCase(); const text = normalizeQueryText(bracketMatch[2] || ""); return text ? { role, text } : null; } const plainMatch = raw.match(/^(user|assistant|用户|助手|ai)\s*[::]\s*([\s\S]*)$/i); if (!plainMatch) return null; const roleToken = String(plainMatch[1] || "").toLowerCase(); const role = roleToken === "assistant" || roleToken === "助手" || roleToken === "ai" ? "assistant" : "user"; const text = normalizeQueryText(plainMatch[2] || ""); return text ? { role, text } : null; } function buildContextQueryBlend( userMessage, recentMessages = [], { enabled = true, assistantWeight = 0.2, previousUserWeight = 0.1, maxTextLength = 400, } = {}, ) { const currentText = normalizeQueryText(userMessage, maxTextLength); let assistantText = ""; let previousUserText = ""; const parsedMessages = Array.isArray(recentMessages) ? recentMessages.map((line) => parseContextLine(line)).filter(Boolean) : []; for (let index = parsedMessages.length - 1; index >= 0; index -= 1) { const item = parsedMessages[index]; if (!assistantText && item.role === "assistant") { assistantText = normalizeQueryText(item.text, maxTextLength); } if ( !previousUserText && item.role === "user" && normalizeQueryText(item.text, maxTextLength).toLowerCase() !== currentText.toLowerCase() ) { previousUserText = normalizeQueryText(item.text, maxTextLength); } if (assistantText && previousUserText) break; } const currentWeight = Math.max( 0, 1 - Number(assistantWeight || 0) - Number(previousUserWeight || 0), ); const rawParts = [ { kind: "currentUser", label: "当前用户消息", text: currentText, weight: enabled ? currentWeight : 1, }, ]; if (enabled && assistantText) { rawParts.push({ kind: "assistantContext", label: "最近 assistant 回复", text: assistantText, weight: Number(assistantWeight || 0), }); } if (enabled && previousUserText) { rawParts.push({ kind: "previousUser", label: "上一条 user 消息", text: previousUserText, weight: Number(previousUserWeight || 0), }); } const dedupedParts = []; const seen = new Set(); for (const part of rawParts) { const text = normalizeQueryText(part.text, maxTextLength); const key = text.toLowerCase(); if (!text || seen.has(key)) continue; seen.add(key); dedupedParts.push({ ...part, text }); } const totalWeight = dedupedParts.reduce( (sum, part) => sum + Math.max(0, Number(part.weight) || 0), 0, ); const parts = dedupedParts.map((part) => ({ ...part, weight: totalWeight > 0 ? Math.round((Math.max(0, Number(part.weight) || 0) / totalWeight) * 1000) / 1000 : Math.round((1 / Math.max(1, dedupedParts.length)) * 1000) / 1000, })); return { active: enabled && parts.length > 1, parts, currentText: currentText || parts[0]?.text || "", assistantText, previousUserText, combinedText: parts.length <= 1 ? parts[0]?.text || "" : parts.map((part) => `${part.label}:\n${part.text}`).join("\n\n"), }; } function buildVectorQueryPlan( blendPlan, { enableMultiIntent = true, maxSegments = 4 } = {}, ) { const plan = []; let currentSegments = []; for (const part of blendPlan?.parts || []) { let queries = [part.text]; if (part.kind === "currentUser" && enableMultiIntent) { currentSegments = splitIntentSegments(part.text, { maxSegments }); queries = uniqueStrings([ part.text, ...currentSegments.filter((item) => item !== part.text), ]); } else { queries = uniqueStrings([part.text]); } plan.push({ kind: part.kind, label: part.label, weight: part.weight, queries, }); } return { plan, currentSegments, }; } function buildLexicalQuerySources( userMessage, { enableMultiIntent = true, maxSegments = 4 } = {}, ) { const currentText = normalizeQueryText(userMessage, 400); const segments = enableMultiIntent ? splitIntentSegments(currentText, { maxSegments }) : []; return { sources: uniqueStrings([currentText, ...segments]), segments, }; } function computeLexicalScoreForShared(node, querySources = []) { const haystack = String( node?.fields?.name || node?.fields?.title || node?.fields?.summary || "", ).toLowerCase(); if (!haystack) return 0; for (const sourceText of querySources) { const normalizedSource = String(sourceText || "").toLowerCase(); if (normalizedSource && haystack.includes(normalizedSource.split(/\s+/)[0])) { return 1; } } return 0; } function extractEntityAnchors(userMessage, activeNodes = []) { const anchors = []; const seen = new Set(); for (const node of activeNodes) { const candidates = [node?.fields?.name, node?.fields?.title] .filter((value) => typeof value === "string") .map((value) => value.trim()) .filter((value) => value.length >= 2); for (const candidate of candidates) { if (!String(userMessage || "").includes(candidate)) continue; const key = `${node.id}:${candidate}`; if (seen.has(key)) continue; seen.add(key); anchors.push({ nodeId: node.id, entity: candidate }); break; } } return anchors; } async function rankNodesForTaskContext({ graph, userMessage, recentMessages = [], embeddingConfig, options = {}, } = {}) { const activeNodes = Array.isArray(options.activeNodes) ? options.activeNodes.filter((node) => node && !node.archived) : (graph?.nodes || []).filter((node) => node && !node.archived); const topK = Math.max(1, Math.floor(Number(options.topK) || 20)); const diffusionTopK = Math.max(1, Math.floor(Number(options.diffusionTopK) || 100)); const enableVectorPrefilter = options.enableVectorPrefilter ?? true; const enableGraphDiffusion = options.enableGraphDiffusion ?? true; const enableContextQueryBlend = options.enableContextQueryBlend ?? true; const enableMultiIntent = options.enableMultiIntent ?? true; const multiIntentMaxSegments = Math.max( 1, Math.floor(Number(options.multiIntentMaxSegments) || 4), ); const contextQueryBlend = buildContextQueryBlend(userMessage, recentMessages, { enabled: enableContextQueryBlend, assistantWeight: Number(options.contextAssistantWeight ?? 0.2), previousUserWeight: Number(options.contextPreviousUserWeight ?? 0.1), maxTextLength: Number(options.maxTextLength || 400), }); const queryPlan = buildVectorQueryPlan(contextQueryBlend, { enableMultiIntent, maxSegments: multiIntentMaxSegments, }); const lexicalQuery = buildLexicalQuerySources( contextQueryBlend.currentText || userMessage, { enableMultiIntent, maxSegments: multiIntentMaxSegments, }, ); const diagnostics = { queryBlendActive: contextQueryBlend.active, queryBlendParts: (contextQueryBlend.parts || []).map((part) => ({ kind: part.kind, label: part.label, weight: part.weight, text: part.text, length: part.text.length, })), queryBlendWeights: Object.fromEntries( (contextQueryBlend.parts || []).map((part) => [part.kind, part.weight]), ), segmentsUsed: [...(queryPlan.currentSegments || [])], vectorValidation: { valid: true }, vectorHits: 0, vectorMergedHits: 0, seedCount: 0, diffusionHits: 0, temporalSyntheticEdgeCount: 0, teleportAlpha: Number(options.teleportAlpha ?? 0.15) || 0.15, lexicalBoostedNodes: 0, lexicalTopHits: [], skipReasons: [], timings: { vector: 0, diffusion: 0 }, }; const activeNodeIds = new Set(activeNodes.map((node) => node.id)); let vectorResults = []; if (enableVectorPrefilter) { const groups = []; for (const part of queryPlan.plan) { for (const queryText of part.queries) { state.vectorCalls.push({ topK, message: queryText }); const results = [ { nodeId: "rule-1", score: 0.9 }, { nodeId: "rule-2", score: 0.8 }, { nodeId: "rule-3", score: 0.7 }, ] .filter((item) => activeNodeIds.has(item.nodeId)) .map((item) => ({ ...item, score: item.score * Math.max(0, Number(part.weight) || 0), })); groups.push(results); } } const merged = mergeVectorResults(groups, Math.max(topK * 2, 24)); diagnostics.vectorHits = merged.rawHitCount; diagnostics.vectorMergedHits = merged.results.length; vectorResults = merged.results; } const exactEntityAnchors = extractEntityAnchors( contextQueryBlend.currentText || userMessage, activeNodes, ); let diffusionResults = []; if (enableGraphDiffusion) { const seedMap = new Map(); for (const item of vectorResults) { seedMap.set(item.nodeId, Math.max(seedMap.get(item.nodeId) || 0, item.score)); } for (const item of exactEntityAnchors) { seedMap.set(item.nodeId, Math.max(seedMap.get(item.nodeId) || 0, 2.0)); } const uniqueSeeds = [...seedMap.entries()].map(([id, energy]) => ({ id, energy })); diagnostics.seedCount = uniqueSeeds.length; if (uniqueSeeds.length > 0) { state.diffusionCalls.push({ seeds: uniqueSeeds, options: { maxSteps: 2, decayFactor: 0.6, topK: diffusionTopK, teleportAlpha: diagnostics.teleportAlpha, }, }); diffusionResults = [ { nodeId: "rule-2", energy: 1.2 }, { nodeId: "rule-3", energy: 0.9 }, ].filter((item) => activeNodeIds.has(item.nodeId)); } } diagnostics.diffusionHits = diffusionResults.length; const scoreMap = new Map(); for (const item of vectorResults) { scoreMap.set(item.nodeId, { graphScore: scoreMap.get(item.nodeId)?.graphScore || 0, vectorScore: item.score, }); } for (const item of diffusionResults) { scoreMap.set(item.nodeId, { graphScore: item.energy, vectorScore: scoreMap.get(item.nodeId)?.vectorScore || 0, }); } if (scoreMap.size === 0) { for (const node of activeNodes) { scoreMap.set(node.id, { graphScore: 0, vectorScore: 0 }); } } const scoredNodes = [...scoreMap.entries()].map(([nodeId, scores]) => { const node = activeNodes.find((item) => item.id === nodeId) || null; const lexicalScore = computeLexicalScoreForShared(node, lexicalQuery.sources); return { nodeId, node, graphScore: scores.graphScore, vectorScore: scores.vectorScore, lexicalScore, finalScore: Number(scores.graphScore || 0) + Number(scores.vectorScore || 0) + Number(lexicalScore || 0) + Number(node?.importance || 0), weightedScore: Number(scores.graphScore || 0) + Number(scores.vectorScore || 0) + Number(lexicalScore || 0) + Number(node?.importance || 0), }; }); diagnostics.lexicalBoostedNodes = scoredNodes.filter( (item) => (Number(item.lexicalScore) || 0) > 0, ).length; diagnostics.lexicalTopHits = scoredNodes .filter((item) => (Number(item.lexicalScore) || 0) > 0) .slice(0, 5) .map((item) => ({ nodeId: item.nodeId, label: item.node?.fields?.name || item.node?.fields?.title || item.nodeId, lexicalScore: item.lexicalScore, finalScore: item.finalScore, })); return { activeNodes, contextQueryBlend, queryPlan, lexicalQuery, vectorResults, exactEntityAnchors, diffusionResults, scoredNodes, diagnostics, }; } const schema = [{ id: "rule", label: "规则", alwaysInject: false }]; const state = { vectorCalls: [], diffusionCalls: [], llmCalls: [], llmCandidateCount: 0, llmResponse: { selected_keys: ["R1", "R2"] }, llmOptions: [], authorityCandidateCalls: [], authorityCandidateEnabled: false, authorityCandidateNodeIds: [], authorityCandidateDiagnostics: null, }; const graph = createGraph(); const helpers = createGraphHelpers(graph); const retrieve = await loadRetrieve({ ...helpers, createPromptNodeReferenceMap, getPromptNodeLabel, rankNodesForTaskContext, async resolveAuthorityRecallCandidates({ availableNodes = [], activeRegion = "", activeStoryContext = {}, activeRecallOwnerKeys = [], options = {}, } = {}) { state.authorityCandidateCalls.push({ availableNodeIds: availableNodes.map((node) => node.id), activeRegion, activeStorySegmentId: String(activeStoryContext?.activeSegmentId || ""), activeRecallOwnerKeys: [...(activeRecallOwnerKeys || [])], minimumUsedCandidateCount: Number(options.minimumUsedCandidateCount || 0) || 0, }); if (!state.authorityCandidateEnabled) { return { available: false, used: false, candidateNodes: [], diagnostics: { provider: "authority-trivium", candidateCount: 0, filteredCount: 0, searchHits: 0, neighborCount: 0, queryTexts: [], fallbackReason: "authority-vector-unavailable", timings: { total: 0, filter: 0, search: 0, neighbors: 0, }, }, }; } const requestedIds = Array.isArray(state.authorityCandidateNodeIds) ? state.authorityCandidateNodeIds : []; const candidateNodes = availableNodes.filter((node) => requestedIds.includes(node.id)); const minimumUsedCandidateCount = Number(options.minimumUsedCandidateCount || 0) || 0; const used = candidateNodes.length > 0 && candidateNodes.length < availableNodes.length && candidateNodes.length >= minimumUsedCandidateCount; const diagnostics = { provider: "authority-trivium", candidateCount: candidateNodes.length, filteredCount: candidateNodes.length, searchHits: candidateNodes.length, neighborCount: 0, queryTexts: ["authority-candidate-query"], fallbackReason: used ? "" : candidateNodes.length === 0 ? "authority-candidate-empty" : candidateNodes.length >= availableNodes.length ? "authority-candidate-not-reduced" : "authority-candidate-too-small", timings: { total: 1, filter: 0.2, search: 0.4, neighbors: 0, }, ...(state.authorityCandidateDiagnostics || {}), }; return { available: true, used, candidateNodes: used ? candidateNodes : [], diagnostics, }; }, STORY_TEMPORAL_BUCKETS: { CURRENT: "current", ADJACENT_PAST: "adjacentPast", DISTANT_PAST: "distantPast", FLASHBACK: "flashback", FUTURE: "future", UNDATED: "undated", }, MEMORY_SCOPE_BUCKETS: { CHARACTER_POV: "characterPov", USER_POV: "userPov", OBJECTIVE_CURRENT_REGION: "objectiveCurrentRegion", OBJECTIVE_ADJACENT_REGION: "objectiveAdjacentRegion", OBJECTIVE_GLOBAL: "objectiveGlobal", OTHER_POV: "otherPov", }, normalizeMemoryScope(scope = {}) { return { layer: scope.layer === "pov" ? "pov" : "objective", ownerType: scope.ownerType || "", ownerId: scope.ownerId || "", ownerName: scope.ownerName || "", regionPrimary: scope.regionPrimary || "", regionPath: Array.isArray(scope.regionPath) ? scope.regionPath : [], regionSecondary: Array.isArray(scope.regionSecondary) ? scope.regionSecondary : [], }; }, getScopeRegionKey(scope = {}) { return String(scope.regionPrimary || ""); }, classifyNodeScopeBucket(node, { activeRegion = "" } = {}) { if (node?.scope?.layer === "pov") { return node?.scope?.ownerType === "user" ? "userPov" : "characterPov"; } if ( activeRegion && String(node?.scope?.regionPrimary || "").trim() === String(activeRegion).trim() ) { return "objectiveCurrentRegion"; } return "objectiveGlobal"; }, resolveScopeBucketWeight(bucket, overrides = {}) { return Number(overrides?.[bucket] ?? 1) || 1; }, computeKnowledgeGateForNode(_graph, _node, _ownerKey, options = {}) { return { visible: true, anchored: false, rescued: false, suppressed: false, suppressedReason: "", visibilityScore: options.scopeBucket === "objectiveCurrentRegion" ? 0.8 : 0.45, mode: "soft-visible", threshold: 0.4, }; }, resolveKnowledgeOwner(_graph, input = {}) { const ownerType = String(input.ownerType || "").trim(); const ownerName = String(input.ownerName || input.ownerId || "").trim(); return { ownerType, ownerName, nodeId: String(input.nodeId || "").trim(), aliases: ownerName ? [ownerName] : [], ownerKey: ownerType && ownerName ? `${ownerType}:${ownerName}` : "", }; }, resolveKnowledgeOwnerKeyFromScope(_graph, scope = {}) { const ownerType = String(scope.ownerType || "").trim(); const ownerName = String(scope.ownerName || scope.ownerId || "").trim(); return ownerType && ownerName ? `${ownerType}:${ownerName}` : ""; }, listKnowledgeOwners(targetGraph) { return (targetGraph?.nodes || []) .filter((node) => node?.type === "character" && !node?.archived) .map((node) => ({ ownerKey: `character:${String(node?.fields?.name || "").trim()}`, ownerType: "character", ownerName: String(node?.fields?.name || "").trim(), nodeId: String(node?.id || "").trim(), aliases: [String(node?.fields?.name || "").trim()].filter(Boolean), updatedAt: 0, })) .filter((entry) => entry.ownerKey && entry.ownerName); }, resolveActiveRegionContext(graph, preferredRegion = "") { return { activeRegion: String(preferredRegion || graph?.historyState?.activeRegion || "").trim(), source: preferredRegion ? "runtime" : "history", }; }, resolveAdjacentRegions() { return { canonicalRegion: "", adjacentRegions: [], }; }, resolveActiveStoryContext(targetGraph, preferred = {}) { const preferredLabel = String(preferred?.label || "").trim(); const preferredSegmentId = String(preferred?.segmentId || "").trim(); const segments = Array.isArray(targetGraph?.timelineState?.segments) ? targetGraph.timelineState.segments : []; const segment = segments.find((item) => item.id === preferredSegmentId) || segments.find((item) => item.label === preferredLabel) || segments.find( (item) => item.id === String(targetGraph?.historyState?.activeStorySegmentId || "").trim(), ) || null; return { activeSegmentId: String( segment?.id || targetGraph?.historyState?.activeStorySegmentId || "", ).trim(), activeStoryTimeLabel: String( segment?.label || targetGraph?.historyState?.activeStoryTimeLabel || "", ).trim(), source: segment ? "history" : "", segment, resolved: Boolean(segment), }; }, resolveStoryCueMode(userMessage = "", recentMessages = []) { const text = [userMessage, ...(Array.isArray(recentMessages) ? recentMessages : [])] .map((value) => String(value || "")) .join("\n"); if (/回忆|以前|过去/.test(text)) return "flashback"; if (/以后|未来|计划|打算/.test(text)) return "future"; return ""; }, describeNodeStoryTime(node = {}) { return String(node?.storyTime?.label || node?.storyTimeSpan?.startLabel || "").trim(); }, classifyStoryTemporalBucket(_graph, node, { activeSegmentId = "", cueMode = "" } = {}) { const label = String(node?.storyTime?.label || node?.storyTimeSpan?.startLabel || "").trim(); if (!label) { return { bucket: "undated", weight: 0.88, suppressed: false, rescued: false, reason: "undated", }; } if (label === activeSegmentId || label === "当前") { return { bucket: "current", weight: 1.15, suppressed: false, rescued: false, reason: "current", }; } if (label === "未来计划") { return { bucket: "future", weight: cueMode === "future" ? 0.74 : 0.22, suppressed: cueMode !== "future", rescued: false, reason: cueMode === "future" ? "future-cue" : "future-suppressed", }; } if (label === "往事") { return { bucket: cueMode === "flashback" ? "flashback" : "distantPast", weight: cueMode === "flashback" ? 1.02 : 0.64, suppressed: false, rescued: cueMode === "flashback", reason: cueMode === "flashback" ? "flashback-rescued" : "distant-past", }; } return { bucket: "adjacentPast", weight: 1.0, suppressed: false, rescued: false, reason: "adjacent-past", }; }, pushRecentRecallOwner(historyState, ownerKey = "") { historyState.activeRecallOwnerKey = ownerKey; historyState.recentRecallOwnerKeys = ownerKey ? [ownerKey] : []; }, describeMemoryScope(scope = {}) { return `${scope.layer || "objective"}:${scope.ownerType || ""}:${scope.regionPrimary || ""}`; }, describeScopeBucket(bucket = "") { return String(bucket || ""); }, EXTRACTION_CONTEXT_REVIEW_HEADER: "--- 以下是上下文回顾(已提取过),仅供理解剧情 ---", RECALL_TARGET_CONTENT_HEADER: "--- 以下是本次需要召回记忆的新对话内容 ---", buildTaskPrompt() { return { systemPrompt: "" }; }, applyTaskRegex(_settings, _taskType, _stage, text) { return text; }, splitIntentSegments(text) { if (String(text).includes("和")) { return String(text).split("和").map((item) => item.trim()); } return []; }, mergeVectorResults(groups, limit) { const merged = new Map(); let rawHitCount = 0; for (const group of groups) { for (const item of group) { rawHitCount += 1; const existing = merged.get(item.nodeId); if (!existing || item.score > existing.score) { merged.set(item.nodeId, item); } } } return { rawHitCount, results: [...merged.values()].slice(0, limit), }; }, collectSupplementalAnchorNodeIds() { return []; }, isEligibleAnchorNode(node) { return Boolean(node?.fields?.title || node?.fields?.name); }, createCooccurrenceIndex() { return { map: new Map(), source: "batchJournal", batchCount: 0, pairCount: 0 }; }, applyCooccurrenceBoost(baseScores) { return { scores: new Map(baseScores), boostedNodes: [] }; }, applyDiversitySampling(candidates, { k }) { return { applied: true, reason: "", selected: candidates.slice(0, k).reverse(), beforeCount: candidates.length, afterCount: Math.min(k, candidates.length), }; }, async runResidualRecall() { return { triggered: false, hits: [], skipReason: "residual-disabled-test" }; }, hybridScore: ({ graphScore = 0, vectorScore = 0, lexicalScore = 0, importance = 0, }) => graphScore + vectorScore + lexicalScore + importance, reinforceAccessBatch() {}, validateVectorConfig() { return { valid: true }; }, async findSimilarNodesByText(_graph, message, _embeddingConfig, topK) { state.vectorCalls.push({ topK, message }); return [ { nodeId: "rule-1", score: 0.9 }, { nodeId: "rule-2", score: 0.8 }, { nodeId: "rule-3", score: 0.7 }, ]; }, diffuseAndRank(_adjacencyMap, seeds, options) { state.diffusionCalls.push({ seeds, options }); return [ { nodeId: "rule-2", energy: 1.2 }, { nodeId: "rule-3", energy: 0.9 }, ]; }, async callLLMForJSON(params = {}) { const { userPrompt = "" } = params; state.llmOptions.push({ ...params }); state.llmCalls.push(userPrompt); state.llmCandidateCount = userPrompt .split("\n") .filter((line) => line.trim().startsWith("[")).length; if (params.returnFailureDetails) { if (state.llmResponse?.ok === false) { return state.llmResponse; } return { ok: true, data: state.llmResponse, errorType: "", failureReason: "", attempts: 1, }; } return state.llmResponse; }, getSTContextForPrompt() { return {}; }, }); state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0; const noStageResult = await retrieve({ graph, userMessage: "只看当前规则", recentMessages: [], embeddingConfig: {}, schema, options: { topK: 2, maxRecallNodes: 2, enableVectorPrefilter: false, enableGraphDiffusion: false, enableLLMRecall: false, }, }); assert.equal(state.vectorCalls.length, 0); assert.equal(state.diffusionCalls.length, 0); assert.equal(state.llmCalls.length, 0); assert.deepEqual(Array.from(noStageResult.selectedNodeIds), ["rule-2", "rule-1"]); state.authorityCandidateCalls.length = 0; state.authorityCandidateEnabled = true; state.authorityCandidateNodeIds = ["rule-2"]; state.authorityCandidateDiagnostics = null; state.vectorCalls.length = 0; state.diffusionCalls.length = 0; const authorityCandidateResult = await retrieve({ graph, userMessage: "只看规则二", recentMessages: ["assistant: 请聚焦最新规则。"], embeddingConfig: { mode: "authority", source: "authority-trivium", failOpen: true, }, schema, options: { topK: 2, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: false, enableLLMRecall: false, authorityCandidateMinCount: 1, }, settings: { authorityGraphQueryEnabled: true, }, }); assert.equal(state.authorityCandidateCalls.length, 1); assert.deepEqual(state.authorityCandidateCalls[0].availableNodeIds, ["rule-1", "rule-2", "rule-3"]); assert.equal(authorityCandidateResult.meta.retrieval.authorityCandidateUsed, true); assert.equal(authorityCandidateResult.meta.retrieval.authorityCandidateCount, 1); assert.equal(authorityCandidateResult.meta.retrieval.rankingNodeCount, 1); assert.deepEqual(Array.from(authorityCandidateResult.selectedNodeIds), ["rule-2"]); state.authorityCandidateEnabled = false; state.authorityCandidateNodeIds = []; state.authorityCandidateDiagnostics = null; state.vectorCalls.length = 0; await retrieve({ graph, userMessage: "他后来怎么做?", recentMessages: [ "[assistant]: 他提到了规则二的限制", "[user]: 我们先看规则一", "[user]: 他后来怎么做?", ], embeddingConfig: {}, schema, options: { topK: 4, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: false, enableLLMRecall: false, enableMultiIntent: false, enableContextQueryBlend: true, }, }); assert.deepEqual( state.vectorCalls.map((item) => item.message), ["他后来怎么做?", "他提到了规则二的限制", "我们先看规则一"], ); state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0; state.llmOptions.length = 0; state.llmCandidateCount = 0; state.llmResponse = { selected_keys: ["R1", "R2"] }; const llmPoolResult = await retrieve({ graph, userMessage: "请根据规则给出结论", recentMessages: ["用户:现在该怎么做?"], embeddingConfig: {}, schema, options: { topK: 4, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: false, enableLLMRecall: true, llmCandidatePool: 2, }, }); assert.deepEqual(state.vectorCalls, [ { topK: 4, message: "请根据规则给出结论" }, { topK: 4, message: "现在该怎么做?" }, ]); assert.equal(state.diffusionCalls.length, 0); assert.equal(state.llmCandidateCount, 2); assert.deepEqual(Array.from(llmPoolResult.selectedNodeIds), ["rule-2", "rule-1"]); assert.equal(llmPoolResult.meta.retrieval.llm.status, "llm"); assert.equal( llmPoolResult.meta.retrieval.llm.selectionProtocol, "candidate-keys-v1", ); assert.deepEqual( Array.from(llmPoolResult.meta.retrieval.llm.rawSelectedKeys), ["R1", "R2"], ); assert.deepEqual( Array.from(llmPoolResult.meta.retrieval.llm.resolvedSelectedNodeIds), ["rule-2", "rule-1"], ); assert.equal( llmPoolResult.meta.retrieval.llm.candidateKeyMapPreview?.R1?.nodeId, "rule-2", ); assert.equal(llmPoolResult.meta.retrieval.llm.legacySelectionUsed, false); assert.equal(llmPoolResult.meta.retrieval.llm.candidatePool, 2); assert.equal(llmPoolResult.meta.retrieval.vectorMergedHits, 3); assert.equal(llmPoolResult.meta.retrieval.diversityApplied, true); assert.equal(llmPoolResult.meta.retrieval.candidatePoolBeforeDpp, 3); assert.equal(llmPoolResult.meta.retrieval.candidatePoolAfterDpp, 2); assert.equal(state.llmOptions[0].returnFailureDetails, true); assert.equal(state.llmOptions[0].maxRetries, 2); assert.equal(state.llmOptions[0].maxCompletionTokens, 512); assert.match(String(state.llmCalls[0] || ""), /\[R1\]/); assert.doesNotMatch(String(state.llmCalls[0] || ""), /\[rule-1\]|\[rule-2\]/); state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0; state.llmOptions.length = 0; state.llmResponse = { selected_keys: ["R2"], selected_ids: ["rule-2"], }; const selectedKeysPriorityResult = await retrieve({ graph, userMessage: "优先吃新协议", recentMessages: ["用户:测试 selected_keys 优先级"], embeddingConfig: {}, schema, options: { topK: 4, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: false, enableLLMRecall: true, llmCandidatePool: 2, }, }); assert.deepEqual(Array.from(selectedKeysPriorityResult.selectedNodeIds), ["rule-1"]); assert.equal( selectedKeysPriorityResult.meta.retrieval.llm.selectionProtocol, "candidate-keys-v1", ); assert.equal( selectedKeysPriorityResult.meta.retrieval.llm.legacySelectionUsed, false, ); state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0; state.llmOptions.length = 0; state.llmResponse = { selected_ids: ["rule-1"] }; const legacySelectionResult = await retrieve({ graph, userMessage: "兼容旧 selected_ids", recentMessages: ["用户:测试 legacy 路径"], embeddingConfig: {}, schema, options: { topK: 4, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: false, enableLLMRecall: true, llmCandidatePool: 2, }, }); assert.deepEqual(Array.from(legacySelectionResult.selectedNodeIds), ["rule-1"]); assert.equal( legacySelectionResult.meta.retrieval.llm.selectionProtocol, "legacy-selected-ids", ); assert.equal( legacySelectionResult.meta.retrieval.llm.legacySelectionUsed, true, ); state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0; state.llmOptions.length = 0; state.llmResponse = { selected_keys: [] }; const emptySelectionFallbackResult = await retrieve({ graph, userMessage: "这次故意空选", recentMessages: ["用户:测试空选回退"], embeddingConfig: {}, schema, options: { topK: 4, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: false, enableLLMRecall: true, llmCandidatePool: 2, }, }); assert.equal(emptySelectionFallbackResult.meta.retrieval.llm.status, "fallback"); assert.equal( emptySelectionFallbackResult.meta.retrieval.llm.fallbackType, "empty-selection", ); assert.equal( emptySelectionFallbackResult.meta.retrieval.llm.emptySelectionAccepted, false, ); assert.deepEqual( Array.from(emptySelectionFallbackResult.selectedNodeIds), ["rule-2", "rule-1"], ); state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0; state.llmOptions.length = 0; state.llmResponse = { selected_keys: ["R99"] }; const invalidKeyFallbackResult = await retrieve({ graph, userMessage: "这次给无效 key", recentMessages: ["用户:测试无效候选回退"], embeddingConfig: {}, schema, options: { topK: 4, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: false, enableLLMRecall: true, llmCandidatePool: 2, }, }); assert.equal(invalidKeyFallbackResult.meta.retrieval.llm.status, "fallback"); assert.equal( invalidKeyFallbackResult.meta.retrieval.llm.fallbackType, "invalid-candidate", ); assert.deepEqual( Array.from(invalidKeyFallbackResult.selectedNodeIds), ["rule-2", "rule-1"], ); state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0; state.llmOptions.length = 0; await retrieve({ graph, userMessage: "规则一和规则二有什么关联", recentMessages: [], embeddingConfig: {}, schema, options: { topK: 3, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: true, diffusionTopK: 7, enableLLMRecall: false, enableMultiIntent: true, multiIntentMaxSegments: 4, enableTemporalLinks: true, temporalLinkStrength: 0.2, teleportAlpha: 0.15, }, }); assert.equal(state.vectorCalls.length, 3); assert.deepEqual( state.vectorCalls.map((item) => item.topK), [3, 3, 3], ); assert.equal(state.diffusionCalls.length, 1); assert.equal(state.diffusionCalls[0].options.topK, 7); assert.equal(state.diffusionCalls[0].options.teleportAlpha, 0.15); assert.equal(noStageResult.meta.retrieval.llm.status, "disabled"); state.vectorCalls.length = 0; state.diffusionCalls.length = 0; state.llmCalls.length = 0; state.llmOptions.length = 0; state.llmResponse = { ok: false, errorType: "invalid-json", failureReason: "输出不是有效 JSON,请严格返回紧凑 JSON 对象", }; const fallbackResult = await retrieve({ graph, userMessage: "LLM 这次会坏掉", recentMessages: ["用户:请回忆相关规则"], embeddingConfig: {}, schema, options: { topK: 4, maxRecallNodes: 2, enableVectorPrefilter: true, enableGraphDiffusion: false, enableLLMRecall: true, llmCandidatePool: 2, }, }); assert.equal(fallbackResult.meta.retrieval.llm.status, "fallback"); assert.match(fallbackResult.meta.retrieval.llm.reason, /有效 JSON|回退到评分排序/); assert.equal(fallbackResult.meta.retrieval.llm.fallbackType, "invalid-json"); const sceneGraph = { nodes: [ { id: "event-1", type: "event", importance: 10, createdTime: 1, archived: false, fields: { title: "事件一" }, seqRange: [1, 1], }, { id: "character-1", type: "character", importance: 6, createdTime: 2, archived: false, fields: { name: "Alice" }, seqRange: [1, 1], }, { id: "location-1", type: "location", importance: 5, createdTime: 3, archived: false, fields: { title: "大厅" }, seqRange: [1, 1], }, ], edges: [ { fromId: "event-1", toId: "character-1", relation: "mentions" }, { fromId: "event-1", toId: "location-1", relation: "occurs_at" }, ], }; const sceneSchema = [ { id: "event", label: "事件", alwaysInject: false }, { id: "character", label: "角色", alwaysInject: false }, { id: "location", label: "地点", alwaysInject: false }, ]; const cappedResult = await retrieve({ graph: sceneGraph, userMessage: "只看这一个场景", recentMessages: [], embeddingConfig: {}, schema: sceneSchema, options: { topK: 3, maxRecallNodes: 1, enableVectorPrefilter: false, enableGraphDiffusion: false, enableLLMRecall: false, enableProbRecall: false, }, }); assert.equal(cappedResult.selectedNodeIds.length, 1); const lexicalGraph = { nodes: [ { id: "char-1", type: "character", importance: 1, createdTime: 1, archived: false, fields: { name: "Alice", summary: "常驻角色" }, seqRange: [1, 1], }, { id: "char-2", type: "character", importance: 1, createdTime: 1, archived: false, fields: { name: "Bob", summary: "常驻角色" }, seqRange: [1, 1], }, ], edges: [], }; const lexicalSchema = [{ id: "character", label: "角色", alwaysInject: false }]; const lexicalResult = await retrieve({ graph: lexicalGraph, userMessage: "Alice 现在怎么样了", recentMessages: [], embeddingConfig: {}, schema: lexicalSchema, options: { topK: 2, maxRecallNodes: 1, enableVectorPrefilter: false, enableGraphDiffusion: false, enableLLMRecall: false, enableDiversitySampling: false, enableLexicalBoost: true, }, }); assert.deepEqual(Array.from(lexicalResult.selectedNodeIds), ["char-1"]); assert.equal(lexicalResult.meta.retrieval.queryBlendActive, false); assert.equal(lexicalResult.meta.retrieval.lexicalBoostedNodes, 1); assert.equal(lexicalResult.meta.retrieval.lexicalTopHits[0]?.nodeId, "char-1"); const scopedGraph = { nodes: [ { id: "obj-global", type: "event", importance: 8, createdTime: 1, archived: false, fields: { title: "旧王都事件" }, seqRange: [1, 1], scope: { layer: "objective", regionPrimary: "旧城区" }, }, { id: "char-pov", type: "pov_memory", importance: 4, createdTime: 2, archived: false, fields: { summary: "艾琳觉得钟楼入口非常可疑" }, seqRange: [2, 2], scope: { layer: "pov", ownerType: "character", ownerId: "艾琳", ownerName: "艾琳", regionPrimary: "钟楼", }, }, ], edges: [], historyState: { activeRegion: "钟楼", activeCharacterPovOwner: "艾琳", activeUserPovOwner: "玩家", }, }; const scopedSchema = [ { id: "event", label: "事件", alwaysInject: true }, { id: "pov_memory", label: "主观记忆", alwaysInject: false }, ]; const scopedResult = await retrieve({ graph: scopedGraph, userMessage: "钟楼里到底有什么", recentMessages: [], embeddingConfig: {}, schema: scopedSchema, options: { topK: 2, maxRecallNodes: 1, enableVectorPrefilter: false, enableGraphDiffusion: false, enableLLMRecall: false, enableDiversitySampling: false, enableScopedMemory: true, activeRegion: "钟楼", activeCharacterPovOwner: "艾琳", }, }); assert.deepEqual(Array.from(scopedResult.selectedNodeIds), ["char-pov"]); assert.equal(scopedResult.meta.retrieval.activeRegion, "钟楼"); assert.ok(Array.isArray(scopedResult.scopeBuckets.characterPov)); assert.equal(scopedResult.scopeBuckets.characterPov[0]?.id, "char-pov"); const multiOwnerGraph = { nodes: [ { id: "char-node-a", type: "character", importance: 6, createdTime: 1, archived: false, fields: { name: "艾琳" }, seqRange: [1, 1], }, { id: "char-node-b", type: "character", importance: 6, createdTime: 1, archived: false, fields: { name: "露西亚" }, seqRange: [1, 1], }, { id: "pov-a", type: "pov_memory", importance: 8, createdTime: 2, archived: false, fields: { summary: "艾琳觉得钟楼里还有第二条暗道" }, seqRange: [2, 2], scope: { layer: "pov", ownerType: "character", ownerId: "艾琳", ownerName: "艾琳", }, }, { id: "pov-b", type: "pov_memory", importance: 7, createdTime: 3, archived: false, fields: { summary: "露西亚认为钟楼守卫在故意拖时间" }, seqRange: [3, 3], scope: { layer: "pov", ownerType: "character", ownerId: "露西亚", ownerName: "露西亚", }, }, ], edges: [], historyState: { activeRegion: "", activeCharacterPovOwner: "", activeUserPovOwner: "玩家", }, }; const multiOwnerSchema = [ { id: "character", label: "角色", alwaysInject: false }, { id: "pov_memory", label: "主观记忆", alwaysInject: false }, ]; state.llmResponse = { selected_ids: ["pov-a", "pov-b"], active_owner_keys: ["character:艾琳", "character:露西亚"], active_owner_scores: [ { ownerKey: "character:艾琳", score: 0.91, reason: "她的 POV 直接命中当前追问" }, { ownerKey: "character:露西亚", score: 0.83, reason: "她也在同一场景并提供互补判断" }, ], }; const multiOwnerResult = await retrieve({ graph: multiOwnerGraph, userMessage: "艾琳和露西亚现在各自怎么看钟楼这件事", recentMessages: ["[assistant]: 她们刚刚一起进入钟楼大厅"], embeddingConfig: {}, schema: multiOwnerSchema, options: { topK: 4, maxRecallNodes: 2, enableVectorPrefilter: false, enableGraphDiffusion: false, enableLLMRecall: true, llmCandidatePool: 4, }, }); assert.equal( multiOwnerResult.meta.retrieval.llm.selectionProtocol, "legacy-selected-ids", ); assert.equal( multiOwnerResult.meta.retrieval.llm.legacySelectionUsed, true, ); assert.deepEqual( Array.from(multiOwnerResult.meta.retrieval.activeRecallOwnerKeys), ["character:艾琳", "character:露西亚"], ); assert.equal(multiOwnerResult.meta.retrieval.sceneOwnerResolutionMode, "llm"); assert.deepEqual( Array.from(multiOwnerResult.scopeBuckets.characterPovOwnerOrder), ["character:艾琳", "character:露西亚"], ); assert.equal( multiOwnerResult.scopeBuckets.characterPovByOwner["character:艾琳"]?.[0]?.id, "pov-a", ); assert.equal( multiOwnerResult.scopeBuckets.characterPovByOwner["character:露西亚"]?.[0]?.id, "pov-b", ); assert.equal( multiOwnerResult.meta.retrieval.selectedByOwner["character:艾琳"]?.[0], "pov-a", ); const temporalGraph = { nodes: [ { id: "evt-current", type: "event", importance: 5, createdTime: 1, archived: false, fields: { title: "当前调查" }, seqRange: [10, 10], storyTime: { label: "当前" }, }, { id: "evt-past", type: "event", importance: 6, createdTime: 2, archived: false, fields: { title: "旧冲突" }, seqRange: [8, 8], storyTime: { label: "往事" }, }, { id: "evt-future", type: "event", importance: 10, createdTime: 3, archived: false, fields: { title: "未来计划" }, seqRange: [12, 12], storyTime: { label: "未来计划", tense: "future" }, }, ], edges: [], historyState: { activeStorySegmentId: "当前", activeStoryTimeLabel: "当前", activeStoryTimeSource: "test", }, timelineState: { segments: [ { id: "当前", label: "当前", order: 2 }, { id: "往事", label: "往事", order: 1 }, { id: "未来计划", label: "未来计划", order: 3 }, ], }, }; const temporalSchema = [{ id: "event", label: "事件", alwaysInject: false }]; const temporalResult = await retrieve({ graph: temporalGraph, userMessage: "现在现场怎么样", recentMessages: [], embeddingConfig: {}, schema: temporalSchema, options: { topK: 3, maxRecallNodes: 2, enableVectorPrefilter: false, enableGraphDiffusion: false, enableLLMRecall: false, enableDiversitySampling: false, enableStoryTimeline: true, storyTimeSoftDirecting: true, activeStorySegmentId: "当前", activeStoryTimeLabel: "当前", }, }); assert.equal(temporalResult.meta.retrieval.activeStorySegmentId, "当前"); assert.equal(temporalResult.meta.retrieval.activeStoryTimeLabel, "当前"); assert.ok(Array.isArray(temporalResult.meta.retrieval.temporalSuppressedNodes)); assert.ok( Array.isArray(temporalResult.meta.retrieval.temporalBuckets?.future) || Array.isArray(temporalResult.meta.retrieval.temporalBuckets?.["future"]), ); assert.ok( !Array.from(temporalResult.selectedNodeIds).includes("evt-future"), ); assert.equal( temporalResult.meta.retrieval.temporalTopHits[0]?.nodeId, "evt-current", ); console.log("retrieval-config tests passed");