dsh-daoing-memory 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +50 -50
- package/lib/index.js +267 -54
- package/lib/types/core.d.ts +21 -3
- package/lib/types/core.js +43 -22
- package/lib/types/service.d.ts +17 -4
- package/lib/types/service.js +155 -6
- package/lib/types/store.d.ts +10 -0
- package/lib/types/store.js +31 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -8632,61 +8632,61 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8632
8632
|
document.head.appendChild(tag);
|
|
8633
8633
|
}
|
|
8634
8634
|
var Workbench_module_css_default = {
|
|
8635
|
-
"
|
|
8636
|
-
"btnDanger": "vPI34G_btnDanger",
|
|
8637
|
-
"warnLine": "vPI34G_warnLine",
|
|
8638
|
-
"badgePinned": "vPI34G_badgePinned",
|
|
8639
|
-
"workbenchTabs": "vPI34G_workbenchTabs",
|
|
8640
|
-
"cardMeta": "vPI34G_cardMeta",
|
|
8641
|
-
"actions": "vPI34G_actions",
|
|
8642
|
-
"statusCandidate": "vPI34G_statusCandidate",
|
|
8643
|
-
"title": "vPI34G_title",
|
|
8635
|
+
"body": "vPI34G_body",
|
|
8644
8636
|
"titleBlock": "vPI34G_titleBlock",
|
|
8645
|
-
"
|
|
8646
|
-
"
|
|
8647
|
-
"
|
|
8648
|
-
"
|
|
8649
|
-
"toolbar": "vPI34G_toolbar",
|
|
8650
|
-
"cardSub": "vPI34G_cardSub",
|
|
8651
|
-
"statusOther": "vPI34G_statusOther",
|
|
8652
|
-
"trust": "vPI34G_trust",
|
|
8653
|
-
"header": "vPI34G_header",
|
|
8654
|
-
"concernBg": "vPI34G_concernBg",
|
|
8655
|
-
"panel": "vPI34G_panel",
|
|
8656
|
-
"workbenchTabActive": "vPI34G_workbenchTabActive",
|
|
8637
|
+
"check": "vPI34G_check",
|
|
8638
|
+
"tabActive": "vPI34G_tabActive",
|
|
8639
|
+
"workbenchTabs": "vPI34G_workbenchTabs",
|
|
8640
|
+
"badgePositive": "vPI34G_badgePositive",
|
|
8657
8641
|
"error": "vPI34G_error",
|
|
8642
|
+
"badgeNegative": "vPI34G_badgeNegative",
|
|
8643
|
+
"detailRow": "vPI34G_detailRow",
|
|
8658
8644
|
"okLine": "vPI34G_okLine",
|
|
8659
|
-
"
|
|
8660
|
-
"payloadFull": "vPI34G_payloadFull",
|
|
8661
|
-
"payloadToggle": "vPI34G_payloadToggle",
|
|
8645
|
+
"tab": "vPI34G_tab",
|
|
8662
8646
|
"badgeConflict": "vPI34G_badgeConflict",
|
|
8663
|
-
"
|
|
8664
|
-
"
|
|
8665
|
-
"
|
|
8647
|
+
"payloadToggle": "vPI34G_payloadToggle",
|
|
8648
|
+
"cellActions": "vPI34G_cellActions",
|
|
8649
|
+
"statusCandidate": "vPI34G_statusCandidate",
|
|
8650
|
+
"page": "vPI34G_page",
|
|
8666
8651
|
"table": "vPI34G_table",
|
|
8667
|
-
"
|
|
8668
|
-
"
|
|
8669
|
-
"
|
|
8670
|
-
"tab": "vPI34G_tab",
|
|
8671
|
-
"field": "vPI34G_field",
|
|
8652
|
+
"trust": "vPI34G_trust",
|
|
8653
|
+
"warnLine": "vPI34G_warnLine",
|
|
8654
|
+
"cardMeta": "vPI34G_cardMeta",
|
|
8672
8655
|
"valueCell": "vPI34G_valueCell",
|
|
8673
|
-
"
|
|
8674
|
-
"tabActive": "vPI34G_tabActive",
|
|
8675
|
-
"tabs": "vPI34G_tabs",
|
|
8656
|
+
"badge": "vPI34G_badge",
|
|
8676
8657
|
"column": "vPI34G_column",
|
|
8677
8658
|
"sectionTitle": "vPI34G_sectionTitle",
|
|
8659
|
+
"workbenchTabActive": "vPI34G_workbenchTabActive",
|
|
8660
|
+
"rules": "vPI34G_rules",
|
|
8661
|
+
"concernBg": "vPI34G_concernBg",
|
|
8662
|
+
"field": "vPI34G_field",
|
|
8663
|
+
"tabs": "vPI34G_tabs",
|
|
8664
|
+
"statusLive": "vPI34G_statusLive",
|
|
8665
|
+
"detailLabel": "vPI34G_detailLabel",
|
|
8666
|
+
"statusOther": "vPI34G_statusOther",
|
|
8667
|
+
"columns": "vPI34G_columns",
|
|
8668
|
+
"badgePinned": "vPI34G_badgePinned",
|
|
8669
|
+
"title": "vPI34G_title",
|
|
8678
8670
|
"statline": "vPI34G_statline",
|
|
8679
|
-
"
|
|
8680
|
-
"
|
|
8671
|
+
"btn": "vPI34G_btn",
|
|
8672
|
+
"cardDetail": "vPI34G_cardDetail",
|
|
8673
|
+
"payloadFull": "vPI34G_payloadFull",
|
|
8674
|
+
"statusChallenged": "vPI34G_statusChallenged",
|
|
8675
|
+
"cardHead": "vPI34G_cardHead",
|
|
8676
|
+
"list": "vPI34G_list",
|
|
8677
|
+
"historyTitle": "vPI34G_historyTitle",
|
|
8678
|
+
"toolbar": "vPI34G_toolbar",
|
|
8681
8679
|
"subtitle": "vPI34G_subtitle",
|
|
8682
|
-
"
|
|
8680
|
+
"cardSub": "vPI34G_cardSub",
|
|
8683
8681
|
"form": "vPI34G_form",
|
|
8684
|
-
"
|
|
8685
|
-
"
|
|
8686
|
-
"
|
|
8682
|
+
"cardTitle": "vPI34G_cardTitle",
|
|
8683
|
+
"panel": "vPI34G_panel",
|
|
8684
|
+
"actions": "vPI34G_actions",
|
|
8685
|
+
"header": "vPI34G_header",
|
|
8686
|
+
"workbenchTab": "vPI34G_workbenchTab",
|
|
8687
8687
|
"empty": "vPI34G_empty",
|
|
8688
|
-
"
|
|
8689
|
-
"
|
|
8688
|
+
"btnDanger": "vPI34G_btnDanger",
|
|
8689
|
+
"card": "vPI34G_card"
|
|
8690
8690
|
};
|
|
8691
8691
|
//#endregion
|
|
8692
8692
|
//#region lib/types/client/ExperiencePage.js
|
|
@@ -9328,17 +9328,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9328
9328
|
document.head.appendChild(tag);
|
|
9329
9329
|
}
|
|
9330
9330
|
var ReasonDialog_module_css_default = {
|
|
9331
|
-
"
|
|
9332
|
-
"
|
|
9333
|
-
"hint": "HH3IWW_hint",
|
|
9331
|
+
"subtitle": "HH3IWW_subtitle",
|
|
9332
|
+
"overlay": "HH3IWW_overlay",
|
|
9334
9333
|
"footer": "HH3IWW_footer",
|
|
9335
|
-
"textarea": "HH3IWW_textarea",
|
|
9336
9334
|
"buttons": "HH3IWW_buttons",
|
|
9337
|
-
"
|
|
9335
|
+
"btnCancel": "HH3IWW_btnCancel",
|
|
9338
9336
|
"dialog": "HH3IWW_dialog",
|
|
9339
|
-
"
|
|
9337
|
+
"btnConfirm": "HH3IWW_btnConfirm",
|
|
9338
|
+
"hint": "HH3IWW_hint",
|
|
9340
9339
|
"header": "HH3IWW_header",
|
|
9341
|
-
"
|
|
9340
|
+
"title": "HH3IWW_title",
|
|
9341
|
+
"textarea": "HH3IWW_textarea"
|
|
9342
9342
|
};
|
|
9343
9343
|
//#endregion
|
|
9344
9344
|
//#region lib/types/client/ReasonDialog.js
|
package/lib/index.js
CHANGED
|
@@ -357,6 +357,35 @@ var MemoryStore = class {
|
|
|
357
357
|
return scored.slice(0, topK);
|
|
358
358
|
}
|
|
359
359
|
/**
|
|
360
|
+
* Near-duplicate candidate set for the information-gain gate (007 flow-log fix).
|
|
361
|
+
* Scores on gist+situation only (the lesson's identity) so identical lessons
|
|
362
|
+
* with divergent limits/paths still match. Returns the top-K matches (rough
|
|
363
|
+
* prefilter for the LLM semantic-dedup verdict), or [] when nothing matches.
|
|
364
|
+
*/
|
|
365
|
+
findNearDuplicates(queryTokens, statuses, topK) {
|
|
366
|
+
if (queryTokens.size === 0 || topK <= 0) return [];
|
|
367
|
+
const sql = `
|
|
368
|
+
SELECT * FROM experiences
|
|
369
|
+
WHERE deleted = 0 AND status IN (${statuses.map(() => "?").join(", ")})
|
|
370
|
+
`;
|
|
371
|
+
const rows = this.db.prepare(sql).all(...statuses);
|
|
372
|
+
const scored = [];
|
|
373
|
+
for (const row of rows) {
|
|
374
|
+
const snapshot = this.rowToExperience(row);
|
|
375
|
+
const hay = new Set(tokenize([snapshot.gist, ...snapshot.situation].join(" ")));
|
|
376
|
+
if (hay.size === 0) continue;
|
|
377
|
+
let hits = 0;
|
|
378
|
+
for (const token of queryTokens) if (hay.has(token)) hits += 1;
|
|
379
|
+
const score = hits / Math.sqrt(queryTokens.size * hay.size);
|
|
380
|
+
if (score > 0) scored.push({
|
|
381
|
+
snapshot,
|
|
382
|
+
score
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
scored.sort((a, b) => b.score - a.score);
|
|
386
|
+
return scored.slice(0, topK);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
360
389
|
* Near-duplicate detection for the information-gain gate (007 flow-log fix).
|
|
361
390
|
* Unlike recallCandidates it scores on gist+situation only (the lesson's
|
|
362
391
|
* identity), so identical lessons with divergent limits/paths still match.
|
|
@@ -1045,7 +1074,7 @@ const INGEST_SOURCE_PRIOR = {
|
|
|
1045
1074
|
}
|
|
1046
1075
|
};
|
|
1047
1076
|
/** Cosine-ish overlap of two token sets (for intra-batch dedup, 007). */
|
|
1048
|
-
function tokenOverlap(a, b) {
|
|
1077
|
+
function tokenOverlap$1(a, b) {
|
|
1049
1078
|
if (a.size === 0 || b.size === 0) return 0;
|
|
1050
1079
|
let hits = 0;
|
|
1051
1080
|
for (const t of a) if (b.has(t)) hits += 1;
|
|
@@ -1074,7 +1103,7 @@ var MemoryCore = class MemoryCore {
|
|
|
1074
1103
|
});
|
|
1075
1104
|
}
|
|
1076
1105
|
/** 生: refine one completed trajectory into a candidate (dual gate). */
|
|
1077
|
-
refine(request, actor) {
|
|
1106
|
+
refine(request, actor, opts) {
|
|
1078
1107
|
if (!((request.evidence.traceRef ?? "").trim() !== "" || (request.evidence.sessionRef ?? "").trim() !== "" || (request.evidence.note ?? "").trim() !== "")) return {
|
|
1079
1108
|
accepted: false,
|
|
1080
1109
|
reason: "rejected-evidence: every assertion needs an episodic evidence pointer (traceRef/sessionRef/note)"
|
|
@@ -1088,25 +1117,27 @@ var MemoryCore = class MemoryCore {
|
|
|
1088
1117
|
accepted: false,
|
|
1089
1118
|
reason: `rejected-complexity: trajectory below both gates (tokens < ${String(this.config.complexityTokenGate)}, steps < ${String(this.config.complexityStepGate)}, no failure, not human-marked)`
|
|
1090
1119
|
};
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1120
|
+
if (opts?.dedup !== false) {
|
|
1121
|
+
const dedupTokens = new Set(tokenize([request.gist, ...request.situation].join(" ")));
|
|
1122
|
+
const near = this.store.findNearDuplicate(dedupTokens, [
|
|
1123
|
+
"candidate",
|
|
1124
|
+
"live",
|
|
1125
|
+
"challenged",
|
|
1126
|
+
"archived",
|
|
1127
|
+
"cold"
|
|
1128
|
+
]);
|
|
1129
|
+
if (near !== void 0 && near.score >= this.config.duplicateOverlapGate) {
|
|
1130
|
+
this.ledger("corroborate", "experience", near.snapshot.id, actor, {
|
|
1131
|
+
family: request.family,
|
|
1132
|
+
gist: request.gist,
|
|
1133
|
+
score: near.score
|
|
1134
|
+
});
|
|
1135
|
+
return {
|
|
1136
|
+
accepted: false,
|
|
1137
|
+
reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead",
|
|
1138
|
+
corroboratedId: near.snapshot.id
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1110
1141
|
}
|
|
1111
1142
|
const now = Date.now();
|
|
1112
1143
|
const snapshot = {
|
|
@@ -1154,7 +1185,7 @@ var MemoryCore = class MemoryCore {
|
|
|
1154
1185
|
* candidate carrying provenance (sourceType + sourceRef), the source-authority
|
|
1155
1186
|
* prior, and the declared context scope. Candidates never recall until verified.
|
|
1156
1187
|
*/
|
|
1157
|
-
ingest(request, actor) {
|
|
1188
|
+
ingest(request, actor, opts) {
|
|
1158
1189
|
if ((request.sourceRef ?? "").trim() === "") throw new Error("memory: ingest requires a non-empty sourceRef provenance");
|
|
1159
1190
|
const prior = INGEST_SOURCE_PRIOR[request.sourceType] ?? INGEST_SOURCE_PRIOR.other;
|
|
1160
1191
|
const context = request.context ?? "";
|
|
@@ -1170,32 +1201,34 @@ var MemoryCore = class MemoryCore {
|
|
|
1170
1201
|
continue;
|
|
1171
1202
|
}
|
|
1172
1203
|
const dedupTokens = new Set(tokenize([item.gist, ...item.situation].join(" ")));
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1204
|
+
if (opts?.dedup !== false) {
|
|
1205
|
+
const cross = this.store.findNearDuplicate(dedupTokens, [
|
|
1206
|
+
"candidate",
|
|
1207
|
+
"live",
|
|
1208
|
+
"challenged",
|
|
1209
|
+
"archived",
|
|
1210
|
+
"cold"
|
|
1211
|
+
]);
|
|
1212
|
+
if (cross !== void 0 && cross.score >= this.config.duplicateOverlapGate) {
|
|
1213
|
+
this.ledger("corroborate", "experience", cross.snapshot.id, actor, {
|
|
1214
|
+
family: item.family,
|
|
1215
|
+
gist: item.gist,
|
|
1216
|
+
score: cross.score,
|
|
1217
|
+
via: "ingest"
|
|
1218
|
+
});
|
|
1219
|
+
rejected.push({
|
|
1220
|
+
gist: item.gist,
|
|
1221
|
+
reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead"
|
|
1222
|
+
});
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (acceptedDedup.some((prev) => tokenOverlap$1(dedupTokens, prev) >= this.config.duplicateOverlapGate)) {
|
|
1226
|
+
rejected.push({
|
|
1227
|
+
gist: item.gist,
|
|
1228
|
+
reason: "rejected-information-gain: near-duplicate of another item in this same ingest batch"
|
|
1229
|
+
});
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1199
1232
|
}
|
|
1200
1233
|
const now = Date.now();
|
|
1201
1234
|
const snapshot = {
|
|
@@ -2346,6 +2379,24 @@ var MemoryCore = class MemoryCore {
|
|
|
2346
2379
|
this.ledger("release-cold", "experience", next.id, actor, { revision: next.revision }, request.reason);
|
|
2347
2380
|
return next;
|
|
2348
2381
|
}
|
|
2382
|
+
/** Record a corroboration ledger event for a rejected near-duplicate.
|
|
2383
|
+
* The LLM semantic-dedup path routes a DUPLICATE verdict here so the
|
|
2384
|
+
* ledger stays consistent with the mechanical-gate path (refine/ingest). */
|
|
2385
|
+
markCorroborate(nearId, actor, req, via, score) {
|
|
2386
|
+
this.ledger("corroborate", "experience", nearId, actor, {
|
|
2387
|
+
family: req.family,
|
|
2388
|
+
gist: req.gist,
|
|
2389
|
+
score,
|
|
2390
|
+
via
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
/** Near-duplicate candidate set for the LLM semantic-dedup prefilter. The
|
|
2394
|
+
* service layer asks here for the mechanical word-overlap top-K before it
|
|
2395
|
+
* runs the LLM verdict; the coarse hit is only a candidate pool, not a gate. */
|
|
2396
|
+
nearDuplicateCandidates(gist, situation, statuses, topK) {
|
|
2397
|
+
const tokens = new Set(tokenize([gist, ...situation].join(" ")));
|
|
2398
|
+
return this.store.findNearDuplicates(tokens, statuses, topK);
|
|
2399
|
+
}
|
|
2349
2400
|
/** Human-approved self-growth: merge new evidence into a live, human-approved
|
|
2350
2401
|
* experience as an incremental new revision (revision+1, parent superseded).
|
|
2351
2402
|
* Only approvedBy='human' + status='live' may evolve; agent/system-approved
|
|
@@ -3630,6 +3681,23 @@ function actorOf(agent) {
|
|
|
3630
3681
|
const sessionId = agent.session?.id;
|
|
3631
3682
|
return sessionId === void 0 ? "agent" : `agent:${sessionId}`;
|
|
3632
3683
|
}
|
|
3684
|
+
/** Top-K coarse candidate pool for the LLM semantic-dedup prefilter. */
|
|
3685
|
+
const DEDUP_TOP_K = 5;
|
|
3686
|
+
/** Statuses scanned for a near-duplicate candidate pool. */
|
|
3687
|
+
const DEDUP_STATUSES = [
|
|
3688
|
+
"candidate",
|
|
3689
|
+
"live",
|
|
3690
|
+
"challenged",
|
|
3691
|
+
"archived",
|
|
3692
|
+
"cold"
|
|
3693
|
+
];
|
|
3694
|
+
/** Cosine-ish overlap of two token sets (for intra-batch dedup, 007). */
|
|
3695
|
+
function tokenOverlap(a, b) {
|
|
3696
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
3697
|
+
let hits = 0;
|
|
3698
|
+
for (const t of a) if (b.has(t)) hits += 1;
|
|
3699
|
+
return hits / Math.sqrt(a.size * b.size);
|
|
3700
|
+
}
|
|
3633
3701
|
/**
|
|
3634
3702
|
* Remote face of the memory library. All methods delegate to the core; the
|
|
3635
3703
|
* core carries the 生·用·修·记 mechanism semantics.
|
|
@@ -4339,9 +4407,39 @@ let MemoryService = (() => {
|
|
|
4339
4407
|
this.llm = llm;
|
|
4340
4408
|
this.defaultModel = defaultModel;
|
|
4341
4409
|
}
|
|
4342
|
-
/** 生: refine a completed trajectory into an experience candidate.
|
|
4343
|
-
|
|
4344
|
-
|
|
4410
|
+
/** 生: refine a completed trajectory into an experience candidate.
|
|
4411
|
+
* Semantic-dedup: coarse word-overlap candidate set first; when it is
|
|
4412
|
+
* non-empty the LLM judges [merge/duplicate/different] on the FULL lesson
|
|
4413
|
+
* (gist+situation+path+reasoning+limits), replacing the mechanical gate. */
|
|
4414
|
+
async refine(agent, request) {
|
|
4415
|
+
const actor = actorOf(agent);
|
|
4416
|
+
const cands = this.core.nearDuplicateCandidates(request.gist, request.situation, DEDUP_STATUSES, DEDUP_TOP_K);
|
|
4417
|
+
if (cands.length === 0) return this.core.refine(request, actor);
|
|
4418
|
+
const verdict = await this.dedupVerdictWithLlm(agent, request, cands);
|
|
4419
|
+
if (verdict === "different") return this.core.refine(request, actor, { dedup: false });
|
|
4420
|
+
const best = cands[0];
|
|
4421
|
+
if (verdict === "merge" && best.snapshot.approvedBy === "human" && best.snapshot.status === "live") return {
|
|
4422
|
+
accepted: false,
|
|
4423
|
+
reason: "semantic-merge: merged into a human-approved live experience",
|
|
4424
|
+
corroboratedId: this.core.evolve({
|
|
4425
|
+
id: best.snapshot.id,
|
|
4426
|
+
reason: "semantic-merge: " + request.gist,
|
|
4427
|
+
gist: request.gist,
|
|
4428
|
+
situation: request.situation,
|
|
4429
|
+
path: request.path,
|
|
4430
|
+
reasoning: request.reasoning,
|
|
4431
|
+
limits: request.limits
|
|
4432
|
+
}, actor).id
|
|
4433
|
+
};
|
|
4434
|
+
this.core.markCorroborate(best.snapshot.id, actor, {
|
|
4435
|
+
family: request.family,
|
|
4436
|
+
gist: request.gist
|
|
4437
|
+
}, "refine", best.score);
|
|
4438
|
+
return {
|
|
4439
|
+
accepted: false,
|
|
4440
|
+
reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead",
|
|
4441
|
+
corroboratedId: best.snapshot.id
|
|
4442
|
+
};
|
|
4345
4443
|
}
|
|
4346
4444
|
/** 用: recall + adjudication + injection budget + negative channel. */
|
|
4347
4445
|
recall(agent, request) {
|
|
@@ -4351,9 +4449,72 @@ let MemoryService = (() => {
|
|
|
4351
4449
|
report(agent, request) {
|
|
4352
4450
|
return this.core.report(request, actorOf(agent));
|
|
4353
4451
|
}
|
|
4354
|
-
/** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
|
|
4355
|
-
|
|
4356
|
-
|
|
4452
|
+
/** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
|
|
4453
|
+
* Semantic-dedup per item: coarse word-overlap candidate set; when non-empty
|
|
4454
|
+
* the LLM judges [merge/duplicate/different] on the FULL lesson. */
|
|
4455
|
+
async ingest(agent, request) {
|
|
4456
|
+
const actor = actorOf(agent);
|
|
4457
|
+
const prior = this.core.ingest({
|
|
4458
|
+
...request,
|
|
4459
|
+
experiences: []
|
|
4460
|
+
}, actor, { dedup: false }).sourcePrior;
|
|
4461
|
+
const accepted = [];
|
|
4462
|
+
const rejected = [];
|
|
4463
|
+
const acceptedDedup = [];
|
|
4464
|
+
for (const item of request.experiences) {
|
|
4465
|
+
if (item.kind === "negative" && (item.failureReason ?? "").trim() === "") {
|
|
4466
|
+
rejected.push({
|
|
4467
|
+
gist: item.gist,
|
|
4468
|
+
reason: "rejected-schema: negative experiences must carry the confirmed failureReason"
|
|
4469
|
+
});
|
|
4470
|
+
continue;
|
|
4471
|
+
}
|
|
4472
|
+
const cands = this.core.nearDuplicateCandidates(item.gist, item.situation, DEDUP_STATUSES, DEDUP_TOP_K);
|
|
4473
|
+
if (cands.length > 0) {
|
|
4474
|
+
const best = cands[0];
|
|
4475
|
+
const verdict = await this.dedupVerdictWithLlm(agent, item, cands);
|
|
4476
|
+
if (verdict !== "different") {
|
|
4477
|
+
if (verdict === "merge" && best.snapshot.approvedBy === "human" && best.snapshot.status === "live") this.core.evolve({
|
|
4478
|
+
id: best.snapshot.id,
|
|
4479
|
+
reason: "semantic-merge: " + item.gist,
|
|
4480
|
+
gist: item.gist,
|
|
4481
|
+
situation: item.situation,
|
|
4482
|
+
path: item.path,
|
|
4483
|
+
reasoning: item.reasoning,
|
|
4484
|
+
limits: item.limits
|
|
4485
|
+
}, actor);
|
|
4486
|
+
else this.core.markCorroborate(best.snapshot.id, actor, {
|
|
4487
|
+
family: item.family,
|
|
4488
|
+
gist: item.gist
|
|
4489
|
+
}, "ingest", best.score);
|
|
4490
|
+
rejected.push({
|
|
4491
|
+
gist: item.gist,
|
|
4492
|
+
reason: "rejected-information-gain: near-duplicate of an existing experience; corroborated it instead"
|
|
4493
|
+
});
|
|
4494
|
+
continue;
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
const tokens = new Set(tokenize([item.gist, ...item.situation].join(" ")));
|
|
4498
|
+
if (acceptedDedup.some((prev) => tokenOverlap(tokens, prev) >= this.core.config.duplicateOverlapGate)) {
|
|
4499
|
+
rejected.push({
|
|
4500
|
+
gist: item.gist,
|
|
4501
|
+
reason: "rejected-information-gain: near-duplicate of another item in this same ingest batch"
|
|
4502
|
+
});
|
|
4503
|
+
continue;
|
|
4504
|
+
}
|
|
4505
|
+
const itemResult = this.core.ingest({
|
|
4506
|
+
...request,
|
|
4507
|
+
experiences: [item]
|
|
4508
|
+
}, actor, { dedup: cands.length === 0 });
|
|
4509
|
+
for (const a of itemResult.accepted) accepted.push(a);
|
|
4510
|
+
for (const r of itemResult.rejected) rejected.push(r);
|
|
4511
|
+
acceptedDedup.push(tokens);
|
|
4512
|
+
}
|
|
4513
|
+
return {
|
|
4514
|
+
accepted,
|
|
4515
|
+
rejected,
|
|
4516
|
+
sourcePrior: prior
|
|
4517
|
+
};
|
|
4357
4518
|
}
|
|
4358
4519
|
/** 修: propose a revised draft for a challenged experience. */
|
|
4359
4520
|
revise(agent, request) {
|
|
@@ -4403,6 +4564,58 @@ let MemoryService = (() => {
|
|
|
4403
4564
|
} catch {}
|
|
4404
4565
|
}
|
|
4405
4566
|
/**
|
|
4567
|
+
* LLM semantic-dedup verdict between a new lesson and a coarse candidate set.
|
|
4568
|
+
* Reads the FULL lesson (gist+situation+path+reasoning+limits) on both sides
|
|
4569
|
+
* and returns 'merge' | 'duplicate' | 'different'. Falls back to 'different'
|
|
4570
|
+
* (accept) when the LLM is unavailable or the model route is missing, so the
|
|
4571
|
+
* mechanical gate in core still governs without an LLM.
|
|
4572
|
+
*/
|
|
4573
|
+
async dedupVerdictWithLlm(agent, lesson, cands) {
|
|
4574
|
+
const header = agent.session.requestHeader()?.config;
|
|
4575
|
+
let provider = header?.provider;
|
|
4576
|
+
let model = header?.model;
|
|
4577
|
+
if (provider === void 0 || model === void 0) {
|
|
4578
|
+
const sel = this.defaultModel?.currentSelection();
|
|
4579
|
+
if (sel !== void 0) {
|
|
4580
|
+
provider = sel.provider;
|
|
4581
|
+
model = sel.model;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
if (provider === void 0 || model === void 0) return "different";
|
|
4585
|
+
if (this.llm === void 0) return "different";
|
|
4586
|
+
const fmtLesson = (l) => "gist: " + l.gist + "\nsituation: " + l.situation.join(" | ") + "\npath:\n" + l.path.map((p) => " " + p.order + ". " + p.action).join("\n") + "\nreasoning: " + l.reasoning + "\nlimits: " + l.limits.join(" | ");
|
|
4587
|
+
const candText = cands.map((c, i) => "候选 " + (i + 1) + "(词面相似度 " + c.score.toFixed(3) + "):\n" + fmtLesson(c.snapshot)).join("\n\n");
|
|
4588
|
+
const systemPrompt = "你是记忆系统的语义去重裁判。给定一条\"新提炼的教训\"和若干\"已存在的候选经验\",判断它们是否指同一件事。\n只输出一个词:\n- merge:语义可合并,新教训是对候选经验的增量补充\n- duplicate:语义重复,新教训无新增信息\n- different:语义不同,是不同的经验\n只输出 merge/duplicate/different 之一,不要任何其它内容。";
|
|
4589
|
+
const messages = [createUserMessage({
|
|
4590
|
+
content: [{
|
|
4591
|
+
type: "text",
|
|
4592
|
+
text: "新提炼的教训:\n" + fmtLesson(lesson) + "\n\n候选经验:\n" + candText + "\n\n判定(merge/duplicate/different):"
|
|
4593
|
+
}],
|
|
4594
|
+
source: {
|
|
4595
|
+
kind: "plugin",
|
|
4596
|
+
plugin: "dsh-daoing-memory"
|
|
4597
|
+
}
|
|
4598
|
+
})];
|
|
4599
|
+
let text = "";
|
|
4600
|
+
const options = {
|
|
4601
|
+
provider,
|
|
4602
|
+
model,
|
|
4603
|
+
messages,
|
|
4604
|
+
system: systemPrompt,
|
|
4605
|
+
maxTokens: 50,
|
|
4606
|
+
sessionId: agent.session.id
|
|
4607
|
+
};
|
|
4608
|
+
try {
|
|
4609
|
+
for await (const chunk of this.llm.stream(options)) if (chunk.type === "text-delta") text += chunk.text;
|
|
4610
|
+
} catch {
|
|
4611
|
+
return "different";
|
|
4612
|
+
}
|
|
4613
|
+
const t = text.trim().toLowerCase();
|
|
4614
|
+
if (t.includes("merge")) return "merge";
|
|
4615
|
+
if (t.includes("duplicate")) return "duplicate";
|
|
4616
|
+
return "different";
|
|
4617
|
+
}
|
|
4618
|
+
/**
|
|
4406
4619
|
* Call the LLM to summarize deletion records into extraction feedback rules.
|
|
4407
4620
|
* Uses ctx.llm.stream() with the agent's current model route.
|
|
4408
4621
|
*/
|
package/lib/types/core.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* @module dsh-daoing-memory/core
|
|
22
22
|
*/
|
|
23
|
-
import type { ConcernTree, ConsolidateRequest, ConsolidateResult, DiaryAppendRequest, DiaryAppendResult, DiaryEntry, ExperienceListFilter, ExperienceSnapshot, ExtractionRecord, ExtractFactsRequest, ExtractFactsResult, FactEntry, EvolveExperienceRequest, HumanAddExperienceRequest, HumanAddFactRequest, HumanAckDiaryRequest, HumanArchiveExperienceRequest, HumanConfirmFactRequest, HumanDeleteConcernRequest, HumanDeleteExperienceRequest, HumanDeleteFactRequest, HumanEditExperienceRequest, HumanEditFactRequest, HumanPinRequest, HumanSetConcernStatusRequest, HumanReleaseColdRequest, IngestRequest, IngestResult, LedgerIntegrityResult, LedgerQueryRequest, MemoryExport, MemoryStats, RecallExperiencesRequest, RecallExperiencesResult, RefineExperienceRequest, RefineExperienceResult, ReportUseRequest, ReportUseResult, ReviseExperienceRequest, RollbackExperienceRequest, VerifyShadowRequest, VerifyShadowResult, SkillArtifact, SkillForm, SkillStatus, ReviewSkillRequest, PublishSkillRequest } from './types.ts';
|
|
23
|
+
import type { ConcernTree, ConsolidateRequest, ConsolidateResult, DiaryAppendRequest, DiaryAppendResult, DiaryEntry, ExperienceListFilter, ExperienceSnapshot, ExperienceStatus, ExtractionRecord, ExtractFactsRequest, ExtractFactsResult, FactEntry, EvolveExperienceRequest, HumanAddExperienceRequest, HumanAddFactRequest, HumanAckDiaryRequest, HumanArchiveExperienceRequest, HumanConfirmFactRequest, HumanDeleteConcernRequest, HumanDeleteExperienceRequest, HumanDeleteFactRequest, HumanEditExperienceRequest, HumanEditFactRequest, HumanPinRequest, HumanSetConcernStatusRequest, HumanReleaseColdRequest, IngestRequest, IngestResult, LedgerIntegrityResult, LedgerQueryRequest, MemoryExport, MemoryStats, RecallExperiencesRequest, RecallExperiencesResult, RefineExperienceRequest, RefineExperienceResult, ReportUseRequest, ReportUseResult, ReviseExperienceRequest, RollbackExperienceRequest, VerifyShadowRequest, VerifyShadowResult, SkillArtifact, SkillForm, SkillStatus, ReviewSkillRequest, PublishSkillRequest } from './types.ts';
|
|
24
24
|
import { MemoryStore } from './store.ts';
|
|
25
25
|
/** Tunable mechanism parameters, all overridable from the plugin config. */
|
|
26
26
|
export interface MemoryCoreConfig {
|
|
@@ -82,13 +82,17 @@ export declare class MemoryCore {
|
|
|
82
82
|
constructor(store: MemoryStore, config: MemoryCoreConfig);
|
|
83
83
|
private ledger;
|
|
84
84
|
/** 生: refine one completed trajectory into a candidate (dual gate). */
|
|
85
|
-
refine(request: RefineExperienceRequest, actor: MemoryActor
|
|
85
|
+
refine(request: RefineExperienceRequest, actor: MemoryActor, opts?: {
|
|
86
|
+
dedup?: boolean;
|
|
87
|
+
}): RefineExperienceResult;
|
|
86
88
|
/**
|
|
87
89
|
* 摄取归一: source-agnostic intake. Every extracted draft becomes an earned
|
|
88
90
|
* candidate carrying provenance (sourceType + sourceRef), the source-authority
|
|
89
91
|
* prior, and the declared context scope. Candidates never recall until verified.
|
|
90
92
|
*/
|
|
91
|
-
ingest(request: IngestRequest, actor: MemoryActor
|
|
93
|
+
ingest(request: IngestRequest, actor: MemoryActor, opts?: {
|
|
94
|
+
dedup?: boolean;
|
|
95
|
+
}): IngestResult;
|
|
92
96
|
/** 用: recall → scope → adjudicate → budget → inject, plus the candidate probe channel. */
|
|
93
97
|
recall(request: RecallExperiencesRequest, actor: MemoryActor): RecallExperiencesResult;
|
|
94
98
|
/** 用·验: one use outcome → Beta update, attribution, quarantine, gates. */
|
|
@@ -234,6 +238,20 @@ export declare class MemoryCore {
|
|
|
234
238
|
humanPromote(id: string, reason: string, actor: MemoryActor): ExperienceSnapshot;
|
|
235
239
|
/** Human re-release (006 §3.2): move a cold-palace revision back to candidate. */
|
|
236
240
|
humanReleaseCold(request: HumanReleaseColdRequest, actor: MemoryActor): ExperienceSnapshot;
|
|
241
|
+
/** Record a corroboration ledger event for a rejected near-duplicate.
|
|
242
|
+
* The LLM semantic-dedup path routes a DUPLICATE verdict here so the
|
|
243
|
+
* ledger stays consistent with the mechanical-gate path (refine/ingest). */
|
|
244
|
+
markCorroborate(nearId: string, actor: MemoryActor, req: {
|
|
245
|
+
family: string;
|
|
246
|
+
gist: string;
|
|
247
|
+
}, via: string, score: number): void;
|
|
248
|
+
/** Near-duplicate candidate set for the LLM semantic-dedup prefilter. The
|
|
249
|
+
* service layer asks here for the mechanical word-overlap top-K before it
|
|
250
|
+
* runs the LLM verdict; the coarse hit is only a candidate pool, not a gate. */
|
|
251
|
+
nearDuplicateCandidates(gist: string, situation: string[], statuses: ExperienceStatus[], topK: number): {
|
|
252
|
+
snapshot: ExperienceSnapshot;
|
|
253
|
+
score: number;
|
|
254
|
+
}[];
|
|
237
255
|
/** Human-approved self-growth: merge new evidence into a live, human-approved
|
|
238
256
|
* experience as an incremental new revision (revision+1, parent superseded).
|
|
239
257
|
* Only approvedBy='human' + status='live' may evolve; agent/system-approved
|
package/lib/types/core.js
CHANGED
|
@@ -99,7 +99,7 @@ export class MemoryCore {
|
|
|
99
99
|
}
|
|
100
100
|
// ── 生: refine ────────────────────────────────────────────────────────────
|
|
101
101
|
/** 生: refine one completed trajectory into a candidate (dual gate). */
|
|
102
|
-
refine(request, actor) {
|
|
102
|
+
refine(request, actor, opts) {
|
|
103
103
|
const evidenceOk = (request.evidence.traceRef ?? '').trim() !== ''
|
|
104
104
|
|| (request.evidence.sessionRef ?? '').trim() !== ''
|
|
105
105
|
|| (request.evidence.note ?? '').trim() !== '';
|
|
@@ -120,15 +120,21 @@ export class MemoryCore {
|
|
|
120
120
|
// Information-gain gate (007 flow-log fix): match on the lesson identity
|
|
121
121
|
// (gist + situation) across the whole library; a near-duplicate corroborates
|
|
122
122
|
// instead of inflating the library into a running log.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
// Information-gain gate: mechanical word-overlap is the DEFAULT (no-LLM)
|
|
124
|
+
// prefilter. When the service layer already ran LLM semantic dedup and
|
|
125
|
+
// decided DIFFERENT, it passes { dedup: false } to accept without a
|
|
126
|
+
// mechanical gate that would otherwise over-reject.
|
|
127
|
+
if (opts?.dedup !== false) {
|
|
128
|
+
const dedupTokens = new Set(tokenize([request.gist, ...request.situation].join(' ')));
|
|
129
|
+
const near = this.store.findNearDuplicate(dedupTokens, ['candidate', 'live', 'challenged', 'archived', 'cold']);
|
|
130
|
+
if (near !== undefined && near.score >= this.config.duplicateOverlapGate) {
|
|
131
|
+
this.ledger('corroborate', 'experience', near.snapshot.id, actor, {
|
|
132
|
+
family: request.family,
|
|
133
|
+
gist: request.gist,
|
|
134
|
+
score: near.score,
|
|
135
|
+
});
|
|
136
|
+
return { accepted: false, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead', corroboratedId: near.snapshot.id };
|
|
137
|
+
}
|
|
132
138
|
}
|
|
133
139
|
const now = Date.now();
|
|
134
140
|
const snapshot = {
|
|
@@ -175,7 +181,7 @@ export class MemoryCore {
|
|
|
175
181
|
* candidate carrying provenance (sourceType + sourceRef), the source-authority
|
|
176
182
|
* prior, and the declared context scope. Candidates never recall until verified.
|
|
177
183
|
*/
|
|
178
|
-
ingest(request, actor) {
|
|
184
|
+
ingest(request, actor, opts) {
|
|
179
185
|
if ((request.sourceRef ?? '').trim() === '') {
|
|
180
186
|
throw new Error('memory: ingest requires a non-empty sourceRef provenance');
|
|
181
187
|
}
|
|
@@ -194,17 +200,19 @@ export class MemoryCore {
|
|
|
194
200
|
// already accepted in this same batch. A near-duplicate corroborates
|
|
195
201
|
// instead of inflating the library into a running log.
|
|
196
202
|
const dedupTokens = new Set(tokenize([item.gist, ...item.situation].join(' ')));
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
if (opts?.dedup !== false) {
|
|
204
|
+
const cross = this.store.findNearDuplicate(dedupTokens, ['candidate', 'live', 'challenged', 'archived', 'cold']);
|
|
205
|
+
if (cross !== undefined && cross.score >= this.config.duplicateOverlapGate) {
|
|
206
|
+
this.ledger('corroborate', 'experience', cross.snapshot.id, actor, {
|
|
207
|
+
family: item.family, gist: item.gist, score: cross.score, via: 'ingest',
|
|
208
|
+
});
|
|
209
|
+
rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead' });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (acceptedDedup.some(prev => tokenOverlap(dedupTokens, prev) >= this.config.duplicateOverlapGate)) {
|
|
213
|
+
rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of another item in this same ingest batch' });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
208
216
|
}
|
|
209
217
|
const now = Date.now();
|
|
210
218
|
const snapshot = {
|
|
@@ -1324,6 +1332,19 @@ export class MemoryCore {
|
|
|
1324
1332
|
this.ledger('release-cold', 'experience', next.id, actor, { revision: next.revision }, request.reason);
|
|
1325
1333
|
return next;
|
|
1326
1334
|
}
|
|
1335
|
+
/** Record a corroboration ledger event for a rejected near-duplicate.
|
|
1336
|
+
* The LLM semantic-dedup path routes a DUPLICATE verdict here so the
|
|
1337
|
+
* ledger stays consistent with the mechanical-gate path (refine/ingest). */
|
|
1338
|
+
markCorroborate(nearId, actor, req, via, score) {
|
|
1339
|
+
this.ledger('corroborate', 'experience', nearId, actor, { family: req.family, gist: req.gist, score, via });
|
|
1340
|
+
}
|
|
1341
|
+
/** Near-duplicate candidate set for the LLM semantic-dedup prefilter. The
|
|
1342
|
+
* service layer asks here for the mechanical word-overlap top-K before it
|
|
1343
|
+
* runs the LLM verdict; the coarse hit is only a candidate pool, not a gate. */
|
|
1344
|
+
nearDuplicateCandidates(gist, situation, statuses, topK) {
|
|
1345
|
+
const tokens = new Set(tokenize([gist, ...situation].join(' ')));
|
|
1346
|
+
return this.store.findNearDuplicates(tokens, statuses, topK);
|
|
1347
|
+
}
|
|
1327
1348
|
/** Human-approved self-growth: merge new evidence into a live, human-approved
|
|
1328
1349
|
* experience as an incremental new revision (revision+1, parent superseded).
|
|
1329
1350
|
* Only approvedBy='human' + status='live' may evolve; agent/system-approved
|
package/lib/types/service.d.ts
CHANGED
|
@@ -36,14 +36,19 @@ export declare class MemoryService extends TypertRemoteService {
|
|
|
36
36
|
model: string;
|
|
37
37
|
};
|
|
38
38
|
} | undefined);
|
|
39
|
-
/** 生: refine a completed trajectory into an experience candidate.
|
|
40
|
-
|
|
39
|
+
/** 生: refine a completed trajectory into an experience candidate.
|
|
40
|
+
* Semantic-dedup: coarse word-overlap candidate set first; when it is
|
|
41
|
+
* non-empty the LLM judges [merge/duplicate/different] on the FULL lesson
|
|
42
|
+
* (gist+situation+path+reasoning+limits), replacing the mechanical gate. */
|
|
43
|
+
refine(agent: Agent, request: RefineExperienceRequest): Promise<RefineExperienceResult>;
|
|
41
44
|
/** 用: recall + adjudication + injection budget + negative channel. */
|
|
42
45
|
recall(agent: Agent, request: RecallExperiencesRequest): RecallExperiencesResult;
|
|
43
46
|
/** 用·验: report one use outcome with attribution (V0 verification). */
|
|
44
47
|
report(agent: Agent, request: ReportUseRequest): ReportUseResult;
|
|
45
|
-
/** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
|
|
46
|
-
|
|
48
|
+
/** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
|
|
49
|
+
* Semantic-dedup per item: coarse word-overlap candidate set; when non-empty
|
|
50
|
+
* the LLM judges [merge/duplicate/different] on the FULL lesson. */
|
|
51
|
+
ingest(agent: Agent, request: IngestRequest): Promise<IngestResult>;
|
|
47
52
|
/** 修: propose a revised draft for a challenged experience. */
|
|
48
53
|
revise(agent: Agent, request: ReviseExperienceRequest): ExperienceSnapshot;
|
|
49
54
|
/** V1 controlled re-enactment: shadow replay verification for a draft. */
|
|
@@ -65,6 +70,14 @@ export declare class MemoryService extends TypertRemoteService {
|
|
|
65
70
|
* Uses the agent's current model route for the LLM call.
|
|
66
71
|
*/
|
|
67
72
|
private maybeRunDeletionFeedbackSummarization;
|
|
73
|
+
/**
|
|
74
|
+
* LLM semantic-dedup verdict between a new lesson and a coarse candidate set.
|
|
75
|
+
* Reads the FULL lesson (gist+situation+path+reasoning+limits) on both sides
|
|
76
|
+
* and returns 'merge' | 'duplicate' | 'different'. Falls back to 'different'
|
|
77
|
+
* (accept) when the LLM is unavailable or the model route is missing, so the
|
|
78
|
+
* mechanical gate in core still governs without an LLM.
|
|
79
|
+
*/
|
|
80
|
+
private dedupVerdictWithLlm;
|
|
68
81
|
/**
|
|
69
82
|
* Call the LLM to summarize deletion records into extraction feedback rules.
|
|
70
83
|
* Uses ctx.llm.stream() with the agent's current model route.
|
package/lib/types/service.js
CHANGED
|
@@ -44,11 +44,26 @@ import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
|
44
44
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
45
45
|
import { join } from 'node:path';
|
|
46
46
|
import { randomUUID } from 'node:crypto';
|
|
47
|
+
import { tokenize } from "./store.js";
|
|
47
48
|
/** Derive the ledger actor label from the wire identity. */
|
|
48
49
|
function actorOf(agent) {
|
|
49
50
|
const sessionId = agent.session?.id;
|
|
50
51
|
return sessionId === undefined ? 'agent' : `agent:${sessionId}`;
|
|
51
52
|
}
|
|
53
|
+
/** Top-K coarse candidate pool for the LLM semantic-dedup prefilter. */
|
|
54
|
+
const DEDUP_TOP_K = 5;
|
|
55
|
+
/** Statuses scanned for a near-duplicate candidate pool. */
|
|
56
|
+
const DEDUP_STATUSES = ['candidate', 'live', 'challenged', 'archived', 'cold'];
|
|
57
|
+
/** Cosine-ish overlap of two token sets (for intra-batch dedup, 007). */
|
|
58
|
+
function tokenOverlap(a, b) {
|
|
59
|
+
if (a.size === 0 || b.size === 0)
|
|
60
|
+
return 0;
|
|
61
|
+
let hits = 0;
|
|
62
|
+
for (const t of a)
|
|
63
|
+
if (b.has(t))
|
|
64
|
+
hits += 1;
|
|
65
|
+
return hits / Math.sqrt(a.size * b.size);
|
|
66
|
+
}
|
|
52
67
|
/**
|
|
53
68
|
* Remote face of the memory library. All methods delegate to the core; the
|
|
54
69
|
* core carries the 生·用·修·记 mechanism semantics.
|
|
@@ -233,9 +248,38 @@ let MemoryService = (() => {
|
|
|
233
248
|
this.llm = llm;
|
|
234
249
|
this.defaultModel = defaultModel;
|
|
235
250
|
}
|
|
236
|
-
/** 生: refine a completed trajectory into an experience candidate.
|
|
237
|
-
|
|
238
|
-
|
|
251
|
+
/** 生: refine a completed trajectory into an experience candidate.
|
|
252
|
+
* Semantic-dedup: coarse word-overlap candidate set first; when it is
|
|
253
|
+
* non-empty the LLM judges [merge/duplicate/different] on the FULL lesson
|
|
254
|
+
* (gist+situation+path+reasoning+limits), replacing the mechanical gate. */
|
|
255
|
+
async refine(agent, request) {
|
|
256
|
+
const actor = actorOf(agent);
|
|
257
|
+
const cands = this.core.nearDuplicateCandidates(request.gist, request.situation, DEDUP_STATUSES, DEDUP_TOP_K);
|
|
258
|
+
if (cands.length === 0) {
|
|
259
|
+
// No coarse hit: accept via the mechanical gate (kept as no-LLM fallback).
|
|
260
|
+
return this.core.refine(request, actor);
|
|
261
|
+
}
|
|
262
|
+
const verdict = await this.dedupVerdictWithLlm(agent, request, cands);
|
|
263
|
+
if (verdict === 'different') {
|
|
264
|
+
// LLM judged semantically different: accept, skipping the mechanical gate
|
|
265
|
+
// that would otherwise over-reject on word overlap.
|
|
266
|
+
return this.core.refine(request, actor, { dedup: false });
|
|
267
|
+
}
|
|
268
|
+
const best = cands[0];
|
|
269
|
+
if (verdict === 'merge' && best.snapshot.approvedBy === 'human' && best.snapshot.status === 'live') {
|
|
270
|
+
const evolved = this.core.evolve({
|
|
271
|
+
id: best.snapshot.id,
|
|
272
|
+
reason: 'semantic-merge: ' + request.gist,
|
|
273
|
+
gist: request.gist,
|
|
274
|
+
situation: request.situation,
|
|
275
|
+
path: request.path,
|
|
276
|
+
reasoning: request.reasoning,
|
|
277
|
+
limits: request.limits,
|
|
278
|
+
}, actor);
|
|
279
|
+
return { accepted: false, reason: 'semantic-merge: merged into a human-approved live experience', corroboratedId: evolved.id };
|
|
280
|
+
}
|
|
281
|
+
this.core.markCorroborate(best.snapshot.id, actor, { family: request.family, gist: request.gist }, 'refine', best.score);
|
|
282
|
+
return { accepted: false, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead', corroboratedId: best.snapshot.id };
|
|
239
283
|
}
|
|
240
284
|
/** 用: recall + adjudication + injection budget + negative channel. */
|
|
241
285
|
recall(agent, request) {
|
|
@@ -245,9 +289,58 @@ let MemoryService = (() => {
|
|
|
245
289
|
report(agent, request) {
|
|
246
290
|
return this.core.report(request, actorOf(agent));
|
|
247
291
|
}
|
|
248
|
-
/** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
|
|
249
|
-
|
|
250
|
-
|
|
292
|
+
/** 摄取归一: source-agnostic intake; drafts become earned candidates (006 §1).
|
|
293
|
+
* Semantic-dedup per item: coarse word-overlap candidate set; when non-empty
|
|
294
|
+
* the LLM judges [merge/duplicate/different] on the FULL lesson. */
|
|
295
|
+
async ingest(agent, request) {
|
|
296
|
+
const actor = actorOf(agent);
|
|
297
|
+
// Source-authority prior, delegated to core so it stays the single truth.
|
|
298
|
+
const prior = this.core.ingest({ ...request, experiences: [] }, actor, { dedup: false }).sourcePrior;
|
|
299
|
+
const accepted = [];
|
|
300
|
+
const rejected = [];
|
|
301
|
+
const acceptedDedup = [];
|
|
302
|
+
for (const item of request.experiences) {
|
|
303
|
+
if (item.kind === 'negative' && (item.failureReason ?? '').trim() === '') {
|
|
304
|
+
rejected.push({ gist: item.gist, reason: 'rejected-schema: negative experiences must carry the confirmed failureReason' });
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const cands = this.core.nearDuplicateCandidates(item.gist, item.situation, DEDUP_STATUSES, DEDUP_TOP_K);
|
|
308
|
+
if (cands.length > 0) {
|
|
309
|
+
const best = cands[0];
|
|
310
|
+
const verdict = await this.dedupVerdictWithLlm(agent, item, cands);
|
|
311
|
+
if (verdict !== 'different') {
|
|
312
|
+
if (verdict === 'merge' && best.snapshot.approvedBy === 'human' && best.snapshot.status === 'live') {
|
|
313
|
+
this.core.evolve({
|
|
314
|
+
id: best.snapshot.id,
|
|
315
|
+
reason: 'semantic-merge: ' + item.gist,
|
|
316
|
+
gist: item.gist,
|
|
317
|
+
situation: item.situation,
|
|
318
|
+
path: item.path,
|
|
319
|
+
reasoning: item.reasoning,
|
|
320
|
+
limits: item.limits,
|
|
321
|
+
}, actor);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
this.core.markCorroborate(best.snapshot.id, actor, { family: item.family, gist: item.gist }, 'ingest', best.score);
|
|
325
|
+
}
|
|
326
|
+
rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of an existing experience; corroborated it instead' });
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// LLM judged different (or no coarse hit): accept. Within-batch dedup via tokenOverlap.
|
|
331
|
+
const tokens = new Set(tokenize([item.gist, ...item.situation].join(' ')));
|
|
332
|
+
if (acceptedDedup.some((prev) => tokenOverlap(tokens, prev) >= this.core.config.duplicateOverlapGate)) {
|
|
333
|
+
rejected.push({ gist: item.gist, reason: 'rejected-information-gain: near-duplicate of another item in this same ingest batch' });
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const itemResult = this.core.ingest({ ...request, experiences: [item] }, actor, { dedup: cands.length === 0 });
|
|
337
|
+
for (const a of itemResult.accepted)
|
|
338
|
+
accepted.push(a);
|
|
339
|
+
for (const r of itemResult.rejected)
|
|
340
|
+
rejected.push(r);
|
|
341
|
+
acceptedDedup.push(tokens);
|
|
342
|
+
}
|
|
343
|
+
return { accepted, rejected, sourcePrior: prior };
|
|
251
344
|
}
|
|
252
345
|
/** 修: propose a revised draft for a challenged experience. */
|
|
253
346
|
revise(agent, request) {
|
|
@@ -310,6 +403,62 @@ let MemoryService = (() => {
|
|
|
310
403
|
// Summarization failure is non-fatal — extraction proceeds without updated feedback.
|
|
311
404
|
}
|
|
312
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* LLM semantic-dedup verdict between a new lesson and a coarse candidate set.
|
|
408
|
+
* Reads the FULL lesson (gist+situation+path+reasoning+limits) on both sides
|
|
409
|
+
* and returns 'merge' | 'duplicate' | 'different'. Falls back to 'different'
|
|
410
|
+
* (accept) when the LLM is unavailable or the model route is missing, so the
|
|
411
|
+
* mechanical gate in core still governs without an LLM.
|
|
412
|
+
*/
|
|
413
|
+
async dedupVerdictWithLlm(agent, lesson, cands) {
|
|
414
|
+
// Try session-specific model first, fall back to the default model config.
|
|
415
|
+
const header = agent.session.requestHeader()?.config;
|
|
416
|
+
let provider = header?.provider;
|
|
417
|
+
let model = header?.model;
|
|
418
|
+
if (provider === undefined || model === undefined) {
|
|
419
|
+
const sel = this.defaultModel?.currentSelection();
|
|
420
|
+
if (sel !== undefined) {
|
|
421
|
+
provider = sel.provider;
|
|
422
|
+
model = sel.model;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (provider === undefined || model === undefined)
|
|
426
|
+
return 'different';
|
|
427
|
+
if (this.llm === undefined)
|
|
428
|
+
return 'different';
|
|
429
|
+
const fmtLesson = (l) => 'gist: ' + l.gist + '\nsituation: ' + l.situation.join(' | ') + '\npath:\n' +
|
|
430
|
+
l.path.map((p) => ' ' + p.order + '. ' + p.action).join('\n') +
|
|
431
|
+
'\nreasoning: ' + l.reasoning + '\nlimits: ' + l.limits.join(' | ');
|
|
432
|
+
const candText = cands.map((c, i) => '候选 ' + (i + 1) + '(词面相似度 ' + c.score.toFixed(3) + '):\n' + fmtLesson(c.snapshot)).join('\n\n');
|
|
433
|
+
const systemPrompt = '你是记忆系统的语义去重裁判。给定一条"新提炼的教训"和若干"已存在的候选经验",判断它们是否指同一件事。\n' +
|
|
434
|
+
'只输出一个词:\n- merge:语义可合并,新教训是对候选经验的增量补充\n- duplicate:语义重复,新教训无新增信息\n- different:语义不同,是不同的经验\n' +
|
|
435
|
+
'只输出 merge/duplicate/different 之一,不要任何其它内容。';
|
|
436
|
+
const userPrompt = '新提炼的教训:\n' + fmtLesson(lesson) + '\n\n候选经验:\n' + candText + '\n\n判定(merge/duplicate/different):';
|
|
437
|
+
const messages = [createUserMessage({
|
|
438
|
+
content: [{ type: 'text', text: userPrompt }],
|
|
439
|
+
source: { kind: 'plugin', plugin: 'dsh-daoing-memory' },
|
|
440
|
+
})];
|
|
441
|
+
let text = '';
|
|
442
|
+
const options = {
|
|
443
|
+
provider, model, messages, system: systemPrompt,
|
|
444
|
+
maxTokens: 50, sessionId: agent.session.id,
|
|
445
|
+
};
|
|
446
|
+
try {
|
|
447
|
+
for await (const chunk of this.llm.stream(options)) {
|
|
448
|
+
if (chunk.type === 'text-delta')
|
|
449
|
+
text += chunk.text;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return 'different';
|
|
454
|
+
}
|
|
455
|
+
const t = text.trim().toLowerCase();
|
|
456
|
+
if (t.includes('merge'))
|
|
457
|
+
return 'merge';
|
|
458
|
+
if (t.includes('duplicate'))
|
|
459
|
+
return 'duplicate';
|
|
460
|
+
return 'different';
|
|
461
|
+
}
|
|
313
462
|
/**
|
|
314
463
|
* Call the LLM to summarize deletion records into extraction feedback rules.
|
|
315
464
|
* Uses ctx.llm.stream() with the agent's current model route.
|
package/lib/types/store.d.ts
CHANGED
|
@@ -84,6 +84,16 @@ export declare class MemoryStore {
|
|
|
84
84
|
snapshot: ExperienceSnapshot;
|
|
85
85
|
score: number;
|
|
86
86
|
}[];
|
|
87
|
+
/**
|
|
88
|
+
* Near-duplicate candidate set for the information-gain gate (007 flow-log fix).
|
|
89
|
+
* Scores on gist+situation only (the lesson's identity) so identical lessons
|
|
90
|
+
* with divergent limits/paths still match. Returns the top-K matches (rough
|
|
91
|
+
* prefilter for the LLM semantic-dedup verdict), or [] when nothing matches.
|
|
92
|
+
*/
|
|
93
|
+
findNearDuplicates(queryTokens: Set<string>, statuses: ExperienceStatus[], topK: number): {
|
|
94
|
+
snapshot: ExperienceSnapshot;
|
|
95
|
+
score: number;
|
|
96
|
+
}[];
|
|
87
97
|
/**
|
|
88
98
|
* Near-duplicate detection for the information-gain gate (007 flow-log fix).
|
|
89
99
|
* Unlike recallCandidates it scores on gist+situation only (the lesson's
|
package/lib/types/store.js
CHANGED
|
@@ -362,6 +362,37 @@ export class MemoryStore {
|
|
|
362
362
|
scored.sort((a, b) => b.score - a.score);
|
|
363
363
|
return scored.slice(0, topK);
|
|
364
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Near-duplicate candidate set for the information-gain gate (007 flow-log fix).
|
|
367
|
+
* Scores on gist+situation only (the lesson's identity) so identical lessons
|
|
368
|
+
* with divergent limits/paths still match. Returns the top-K matches (rough
|
|
369
|
+
* prefilter for the LLM semantic-dedup verdict), or [] when nothing matches.
|
|
370
|
+
*/
|
|
371
|
+
findNearDuplicates(queryTokens, statuses, topK) {
|
|
372
|
+
if (queryTokens.size === 0 || topK <= 0)
|
|
373
|
+
return [];
|
|
374
|
+
const sql = `
|
|
375
|
+
SELECT * FROM experiences
|
|
376
|
+
WHERE deleted = 0 AND status IN (${statuses.map(() => '?').join(', ')})
|
|
377
|
+
`;
|
|
378
|
+
const rows = this.db.prepare(sql).all(...statuses);
|
|
379
|
+
const scored = [];
|
|
380
|
+
for (const row of rows) {
|
|
381
|
+
const snapshot = this.rowToExperience(row);
|
|
382
|
+
const hay = new Set(tokenize([snapshot.gist, ...snapshot.situation].join(' ')));
|
|
383
|
+
if (hay.size === 0)
|
|
384
|
+
continue;
|
|
385
|
+
let hits = 0;
|
|
386
|
+
for (const token of queryTokens)
|
|
387
|
+
if (hay.has(token))
|
|
388
|
+
hits += 1;
|
|
389
|
+
const score = hits / Math.sqrt(queryTokens.size * hay.size);
|
|
390
|
+
if (score > 0)
|
|
391
|
+
scored.push({ snapshot, score });
|
|
392
|
+
}
|
|
393
|
+
scored.sort((a, b) => b.score - a.score);
|
|
394
|
+
return scored.slice(0, topK);
|
|
395
|
+
}
|
|
365
396
|
/**
|
|
366
397
|
* Near-duplicate detection for the information-gain gate (007 flow-log fix).
|
|
367
398
|
* Unlike recallCandidates it scores on gist+situation only (the lesson's
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-daoing-memory",
|
|
3
3
|
"description": "Self-evolving memory for DeepSeek Harness (DSH): earned experiences, diary/fact semantic memory, concern tracking, and an append-only audit ledger.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.19",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "daoing",
|