feat(cognition): finish multi-character knowledge and monitor workflow

This commit is contained in:
Youzini-afk
2026-04-08 18:21:27 +08:00
parent 5818562145
commit a4fed87e6e
20 changed files with 3451 additions and 19 deletions

View File

@@ -46,6 +46,10 @@ assert.equal(defaultSettings.recallObjectiveAdjacentRegionWeight, 0.9);
assert.equal(defaultSettings.recallObjectiveGlobalWeight, 0.75);
assert.equal(defaultSettings.injectUserPovMemory, true);
assert.equal(defaultSettings.injectObjectiveGlobalMemory, true);
assert.equal(defaultSettings.enableCognitiveMemory, true);
assert.equal(defaultSettings.enableSpatialAdjacency, true);
assert.equal(defaultSettings.enableAiMonitor, false);
assert.equal(defaultSettings.injectLowConfidenceObjectiveMemory, false);
assert.equal(defaultSettings.injectDepth, 9999);
assert.equal(defaultSettings.enabled, true);
assert.equal(defaultSettings.debugLoggingEnabled, false);

123
tests/knowledge-state.mjs Normal file
View File

@@ -0,0 +1,123 @@
import assert from "node:assert/strict";
import { createEmptyGraph, createNode, addNode } from "../graph/graph.js";
import {
applyCognitionUpdates,
applyManualKnowledgeOverride,
clearManualKnowledgeOverride,
applyRegionUpdates,
computeKnowledgeGateForNode,
listKnowledgeOwners,
resolveActiveRegionContext,
resolveAdjacentRegions,
resolveKnowledgeOwner,
setManualActiveRegion,
} from "../graph/knowledge-state.js";
const graph = createEmptyGraph();
const erinA = createNode({
type: "character",
fields: { name: "艾琳", state: "守塔人" },
seq: 1,
});
const erinB = createNode({
type: "character",
fields: { name: "艾琳", state: "伪装者" },
seq: 2,
});
const lucia = createNode({
type: "character",
fields: { name: "露西亚", state: "旁观者" },
seq: 2,
});
const bellEvent = createNode({
type: "event",
fields: { title: "钟楼异响", summary: "钟楼深夜传出异响" },
seq: 3,
scope: { layer: "objective", regionPrimary: "钟楼" },
});
addNode(graph, erinA);
addNode(graph, erinB);
addNode(graph, lucia);
addNode(graph, bellEvent);
const ownerA = resolveKnowledgeOwner(graph, {
ownerType: "character",
ownerName: "艾琳",
nodeId: erinA.id,
});
const ownerB = resolveKnowledgeOwner(graph, {
ownerType: "character",
ownerName: "艾琳",
nodeId: erinB.id,
});
assert.notEqual(ownerA.ownerKey, ownerB.ownerKey);
applyCognitionUpdates(
graph,
[
{
ownerType: "character",
ownerName: "艾琳",
ownerNodeId: erinA.id,
knownRefs: [bellEvent.id],
visibility: [{ ref: bellEvent.id, score: 1 }],
},
],
{
changedNodeIds: [bellEvent.id],
scopeRuntime: {
activeCharacterOwner: "艾琳",
activeUserOwner: "玩家",
},
},
);
const gateVisible = computeKnowledgeGateForNode(graph, bellEvent, ownerA.ownerKey, {
scopeBucket: "objectiveCurrentRegion",
});
assert.equal(gateVisible.visible, true);
assert.equal(gateVisible.anchored, true);
applyManualKnowledgeOverride(graph, {
ownerKey: ownerA.ownerKey,
nodeId: bellEvent.id,
mode: "mistaken",
});
const gateSuppressed = computeKnowledgeGateForNode(graph, bellEvent, ownerA.ownerKey, {
scopeBucket: "objectiveCurrentRegion",
});
assert.equal(gateSuppressed.visible, false);
assert.equal(gateSuppressed.suppressedReason, "mistaken-objective");
const clearedOverride = clearManualKnowledgeOverride(graph, {
ownerKey: ownerA.ownerKey,
nodeId: bellEvent.id,
});
assert.equal(clearedOverride.ok, true);
const gateRestored = computeKnowledgeGateForNode(graph, bellEvent, ownerA.ownerKey, {
scopeBucket: "objectiveCurrentRegion",
});
assert.equal(gateRestored.visible, true);
assert.notEqual(gateRestored.suppressedReason, "mistaken-objective");
applyRegionUpdates(graph, {
activeRegionHint: "钟楼",
adjacency: [{ region: "钟楼", adjacent: ["旧城区", "内廷"] }],
});
assert.equal(resolveActiveRegionContext(graph).activeRegion, "钟楼");
assert.deepEqual(resolveAdjacentRegions(graph, "钟楼").adjacentRegions, ["旧城区", "内廷"]);
setManualActiveRegion(graph, "旧城区");
assert.equal(resolveActiveRegionContext(graph).source, "manual");
assert.equal(resolveActiveRegionContext(graph).activeRegion, "旧城区");
const ownerList = listKnowledgeOwners(graph);
assert.ok(ownerList.some((entry) => entry.ownerKey === ownerA.ownerKey));
assert.ok(
ownerList.some(
(entry) => entry.ownerName === "露西亚" && entry.knownCount === 0,
),
);
console.log("knowledge-state tests passed");

View File

@@ -79,6 +79,15 @@ assert.deepEqual(
.map((message) => message.blockName),
["输出格式", "行为规则"],
);
const extractFormatBlock = extractPayload.promptMessages.find(
(message) => message.blockName === "输出格式",
);
const extractRulesBlock = extractPayload.promptMessages.find(
(message) => message.blockName === "行为规则",
);
assert.match(String(extractFormatBlock?.content || ""), /cognitionUpdates/);
assert.match(String(extractFormatBlock?.content || ""), /regionUpdates/);
assert.match(String(extractRulesBlock?.content || ""), /涉及到的角色都尽量尝试补 cognitionUpdates/);
assert.deepEqual(
extractPayload.promptMessages
.map((message) => message.sourceKey)

View File

@@ -134,6 +134,47 @@ const retrieve = await loadRetrieve({
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}` : "",
};
},
resolveActiveRegionContext(graph, preferredRegion = "") {
return {
activeRegion:
String(preferredRegion || graph?.historyState?.activeRegion || "").trim(),
source: preferredRegion ? "runtime" : "history",
};
},
resolveAdjacentRegions() {
return {
canonicalRegion: "",
adjacentRegions: [],
};
},
pushRecentRecallOwner(historyState, ownerKey = "") {
historyState.activeRecallOwnerKey = ownerKey;
historyState.recentRecallOwnerKeys = ownerKey ? [ownerKey] : [];
},
describeMemoryScope(scope = {}) {
return `${scope.layer || "objective"}:${scope.ownerType || ""}:${scope.regionPrimary || ""}`;
},

View File

@@ -54,7 +54,7 @@ assert.equal(latestObjective?.id, objectiveNode.id);
assert.equal(latestPov?.id, povNode.id);
const legacyGraph = deserializeGraph({
version: 5,
version: 6,
lastProcessedSeq: 0,
nodes: [
{
@@ -79,10 +79,46 @@ const legacyGraph = deserializeGraph({
edges: [],
});
assert.equal(legacyGraph.nodes[0]?.scope?.layer, "objective");
assert.equal(legacyGraph.version, 6);
assert.equal(legacyGraph.version, 7);
assert.equal(legacyGraph.knowledgeState?.version, 1);
assert.equal(legacyGraph.regionState?.version, 1);
assert.equal(legacyGraph.historyState?.activeRegionSource, "");
assert.deepEqual(legacyGraph.historyState?.recentRecallOwnerKeys, []);
const restored = deserializeGraph(serializeGraph(graph));
assert.equal(restored.nodes.find((node) => node.id === povNode.id)?.scope?.ownerType, "character");
assert.equal(restored.nodes.find((node) => node.id === povNode.id)?.scope?.regionPrimary, "钟楼");
assert.equal(restored.knowledgeState?.version, 1);
assert.equal(restored.regionState?.version, 1);
restored.knowledgeState.owners["character:艾琳"] = {
ownerType: "character",
ownerKey: "character:艾琳",
ownerName: "艾琳",
nodeId: "",
aliases: ["艾琳"],
knownNodeIds: [objectiveNode.id],
mistakenNodeIds: [],
visibilityScores: { [objectiveNode.id]: 1 },
manualKnownNodeIds: [],
manualHiddenNodeIds: [],
updatedAt: Date.now(),
lastSource: "test",
};
restored.regionState.adjacencyMap["钟楼"] = {
adjacent: ["旧城区"],
aliases: [],
source: "test",
updatedAt: Date.now(),
};
const roundTrip = deserializeGraph(serializeGraph(restored));
assert.equal(
roundTrip.knowledgeState?.owners?.["character:艾琳"]?.knownNodeIds?.[0],
objectiveNode.id,
);
assert.equal(
roundTrip.regionState?.adjacencyMap?.["钟楼"]?.adjacent?.[0],
"旧城区",
);
console.log("scoped-memory tests passed");

View File

@@ -301,6 +301,11 @@ assert.equal(
refreshedDefaultExtract.metadata.defaultTemplateFingerprint,
currentDefaultExtract.metadata.defaultTemplateFingerprint,
);
assert.match(
refreshedDefaultExtract.blocks.find((block) => block.id === "default-format")
?.content || "",
/cognitionUpdates/,
);
assert.ok(preservedCustomExtract);
assert.equal(
preservedCustomExtract.blocks[0].content,