@graphorin/memory 0.5.0
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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/dist/conflict/index.d.ts +3 -0
- package/dist/conflict/index.js +7 -0
- package/dist/conflict/locale-packs/en.js +262 -0
- package/dist/conflict/locale-packs/en.js.map +1 -0
- package/dist/conflict/locale-packs/index.js +4 -0
- package/dist/conflict/locale-packs/types.d.ts +103 -0
- package/dist/conflict/locale-packs/types.d.ts.map +1 -0
- package/dist/conflict/locale-packs/types.js +55 -0
- package/dist/conflict/locale-packs/types.js.map +1 -0
- package/dist/conflict/pipeline.d.ts +35 -0
- package/dist/conflict/pipeline.d.ts.map +1 -0
- package/dist/conflict/pipeline.js +257 -0
- package/dist/conflict/pipeline.js.map +1 -0
- package/dist/conflict/stages/helpers.js +66 -0
- package/dist/conflict/stages/helpers.js.map +1 -0
- package/dist/conflict/stages/stage1-exact-dedup.js +21 -0
- package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -0
- package/dist/conflict/stages/stage2-embedding-three-zone.js +37 -0
- package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -0
- package/dist/conflict/stages/stage3-heuristic-regex.js +45 -0
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -0
- package/dist/conflict/stages/stage4-subject-predicate.js +31 -0
- package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -0
- package/dist/conflict/stages/stage5-defer-to-deep.js +22 -0
- package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -0
- package/dist/conflict/types.d.ts +222 -0
- package/dist/conflict/types.d.ts.map +1 -0
- package/dist/conflict/types.js +50 -0
- package/dist/conflict/types.js.map +1 -0
- package/dist/consolidator/budget.d.ts +1 -0
- package/dist/consolidator/budget.js +214 -0
- package/dist/consolidator/budget.js.map +1 -0
- package/dist/consolidator/decay.d.ts +35 -0
- package/dist/consolidator/decay.d.ts.map +1 -0
- package/dist/consolidator/decay.js +91 -0
- package/dist/consolidator/decay.js.map +1 -0
- package/dist/consolidator/dlq.js +58 -0
- package/dist/consolidator/dlq.js.map +1 -0
- package/dist/consolidator/errors.d.ts +54 -0
- package/dist/consolidator/errors.d.ts.map +1 -0
- package/dist/consolidator/errors.js +67 -0
- package/dist/consolidator/errors.js.map +1 -0
- package/dist/consolidator/idempotency.d.ts +1 -0
- package/dist/consolidator/idempotency.js +22 -0
- package/dist/consolidator/idempotency.js.map +1 -0
- package/dist/consolidator/index.d.ts +34 -0
- package/dist/consolidator/index.d.ts.map +1 -0
- package/dist/consolidator/index.js +163 -0
- package/dist/consolidator/index.js.map +1 -0
- package/dist/consolidator/lock.d.ts +2 -0
- package/dist/consolidator/lock.js +89 -0
- package/dist/consolidator/lock.js.map +1 -0
- package/dist/consolidator/noise-filter.d.ts +2 -0
- package/dist/consolidator/noise-filter.js +173 -0
- package/dist/consolidator/noise-filter.js.map +1 -0
- package/dist/consolidator/phases/deep.d.ts +4 -0
- package/dist/consolidator/phases/deep.js +198 -0
- package/dist/consolidator/phases/deep.js.map +1 -0
- package/dist/consolidator/phases/induce.d.ts +154 -0
- package/dist/consolidator/phases/induce.d.ts.map +1 -0
- package/dist/consolidator/phases/induce.js +253 -0
- package/dist/consolidator/phases/induce.js.map +1 -0
- package/dist/consolidator/phases/light.d.ts +4 -0
- package/dist/consolidator/phases/light.js +99 -0
- package/dist/consolidator/phases/light.js.map +1 -0
- package/dist/consolidator/phases/reflect.js +287 -0
- package/dist/consolidator/phases/reflect.js.map +1 -0
- package/dist/consolidator/phases/standard.d.ts +8 -0
- package/dist/consolidator/phases/standard.js +499 -0
- package/dist/consolidator/phases/standard.js.map +1 -0
- package/dist/consolidator/reconcile.js +192 -0
- package/dist/consolidator/reconcile.js.map +1 -0
- package/dist/consolidator/runtime.d.ts +62 -0
- package/dist/consolidator/runtime.d.ts.map +1 -0
- package/dist/consolidator/runtime.js +580 -0
- package/dist/consolidator/runtime.js.map +1 -0
- package/dist/consolidator/scheduler.d.ts +111 -0
- package/dist/consolidator/scheduler.d.ts.map +1 -0
- package/dist/consolidator/scheduler.js +71 -0
- package/dist/consolidator/scheduler.js.map +1 -0
- package/dist/consolidator/triggers.d.ts +40 -0
- package/dist/consolidator/triggers.d.ts.map +1 -0
- package/dist/consolidator/triggers.js +94 -0
- package/dist/consolidator/triggers.js.map +1 -0
- package/dist/consolidator/types.d.ts +397 -0
- package/dist/consolidator/types.d.ts.map +1 -0
- package/dist/consolidator/types.js +117 -0
- package/dist/consolidator/types.js.map +1 -0
- package/dist/context-engine/annotations.d.ts +118 -0
- package/dist/context-engine/annotations.d.ts.map +1 -0
- package/dist/context-engine/annotations.js +102 -0
- package/dist/context-engine/annotations.js.map +1 -0
- package/dist/context-engine/auto-recall.d.ts +52 -0
- package/dist/context-engine/auto-recall.d.ts.map +1 -0
- package/dist/context-engine/auto-recall.js +41 -0
- package/dist/context-engine/auto-recall.js.map +1 -0
- package/dist/context-engine/compaction/clear-tool-results.d.ts +55 -0
- package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -0
- package/dist/context-engine/compaction/clear-tool-results.js +97 -0
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -0
- package/dist/context-engine/compaction/compactor.d.ts +52 -0
- package/dist/context-engine/compaction/compactor.d.ts.map +1 -0
- package/dist/context-engine/compaction/compactor.js +196 -0
- package/dist/context-engine/compaction/compactor.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts +15 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +27 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts +18 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +47 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts +15 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +32 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -0
- package/dist/context-engine/compaction/hooks/types.d.ts +42 -0
- package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -0
- package/dist/context-engine/compaction/index.d.ts +9 -0
- package/dist/context-engine/compaction/index.js +9 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +84 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -0
- package/dist/context-engine/compaction/templates/summary-9-section.js +84 -0
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -0
- package/dist/context-engine/compaction/thresholds.d.ts +68 -0
- package/dist/context-engine/compaction/thresholds.d.ts.map +1 -0
- package/dist/context-engine/compaction/thresholds.js +63 -0
- package/dist/context-engine/compaction/thresholds.js.map +1 -0
- package/dist/context-engine/compaction/types.d.ts +202 -0
- package/dist/context-engine/compaction/types.d.ts.map +1 -0
- package/dist/context-engine/engine.d.ts +258 -0
- package/dist/context-engine/engine.d.ts.map +1 -0
- package/dist/context-engine/engine.js +424 -0
- package/dist/context-engine/engine.js.map +1 -0
- package/dist/context-engine/index.d.ts +68 -0
- package/dist/context-engine/index.d.ts.map +1 -0
- package/dist/context-engine/index.js +24 -0
- package/dist/context-engine/locale-packs/en.d.ts +14 -0
- package/dist/context-engine/locale-packs/en.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/en.js +87 -0
- package/dist/context-engine/locale-packs/en.js.map +1 -0
- package/dist/context-engine/locale-packs/index.js +4 -0
- package/dist/context-engine/locale-packs/resolver.d.ts +41 -0
- package/dist/context-engine/locale-packs/resolver.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/resolver.js +119 -0
- package/dist/context-engine/locale-packs/resolver.js.map +1 -0
- package/dist/context-engine/locale-packs/types.d.ts +119 -0
- package/dist/context-engine/locale-packs/types.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/types.js +24 -0
- package/dist/context-engine/locale-packs/types.js.map +1 -0
- package/dist/context-engine/metadata.d.ts +40 -0
- package/dist/context-engine/metadata.d.ts.map +1 -0
- package/dist/context-engine/metadata.js +66 -0
- package/dist/context-engine/metadata.js.map +1 -0
- package/dist/context-engine/preambles/inbound-en.d.ts +20 -0
- package/dist/context-engine/preambles/inbound-en.d.ts.map +1 -0
- package/dist/context-engine/preambles/inbound-en.js +23 -0
- package/dist/context-engine/preambles/inbound-en.js.map +1 -0
- package/dist/context-engine/privacy-filter.d.ts +108 -0
- package/dist/context-engine/privacy-filter.d.ts.map +1 -0
- package/dist/context-engine/privacy-filter.js +113 -0
- package/dist/context-engine/privacy-filter.js.map +1 -0
- package/dist/context-engine/templates/base-en.d.ts +19 -0
- package/dist/context-engine/templates/base-en.d.ts.map +1 -0
- package/dist/context-engine/templates/base-en.js +22 -0
- package/dist/context-engine/templates/base-en.js.map +1 -0
- package/dist/context-engine/templates/composer.d.ts +55 -0
- package/dist/context-engine/templates/composer.d.ts.map +1 -0
- package/dist/context-engine/templates/composer.js +51 -0
- package/dist/context-engine/templates/composer.js.map +1 -0
- package/dist/context-engine/token-budget.d.ts +99 -0
- package/dist/context-engine/token-budget.d.ts.map +1 -0
- package/dist/context-engine/token-budget.js +211 -0
- package/dist/context-engine/token-budget.js.map +1 -0
- package/dist/context-engine/token-counter.d.ts +54 -0
- package/dist/context-engine/token-counter.d.ts.map +1 -0
- package/dist/context-engine/token-counter.js +81 -0
- package/dist/context-engine/token-counter.js.map +1 -0
- package/dist/errors/index.d.ts +171 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +206 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/facade.d.ts +278 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +265 -0
- package/dist/facade.js.map +1 -0
- package/dist/graph/entity-resolver.d.ts +135 -0
- package/dist/graph/entity-resolver.d.ts.map +1 -0
- package/dist/graph/entity-resolver.js +202 -0
- package/dist/graph/entity-resolver.js.map +1 -0
- package/dist/graph/index.d.ts +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/contextualize.d.ts +14 -0
- package/dist/internal/contextualize.d.ts.map +1 -0
- package/dist/internal/contextualize.js +110 -0
- package/dist/internal/contextualize.js.map +1 -0
- package/dist/internal/embedder-binding.js +50 -0
- package/dist/internal/embedder-binding.js.map +1 -0
- package/dist/internal/id.js +24 -0
- package/dist/internal/id.js.map +1 -0
- package/dist/internal/injection-heuristics.js +64 -0
- package/dist/internal/injection-heuristics.js.map +1 -0
- package/dist/internal/spans.js +35 -0
- package/dist/internal/spans.js.map +1 -0
- package/dist/internal/storage-adapter.d.ts +742 -0
- package/dist/internal/storage-adapter.d.ts.map +1 -0
- package/dist/migration/embedder-migration.d.ts +124 -0
- package/dist/migration/embedder-migration.d.ts.map +1 -0
- package/dist/migration/embedder-migration.js +109 -0
- package/dist/migration/embedder-migration.js.map +1 -0
- package/dist/migration/index.d.ts +2 -0
- package/dist/migration/index.js +3 -0
- package/dist/search/explain.d.ts +74 -0
- package/dist/search/explain.d.ts.map +1 -0
- package/dist/search/explain.js +50 -0
- package/dist/search/explain.js.map +1 -0
- package/dist/search/index.d.ts +6 -0
- package/dist/search/index.js +6 -0
- package/dist/search/iterative.d.ts +223 -0
- package/dist/search/iterative.d.ts.map +1 -0
- package/dist/search/iterative.js +330 -0
- package/dist/search/iterative.js.map +1 -0
- package/dist/search/query-transform.d.ts +112 -0
- package/dist/search/query-transform.d.ts.map +1 -0
- package/dist/search/query-transform.js +184 -0
- package/dist/search/query-transform.js.map +1 -0
- package/dist/search/rrf.d.ts +95 -0
- package/dist/search/rrf.d.ts.map +1 -0
- package/dist/search/rrf.js +166 -0
- package/dist/search/rrf.js.map +1 -0
- package/dist/search/types.d.ts +50 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/tiers/episodic-memory.d.ts +142 -0
- package/dist/tiers/episodic-memory.d.ts.map +1 -0
- package/dist/tiers/episodic-memory.js +254 -0
- package/dist/tiers/episodic-memory.js.map +1 -0
- package/dist/tiers/index.d.ts +8 -0
- package/dist/tiers/index.js +9 -0
- package/dist/tiers/insight-memory.d.ts +71 -0
- package/dist/tiers/insight-memory.d.ts.map +1 -0
- package/dist/tiers/insight-memory.js +133 -0
- package/dist/tiers/insight-memory.js.map +1 -0
- package/dist/tiers/procedural-memory.d.ts +147 -0
- package/dist/tiers/procedural-memory.d.ts.map +1 -0
- package/dist/tiers/procedural-memory.js +267 -0
- package/dist/tiers/procedural-memory.js.map +1 -0
- package/dist/tiers/semantic-memory.d.ts +461 -0
- package/dist/tiers/semantic-memory.d.ts.map +1 -0
- package/dist/tiers/semantic-memory.js +631 -0
- package/dist/tiers/semantic-memory.js.map +1 -0
- package/dist/tiers/session-memory.d.ts +117 -0
- package/dist/tiers/session-memory.d.ts.map +1 -0
- package/dist/tiers/session-memory.js +163 -0
- package/dist/tiers/session-memory.js.map +1 -0
- package/dist/tiers/shared-memory.d.ts +29 -0
- package/dist/tiers/shared-memory.d.ts.map +1 -0
- package/dist/tiers/shared-memory.js +58 -0
- package/dist/tiers/shared-memory.js.map +1 -0
- package/dist/tiers/working-memory.d.ts +129 -0
- package/dist/tiers/working-memory.d.ts.map +1 -0
- package/dist/tiers/working-memory.js +229 -0
- package/dist/tiers/working-memory.js.map +1 -0
- package/dist/tools/block-tools.d.ts +98 -0
- package/dist/tools/block-tools.d.ts.map +1 -0
- package/dist/tools/block-tools.js +114 -0
- package/dist/tools/block-tools.js.map +1 -0
- package/dist/tools/fact-tools.d.ts +294 -0
- package/dist/tools/fact-tools.d.ts.map +1 -0
- package/dist/tools/fact-tools.js +293 -0
- package/dist/tools/fact-tools.js.map +1 -0
- package/dist/tools/index.d.ts +35 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/recall-tools.d.ts +209 -0
- package/dist/tools/recall-tools.d.ts.map +1 -0
- package/dist/tools/recall-tools.js +170 -0
- package/dist/tools/recall-tools.js.map +1 -0
- package/dist/tools/types.d.ts +40 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/package.json +119 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import { withMemorySpan } from "../internal/spans.js";
|
|
2
|
+
import { QuarantinePromotionRefusedError } from "../errors/index.js";
|
|
3
|
+
import { contextualize } from "../internal/contextualize.js";
|
|
4
|
+
import { newMemoryId } from "../internal/id.js";
|
|
5
|
+
import { DEFAULT_MAX_ITERATIONS, runIterativeRetrieval } from "../search/iterative.js";
|
|
6
|
+
import { WeightedRRFReranker, fuseRrf, fuseWeighted } from "../search/rrf.js";
|
|
7
|
+
import { detectMemoryInjection } from "../internal/injection-heuristics.js";
|
|
8
|
+
import { explainRecall } from "../search/explain.js";
|
|
9
|
+
import { md5 } from "@graphorin/core";
|
|
10
|
+
|
|
11
|
+
//#region src/tiers/semantic-memory.ts
|
|
12
|
+
/**
|
|
13
|
+
* `SemanticMemory` — long-term factual store. Hybrid (vector + FTS5)
|
|
14
|
+
* search merges the two ranked lists through the configured
|
|
15
|
+
* {@link ReRanker} (default {@link RRFReranker} with `k = 60`).
|
|
16
|
+
*
|
|
17
|
+
* Phase 10a wrote facts straight through with MD5 deduplication;
|
|
18
|
+
* Phase 10b routes every `remember(...)` call through the multi-stage
|
|
19
|
+
* conflict resolution pipeline (DEC-117 / ADR-018 ext / RB-02). The
|
|
20
|
+
* pipeline can be disabled per-call (`pipeline: 'off'`) or per-`Memory`
|
|
21
|
+
* instance (`createMemory({ conflictPipeline: { mode: 'off' } })`).
|
|
22
|
+
*
|
|
23
|
+
* @stable
|
|
24
|
+
*/
|
|
25
|
+
var SemanticMemory = class {
|
|
26
|
+
#store;
|
|
27
|
+
#tracer;
|
|
28
|
+
#embedder;
|
|
29
|
+
#embedderIdProvider;
|
|
30
|
+
#pipeline;
|
|
31
|
+
#contextualMode;
|
|
32
|
+
#queryTransformer;
|
|
33
|
+
#entityResolver;
|
|
34
|
+
#grader;
|
|
35
|
+
#iterativeMaxIterations;
|
|
36
|
+
#reranker;
|
|
37
|
+
constructor(args) {
|
|
38
|
+
this.#store = args.store;
|
|
39
|
+
this.#tracer = args.tracer;
|
|
40
|
+
this.#embedder = args.embedder;
|
|
41
|
+
this.#embedderIdProvider = args.embedderIdProvider;
|
|
42
|
+
this.#reranker = args.reranker;
|
|
43
|
+
this.#pipeline = args.conflictPipeline ?? null;
|
|
44
|
+
this.#contextualMode = args.contextualRetrieval ?? "late-chunk";
|
|
45
|
+
this.#queryTransformer = args.queryTransformer ?? null;
|
|
46
|
+
this.#entityResolver = args.entityResolver ?? null;
|
|
47
|
+
this.#grader = args.grader ?? null;
|
|
48
|
+
this.#iterativeMaxIterations = args.iterativeMaxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
49
|
+
}
|
|
50
|
+
/** Replace the active reranker. Returns the previous instance. */
|
|
51
|
+
setReranker(reranker) {
|
|
52
|
+
const previous = this.#reranker;
|
|
53
|
+
this.#reranker = reranker;
|
|
54
|
+
return previous;
|
|
55
|
+
}
|
|
56
|
+
/** Currently active reranker. */
|
|
57
|
+
reranker() {
|
|
58
|
+
return this.#reranker;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Persist a fact. Returns the stored record. Phase 10b routes every
|
|
62
|
+
* call through the multi-stage conflict resolution pipeline; the
|
|
63
|
+
* legacy straight-through path is reachable per-call via
|
|
64
|
+
* `{ pipeline: 'off' }` (operators may disable the pipeline globally
|
|
65
|
+
* via `createMemory({ conflictPipeline: { mode: 'off' } })`).
|
|
66
|
+
*/
|
|
67
|
+
async remember(scope, input, options = {}) {
|
|
68
|
+
const { fact } = await this.rememberWithDecision(scope, input, options);
|
|
69
|
+
return fact;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Like {@link remember} but returns the pipeline `decision` alongside
|
|
73
|
+
* the stored fact. Useful for callers that need to distinguish
|
|
74
|
+
* silent dedups (`decision.kind === 'dedup'`) from fresh inserts.
|
|
75
|
+
*
|
|
76
|
+
* @stable
|
|
77
|
+
*/
|
|
78
|
+
async rememberWithDecision(scope, input, options = {}) {
|
|
79
|
+
return withMemorySpan(this.#tracer, "memory.write.semantic", scope, {}, async (span) => {
|
|
80
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
81
|
+
const text = input.text;
|
|
82
|
+
const provenance = input.provenance;
|
|
83
|
+
const synthesized = provenance === "extraction" || provenance === "reflection" || provenance === "induction";
|
|
84
|
+
const injection = detectMemoryInjection(text);
|
|
85
|
+
const autoPromote = options.autoPromoteSynthesized === true;
|
|
86
|
+
const status = injection.flagged ? "quarantined" : synthesized && !autoPromote ? "quarantined" : "active";
|
|
87
|
+
const quarantineReason = status === "quarantined" ? injection.flagged ? "injection" : "synthesized" : void 0;
|
|
88
|
+
const fact = {
|
|
89
|
+
id: newMemoryId("fact"),
|
|
90
|
+
kind: "semantic",
|
|
91
|
+
userId: scope.userId,
|
|
92
|
+
...scope.sessionId !== void 0 ? { sessionId: scope.sessionId } : {},
|
|
93
|
+
...scope.agentId !== void 0 ? { agentId: scope.agentId } : {},
|
|
94
|
+
sensitivity: input.sensitivity ?? "internal",
|
|
95
|
+
text,
|
|
96
|
+
...input.subject !== void 0 ? { subject: input.subject } : {},
|
|
97
|
+
...input.predicate !== void 0 ? { predicate: input.predicate } : {},
|
|
98
|
+
...input.object !== void 0 ? { object: input.object } : {},
|
|
99
|
+
...input.confidence !== void 0 ? { confidence: input.confidence } : {},
|
|
100
|
+
...typeof input.importance === "number" && Number.isFinite(input.importance) ? { importance: Math.min(1, Math.max(0, input.importance)) } : {},
|
|
101
|
+
...input.tags !== void 0 ? { tags: Object.freeze([...input.tags]) } : {},
|
|
102
|
+
...input.validFrom !== void 0 ? { validFrom: input.validFrom } : { validFrom: now },
|
|
103
|
+
...input.validTo !== void 0 ? { validTo: input.validTo } : {},
|
|
104
|
+
...input.supersedes !== void 0 ? { supersedes: input.supersedes } : {},
|
|
105
|
+
...provenance !== void 0 ? { provenance } : {},
|
|
106
|
+
status,
|
|
107
|
+
createdAt: now,
|
|
108
|
+
updatedAt: now
|
|
109
|
+
};
|
|
110
|
+
const embedderId = this.#embedderIdProvider();
|
|
111
|
+
const embedder = this.#embedder;
|
|
112
|
+
const indexText = this.#resolveIndexText(input, fact.text, options.indexText);
|
|
113
|
+
const pipelineEnabled = this.#pipeline !== null && (options.pipeline ?? "on") === "on";
|
|
114
|
+
const decision = pipelineEnabled && this.#pipeline !== null ? await this.#pipeline.run({
|
|
115
|
+
store: this.#store,
|
|
116
|
+
tracer: this.#tracer,
|
|
117
|
+
embedder,
|
|
118
|
+
embedderId,
|
|
119
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
120
|
+
}, fact) : {
|
|
121
|
+
kind: "admit",
|
|
122
|
+
stage: "exact-dedup",
|
|
123
|
+
reason: "pipeline-skipped"
|
|
124
|
+
};
|
|
125
|
+
span.setAttributes({
|
|
126
|
+
"memory.semantic.text_length": text.length,
|
|
127
|
+
"memory.semantic.hash": md5(text),
|
|
128
|
+
"memory.semantic.pipeline.decision": decision.kind,
|
|
129
|
+
"memory.semantic.pipeline.stage": decision.stage,
|
|
130
|
+
"memory.semantic.pipeline.enabled": pipelineEnabled,
|
|
131
|
+
"memory.semantic.status": status,
|
|
132
|
+
"memory.semantic.contextualized": indexText !== text,
|
|
133
|
+
...provenance !== void 0 ? { "memory.semantic.provenance": provenance } : {},
|
|
134
|
+
"memory.semantic.injection_flagged": injection.flagged,
|
|
135
|
+
...injection.flagged ? { "memory.semantic.injection_markers": injection.markers.join(",") } : {}
|
|
136
|
+
});
|
|
137
|
+
const reasonField = quarantineReason !== void 0 ? { quarantineReason } : {};
|
|
138
|
+
switch (decision.kind) {
|
|
139
|
+
case "admit":
|
|
140
|
+
case "pending":
|
|
141
|
+
await this.#commitFact(fact, embedder, embedderId, indexText);
|
|
142
|
+
await this.#linkEntities(scope, fact, options.signal);
|
|
143
|
+
return {
|
|
144
|
+
fact,
|
|
145
|
+
decision,
|
|
146
|
+
...reasonField
|
|
147
|
+
};
|
|
148
|
+
case "dedup": return {
|
|
149
|
+
fact: await this.#fetchExisting(decision.existingId) ?? fact,
|
|
150
|
+
decision
|
|
151
|
+
};
|
|
152
|
+
case "supersede":
|
|
153
|
+
await this.#commitFact(fact, embedder, embedderId, indexText);
|
|
154
|
+
await this.#store.semantic.supersede(decision.existingId, fact, decision.reason);
|
|
155
|
+
await this.#linkEntities(scope, fact, options.signal);
|
|
156
|
+
return {
|
|
157
|
+
fact,
|
|
158
|
+
decision,
|
|
159
|
+
...reasonField
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Resolve the contextual-retrieval index text (P1-3). A caller
|
|
166
|
+
* override (the consolidator's `'llm'` mode) takes precedence; then
|
|
167
|
+
* the instance mode decides. Late-chunk derives the situating context
|
|
168
|
+
* from the *author-supplied* signals on `input` (so an extraction's
|
|
169
|
+
* subject/predicate/object survive even though the persisted `Fact`
|
|
170
|
+
* drops them) and never from framework-defaulted fields, so a plain
|
|
171
|
+
* `remember({ text })` write returns the canonical text unchanged.
|
|
172
|
+
*/
|
|
173
|
+
#resolveIndexText(input, canonicalText, override) {
|
|
174
|
+
if (override !== void 0) return override;
|
|
175
|
+
if (this.#contextualMode === "off") return canonicalText;
|
|
176
|
+
return contextualize({
|
|
177
|
+
text: canonicalText,
|
|
178
|
+
...input.subject !== void 0 ? { subject: input.subject } : {},
|
|
179
|
+
...input.predicate !== void 0 ? { predicate: input.predicate } : {},
|
|
180
|
+
...input.object !== void 0 ? { object: input.object } : {},
|
|
181
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
182
|
+
...input.validFrom !== void 0 ? { validFrom: input.validFrom } : {}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async #commitFact(fact, embedder, embedderId, indexText) {
|
|
186
|
+
const adapterSupportsEmbeddedWrite = typeof this.#store.semantic.rememberWithEmbedding === "function";
|
|
187
|
+
const contextualized = indexText !== fact.text;
|
|
188
|
+
if (embedder !== null && embedderId !== null && adapterSupportsEmbeddedWrite) {
|
|
189
|
+
const [vector] = await embedder.embed([indexText], { taskType: "passage" });
|
|
190
|
+
if (vector !== void 0 && this.#store.semantic.rememberWithEmbedding !== void 0) {
|
|
191
|
+
await this.#store.semantic.rememberWithEmbedding(fact, {
|
|
192
|
+
embedding: {
|
|
193
|
+
embedderId,
|
|
194
|
+
vector
|
|
195
|
+
},
|
|
196
|
+
...contextualized ? { indexText } : {}
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (contextualized && adapterSupportsEmbeddedWrite && this.#store.semantic.rememberWithEmbedding !== void 0) {
|
|
202
|
+
await this.#store.semantic.rememberWithEmbedding(fact, { indexText });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await this.#store.semantic.remember(fact);
|
|
206
|
+
}
|
|
207
|
+
async #fetchExisting(factId) {
|
|
208
|
+
if (typeof this.#store.semantic.get !== "function") return null;
|
|
209
|
+
return this.#store.semantic.get(factId);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* P2-1: resolve the fact's subject / object to canonical entities and
|
|
213
|
+
* link them so one-hop expansion can traverse this fact. A no-op when
|
|
214
|
+
* no resolver is configured (the default) or the fact has no s/p/o, and
|
|
215
|
+
* resilient — a resolution failure never breaks the write that just
|
|
216
|
+
* committed.
|
|
217
|
+
*/
|
|
218
|
+
async #linkEntities(scope, fact, signal) {
|
|
219
|
+
if (this.#entityResolver === null) return;
|
|
220
|
+
if (fact.subject === void 0 && fact.object === void 0) return;
|
|
221
|
+
try {
|
|
222
|
+
await this.#entityResolver.linkFact(scope, fact, { ...signal !== void 0 ? { signal } : {} });
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
/** Hybrid (vector + FTS5) search merged through the configured reranker. */
|
|
226
|
+
async search(scope, query, opts = {}) {
|
|
227
|
+
return withMemorySpan(this.#tracer, "memory.search.semantic", scope, { "memory.search.query_length": query.length }, async (span) => {
|
|
228
|
+
const candidateTopK = opts.candidateTopK ?? 60;
|
|
229
|
+
const finalTopK = opts.topK ?? 10;
|
|
230
|
+
const weighted = opts.fusion !== void 0 && opts.fusion.strategy === "weighted" ? opts.fusion : null;
|
|
231
|
+
const wFts = weighted?.weights.fts ?? 1;
|
|
232
|
+
const wVector = weighted?.weights.vector ?? 1;
|
|
233
|
+
const queries = await this.#expandQueries(query, opts);
|
|
234
|
+
const lists = [];
|
|
235
|
+
const listWeights = [];
|
|
236
|
+
const listLabels = [];
|
|
237
|
+
let primaryFtsCount = 0;
|
|
238
|
+
let primaryVectorCount = 0;
|
|
239
|
+
let isPrimary = true;
|
|
240
|
+
for (const q of queries) {
|
|
241
|
+
const ftsHits = await this.#store.semantic.search(scope, {
|
|
242
|
+
query: q,
|
|
243
|
+
topK: candidateTopK,
|
|
244
|
+
...opts.asOf !== void 0 ? { asOf: opts.asOf } : {},
|
|
245
|
+
...opts.tags !== void 0 && opts.tags.length > 0 ? { tags: opts.tags } : {},
|
|
246
|
+
...opts.includeQuarantined === true ? { includeQuarantined: true } : {},
|
|
247
|
+
...opts.signal !== void 0 ? { signal: opts.signal } : {}
|
|
248
|
+
});
|
|
249
|
+
lists.push(ftsHits);
|
|
250
|
+
listWeights.push(wFts);
|
|
251
|
+
listLabels.push(`fts_${queries.indexOf(q)}`);
|
|
252
|
+
const vectorHits = await this.#tryVectorSearch(scope, q, candidateTopK, opts.asOf, opts.includeQuarantined);
|
|
253
|
+
if (vectorHits.length > 0) {
|
|
254
|
+
lists.push(vectorHits);
|
|
255
|
+
listWeights.push(wVector);
|
|
256
|
+
listLabels.push(`vector_${queries.indexOf(q)}`);
|
|
257
|
+
}
|
|
258
|
+
if (isPrimary) {
|
|
259
|
+
primaryFtsCount = ftsHits.length;
|
|
260
|
+
primaryVectorCount = vectorHits.length;
|
|
261
|
+
isPrimary = false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const hydeHits = await this.#tryHyde(scope, query, opts, candidateTopK);
|
|
265
|
+
if (hydeHits.length > 0) {
|
|
266
|
+
lists.push(hydeHits);
|
|
267
|
+
listWeights.push(wVector);
|
|
268
|
+
listLabels.push("hyde");
|
|
269
|
+
}
|
|
270
|
+
const graphHits = await this.#tryExpandHops(scope, lists, opts, candidateTopK);
|
|
271
|
+
if (graphHits.length > 0) {
|
|
272
|
+
lists.push(graphHits);
|
|
273
|
+
listWeights.push(1);
|
|
274
|
+
listLabels.push("graph");
|
|
275
|
+
}
|
|
276
|
+
const reranker = weighted !== null ? new WeightedRRFReranker({
|
|
277
|
+
weights: listWeights,
|
|
278
|
+
...weighted.k !== void 0 ? { k: weighted.k } : {}
|
|
279
|
+
}) : this.#reranker;
|
|
280
|
+
const fusedTopK = opts.decay !== void 0 ? Math.max(finalTopK, lists.reduce((n, l) => n + l.length, 0)) : finalTopK;
|
|
281
|
+
const fused = await reranker.rerank(query, lists, {
|
|
282
|
+
topK: fusedTopK,
|
|
283
|
+
labels: listLabels,
|
|
284
|
+
...opts.signal !== void 0 ? { signal: opts.signal } : {}
|
|
285
|
+
});
|
|
286
|
+
const decayed = await this.#applyDecay(scope, fused, opts.decay);
|
|
287
|
+
const ranked = (opts.tags !== void 0 && opts.tags.length > 0 ? decayed.filter((h) => {
|
|
288
|
+
const recordTags = h.record.tags;
|
|
289
|
+
if (recordTags === void 0 || recordTags.length === 0) return false;
|
|
290
|
+
return opts.tags?.some((t) => recordTags.includes(t)) === true;
|
|
291
|
+
}) : decayed).slice(0, finalTopK);
|
|
292
|
+
if (ranked.length > 0 && typeof this.#store.semantic.markAccessed === "function") try {
|
|
293
|
+
await this.#store.semantic.markAccessed(ranked.map((h) => h.record.id));
|
|
294
|
+
} catch {}
|
|
295
|
+
const explanation = explainRecall(ranked, {
|
|
296
|
+
query,
|
|
297
|
+
rerankerId: reranker.id
|
|
298
|
+
});
|
|
299
|
+
span.setAttributes({
|
|
300
|
+
"memory.search.semantic.fts_count": primaryFtsCount,
|
|
301
|
+
"memory.search.semantic.vector_count": primaryVectorCount,
|
|
302
|
+
"memory.search.semantic.final_count": ranked.length,
|
|
303
|
+
"memory.search.semantic.reranker_id": reranker.id,
|
|
304
|
+
"memory.search.semantic.decay_applied": opts.decay !== void 0,
|
|
305
|
+
"memory.search.semantic.query_count": queries.length,
|
|
306
|
+
"memory.search.semantic.hyde_applied": hydeHits.length > 0,
|
|
307
|
+
"memory.search.semantic.expand_hops": opts.expandHops ?? 0,
|
|
308
|
+
"memory.search.semantic.graph_count": graphHits.length,
|
|
309
|
+
"memory.search.semantic.explain": JSON.stringify(explanation.results),
|
|
310
|
+
...opts.asOf !== void 0 ? { "memory.search.semantic.as_of": opts.asOf } : {},
|
|
311
|
+
...opts.includeQuarantined === true ? { "memory.search.semantic.include_quarantined": true } : {}
|
|
312
|
+
});
|
|
313
|
+
return ranked;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Gated, iterative ("deep") recall for hard queries (P2-4). A cheap
|
|
318
|
+
* local heuristic ({@link assessQueryDifficulty}) decides whether the
|
|
319
|
+
* query is even a loop candidate; simple lookups take exactly one
|
|
320
|
+
* {@link search} pass and make no provider call. For a query judged
|
|
321
|
+
* hard *and* with a grader configured
|
|
322
|
+
* (`createMemory({ iterativeRetrieval })`), the retrieved set is graded
|
|
323
|
+
* for sufficiency and, when weak, the query is reformulated and
|
|
324
|
+
* retrieved again — **widening to one-hop graph expansion**
|
|
325
|
+
* (`expandHops: 1`) on each reformulation pass — up to `maxIterations`
|
|
326
|
+
* (hard-capped at 5). When still insufficient it returns
|
|
327
|
+
* `abstained: true` so the caller can decline to answer instead of
|
|
328
|
+
* confabulating.
|
|
329
|
+
*
|
|
330
|
+
* Without a grader (the offline default) this degrades to a single,
|
|
331
|
+
* difficulty-gated `search` and never calls a provider.
|
|
332
|
+
*
|
|
333
|
+
* @stable
|
|
334
|
+
*/
|
|
335
|
+
async searchIterative(scope, query, opts = {}) {
|
|
336
|
+
return withMemorySpan(this.#tracer, "memory.search.semantic", scope, { "memory.search.query_length": query.length }, async (span) => {
|
|
337
|
+
const result = await runIterativeRetrieval(query, {
|
|
338
|
+
retrieve: (q, widen, signal) => this.search(scope, q, {
|
|
339
|
+
...opts,
|
|
340
|
+
...widen ? { expandHops: 1 } : {},
|
|
341
|
+
...signal !== void 0 ? { signal } : {}
|
|
342
|
+
}),
|
|
343
|
+
snippetOf: (hit) => hit.record.text,
|
|
344
|
+
idOf: (hit) => hit.record.id,
|
|
345
|
+
grader: this.#grader,
|
|
346
|
+
fuse: (lists) => fuseRrf(lists, 60)
|
|
347
|
+
}, {
|
|
348
|
+
maxIterations: opts.maxIterations ?? this.#iterativeMaxIterations,
|
|
349
|
+
maxResults: opts.topK ?? 10,
|
|
350
|
+
...opts.forceHard !== void 0 ? { forceHard: opts.forceHard } : {},
|
|
351
|
+
...opts.signal !== void 0 ? { signal: opts.signal } : {}
|
|
352
|
+
});
|
|
353
|
+
span.setAttributes({
|
|
354
|
+
"memory.search.semantic.iterative.gate_hard": result.gateHard,
|
|
355
|
+
"memory.search.semantic.iterative.iterations": result.iterations,
|
|
356
|
+
"memory.search.semantic.iterative.sufficient": result.sufficient,
|
|
357
|
+
"memory.search.semantic.iterative.abstained": result.abstained,
|
|
358
|
+
"memory.search.semantic.iterative.query_count": result.queries.length,
|
|
359
|
+
"memory.search.semantic.iterative.final_count": result.hits.length
|
|
360
|
+
});
|
|
361
|
+
return result;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Raw vector KNN neighbours for the consolidator's reconcile
|
|
366
|
+
* pre-filter (P0-3). Unlike {@link search} this skips FTS, reranking,
|
|
367
|
+
* and decay so the cosine scores survive intact (the conflict-pipeline
|
|
368
|
+
* zone thresholds are calibrated against them), and it **includes
|
|
369
|
+
* quarantined facts** so prior synthesized memories are visible to
|
|
370
|
+
* reconciliation. Returns `[]` when no embedder / vector adapter is
|
|
371
|
+
* configured — the consolidator then treats every candidate as a
|
|
372
|
+
* fresh `add`, degrading gracefully to the pre-reconcile behaviour.
|
|
373
|
+
*
|
|
374
|
+
* @stable
|
|
375
|
+
*/
|
|
376
|
+
async neighbors(scope, text, opts = {}) {
|
|
377
|
+
return this.#tryVectorSearch(scope, text, opts.topK ?? 10, void 0, true);
|
|
378
|
+
}
|
|
379
|
+
/** Lookup a single fact by id. Returns `null` for soft-deleted / missing. */
|
|
380
|
+
async get(scope, factId) {
|
|
381
|
+
return withMemorySpan(this.#tracer, "memory.read.semantic", scope, { "memory.semantic.fact_id": factId }, async (span) => {
|
|
382
|
+
if (typeof this.#store.semantic.get !== "function") throw new TypeError("[graphorin/memory] SemanticMemory.get(...) requires a storage adapter that implements `semantic.get(id)`. The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.");
|
|
383
|
+
const fact = await this.#store.semantic.get(factId);
|
|
384
|
+
span.setAttributes({ "memory.read.semantic.found": fact !== null });
|
|
385
|
+
return fact;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Return the full bi-temporal supersede chain that `factId` belongs
|
|
390
|
+
* to, oldest → newest, including superseded / soft-deleted rows so
|
|
391
|
+
* callers can answer "how did this fact change over time". Requires
|
|
392
|
+
* a storage adapter that implements
|
|
393
|
+
* `SemanticMemoryStoreExt.historyOf(...)` — the default
|
|
394
|
+
* `@graphorin/store-sqlite` adapter wires this through. P0-2.
|
|
395
|
+
*
|
|
396
|
+
* @stable
|
|
397
|
+
*/
|
|
398
|
+
async history(scope, factId) {
|
|
399
|
+
return withMemorySpan(this.#tracer, "memory.read.semantic", scope, {
|
|
400
|
+
"memory.semantic.action": "history",
|
|
401
|
+
"memory.semantic.fact_id": factId
|
|
402
|
+
}, async (span) => {
|
|
403
|
+
if (typeof this.#store.semantic.historyOf !== "function") throw new TypeError("[graphorin/memory] SemanticMemory.history(...) requires a storage adapter that implements `semantic.historyOf(scope, id)`. The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.");
|
|
404
|
+
const chain = await this.#store.semantic.historyOf(scope, factId);
|
|
405
|
+
span.setAttributes({ "memory.read.semantic.history_length": chain.length });
|
|
406
|
+
return chain;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Promote a quarantined fact to `active` (P1-4). The validation path
|
|
411
|
+
* that admits a synthesized memory into action-driving recall once a
|
|
412
|
+
* human (or trusted non-agent caller) has reviewed it. Writes a
|
|
413
|
+
* `memory_history` audit row. Requires a storage adapter that
|
|
414
|
+
* implements `SemanticMemoryStoreExt.setStatus(...)` — the default
|
|
415
|
+
* `@graphorin/store-sqlite` adapter wires this through.
|
|
416
|
+
*
|
|
417
|
+
* MRET-3 / MST-1: promotion of a fact whose text still trips the
|
|
418
|
+
* offline injection heuristics is **refused** with
|
|
419
|
+
* {@link QuarantinePromotionRefusedError} — the model-facing
|
|
420
|
+
* `fact_validate` tool calls this with no `force`, so a poisoned
|
|
421
|
+
* memory can never be promoted by the agent itself (the one-turn
|
|
422
|
+
* `fact_remember(poison)` → `fact_validate(id)` chain is closed). An
|
|
423
|
+
* operator can override after review by passing `{ force: true }`
|
|
424
|
+
* from a trusted (non-agent) context. Synthesized-but-clean writes
|
|
425
|
+
* (consolidator / reflection) promote normally.
|
|
426
|
+
*
|
|
427
|
+
* @stable
|
|
428
|
+
*/
|
|
429
|
+
async validate(scope, factId, reason, options) {
|
|
430
|
+
await withMemorySpan(this.#tracer, "memory.write.semantic", scope, {
|
|
431
|
+
"memory.semantic.action": "validate",
|
|
432
|
+
"memory.semantic.fact_id": factId
|
|
433
|
+
}, async (span) => {
|
|
434
|
+
if (typeof this.#store.semantic.setStatus !== "function") throw new TypeError("[graphorin/memory] SemanticMemory.validate(...) requires a storage adapter that implements `semantic.setStatus(id, status)`. The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via SemanticMemoryStoreExt.");
|
|
435
|
+
const force = options?.force === true;
|
|
436
|
+
const existing = await this.#fetchExisting(factId);
|
|
437
|
+
if (existing !== null && !force) {
|
|
438
|
+
const injection = detectMemoryInjection(existing.text);
|
|
439
|
+
if (injection.flagged) {
|
|
440
|
+
span.setAttributes({
|
|
441
|
+
"memory.semantic.validate.refused": true,
|
|
442
|
+
"memory.semantic.injection_markers": injection.markers.join(",")
|
|
443
|
+
});
|
|
444
|
+
throw new QuarantinePromotionRefusedError(factId, injection.markers);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
span.setAttributes({ "memory.semantic.validate.forced": force });
|
|
448
|
+
await this.#store.semantic.setStatus(factId, "active", reason);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/** Mark `oldId` superseded by a new fact. Returns the new record. */
|
|
452
|
+
async supersede(scope, oldId, newInput, reason) {
|
|
453
|
+
return withMemorySpan(this.#tracer, "memory.write.semantic", scope, { "memory.semantic.action": "supersede" }, async () => {
|
|
454
|
+
const newFact = await this.remember(scope, {
|
|
455
|
+
...newInput,
|
|
456
|
+
supersedes: oldId
|
|
457
|
+
}, { pipeline: "off" });
|
|
458
|
+
await this.#store.semantic.supersede(oldId, newFact, reason);
|
|
459
|
+
return {
|
|
460
|
+
old: oldId,
|
|
461
|
+
new: newFact
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
/** Soft-delete a fact (kept for replay; never hard-deleted). */
|
|
466
|
+
async forget(scope, factId, reason) {
|
|
467
|
+
await withMemorySpan(this.#tracer, "memory.write.semantic", scope, {
|
|
468
|
+
"memory.semantic.action": "forget",
|
|
469
|
+
"memory.semantic.fact_id": factId
|
|
470
|
+
}, async () => {
|
|
471
|
+
await this.#store.semantic.forget(factId, reason);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Hard-delete a fact (GDPR path). Distinct from {@link forget}: the
|
|
476
|
+
* record is removed from storage entirely instead of soft-archived.
|
|
477
|
+
* Requires a storage adapter that implements
|
|
478
|
+
* `SemanticMemoryStoreExt.purge(...)` — the default
|
|
479
|
+
* `@graphorin/store-sqlite` adapter wires this through.
|
|
480
|
+
*/
|
|
481
|
+
async purge(scope, factId, reason) {
|
|
482
|
+
await withMemorySpan(this.#tracer, "memory.write.semantic", scope, {
|
|
483
|
+
"memory.semantic.action": "purge",
|
|
484
|
+
"memory.semantic.fact_id": factId
|
|
485
|
+
}, async () => {
|
|
486
|
+
if (typeof this.#store.semantic.purge !== "function") throw new TypeError("[graphorin/memory] SemanticMemory.purge(...) requires a storage adapter that implements `semantic.purge(id)`. For storage adapters without GDPR-grade hard-delete, prefer `SemanticMemory.forget(...)`.");
|
|
487
|
+
await this.#store.semantic.purge(factId, reason);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/** Fuse multiple ranked lists outside of a `search()` call. */
|
|
491
|
+
async fuse(query, lists, options = {}) {
|
|
492
|
+
return this.#reranker.rerank(query, lists, options);
|
|
493
|
+
}
|
|
494
|
+
/** Pure-fusion helper — exposed for callers that already collected results. */
|
|
495
|
+
static fuseRrf(lists, k = 60) {
|
|
496
|
+
return fuseRrf(lists, k);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Pure weighted-fusion helper (X-2) — like {@link SemanticMemory.fuseRrf}
|
|
500
|
+
* but scales each list `i`'s reciprocal-rank contribution by
|
|
501
|
+
* `weights[i]`. A missing / invalid entry defaults to `1`, so equal or
|
|
502
|
+
* absent weights reproduce RRF.
|
|
503
|
+
*/
|
|
504
|
+
static fuseWeighted(lists, weights, k = 60) {
|
|
505
|
+
return fuseWeighted(lists, weights, k);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Multi-query expansion (P2-3). Returns the original query followed by
|
|
509
|
+
* up to `multiQuery - 1` deduped reworded variants. A no-op (`[query]`)
|
|
510
|
+
* when `multiQuery` is unset / `<= 1` or no transformer is configured —
|
|
511
|
+
* so the default path makes no provider call. A transformer failure
|
|
512
|
+
* degrades to `[query]` rather than breaking recall.
|
|
513
|
+
*/
|
|
514
|
+
async #expandQueries(query, opts) {
|
|
515
|
+
const n = opts.multiQuery;
|
|
516
|
+
if (n === void 0 || n <= 1 || this.#queryTransformer === null) return [query];
|
|
517
|
+
try {
|
|
518
|
+
const variants = await this.#queryTransformer.expand(query, n - 1, { ...opts.signal !== void 0 ? { signal: opts.signal } : {} });
|
|
519
|
+
const seen = new Set([query.trim().toLowerCase()]);
|
|
520
|
+
const out = [query];
|
|
521
|
+
for (const variant of variants) {
|
|
522
|
+
const trimmed = variant.trim();
|
|
523
|
+
const key = trimmed.toLowerCase();
|
|
524
|
+
if (trimmed.length === 0 || seen.has(key)) continue;
|
|
525
|
+
seen.add(key);
|
|
526
|
+
out.push(trimmed);
|
|
527
|
+
if (out.length >= n) break;
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
} catch {
|
|
531
|
+
return [query];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* HyDE retrieval (P2-3). Generates a hypothetical answer, embeds it,
|
|
536
|
+
* and returns its vector neighbours to fuse into the result. A no-op
|
|
537
|
+
* (`[]`) unless `hyde` is set, a transformer is configured, **and** an
|
|
538
|
+
* embedder + vector surface exist — the embedder guard is checked
|
|
539
|
+
* *first* so a missing embedder skips the LLM call entirely rather than
|
|
540
|
+
* generating a passage that can never be embedded.
|
|
541
|
+
*/
|
|
542
|
+
async #tryHyde(scope, query, opts, topK) {
|
|
543
|
+
if (opts.hyde !== true || this.#queryTransformer === null) return [];
|
|
544
|
+
if (this.#embedder === null || this.#embedderIdProvider() === null || typeof this.#store.semantic.searchVector !== "function") return [];
|
|
545
|
+
try {
|
|
546
|
+
const pseudo = await this.#queryTransformer.hypothetical(query, { ...opts.signal !== void 0 ? { signal: opts.signal } : {} });
|
|
547
|
+
if (pseudo === null || pseudo.trim().length === 0) return [];
|
|
548
|
+
return this.#tryVectorSearch(scope, pseudo, topK, opts.asOf, opts.includeQuarantined);
|
|
549
|
+
} catch {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* One-hop graph expansion (P2-1). Seeds on the unique fact ids already
|
|
555
|
+
* gathered into `lists` and fuses in facts sharing a canonical entity,
|
|
556
|
+
* via the adapter's recursive-CTE `graph.expandOneHop`. A no-op (`[]`)
|
|
557
|
+
* unless `expandHops >= 1` and the adapter exposes `graph` — and
|
|
558
|
+
* resilient: a traversal error degrades to no expansion rather than
|
|
559
|
+
* breaking recall. Neighbours carry a `graph` signal so explanations
|
|
560
|
+
* (X-3) can attribute a hit to the hop.
|
|
561
|
+
*/
|
|
562
|
+
async #tryExpandHops(scope, lists, opts, limit) {
|
|
563
|
+
const hops = opts.expandHops ?? 0;
|
|
564
|
+
const graphStore = this.#store.graph;
|
|
565
|
+
if (hops < 1 || graphStore === void 0 || typeof graphStore.expandOneHop !== "function") return [];
|
|
566
|
+
const seedIds = [];
|
|
567
|
+
const seen = /* @__PURE__ */ new Set();
|
|
568
|
+
for (const list of lists) for (const hit of list) if (!seen.has(hit.record.id)) {
|
|
569
|
+
seen.add(hit.record.id);
|
|
570
|
+
seedIds.push(hit.record.id);
|
|
571
|
+
}
|
|
572
|
+
if (seedIds.length === 0) return [];
|
|
573
|
+
try {
|
|
574
|
+
return (await graphStore.expandOneHop(scope, seedIds, {
|
|
575
|
+
maxHops: hops,
|
|
576
|
+
limit,
|
|
577
|
+
...opts.includeQuarantined === true ? { includeQuarantined: true } : {},
|
|
578
|
+
...opts.asOf !== void 0 ? { asOf: opts.asOf } : {}
|
|
579
|
+
})).map((fact) => ({
|
|
580
|
+
record: fact,
|
|
581
|
+
score: 1,
|
|
582
|
+
signals: Object.freeze({ graph: 1 })
|
|
583
|
+
}));
|
|
584
|
+
} catch {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async #tryVectorSearch(scope, query, topK, asOf, includeQuarantined) {
|
|
589
|
+
const embedderId = this.#embedderIdProvider();
|
|
590
|
+
if (this.#embedder === null || embedderId === null || typeof this.#store.semantic.searchVector !== "function") return [];
|
|
591
|
+
const [vector] = await this.#embedder.embed([query], { taskType: "query" });
|
|
592
|
+
if (vector === void 0) return [];
|
|
593
|
+
return this.#store.semantic.searchVector(scope, vector, embedderId, topK, asOf, includeQuarantined);
|
|
594
|
+
}
|
|
595
|
+
async #applyDecay(scope, hits, decay) {
|
|
596
|
+
if (decay === void 0 || hits.length === 0) return hits;
|
|
597
|
+
const semantic = this.#store.semantic;
|
|
598
|
+
let rows;
|
|
599
|
+
if (typeof semantic.listDecaySignals === "function") rows = await semantic.listDecaySignals(hits.map((h) => h.record.id));
|
|
600
|
+
else if (typeof semantic.listForDecay === "function") rows = await semantic.listForDecay(scope, 1e3);
|
|
601
|
+
else return hits;
|
|
602
|
+
if (rows.length === 0) return hits;
|
|
603
|
+
const signals = new Map(rows.map((row) => [row.id, {
|
|
604
|
+
strength: row.strength,
|
|
605
|
+
lastAccessedAt: row.lastAccessedAt,
|
|
606
|
+
createdAt: row.createdAt
|
|
607
|
+
}]));
|
|
608
|
+
const now = (decay.now ?? Date.now)();
|
|
609
|
+
const tauMs = Math.max(1, decay.tauDays) * 24 * 60 * 60 * 1e3;
|
|
610
|
+
const sorted = [...hits.map((hit) => {
|
|
611
|
+
const sig = signals.get(hit.record.id);
|
|
612
|
+
if (sig === void 0) return hit;
|
|
613
|
+
const reference = sig.lastAccessedAt ?? sig.createdAt;
|
|
614
|
+
const elapsed = Math.max(0, now - reference);
|
|
615
|
+
const retention = Math.exp(-elapsed / (tauMs * Math.max(.5, sig.strength)));
|
|
616
|
+
return {
|
|
617
|
+
...hit,
|
|
618
|
+
score: hit.score * retention,
|
|
619
|
+
signals: Object.freeze({
|
|
620
|
+
...hit.signals ?? {},
|
|
621
|
+
decay: retention
|
|
622
|
+
})
|
|
623
|
+
};
|
|
624
|
+
})].sort((a, b) => b.score - a.score);
|
|
625
|
+
return Object.freeze(sorted);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
//#endregion
|
|
630
|
+
export { SemanticMemory };
|
|
631
|
+
//# sourceMappingURL=semantic-memory.js.map
|