feat: migrate compress/consolidation/reflection graphStats to shared ranking core + prompt node refs

- New maintenance/task-graph-stats.js: shared helper wrapping rankNodesForTaskContext + createPromptNodeReferenceMap
- extractor.js: extract and reflection graphStats now use buildTaskGraphStats
- compressor.js: compression graphStats uses shared helper, excludes current batch from relevant ranking
- consolidator.js: consolidation graphStats uses shared helper, excludes new nodes from relevant ranking
- index.js: passes schema + embeddingConfig into generateReflection
- p0-regressions.mjs: added graphStats assertions for compress/consolidation/reflection (prompt-facing refs, no raw UUIDs)
This commit is contained in:
Youzini-afk
2026-04-12 15:27:07 +08:00
parent 9c6f0954a1
commit 96b43c7860
6 changed files with 466 additions and 104 deletions

View File

@@ -11126,6 +11126,8 @@ async function handleExtractionSuccess(
await generateReflection({
graph: currentGraph,
currentSeq: endIdx,
schema: getSchema(),
embeddingConfig: getEmbeddingConfig(),
settings,
signal,
});

View File

@@ -30,6 +30,7 @@ import {
} from "../prompting/prompt-builder.js";
import { getSTContextForPrompt } from "../host/st-context.js";
import { applyTaskRegex } from "../prompting/task-regex.js";
import { buildTaskGraphStats } from "./task-graph-stats.js";
import { isDirectVectorConfig } from "../vector/vector-index.js";
function createAbortError(message = "操作已终止") {
@@ -109,6 +110,30 @@ function normalizeCompressionFieldValue(value) {
return String(value).trim();
}
function buildCompressionRankingQueryText(nodes = [], typeDef = {}) {
const typeLabel = String(typeDef?.label || typeDef?.id || "节点").trim() || "节点";
const lines = (Array.isArray(nodes) ? nodes : [])
.map((node, index) => {
const fieldsText = Object.entries(node?.fields || {})
.map(([key, value]) => {
const normalizedValue = normalizeCompressionFieldValue(value);
return normalizedValue ? `${key}: ${normalizedValue}` : "";
})
.filter(Boolean)
.join(" | ");
const storyTimeLabel = describeNodeStoryTime(node);
return [
`${typeLabel}#${index + 1}`,
storyTimeLabel ? `剧情时间=${storyTimeLabel}` : "",
fieldsText,
]
.filter(Boolean)
.join(" | ");
})
.filter(Boolean);
return lines.length > 0 ? [`压缩批次 ${typeLabel}`, ...lines].join("\n") : "";
}
function buildCompressionFallbackSummary(batch = []) {
return batch
.map((node) =>
@@ -187,6 +212,7 @@ export async function compressType({
graph,
typeDef,
embeddingConfig,
schema = [],
force = false,
customPrompt,
signal,
@@ -211,6 +237,7 @@ export async function compressType({
typeDef,
level,
embeddingConfig,
schema,
force,
customPrompt,
signal,
@@ -235,6 +262,7 @@ async function compressLevel({
typeDef,
level,
embeddingConfig,
schema = [],
force,
customPrompt,
signal,
@@ -271,6 +299,9 @@ async function compressLevel({
const summaryResult = await summarizeBatch(
batch,
typeDef,
graph,
embeddingConfig,
schema,
customPrompt,
signal,
settings,
@@ -476,6 +507,9 @@ function migrateBatchEdges(graph, batch, compressedNode) {
async function summarizeBatch(
nodes,
typeDef,
graph,
embeddingConfig,
schema = [],
customPrompt,
signal,
settings = {},
@@ -493,13 +527,35 @@ async function summarizeBatch(
const instruction =
typeDef.compression.instruction || "将以下节点压缩总结为一条精炼记录。";
const excludedNodeIds = new Set(
(Array.isArray(nodes) ? nodes : []).map((node) => String(node?.id || "").trim()),
);
const compressionGraphStats = await buildTaskGraphStats({
graph,
schema: Array.isArray(schema) && schema.length > 0 ? schema : [typeDef],
userMessage: buildCompressionRankingQueryText(nodes, typeDef),
recentMessages: [],
embeddingConfig,
signal,
activeNodes: getActiveNodes(graph).filter(
(node) => !excludedNodeIds.has(String(node?.id || "").trim()),
),
rankingOptions: {
topK: 12,
diffusionTopK: 48,
enableContextQueryBlend: false,
enableMultiIntent: true,
maxTextLength: 1200,
},
relevantHeading: "与当前压缩批次最相关的既有节点",
});
const compressPromptBuild = await buildTaskPrompt(settings, "compress", {
taskName: "compress",
nodeContent: nodeDescriptions,
candidateNodes: nodeDescriptions,
currentRange: `${nodes[0]?.seq ?? "?"} ~ ${nodes[nodes.length - 1]?.seq ?? "?"}`,
graphStats: `node_count=${nodes.length}, node_type=${typeDef.id}`,
graphStats: compressionGraphStats.graphStats,
...getSTContextForPrompt(),
});
const compressRegexInput = { entries: [] };
@@ -581,6 +637,7 @@ export async function compressAll(
graph,
typeDef,
embeddingConfig,
schema,
force,
customPrompt,
signal,

View File

@@ -22,6 +22,7 @@ import {
} from "../prompting/prompt-builder.js";
import { getSTContextForPrompt } from "../host/st-context.js";
import { applyTaskRegex } from "../prompting/task-regex.js";
import { buildTaskGraphStats } from "./task-graph-stats.js";
import {
buildNodeVectorText,
findSimilarNodesByText,
@@ -132,6 +133,27 @@ function canMergeTemporalScopedMemories(leftNode, rightNode) {
return isStoryTimeCompatible(leftNode, rightNode).compatible;
}
function buildConsolidationRankingQueryText(newEntries = []) {
return (Array.isArray(newEntries) ? newEntries : [])
.map((entry, index) => {
const node = entry?.node;
const fieldsText = Object.entries(node?.fields || {})
.map(([key, value]) => `${key}: ${value}`)
.join(", ");
const storyTimeLabel = describeNodeStoryTime(node);
return [
`新记忆#${index + 1}`,
`类型=${String(node?.type || "").trim()}`,
storyTimeLabel ? `剧情时间=${storyTimeLabel}` : "",
fieldsText,
]
.filter(Boolean)
.join(" | ");
})
.filter(Boolean)
.join("\n");
}
export async function analyzeAutoConsolidationGate({
graph,
newNodeIds,
@@ -297,6 +319,7 @@ export async function consolidateMemories({
graph,
newNodeIds,
embeddingConfig,
schema = [],
options = {},
customPrompt,
signal,
@@ -491,13 +514,33 @@ export async function consolidateMemories({
}
const userPrompt = userPromptSections.join("\n\n");
const newNodeIdSet = new Set(newEntries.map((entry) => String(entry?.id || "").trim()));
const consolidationGraphStats = await buildTaskGraphStats({
graph,
schema,
userMessage: buildConsolidationRankingQueryText(newEntries),
recentMessages: [],
embeddingConfig,
signal,
activeNodes: activeNodes.filter(
(node) => !newNodeIdSet.has(String(node?.id || "").trim()),
),
rankingOptions: {
topK: 12,
diffusionTopK: 48,
enableContextQueryBlend: false,
enableMultiIntent: true,
maxTextLength: 1200,
},
relevantHeading: "与本轮整合最相关的既有节点",
});
let decision;
const consolidationPromptBuild = await buildTaskPrompt(settings, "consolidation", {
taskName: "consolidation",
candidateNodes: userPrompt,
candidateText: userPrompt,
graphStats: `new_entries=${newEntries.length}, threshold=${conflictThreshold}`,
graphStats: consolidationGraphStats.graphStats,
...getSTContextForPrompt(),
});
const consolidationRegexInput = { entries: [] };

View File

@@ -41,12 +41,11 @@ import {
buildTaskLlmPayload,
buildTaskPrompt,
} from "../prompting/prompt-builder.js";
import { createPromptNodeReferenceMap } from "../prompting/prompt-node-references.js";
import { RELATION_TYPES } from "../graph/schema.js";
import { applyTaskRegex } from "../prompting/task-regex.js";
import { rankNodesForTaskContext } from "../retrieval/shared-ranking.js";
import { getSTContextForPrompt, getSTContextSnapshot } from "../host/st-context.js";
import { buildExtractionInputContext } from "./extraction-context.js";
import { buildTaskGraphStats } from "./task-graph-stats.js";
import {
aliasSetMatchesValue,
buildUserPovAliasNormalizedSet,
@@ -174,36 +173,20 @@ function buildExtractRankingQueryText(messages = []) {
.join("\n");
}
function buildExtractRelevantNodeReferenceMap(scoredNodes = [], schema = [], maxCount = 6) {
const typeLabelById = new Map(
(Array.isArray(schema) ? schema : []).map((typeDef) => [
String(typeDef?.id || "").trim(),
String(typeDef?.label || typeDef?.id || "").trim(),
]),
);
const relevantNodes = (Array.isArray(scoredNodes) ? scoredNodes : [])
.filter((entry) =>
entry?.node &&
!entry.node.archived &&
((Number(entry?.vectorScore) || 0) > 0 ||
(Number(entry?.graphScore) || 0) > 0 ||
(Number(entry?.lexicalScore) || 0) > 0),
)
.slice(0, Math.max(1, maxCount));
return createPromptNodeReferenceMap(relevantNodes, {
prefix: "G",
maxLength: 28,
buildMeta: ({ entry, node }) => ({
typeLabel:
typeLabelById.get(String(node?.type || "").trim()) ||
String(node?.type || "节点").trim() ||
"节点",
score:
Math.round(
(Number(entry?.weightedScore ?? entry?.finalScore) || 0) * 1000,
) / 1000,
}),
});
function buildReflectionRankingQueryText({
eventSummary = "",
characterSummary = "",
threadSummary = "",
contradictionSummary = "",
} = {}) {
return [
eventSummary ? `最近事件:\n${eventSummary}` : "",
characterSummary ? `近期角色状态:\n${characterSummary}` : "",
threadSummary ? `当前主线:\n${threadSummary}` : "",
contradictionSummary ? `已知矛盾:\n${contradictionSummary}` : "",
]
.filter(Boolean)
.join("\n\n");
}
function isAbortError(error) {
@@ -959,30 +942,25 @@ export async function extractMemories({
: structuredMessages;
const extractGraphRankingQuery = buildExtractRankingQueryText(structuredMessages);
const extractGraphRanking =
graph?.nodes?.some((node) => !node?.archived) && extractGraphRankingQuery
? await rankNodesForTaskContext({
graph,
userMessage: extractGraphRankingQuery,
recentMessages: [],
embeddingConfig,
signal,
options: {
topK: 12,
diffusionTopK: 48,
enableContextQueryBlend: false,
enableMultiIntent: true,
maxTextLength: 1200,
},
})
: null;
const extractGraphRelevantNodes = buildExtractRelevantNodeReferenceMap(
extractGraphRanking?.scoredNodes,
const extractGraphStats = await buildTaskGraphStats({
graph,
schema,
);
// 构建当前图概览(让 LLM 知道已有哪些节点,避免重复)
const graphOverview = buildGraphOverview(graph, schema, extractGraphRelevantNodes);
userMessage: extractGraphRankingQuery,
recentMessages: [],
embeddingConfig,
signal,
rankingOptions: {
topK: 12,
diffusionTopK: 48,
enableContextQueryBlend: false,
enableMultiIntent: true,
maxTextLength: 1200,
},
relevantHeading: "与当前提取片段最相关的既有节点",
});
const extractGraphRanking = extractGraphStats.ranking;
const extractGraphRelevantNodes = extractGraphStats.relevantReferenceMap;
const graphOverview = extractGraphStats.graphStats;
// 构建 Schema 描述
const schemaDescription = buildSchemaDescription(schema);
@@ -1839,40 +1817,6 @@ async function generateNodeEmbeddings(graph, embeddingConfig, signal) {
}
}
/**
* 构建图谱概览文本(给 LLM 看)
*/
function buildGraphOverview(graph, schema, relevantReferenceMap = null) {
const activeNodes = graph.nodes
.filter((n) => !n.archived)
.sort((a, b) => (a.seq || 0) - (b.seq || 0));
if (activeNodes.length === 0) return "";
const lines = [];
lines.push("### 图谱节点统计");
for (const typeDef of schema) {
const nodesOfType = activeNodes.filter((n) => n.type === typeDef.id);
if (nodesOfType.length === 0) continue;
lines.push(` - ${typeDef.label}: ${nodesOfType.length}`);
}
const references = Array.isArray(relevantReferenceMap?.references)
? relevantReferenceMap.references
: [];
if (references.length > 0) {
lines.push("", "### 与当前提取片段最相关的既有节点");
for (const reference of references) {
const typeLabel = String(reference?.meta?.typeLabel || reference?.meta?.type || "节点").trim() || "节点";
const label = String(reference?.meta?.label || "—").trim() || "—";
const score = Number(reference?.meta?.score || 0).toFixed(3);
lines.push(` - [${reference.key}|${typeLabel}] ${label} (score=${score})`);
}
}
return lines.join("\n");
}
/**
* 构建 Schema 描述文本
*/
@@ -2140,6 +2084,8 @@ export async function generateSynopsis({
export async function generateReflection({
graph,
currentSeq,
schema = [],
embeddingConfig,
customPrompt,
signal,
settings = {},
@@ -2189,6 +2135,27 @@ export async function generateReflection({
const contradictionSummary = contradictEdges
.map((e) => `${e.fromId} -> ${e.toId} (${e.relation})`)
.join("\n");
const reflectionGraphStats = await buildTaskGraphStats({
graph,
schema,
userMessage: buildReflectionRankingQueryText({
eventSummary,
characterSummary,
threadSummary,
contradictionSummary,
}),
recentMessages: [],
embeddingConfig,
signal,
rankingOptions: {
topK: 12,
diffusionTopK: 48,
enableContextQueryBlend: false,
enableMultiIntent: true,
maxTextLength: 1200,
},
relevantHeading: "与当前反思最相关的既有节点",
});
const reflectionPromptBuild = await buildTaskPrompt(settings, "reflection", {
taskName: "reflection",
@@ -2196,7 +2163,7 @@ export async function generateReflection({
characterSummary: characterSummary || "(无)",
threadSummary: threadSummary || "(无)",
contradictionSummary: contradictionSummary || "(无)",
graphStats: `event=${recentEvents.length}, character=${recentCharacters.length}, thread=${recentThreads.length}`,
graphStats: reflectionGraphStats.graphStats,
...getSTContextForPrompt(),
});
const reflectionRegexInput = { entries: [] };

View File

@@ -0,0 +1,203 @@
import { getActiveNodes } from "../graph/graph.js";
import { createPromptNodeReferenceMap } from "../prompting/prompt-node-references.js";
import { rankNodesForTaskContext } from "../retrieval/shared-ranking.js";
const DEFAULT_TYPE_LABELS = Object.freeze({
event: "事件",
character: "角色",
location: "地点",
rule: "规则",
thread: "主线",
synopsis: "全局概要",
reflection: "反思",
pov_memory: "主观记忆",
});
function createTypeLabelMap(schema = []) {
return new Map(
(Array.isArray(schema) ? schema : [])
.filter((typeDef) => String(typeDef?.id || "").trim())
.map((typeDef) => [
String(typeDef?.id || "").trim(),
String(typeDef?.label || typeDef?.id || "").trim(),
]),
);
}
function resolveTypeLabel(typeId = "", typeLabelMap = new Map()) {
const normalizedTypeId = String(typeId || "").trim();
return (
typeLabelMap.get(normalizedTypeId) ||
DEFAULT_TYPE_LABELS[normalizedTypeId] ||
normalizedTypeId ||
"节点"
);
}
function listGraphTypeCounts(activeNodes = [], schema = [], typeLabelMap = new Map()) {
const safeActiveNodes = Array.isArray(activeNodes) ? activeNodes : [];
if (Array.isArray(schema) && schema.length > 0) {
return schema
.map((typeDef) => {
const typeId = String(typeDef?.id || "").trim();
const count = safeActiveNodes.filter((node) => node?.type === typeId).length;
return {
typeId,
label: resolveTypeLabel(typeId, typeLabelMap),
count,
};
})
.filter((entry) => entry.count > 0);
}
const countMap = new Map();
for (const node of safeActiveNodes) {
const typeId = String(node?.type || "").trim();
if (!typeId) continue;
countMap.set(typeId, (countMap.get(typeId) || 0) + 1);
}
return [...countMap.entries()]
.map(([typeId, count]) => ({
typeId,
label: resolveTypeLabel(typeId, typeLabelMap),
count,
}))
.sort((left, right) => left.typeId.localeCompare(right.typeId));
}
export function buildRelevantNodeReferenceMap(
scoredNodes = [],
schema = [],
{
maxCount = 6,
prefix = "G",
maxLength = 28,
} = {},
) {
const typeLabelMap = createTypeLabelMap(schema);
const relevantNodes = (Array.isArray(scoredNodes) ? scoredNodes : [])
.filter(
(entry) =>
entry?.node &&
!entry.node.archived &&
((Number(entry?.vectorScore) || 0) > 0 ||
(Number(entry?.graphScore) || 0) > 0 ||
(Number(entry?.lexicalScore) || 0) > 0),
)
.slice(0, Math.max(1, maxCount));
return createPromptNodeReferenceMap(relevantNodes, {
prefix,
maxLength,
buildMeta: ({ entry, node }) => ({
typeLabel: resolveTypeLabel(node?.type, typeLabelMap),
score:
Math.round((Number(entry?.weightedScore ?? entry?.finalScore) || 0) * 1000) /
1000,
}),
});
}
export function buildGraphOverview(
graph,
schema = [],
relevantReferenceMap = null,
{
relevantHeading = "与当前任务最相关的既有节点",
} = {},
) {
const activeNodes = graph?.nodes
?.filter((node) => node && !node.archived)
?.sort((left, right) => (left.seq || 0) - (right.seq || 0));
if (!Array.isArray(activeNodes) || activeNodes.length === 0) {
return "";
}
const typeLabelMap = createTypeLabelMap(schema);
const typeCounts = listGraphTypeCounts(activeNodes, schema, typeLabelMap);
const lines = ["### 图谱节点统计"];
for (const entry of typeCounts) {
lines.push(` - ${entry.label}: ${entry.count}`);
}
const references = Array.isArray(relevantReferenceMap?.references)
? relevantReferenceMap.references
: [];
if (references.length > 0) {
lines.push("", `### ${String(relevantHeading || "与当前任务最相关的既有节点").trim() || "与当前任务最相关的既有节点"}`);
for (const reference of references) {
const typeLabel =
String(reference?.meta?.typeLabel || reference?.meta?.type || "节点").trim() ||
"节点";
const label = String(reference?.meta?.label || "—").trim() || "—";
const score = Number(reference?.meta?.score || 0).toFixed(3);
lines.push(` - [${reference.key}|${typeLabel}] ${label} (score=${score})`);
}
}
return lines.join("\n");
}
function normalizeActiveNodes(graph, activeNodes = null) {
if (Array.isArray(activeNodes)) {
return activeNodes.filter((node) => node && !node.archived);
}
return getActiveNodes(graph).filter((node) => node && !node.archived);
}
export async function buildTaskGraphStats({
graph,
schema = [],
userMessage = "",
recentMessages = [],
embeddingConfig,
signal,
activeNodes = null,
rankingOptions = {},
relevantHeading = "与当前任务最相关的既有节点",
maxRelevantNodes = 6,
prefix = "G",
maxLabelLength = 28,
} = {}) {
const normalizedActiveNodes = normalizeActiveNodes(graph, activeNodes);
const normalizedUserMessage = String(userMessage || "").trim();
let ranking = null;
if (graph && normalizedActiveNodes.length > 0 && normalizedUserMessage) {
ranking = await rankNodesForTaskContext({
graph,
userMessage: normalizedUserMessage,
recentMessages,
embeddingConfig,
signal,
options: {
activeNodes: normalizedActiveNodes,
topK: 12,
diffusionTopK: 48,
enableContextQueryBlend: false,
enableMultiIntent: true,
maxTextLength: 1200,
...rankingOptions,
},
});
}
const relevantReferenceMap = buildRelevantNodeReferenceMap(
ranking?.scoredNodes,
schema,
{
maxCount: maxRelevantNodes,
prefix,
maxLength: maxLabelLength,
},
);
return {
ranking,
relevantReferenceMap,
graphStats: buildGraphOverview(graph, schema, relevantReferenceMap, {
relevantHeading,
}),
};
}

View File

@@ -1997,14 +1997,34 @@ async function testCompressTypeAcceptsTopLevelFieldsResult() {
keepRecentLeaves: 0,
},
};
const compressionSchema = [
typeDef,
{
id: "thread",
label: "主线",
columns: [{ name: "title" }, { name: "summary" }, { name: "status" }],
},
];
const first = makeEvent(1, "事件甲");
const second = makeEvent(2, "事件乙");
const relatedThread = createNode({
type: "thread",
seq: 3,
fields: {
title: "事件甲余波",
summary: "Alice 被卷入的后续波动。",
status: "active",
},
});
addNode(graph, first);
addNode(graph, second);
addNode(graph, relatedThread);
const captured = [];
const restoreOverrides = pushTestOverrides({
llm: {
async callLLMForJSON() {
async callLLMForJSON(params = {}) {
captured.push(params);
return {
title: "压缩事件",
summary: "顶层返回的合并摘要",
@@ -2020,8 +2040,12 @@ async function testCompressTypeAcceptsTopLevelFieldsResult() {
graph,
typeDef,
embeddingConfig: null,
schema: compressionSchema,
force: true,
settings: {},
settings: {
taskProfilesVersion: 3,
taskProfiles: createDefaultTaskProfiles(),
},
});
assert.equal(result.created, 1);
const compressed = graph.nodes.find(
@@ -2029,6 +2053,21 @@ async function testCompressTypeAcceptsTopLevelFieldsResult() {
);
assert.equal(compressed?.fields?.summary, "顶层返回的合并摘要");
assert.equal(compressed?.fields?.title, "压缩事件");
assert.equal(captured.length, 1);
const graphStatsBlock = (Array.isArray(captured[0].promptMessages)
? captured[0].promptMessages
: []
).find((message) => message.sourceKey === "graphStats");
assert.ok(graphStatsBlock, "compress graphStats block should exist");
const graphStatsContent = String(graphStatsBlock.content || "");
assert.match(graphStatsContent, /### 图谱节点统计/);
assert.match(graphStatsContent, /事件: 2/);
assert.match(graphStatsContent, /主线: 1/);
assert.match(graphStatsContent, /\[G1\|主线\] 事件甲余波/);
assert.doesNotMatch(
graphStatsContent,
new RegExp(relatedThread.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
);
} finally {
restoreOverrides();
}
@@ -2060,17 +2099,22 @@ async function testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing() {
addNode(graph, target);
addNode(graph, incoming);
const captured = [];
const restoreOverrides = pushTestOverrides({
embedding: {
async embedBatch() {
return [[0.2, 0.3]];
},
async embedText() {
return [0.2, 0.3];
},
searchSimilar() {
return [{ nodeId: target.id, score: 0.99 }];
},
},
llm: {
async callLLMForJSON() {
async callLLMForJSON(params = {}) {
captured.push(params);
return {
results: [
{
@@ -2095,13 +2139,31 @@ async function testConsolidatorMergeFallbackKeepsNodeWhenTargetMissing() {
apiUrl: "https://example.com/v1",
model: "text-embedding-3-small",
},
settings: {},
schema,
settings: {
taskProfilesVersion: 3,
taskProfiles: createDefaultTaskProfiles(),
},
});
assert.equal(stats.merged, 0);
assert.equal(stats.kept, 1);
assert.equal(incoming.archived, false);
assert.deepEqual(target.embedding, [0.9, 0.1]);
assert.equal(captured.length, 1);
const graphStatsBlock = (Array.isArray(captured[0].promptMessages)
? captured[0].promptMessages
: []
).find((message) => message.sourceKey === "graphStats");
assert.ok(graphStatsBlock, "consolidation graphStats block should exist");
const graphStatsContent = String(graphStatsBlock.content || "");
assert.match(graphStatsContent, /### 图谱节点统计/);
assert.match(graphStatsContent, /事件: 2/);
assert.match(graphStatsContent, /\[G1\|事件\] 旧记忆/);
assert.doesNotMatch(
graphStatsContent,
new RegExp(target.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
);
} finally {
restoreOverrides();
}
@@ -2260,6 +2322,9 @@ async function testConsolidatorMergeUpdatesSeqRange() {
async embedBatch() {
return [[0.4, 0.5]];
},
async embedText() {
return [0.4, 0.5];
},
searchSimilar() {
return [{ nodeId: target.id, score: 0.99 }];
},
@@ -6212,17 +6277,26 @@ async function testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt() {
},
}),
);
const threadNode = createNode({
type: "thread",
seq: 5,
fields: {
title: "信任危机",
status: "active",
},
});
addNode(
graph,
createNode({
type: "thread",
seq: 5,
fields: {
title: "信任危机",
status: "active",
},
}),
threadNode,
);
const reflectionSchema = [
...schema,
{
id: "thread",
label: "主线",
columns: [{ name: "title" }, { name: "status" }],
},
];
const captured = [];
const restoreOverrides = pushTestOverrides({
@@ -6243,6 +6317,7 @@ async function testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt() {
const result = await generateReflection({
graph,
currentSeq: 5,
schema: reflectionSchema,
settings: {
taskProfilesVersion: 3,
taskProfiles: createDefaultTaskProfiles(),
@@ -6254,6 +6329,21 @@ async function testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt() {
assert.equal(Array.isArray(captured[0].promptMessages), true);
assert.ok(captured[0].promptMessages.length > 0);
assert.equal(captured[0].systemPrompt, "");
const graphStatsBlock = (Array.isArray(captured[0].promptMessages)
? captured[0].promptMessages
: []
).find((message) => message.sourceKey === "graphStats");
assert.ok(graphStatsBlock, "reflection graphStats block should exist");
const graphStatsContent = String(graphStatsBlock.content || "");
assert.match(graphStatsContent, /### 图谱节点统计/);
assert.match(graphStatsContent, /事件: 2/);
assert.match(graphStatsContent, /角色: 1/);
assert.match(graphStatsContent, /主线: 1/);
assert.match(graphStatsContent, /\[G1\|主线\] 信任危机/);
assert.doesNotMatch(
graphStatsContent,
new RegExp(threadNode.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
);
const reflectionNode = graph.nodes.find((node) => node.id === result);
assert.equal(
reflectionNode?.fields?.insight,