@graphorin/memory 0.5.0 → 0.6.1
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 +102 -1
- package/README.md +54 -37
- package/dist/conflict/index.d.ts +3 -1
- package/dist/conflict/index.js +1 -1
- package/dist/conflict/locale-packs/en.d.ts +13 -0
- package/dist/conflict/locale-packs/en.d.ts.map +1 -0
- package/dist/conflict/locale-packs/en.js +1 -1
- package/dist/conflict/locale-packs/en.js.map +1 -1
- package/dist/conflict/pipeline.d.ts +2 -2
- package/dist/conflict/pipeline.js +3 -3
- package/dist/conflict/pipeline.js.map +1 -1
- package/dist/conflict/stages/helpers.js +1 -1
- package/dist/conflict/stages/helpers.js.map +1 -1
- package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -1
- package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
- package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -1
- package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -1
- package/dist/conflict/types.d.ts +7 -7
- package/dist/conflict/types.js +1 -1
- package/dist/conflict/types.js.map +1 -1
- package/dist/consolidator/budget.js +16 -3
- package/dist/consolidator/budget.js.map +1 -1
- package/dist/consolidator/decay.d.ts +13 -2
- package/dist/consolidator/decay.js +17 -5
- package/dist/consolidator/decay.js.map +1 -1
- package/dist/consolidator/dlq.js +1 -1
- package/dist/consolidator/dlq.js.map +1 -1
- package/dist/consolidator/errors.js +2 -2
- package/dist/consolidator/errors.js.map +1 -1
- package/dist/consolidator/index.js +8 -6
- package/dist/consolidator/index.js.map +1 -1
- package/dist/consolidator/lock.js +1 -1
- package/dist/consolidator/lock.js.map +1 -1
- package/dist/consolidator/noise-filter.js.map +1 -1
- package/dist/consolidator/phases/deep.js +6 -2
- package/dist/consolidator/phases/deep.js.map +1 -1
- package/dist/consolidator/phases/induce.d.ts +10 -10
- package/dist/consolidator/phases/induce.js +6 -6
- package/dist/consolidator/phases/induce.js.map +1 -1
- package/dist/consolidator/phases/learned-context.js +129 -0
- package/dist/consolidator/phases/learned-context.js.map +1 -0
- package/dist/consolidator/phases/light.js +2 -1
- package/dist/consolidator/phases/light.js.map +1 -1
- package/dist/consolidator/phases/reflect.js +5 -4
- package/dist/consolidator/phases/reflect.js.map +1 -1
- package/dist/consolidator/phases/standard.js +37 -8
- package/dist/consolidator/phases/standard.js.map +1 -1
- package/dist/consolidator/reconcile.js +1 -1
- package/dist/consolidator/reconcile.js.map +1 -1
- package/dist/consolidator/runtime.d.ts +4 -4
- package/dist/consolidator/runtime.d.ts.map +1 -1
- package/dist/consolidator/runtime.js +73 -16
- package/dist/consolidator/runtime.js.map +1 -1
- package/dist/consolidator/scheduler.d.ts +5 -5
- package/dist/consolidator/scheduler.js +1 -1
- package/dist/consolidator/scheduler.js.map +1 -1
- package/dist/consolidator/triggers.js +7 -7
- package/dist/consolidator/triggers.js.map +1 -1
- package/dist/consolidator/types.d.ts +68 -20
- package/dist/consolidator/types.d.ts.map +1 -1
- package/dist/consolidator/types.js +16 -6
- package/dist/consolidator/types.js.map +1 -1
- package/dist/context-engine/annotations.d.ts +9 -9
- package/dist/context-engine/annotations.js +3 -3
- package/dist/context-engine/annotations.js.map +1 -1
- package/dist/context-engine/auto-recall.js.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.d.ts +18 -3
- package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js +26 -5
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
- package/dist/context-engine/compaction/compactor.js +19 -6
- package/dist/context-engine/compaction/compactor.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +4 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +2 -1
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts +28 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.js +71 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.js.map +1 -0
- package/dist/context-engine/compaction/hooks/types.d.ts +15 -3
- package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -1
- package/dist/context-engine/compaction/index.d.ts +1 -0
- package/dist/context-engine/compaction/index.js +2 -1
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +10 -3
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.js +42 -5
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
- package/dist/context-engine/compaction/thresholds.js.map +1 -1
- package/dist/context-engine/compaction/types.d.ts +47 -7
- package/dist/context-engine/compaction/types.d.ts.map +1 -1
- package/dist/context-engine/engine.d.ts +23 -4
- package/dist/context-engine/engine.d.ts.map +1 -1
- package/dist/context-engine/engine.js +69 -15
- package/dist/context-engine/engine.js.map +1 -1
- package/dist/context-engine/index.d.ts +1 -0
- package/dist/context-engine/index.d.ts.map +1 -1
- package/dist/context-engine/index.js +2 -1
- package/dist/context-engine/locale-packs/en.js +14 -13
- package/dist/context-engine/locale-packs/en.js.map +1 -1
- package/dist/context-engine/locale-packs/types.d.ts +10 -10
- package/dist/context-engine/locale-packs/types.d.ts.map +1 -1
- package/dist/context-engine/locale-packs/types.js.map +1 -1
- package/dist/context-engine/metadata.d.ts +1 -1
- package/dist/context-engine/metadata.js +2 -2
- package/dist/context-engine/metadata.js.map +1 -1
- package/dist/context-engine/preambles/inbound-en.d.ts +1 -1
- package/dist/context-engine/preambles/inbound-en.js +1 -1
- package/dist/context-engine/preambles/inbound-en.js.map +1 -1
- package/dist/context-engine/privacy-filter.d.ts +2 -2
- package/dist/context-engine/privacy-filter.js +1 -1
- package/dist/context-engine/privacy-filter.js.map +1 -1
- package/dist/context-engine/templates/base-en.d.ts +1 -1
- package/dist/context-engine/templates/base-en.js +1 -1
- package/dist/context-engine/templates/base-en.js.map +1 -1
- package/dist/context-engine/templates/composer.d.ts +1 -1
- package/dist/context-engine/templates/composer.js.map +1 -1
- package/dist/context-engine/token-budget.d.ts +1 -1
- package/dist/context-engine/token-budget.js +1 -1
- package/dist/context-engine/token-budget.js.map +1 -1
- package/dist/context-engine/token-counter.d.ts +16 -7
- package/dist/context-engine/token-counter.d.ts.map +1 -1
- package/dist/context-engine/token-counter.js +31 -9
- package/dist/context-engine/token-counter.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +4 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/facade.d.ts +44 -15
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +16 -8
- package/dist/facade.js.map +1 -1
- package/dist/graph/entity-resolver.d.ts +2 -2
- package/dist/graph/entity-resolver.js +2 -2
- package/dist/graph/entity-resolver.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/contextualize.js +2 -2
- package/dist/internal/contextualize.js.map +1 -1
- package/dist/internal/embedder-binding.js +1 -1
- package/dist/internal/embedder-binding.js.map +1 -1
- package/dist/internal/id.js +1 -1
- package/dist/internal/id.js.map +1 -1
- package/dist/internal/injection-heuristics.js +25 -2
- package/dist/internal/injection-heuristics.js.map +1 -1
- package/dist/internal/spans.js +1 -1
- package/dist/internal/spans.js.map +1 -1
- package/dist/internal/storage-adapter.d.ts +71 -22
- package/dist/internal/storage-adapter.d.ts.map +1 -1
- package/dist/migration/embedder-migration.d.ts +7 -7
- package/dist/migration/embedder-migration.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/search/explain.d.ts +7 -4
- package/dist/search/explain.d.ts.map +1 -1
- package/dist/search/explain.js +1 -1
- package/dist/search/explain.js.map +1 -1
- package/dist/search/fit-weights.d.ts +48 -0
- package/dist/search/fit-weights.d.ts.map +1 -0
- package/dist/search/fit-weights.js +70 -0
- package/dist/search/fit-weights.js.map +1 -0
- package/dist/search/graph-ppr.d.ts +36 -0
- package/dist/search/graph-ppr.d.ts.map +1 -0
- package/dist/search/graph-ppr.js +38 -0
- package/dist/search/graph-ppr.js.map +1 -0
- package/dist/search/index.d.ts +4 -1
- package/dist/search/index.js +4 -1
- package/dist/search/iterative.d.ts +15 -8
- package/dist/search/iterative.d.ts.map +1 -1
- package/dist/search/iterative.js +10 -9
- package/dist/search/iterative.js.map +1 -1
- package/dist/search/query-transform.d.ts +6 -6
- package/dist/search/query-transform.js +6 -6
- package/dist/search/query-transform.js.map +1 -1
- package/dist/search/rrf.d.ts +4 -4
- package/dist/search/rrf.js +4 -4
- package/dist/search/rrf.js.map +1 -1
- package/dist/search/trust.d.ts +30 -0
- package/dist/search/trust.d.ts.map +1 -0
- package/dist/search/trust.js +50 -0
- package/dist/search/trust.js.map +1 -0
- package/dist/search/types.d.ts +1 -1
- package/dist/tiers/episodic-memory.d.ts +6 -4
- package/dist/tiers/episodic-memory.d.ts.map +1 -1
- package/dist/tiers/episodic-memory.js +3 -2
- package/dist/tiers/episodic-memory.js.map +1 -1
- package/dist/tiers/index.d.ts +1 -1
- package/dist/tiers/index.js +1 -1
- package/dist/tiers/insight-memory.d.ts +4 -4
- package/dist/tiers/insight-memory.js +3 -3
- package/dist/tiers/insight-memory.js.map +1 -1
- package/dist/tiers/procedural-memory.d.ts +29 -7
- package/dist/tiers/procedural-memory.d.ts.map +1 -1
- package/dist/tiers/procedural-memory.js +66 -6
- package/dist/tiers/procedural-memory.js.map +1 -1
- package/dist/tiers/semantic-memory.d.ts +111 -30
- package/dist/tiers/semantic-memory.d.ts.map +1 -1
- package/dist/tiers/semantic-memory.js +120 -27
- package/dist/tiers/semantic-memory.js.map +1 -1
- package/dist/tiers/session-memory.d.ts +7 -7
- package/dist/tiers/session-memory.js +7 -7
- package/dist/tiers/session-memory.js.map +1 -1
- package/dist/tiers/shared-memory.d.ts +1 -1
- package/dist/tiers/shared-memory.js +1 -1
- package/dist/tiers/shared-memory.js.map +1 -1
- package/dist/tiers/working-memory.d.ts +3 -3
- package/dist/tiers/working-memory.js +2 -2
- package/dist/tiers/working-memory.js.map +1 -1
- package/dist/tools/block-tools.d.ts +3 -3
- package/dist/tools/block-tools.js +3 -3
- package/dist/tools/block-tools.js.map +1 -1
- package/dist/tools/fact-tools.d.ts +48 -31
- package/dist/tools/fact-tools.d.ts.map +1 -1
- package/dist/tools/fact-tools.js +30 -18
- package/dist/tools/fact-tools.js.map +1 -1
- package/dist/tools/index.d.ts +9 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/recall-tools.d.ts +40 -20
- package/dist/tools/recall-tools.d.ts.map +1 -1
- package/dist/tools/recall-tools.js +29 -10
- package/dist/tools/recall-tools.js.map +1 -1
- package/dist/tools/runbook-tools.d.ts +76 -0
- package/dist/tools/runbook-tools.d.ts.map +1 -0
- package/dist/tools/runbook-tools.js +53 -0
- package/dist/tools/runbook-tools.js.map +1 -0
- package/dist/tools/taint.js +33 -0
- package/dist/tools/taint.js.map +1 -0
- package/dist/tools/types.d.ts +1 -1
- package/package.json +8 -8
package/dist/search/rrf.js
CHANGED
|
@@ -42,7 +42,7 @@ var RRFReranker = class {
|
|
|
42
42
|
* Weighted-RRF reranker (X-2). Fuses parallel ranked lists through
|
|
43
43
|
* {@link fuseWeighted}, applying a per-list `weights[i]` so a caller who
|
|
44
44
|
* has calibrated list reliability against labels (the P0-1 eval harness)
|
|
45
|
-
* can trust one retriever over another
|
|
45
|
+
* can trust one retriever over another - e.g. up-weight dense vector
|
|
46
46
|
* hits over lexical FTS hits. `weights` is **positional**: aligned to the
|
|
47
47
|
* input `lists` order (the built-in hybrid search passes FTS first, then
|
|
48
48
|
* vector). At equal weights it is identical to {@link RRFReranker}; RRF
|
|
@@ -72,7 +72,7 @@ var WeightedRRFReranker = class {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
|
-
* Pure functional core of the RRF reranker
|
|
75
|
+
* Pure functional core of the RRF reranker - the equal-weight case of
|
|
76
76
|
* {@link fuseWeighted}. Exported separately so the test suite (and the
|
|
77
77
|
* property-based fuzzer) can exercise the math without the `Promise<…>`
|
|
78
78
|
* wrapping of the public surface.
|
|
@@ -95,9 +95,9 @@ function fuseRrf(lists, k, labels) {
|
|
|
95
95
|
*
|
|
96
96
|
* Like {@link fuseRrf} the fusion is deterministic, tie-broken by
|
|
97
97
|
* first-seen order, and preserves any upstream `signals` (FTS `bm25`,
|
|
98
|
-
* vector similarity) the hits carried in
|
|
98
|
+
* vector similarity) the hits carried in - the `rrf` / `rrf.list_N`
|
|
99
99
|
* signals it records are the *weighted* contributions, so the fused
|
|
100
|
-
* `score` still equals their sum. Note that
|
|
100
|
+
* `score` still equals their sum. Note that - unlike RRF - the result
|
|
101
101
|
* depends on input list *order*, because each weight is bound to a list
|
|
102
102
|
* position.
|
|
103
103
|
*
|
package/dist/search/rrf.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rrf.js","names":["signals: Record<string, number>","fused: MemoryHit<TRecord>[]"],"sources":["../../src/search/rrf.ts"],"sourcesContent":["import type { MemoryHit, MemoryRecord } from '@graphorin/core';\nimport type { ReRanker, ReRankOptions } from './types.js';\n\n/**\n * Industry-standard Reciprocal Rank Fusion constant. Lower `k` makes\n * top-ranked items dominate; higher `k` smooths the contribution of\n * lower-ranked items. `k = 60` is the value popularised by Cormack,\n * Clarke, and Büttcher (2009) and is the framework default.\n *\n * @stable\n */\nexport const RRF_DEFAULT_K = 60;\n\n/**\n * Built-in Reciprocal Rank Fusion reranker. Combines multiple ranked\n * lists (vector hits, FTS5 hits, optional entity boost) into a single\n * fused ranking by summing `1 / (k + rank)` per list each item appears\n * in.\n *\n * Properties (verified by the property-based test suite):\n *\n * - **Deterministic.** Identical input lists yield identical output.\n * - **Stable under permutation of input lists.** The fusion is\n * independent of the order in which the input lists are passed in.\n * - **Tie-broken by stable record id.** Records with equal RRF\n * scores keep their first-seen order.\n *\n * @stable\n */\nexport class RRFReranker implements ReRanker {\n readonly id = 'rrf' as const;\n readonly k: number;\n\n constructor(k: number = RRF_DEFAULT_K) {\n if (!Number.isFinite(k) || k <= 0) {\n throw new TypeError(\n `[graphorin/memory] RRFReranker: 'k' must be a positive finite number, got ${String(k)}.`,\n );\n }\n this.k = k;\n }\n\n async rerank<TRecord extends MemoryRecord>(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n options: ReRankOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<TRecord>>> {\n void query;\n if (options.signal?.aborted === true) {\n throw new DOMException('RRFReranker aborted', 'AbortError');\n }\n const fused = fuseRrf(lists, this.k, options.labels);\n const topK = options.topK ?? 10;\n return fused.slice(0, Math.max(0, topK));\n }\n}\n\n/**\n * Weighted-RRF reranker (X-2). Fuses parallel ranked lists through\n * {@link fuseWeighted}, applying a per-list `weights[i]` so a caller who\n * has calibrated list reliability against labels (the P0-1 eval harness)\n * can trust one retriever over another — e.g. up-weight dense vector\n * hits over lexical FTS hits. `weights` is **positional**: aligned to the\n * input `lists` order (the built-in hybrid search passes FTS first, then\n * vector). At equal weights it is identical to {@link RRFReranker}; RRF\n * stays the framework default.\n *\n * Like the RRF reranker it is deterministic and tie-broken by stable\n * record id, and it preserves the upstream `signals` on each hit.\n *\n * @stable\n */\nexport class WeightedRRFReranker implements ReRanker {\n readonly id = 'weighted-rrf' as const;\n readonly k: number;\n readonly weights: ReadonlyArray<number>;\n\n constructor(args: { weights: ReadonlyArray<number>; k?: number }) {\n const k = args.k ?? RRF_DEFAULT_K;\n if (!Number.isFinite(k) || k <= 0) {\n throw new TypeError(\n `[graphorin/memory] WeightedRRFReranker: 'k' must be a positive finite number, got ${String(k)}.`,\n );\n }\n for (const w of args.weights) {\n if (!Number.isFinite(w) || w < 0) {\n throw new TypeError(\n `[graphorin/memory] WeightedRRFReranker: every weight must be a non-negative finite number, got ${String(w)}.`,\n );\n }\n }\n this.k = k;\n this.weights = args.weights;\n }\n\n async rerank<TRecord extends MemoryRecord>(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n options: ReRankOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<TRecord>>> {\n void query;\n if (options.signal?.aborted === true) {\n throw new DOMException('WeightedRRFReranker aborted', 'AbortError');\n }\n const fused = fuseWeighted(lists, this.weights, this.k, options.labels);\n const topK = options.topK ?? 10;\n return fused.slice(0, Math.max(0, topK));\n }\n}\n\n/**\n * Pure functional core of the RRF reranker — the equal-weight case of\n * {@link fuseWeighted}. Exported separately so the test suite (and the\n * property-based fuzzer) can exercise the math without the `Promise<…>`\n * wrapping of the public surface.\n *\n * @stable\n */\nexport function fuseRrf<TRecord extends MemoryRecord>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n k: number,\n labels?: ReadonlyArray<string>,\n): ReadonlyArray<MemoryHit<TRecord>> {\n return fuseWeighted(lists, undefined, k, labels);\n}\n\n/**\n * Weighted / convex generalization of {@link fuseRrf} (X-2). Each input\n * list `i` contributes `weights[i] · 1 / (k + rank)` to a record's fused\n * score instead of the flat `1 / (k + rank)`, so a caller who has\n * calibrated list reliability against labels (the P0-1 eval harness) can\n * trust one retriever over another. RRF stays the zero-tuning default: a\n * `weights` of `undefined` (or all-`1`) is byte-for-byte identical to\n * {@link fuseRrf}. A missing, non-finite, or negative entry falls back to\n * the neutral weight `1`, so a partial / malformed `weights` never throws\n * or poisons the ranking.\n *\n * Like {@link fuseRrf} the fusion is deterministic, tie-broken by\n * first-seen order, and preserves any upstream `signals` (FTS `bm25`,\n * vector similarity) the hits carried in — the `rrf` / `rrf.list_N`\n * signals it records are the *weighted* contributions, so the fused\n * `score` still equals their sum. Note that — unlike RRF — the result\n * depends on input list *order*, because each weight is bound to a list\n * position.\n *\n * @stable\n */\nexport function fuseWeighted<TRecord extends MemoryRecord>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n weights: ReadonlyArray<number> | undefined,\n k: number,\n labels?: ReadonlyArray<string>,\n): ReadonlyArray<MemoryHit<TRecord>> {\n type Aggregate = {\n record: TRecord;\n score: number;\n signals: Record<string, number>;\n firstSeenOrder: number;\n };\n const aggregates = new Map<string, Aggregate>();\n let nextOrder = 0;\n for (let listIdx = 0; listIdx < lists.length; listIdx++) {\n const list = lists[listIdx] ?? [];\n const weight = resolveWeight(weights, listIdx);\n // MRET-13: explanation signals key by the caller's retriever-kind\n // label when supplied — `rrf.list_N` is an ephemeral position that\n // changes meaning whenever a leg (vector / HyDE / graph) is\n // conditionally absent from the fan-out.\n const signalKey = `rrf.${labels?.[listIdx] ?? `list_${listIdx}`}`;\n for (let i = 0; i < list.length; i++) {\n const hit = list[i];\n if (hit === undefined) continue;\n const rank = i + 1;\n const contribution = weight / (k + rank);\n const id = hit.record.id;\n const existing = aggregates.get(id);\n if (existing === undefined) {\n const signals: Record<string, number> = {\n ...(hit.signals ?? {}),\n rrf: contribution,\n [signalKey]: contribution,\n };\n aggregates.set(id, {\n record: hit.record,\n score: contribution,\n signals,\n firstSeenOrder: nextOrder++,\n });\n } else {\n existing.score += contribution;\n existing.signals[signalKey] = contribution;\n existing.signals.rrf = (existing.signals.rrf ?? 0) + contribution;\n for (const [key, value] of Object.entries(hit.signals ?? {})) {\n if (key in existing.signals) continue;\n existing.signals[key] = value;\n }\n }\n }\n }\n const fused: MemoryHit<TRecord>[] = [];\n for (const a of aggregates.values()) {\n fused.push({\n record: a.record,\n score: a.score,\n signals: Object.freeze({ ...a.signals }),\n });\n }\n fused.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n const orderA = aggregates.get(a.record.id)?.firstSeenOrder ?? 0;\n const orderB = aggregates.get(b.record.id)?.firstSeenOrder ?? 0;\n return orderA - orderB;\n });\n return fused;\n}\n\n/**\n * Resolve list `index`'s fusion weight, defaulting a missing / non-finite\n * / negative entry to the neutral `1` (so an absent or malformed\n * `weights` degrades to plain RRF rather than poisoning the ranking).\n */\nfunction resolveWeight(weights: ReadonlyArray<number> | undefined, index: number): number {\n const raw = weights?.[index];\n return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : 1;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rrf.js","names":["signals: Record<string, number>","fused: MemoryHit<TRecord>[]"],"sources":["../../src/search/rrf.ts"],"sourcesContent":["import type { MemoryHit, MemoryRecord } from '@graphorin/core';\nimport type { ReRanker, ReRankOptions } from './types.js';\n\n/**\n * Industry-standard Reciprocal Rank Fusion constant. Lower `k` makes\n * top-ranked items dominate; higher `k` smooths the contribution of\n * lower-ranked items. `k = 60` is the value popularised by Cormack,\n * Clarke, and Büttcher (2009) and is the framework default.\n *\n * @stable\n */\nexport const RRF_DEFAULT_K = 60;\n\n/**\n * Built-in Reciprocal Rank Fusion reranker. Combines multiple ranked\n * lists (vector hits, FTS5 hits, optional entity boost) into a single\n * fused ranking by summing `1 / (k + rank)` per list each item appears\n * in.\n *\n * Properties (verified by the property-based test suite):\n *\n * - **Deterministic.** Identical input lists yield identical output.\n * - **Stable under permutation of input lists.** The fusion is\n * independent of the order in which the input lists are passed in.\n * - **Tie-broken by stable record id.** Records with equal RRF\n * scores keep their first-seen order.\n *\n * @stable\n */\nexport class RRFReranker implements ReRanker {\n readonly id = 'rrf' as const;\n readonly k: number;\n\n constructor(k: number = RRF_DEFAULT_K) {\n if (!Number.isFinite(k) || k <= 0) {\n throw new TypeError(\n `[graphorin/memory] RRFReranker: 'k' must be a positive finite number, got ${String(k)}.`,\n );\n }\n this.k = k;\n }\n\n async rerank<TRecord extends MemoryRecord>(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n options: ReRankOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<TRecord>>> {\n void query;\n if (options.signal?.aborted === true) {\n throw new DOMException('RRFReranker aborted', 'AbortError');\n }\n const fused = fuseRrf(lists, this.k, options.labels);\n const topK = options.topK ?? 10;\n return fused.slice(0, Math.max(0, topK));\n }\n}\n\n/**\n * Weighted-RRF reranker (X-2). Fuses parallel ranked lists through\n * {@link fuseWeighted}, applying a per-list `weights[i]` so a caller who\n * has calibrated list reliability against labels (the P0-1 eval harness)\n * can trust one retriever over another - e.g. up-weight dense vector\n * hits over lexical FTS hits. `weights` is **positional**: aligned to the\n * input `lists` order (the built-in hybrid search passes FTS first, then\n * vector). At equal weights it is identical to {@link RRFReranker}; RRF\n * stays the framework default.\n *\n * Like the RRF reranker it is deterministic and tie-broken by stable\n * record id, and it preserves the upstream `signals` on each hit.\n *\n * @stable\n */\nexport class WeightedRRFReranker implements ReRanker {\n readonly id = 'weighted-rrf' as const;\n readonly k: number;\n readonly weights: ReadonlyArray<number>;\n\n constructor(args: { weights: ReadonlyArray<number>; k?: number }) {\n const k = args.k ?? RRF_DEFAULT_K;\n if (!Number.isFinite(k) || k <= 0) {\n throw new TypeError(\n `[graphorin/memory] WeightedRRFReranker: 'k' must be a positive finite number, got ${String(k)}.`,\n );\n }\n for (const w of args.weights) {\n if (!Number.isFinite(w) || w < 0) {\n throw new TypeError(\n `[graphorin/memory] WeightedRRFReranker: every weight must be a non-negative finite number, got ${String(w)}.`,\n );\n }\n }\n this.k = k;\n this.weights = args.weights;\n }\n\n async rerank<TRecord extends MemoryRecord>(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n options: ReRankOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<TRecord>>> {\n void query;\n if (options.signal?.aborted === true) {\n throw new DOMException('WeightedRRFReranker aborted', 'AbortError');\n }\n const fused = fuseWeighted(lists, this.weights, this.k, options.labels);\n const topK = options.topK ?? 10;\n return fused.slice(0, Math.max(0, topK));\n }\n}\n\n/**\n * Pure functional core of the RRF reranker - the equal-weight case of\n * {@link fuseWeighted}. Exported separately so the test suite (and the\n * property-based fuzzer) can exercise the math without the `Promise<…>`\n * wrapping of the public surface.\n *\n * @stable\n */\nexport function fuseRrf<TRecord extends MemoryRecord>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n k: number,\n labels?: ReadonlyArray<string>,\n): ReadonlyArray<MemoryHit<TRecord>> {\n return fuseWeighted(lists, undefined, k, labels);\n}\n\n/**\n * Weighted / convex generalization of {@link fuseRrf} (X-2). Each input\n * list `i` contributes `weights[i] · 1 / (k + rank)` to a record's fused\n * score instead of the flat `1 / (k + rank)`, so a caller who has\n * calibrated list reliability against labels (the P0-1 eval harness) can\n * trust one retriever over another. RRF stays the zero-tuning default: a\n * `weights` of `undefined` (or all-`1`) is byte-for-byte identical to\n * {@link fuseRrf}. A missing, non-finite, or negative entry falls back to\n * the neutral weight `1`, so a partial / malformed `weights` never throws\n * or poisons the ranking.\n *\n * Like {@link fuseRrf} the fusion is deterministic, tie-broken by\n * first-seen order, and preserves any upstream `signals` (FTS `bm25`,\n * vector similarity) the hits carried in - the `rrf` / `rrf.list_N`\n * signals it records are the *weighted* contributions, so the fused\n * `score` still equals their sum. Note that - unlike RRF - the result\n * depends on input list *order*, because each weight is bound to a list\n * position.\n *\n * @stable\n */\nexport function fuseWeighted<TRecord extends MemoryRecord>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n weights: ReadonlyArray<number> | undefined,\n k: number,\n labels?: ReadonlyArray<string>,\n): ReadonlyArray<MemoryHit<TRecord>> {\n type Aggregate = {\n record: TRecord;\n score: number;\n signals: Record<string, number>;\n firstSeenOrder: number;\n };\n const aggregates = new Map<string, Aggregate>();\n let nextOrder = 0;\n for (let listIdx = 0; listIdx < lists.length; listIdx++) {\n const list = lists[listIdx] ?? [];\n const weight = resolveWeight(weights, listIdx);\n // MRET-13: explanation signals key by the caller's retriever-kind\n // label when supplied - `rrf.list_N` is an ephemeral position that\n // changes meaning whenever a leg (vector / HyDE / graph) is\n // conditionally absent from the fan-out.\n const signalKey = `rrf.${labels?.[listIdx] ?? `list_${listIdx}`}`;\n for (let i = 0; i < list.length; i++) {\n const hit = list[i];\n if (hit === undefined) continue;\n const rank = i + 1;\n const contribution = weight / (k + rank);\n const id = hit.record.id;\n const existing = aggregates.get(id);\n if (existing === undefined) {\n const signals: Record<string, number> = {\n ...(hit.signals ?? {}),\n rrf: contribution,\n [signalKey]: contribution,\n };\n aggregates.set(id, {\n record: hit.record,\n score: contribution,\n signals,\n firstSeenOrder: nextOrder++,\n });\n } else {\n existing.score += contribution;\n existing.signals[signalKey] = contribution;\n existing.signals.rrf = (existing.signals.rrf ?? 0) + contribution;\n for (const [key, value] of Object.entries(hit.signals ?? {})) {\n if (key in existing.signals) continue;\n existing.signals[key] = value;\n }\n }\n }\n }\n const fused: MemoryHit<TRecord>[] = [];\n for (const a of aggregates.values()) {\n fused.push({\n record: a.record,\n score: a.score,\n signals: Object.freeze({ ...a.signals }),\n });\n }\n fused.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n const orderA = aggregates.get(a.record.id)?.firstSeenOrder ?? 0;\n const orderB = aggregates.get(b.record.id)?.firstSeenOrder ?? 0;\n return orderA - orderB;\n });\n return fused;\n}\n\n/**\n * Resolve list `index`'s fusion weight, defaulting a missing / non-finite\n * / negative entry to the neutral `1` (so an absent or malformed\n * `weights` degrades to plain RRF rather than poisoning the ranking).\n */\nfunction resolveWeight(weights: ReadonlyArray<number> | undefined, index: number): number {\n const raw = weights?.[index];\n return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : 1;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SalienceWeights } from "../consolidator/decay.js";
|
|
2
|
+
|
|
3
|
+
//#region src/search/trust.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `true` for provenance that did not originate first-party (P1-4) -
|
|
7
|
+
* mirrors the eviction-path classification: `null` (legacy / direct
|
|
8
|
+
* write), `'user'`, and `'extraction'` (the consolidator distilling the
|
|
9
|
+
* user's own session) are first-party; `'tool'`, `'imported'`,
|
|
10
|
+
* `'reflection'`, and `'induction'` are foreign.
|
|
11
|
+
*
|
|
12
|
+
* @stable
|
|
13
|
+
*/
|
|
14
|
+
declare function isForeignProvenance(provenance: string | null | undefined): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Rank-time trust multiplier for one record. `1` for first-party active
|
|
17
|
+
* facts (the common case - zero ranking change), `1 - quarantine` for
|
|
18
|
+
* quarantined-but-included rows, `1 - foreignProvenance` for foreign
|
|
19
|
+
* origin. Quarantine wins when both apply (same branch order as
|
|
20
|
+
* `salience()`).
|
|
21
|
+
*
|
|
22
|
+
* @stable
|
|
23
|
+
*/
|
|
24
|
+
declare function trustDiscount(record: {
|
|
25
|
+
readonly status?: string;
|
|
26
|
+
readonly provenance?: string | null;
|
|
27
|
+
}, weights?: SalienceWeights): number;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { isForeignProvenance, trustDiscount };
|
|
30
|
+
//# sourceMappingURL=trust.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trust.d.ts","names":[],"sources":["../../src/search/trust.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;iBA6BgB,mBAAA;;;;;;;;;;iBAkBA,aAAA;;;aAEL"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DEFAULT_SALIENCE_WEIGHTS } from "../consolidator/decay.js";
|
|
2
|
+
|
|
3
|
+
//#region src/search/trust.ts
|
|
4
|
+
/**
|
|
5
|
+
* C5: trust-aware retrieval ranking (pairs memory-retrieval-01's ranking
|
|
6
|
+
* leg / security-04). The provenance + quarantine discounts that the
|
|
7
|
+
* eviction-time `salience()` composite already applies now also apply at
|
|
8
|
+
* search SCORING time, so a poisoned or foreign-origin memory cannot
|
|
9
|
+
* outrank the user's own words purely on lexical/vector similarity - the
|
|
10
|
+
* direct defense against MINJA-class memory poisoning (query-only
|
|
11
|
+
* injection >95% ASR without rank-time trust).
|
|
12
|
+
*
|
|
13
|
+
* The factor reuses the {@link SalienceWeights} semantics verbatim:
|
|
14
|
+
* quarantined -> `1 - weights.quarantine` (default 0.3x), foreign
|
|
15
|
+
* provenance -> `1 - weights.foreignProvenance` (default 0.8x), first
|
|
16
|
+
* party -> `1` (neutral: default rankings of ordinary user/extraction
|
|
17
|
+
* facts are byte-identical).
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* `true` for provenance that did not originate first-party (P1-4) -
|
|
23
|
+
* mirrors the eviction-path classification: `null` (legacy / direct
|
|
24
|
+
* write), `'user'`, and `'extraction'` (the consolidator distilling the
|
|
25
|
+
* user's own session) are first-party; `'tool'`, `'imported'`,
|
|
26
|
+
* `'reflection'`, and `'induction'` are foreign.
|
|
27
|
+
*
|
|
28
|
+
* @stable
|
|
29
|
+
*/
|
|
30
|
+
function isForeignProvenance(provenance) {
|
|
31
|
+
return provenance !== null && provenance !== void 0 && provenance !== "user" && provenance !== "extraction";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Rank-time trust multiplier for one record. `1` for first-party active
|
|
35
|
+
* facts (the common case - zero ranking change), `1 - quarantine` for
|
|
36
|
+
* quarantined-but-included rows, `1 - foreignProvenance` for foreign
|
|
37
|
+
* origin. Quarantine wins when both apply (same branch order as
|
|
38
|
+
* `salience()`).
|
|
39
|
+
*
|
|
40
|
+
* @stable
|
|
41
|
+
*/
|
|
42
|
+
function trustDiscount(record, weights = DEFAULT_SALIENCE_WEIGHTS) {
|
|
43
|
+
if (record.status === "quarantined") return Math.max(0, 1 - weights.quarantine);
|
|
44
|
+
if (isForeignProvenance(record.provenance)) return Math.max(0, 1 - weights.foreignProvenance);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { isForeignProvenance, trustDiscount };
|
|
50
|
+
//# sourceMappingURL=trust.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trust.js","names":[],"sources":["../../src/search/trust.ts"],"sourcesContent":["/**\n * C5: trust-aware retrieval ranking (pairs memory-retrieval-01's ranking\n * leg / security-04). The provenance + quarantine discounts that the\n * eviction-time `salience()` composite already applies now also apply at\n * search SCORING time, so a poisoned or foreign-origin memory cannot\n * outrank the user's own words purely on lexical/vector similarity - the\n * direct defense against MINJA-class memory poisoning (query-only\n * injection >95% ASR without rank-time trust).\n *\n * The factor reuses the {@link SalienceWeights} semantics verbatim:\n * quarantined -> `1 - weights.quarantine` (default 0.3x), foreign\n * provenance -> `1 - weights.foreignProvenance` (default 0.8x), first\n * party -> `1` (neutral: default rankings of ordinary user/extraction\n * facts are byte-identical).\n *\n * @packageDocumentation\n */\n\nimport { DEFAULT_SALIENCE_WEIGHTS, type SalienceWeights } from '../consolidator/decay.js';\n\n/**\n * `true` for provenance that did not originate first-party (P1-4) -\n * mirrors the eviction-path classification: `null` (legacy / direct\n * write), `'user'`, and `'extraction'` (the consolidator distilling the\n * user's own session) are first-party; `'tool'`, `'imported'`,\n * `'reflection'`, and `'induction'` are foreign.\n *\n * @stable\n */\nexport function isForeignProvenance(provenance: string | null | undefined): boolean {\n return (\n provenance !== null &&\n provenance !== undefined &&\n provenance !== 'user' &&\n provenance !== 'extraction'\n );\n}\n\n/**\n * Rank-time trust multiplier for one record. `1` for first-party active\n * facts (the common case - zero ranking change), `1 - quarantine` for\n * quarantined-but-included rows, `1 - foreignProvenance` for foreign\n * origin. Quarantine wins when both apply (same branch order as\n * `salience()`).\n *\n * @stable\n */\nexport function trustDiscount(\n record: { readonly status?: string; readonly provenance?: string | null },\n weights: SalienceWeights = DEFAULT_SALIENCE_WEIGHTS,\n): number {\n if (record.status === 'quarantined') return Math.max(0, 1 - weights.quarantine);\n if (isForeignProvenance(record.provenance)) return Math.max(0, 1 - weights.foreignProvenance);\n return 1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,oBAAoB,YAAgD;AAClF,QACE,eAAe,QACf,eAAe,UACf,eAAe,UACf,eAAe;;;;;;;;;;;AAanB,SAAgB,cACd,QACA,UAA2B,0BACnB;AACR,KAAI,OAAO,WAAW,cAAe,QAAO,KAAK,IAAI,GAAG,IAAI,QAAQ,WAAW;AAC/E,KAAI,oBAAoB,OAAO,WAAW,CAAE,QAAO,KAAK,IAAI,GAAG,IAAI,QAAQ,kBAAkB;AAC7F,QAAO"}
|
package/dist/search/types.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ interface ReRankOptions {
|
|
|
36
36
|
readonly topK?: number;
|
|
37
37
|
readonly signal?: AbortSignal;
|
|
38
38
|
/**
|
|
39
|
-
* Stable per-list labels for the explanation signals (MRET-13)
|
|
39
|
+
* Stable per-list labels for the explanation signals (MRET-13) -
|
|
40
40
|
* `rrf.<label>` instead of the ephemeral `rrf.list_<index>`. Callers
|
|
41
41
|
* that fan lists out conditionally (multi-query, HyDE, graph) pass
|
|
42
42
|
* retriever-kind labels so X-3 consumers can identify which leg a
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MemoryStoreAdapter } from "../internal/storage-adapter.js";
|
|
2
|
-
import { EmbedderProvider, Episode, MemoryHit, MemoryProvenance, MemoryStatus, Sensitivity, SessionScope, Tracer } from "@graphorin/core";
|
|
2
|
+
import { EmbedderProvider, Episode, MemoryHit, MemoryOwner, MemoryProvenance, MemoryStatus, Sensitivity, SessionScope, Tracer } from "@graphorin/core";
|
|
3
3
|
|
|
4
4
|
//#region src/tiers/episodic-memory.d.ts
|
|
5
5
|
|
|
@@ -31,6 +31,8 @@ interface EpisodeInput {
|
|
|
31
31
|
* they are excluded from action-driving recall until validated.
|
|
32
32
|
*/
|
|
33
33
|
readonly status?: MemoryStatus;
|
|
34
|
+
/** Principal dimension (D3). `'agent'` on auto-formed episodes. */
|
|
35
|
+
readonly owner?: MemoryOwner;
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* Triple-signal episode retrieval weights. Defaults match DEC-105:
|
|
@@ -68,7 +70,7 @@ interface EpisodeSearchOptions {
|
|
|
68
70
|
/**
|
|
69
71
|
* Include quarantined episodes in the result set (P1-4). Defaults to
|
|
70
72
|
* `false`: action-driving recall never returns quarantined rows. Set
|
|
71
|
-
* `true` only for the validation / inspector path
|
|
73
|
+
* `true` only for the validation / inspector path - never for
|
|
72
74
|
* auto-recall fed back into the model. Auto-formed episodes (P1-2)
|
|
73
75
|
* land quarantined, so this is how an operator surfaces them for
|
|
74
76
|
* review.
|
|
@@ -78,7 +80,7 @@ interface EpisodeSearchOptions {
|
|
|
78
80
|
readonly includeQuarantined?: boolean;
|
|
79
81
|
}
|
|
80
82
|
/**
|
|
81
|
-
* `EpisodicMemory`
|
|
83
|
+
* `EpisodicMemory` - record + retrieve summarized stretches of past
|
|
82
84
|
* activity. Stored embeddings power triple-signal retrieval (recency
|
|
83
85
|
* × relevance × importance).
|
|
84
86
|
*
|
|
@@ -115,7 +117,7 @@ declare class EpisodicMemory {
|
|
|
115
117
|
archive(scope: SessionScope, episodeId: string, reason?: string): Promise<void>;
|
|
116
118
|
/**
|
|
117
119
|
* Most-recent episodes by end time (newest first), with no embedding / FTS
|
|
118
|
-
* query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent`
|
|
120
|
+
* query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent` - the default
|
|
119
121
|
* `@graphorin/store-sqlite` adapter implements it. Optionally includes
|
|
120
122
|
* quarantined episodes (the importance source for the reflection gate).
|
|
121
123
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"episodic-memory.d.ts","names":[],"sources":["../../src/tiers/episodic-memory.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"episodic-memory.d.ts","names":[],"sources":["../../src/tiers/episodic-memory.ts"],"sourcesContent":[],"mappings":";;;;;;;AAyBA;;;;;;AAqB8B,UArBb,YAAA,CAqBa;EAUb,SAAA,OAAA,EAAA,MAAA;EAiBA,SAAA,SAAA,EAAA,MAAoB;EAiCxB,SAAA,OAAA,EAAc,MAAA;EAOhB;EACC,SAAA,UAAA,CAAA,EAAA,MAAA;EACE,SAAA,WAAA,CAAA,EApFW,WAoFX;EAUQ,SAAA,IAAA,CAAA,EA7FJ,aA6FI,CAAA,MAAA,CAAA;EAAqB;;;;;EA8DhC,SAAA,UAAA,CAAA,EArJa,gBAqJb;EAED;;;;;EA+Da,SAAA,MAAA,CAAA,EAhNH,YAgNG;EAAmD;EAyB/D,SAAA,KAAA,CAAA,EAvOQ,WAuOR;;;;;;;;;AAmCN,UAhQY,uBAAA,CAgQZ;EAAO,SAAA,OAAA,EAAA,MAAA;;;;;;;;;UA/OK,oBAAA;;oBAEG;qBACC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BR,cAAA;;;WAOF;YACC;cACE;;;;gBAUQ,qBAAqB,eAAe,QAAQ;;mBAkDzC,QAAQ;;;;;;;;gBAYtB,oCAED,uBACL,QAAQ,cAAc,UAAU;;;;;;;;;iBA8Dd,mDAAmD;;;;;;;oBAyB/D;;MAGN,QAAQ,cAAc;;gBAgBL;;MAA6C,QAAQ,cAAc;;;;;;;;kBAY9E;;MAIN"}
|
|
@@ -10,7 +10,7 @@ const DEFAULT_WEIGHTS = Object.freeze({
|
|
|
10
10
|
importance: .2
|
|
11
11
|
});
|
|
12
12
|
/**
|
|
13
|
-
* `EpisodicMemory`
|
|
13
|
+
* `EpisodicMemory` - record + retrieve summarized stretches of past
|
|
14
14
|
* activity. Stored embeddings power triple-signal retrieval (recency
|
|
15
15
|
* × relevance × importance).
|
|
16
16
|
*
|
|
@@ -45,6 +45,7 @@ var EpisodicMemory = class {
|
|
|
45
45
|
...input.tags !== void 0 ? { tags: Object.freeze([...input.tags]) } : {},
|
|
46
46
|
...input.provenance !== void 0 ? { provenance: input.provenance } : {},
|
|
47
47
|
...input.status !== void 0 ? { status: input.status } : {},
|
|
48
|
+
...input.owner !== void 0 ? { owner: input.owner } : {},
|
|
48
49
|
createdAt: now,
|
|
49
50
|
updatedAt: now
|
|
50
51
|
};
|
|
@@ -123,7 +124,7 @@ var EpisodicMemory = class {
|
|
|
123
124
|
}
|
|
124
125
|
/**
|
|
125
126
|
* Most-recent episodes by end time (newest first), with no embedding / FTS
|
|
126
|
-
* query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent`
|
|
127
|
+
* query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent` - the default
|
|
127
128
|
* `@graphorin/store-sqlite` adapter implements it. Optionally includes
|
|
128
129
|
* quarantined episodes (the importance source for the reflection gate).
|
|
129
130
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"episodic-memory.js","names":["DEFAULT_WEIGHTS: EpisodeRetrievalWeights","#store","#tracer","#embedder","#embedderIdProvider","episode: Episode","#tryVectorSearch"],"sources":["../../src/tiers/episodic-memory.ts"],"sourcesContent":["import type {\n EmbedderProvider,\n Episode,\n MemoryHit,\n MemoryProvenance,\n MemoryStatus,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { newMemoryId } from '../internal/id.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/**\n * Author-time episode payload. The framework derives `id`,\n * `kind: 'episodic'`, `userId`, `createdAt`, `updatedAt`, and the\n * `embedder_id` from the surrounding `EpisodicMemory.record(...)`\n * call.\n *\n * @stable\n */\nexport interface EpisodeInput {\n readonly summary: string;\n readonly startedAt: string;\n readonly endedAt: string;\n /** Optional importance score in `[0, 1]`. */\n readonly importance?: number;\n readonly sensitivity?: Sensitivity;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Trust-provenance tag (P1-4). Episodes auto-formed by the\n * consolidator pass `'extraction'` so they land quarantined; omit\n * (defaults to first-party `active`) for user-authored episodes.\n */\n readonly provenance?: MemoryProvenance;\n /**\n * Retrieval-trust state (P1-4). Defaults to `active`; the\n * consolidator records auto-formed episodes as `'quarantined'` so\n * they are excluded from action-driving recall until validated.\n */\n readonly status?: MemoryStatus;\n}\n\n/**\n * Triple-signal episode retrieval weights. Defaults match DEC-105:\n * `recency 0.3`, `relevance 0.5`, `importance 0.2`. Implementations\n * normalize the weighted sum back to `[0, 1]`.\n *\n * @stable\n */\nexport interface EpisodeRetrievalWeights {\n readonly recency: number;\n readonly relevance: number;\n readonly importance: number;\n}\n\nconst DEFAULT_WEIGHTS: EpisodeRetrievalWeights = Object.freeze({\n recency: 0.3,\n relevance: 0.5,\n importance: 0.2,\n});\n\n/**\n * Per-call options accepted by {@link EpisodicMemory.search}.\n *\n * @stable\n */\nexport interface EpisodeSearchOptions {\n readonly topK?: number;\n readonly signal?: AbortSignal;\n readonly weights?: EpisodeRetrievalWeights;\n readonly dateRange?: { readonly from?: string; readonly to?: string };\n /**\n * Point-in-time (\"as of\") read. When set, only episodes that had\n * started by this instant (`started_at <= asOf`) are returned.\n * ISO-8601. Absent ⇒ current behaviour is unchanged. P0-2.\n *\n * @stable\n */\n readonly asOf?: string;\n /**\n * Include quarantined episodes in the result set (P1-4). Defaults to\n * `false`: action-driving recall never returns quarantined rows. Set\n * `true` only for the validation / inspector path — never for\n * auto-recall fed back into the model. Auto-formed episodes (P1-2)\n * land quarantined, so this is how an operator surfaces them for\n * review.\n *\n * @stable\n */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `EpisodicMemory` — record + retrieve summarized stretches of past\n * activity. Stored embeddings power triple-signal retrieval (recency\n * × relevance × importance).\n *\n * @stable\n */\nexport class EpisodicMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n readonly #embedder: EmbedderProvider | null;\n readonly #embedderIdProvider: () => string | null;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n embedder: EmbedderProvider | null;\n embedderIdProvider: () => string | null;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#embedder = args.embedder;\n this.#embedderIdProvider = args.embedderIdProvider;\n }\n\n /** Persist an episode + its embedding (when an embedder is configured). */\n async record(scope: SessionScope, input: EpisodeInput): Promise<Episode> {\n return withMemorySpan(this.#tracer, 'memory.write.episodic', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const episode: Episode = {\n id: newMemoryId('ep'),\n kind: 'episodic',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'internal',\n summary: input.summary,\n startedAt: input.startedAt,\n endedAt: input.endedAt,\n ...(input.importance !== undefined ? { importance: input.importance } : {}),\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.provenance !== undefined ? { provenance: input.provenance } : {}),\n ...(input.status !== undefined ? { status: input.status } : {}),\n createdAt: now,\n updatedAt: now,\n };\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder !== null &&\n embedderId !== null &&\n typeof this.#store.episodic.putWithEmbedding === 'function'\n ) {\n // PS-10: a stored episode summary is the `passage` role for E5.\n const [vector] = await this.#embedder.embed([input.summary], { taskType: 'passage' });\n if (vector !== undefined) {\n await this.#store.episodic.putWithEmbedding(episode, {\n embedding: { embedderId, vector },\n });\n } else {\n await this.#store.episodic.put(episode);\n }\n } else {\n await this.#store.episodic.put(episode);\n }\n span.setAttributes({\n 'memory.episodic.summary_length': input.summary.length,\n ...(input.importance !== undefined\n ? { 'memory.episodic.importance': input.importance }\n : {}),\n });\n return episode;\n });\n }\n\n /** Lookup a single episode by id. */\n async get(id: string): Promise<Episode | null> {\n return this.#store.episodic.get(id);\n }\n\n /**\n * Triple-signal episode retrieval (`recency × relevance ×\n * importance`). The vector signal is computed on demand when an\n * embedder is configured AND the storage adapter exposes\n * `searchVector`; otherwise the FTS5 BM25 score is fed into the\n * relevance term as a normalized fallback.\n */\n async search(\n scope: SessionScope,\n query: string,\n opts: EpisodeSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Episode>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.episodic',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const topK = opts.topK ?? 10;\n const weights = opts.weights ?? DEFAULT_WEIGHTS;\n const ftsHits = await this.#store.episodic.search(scope, {\n query,\n topK: topK * 2,\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n // MRET-4: forward the date filter — it was accepted by the\n // recall_episodes tool and silently dropped right here.\n ...(opts.dateRange !== undefined ? { dateRange: opts.dateRange } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n const vectorHits = await this.#tryVectorSearch(\n scope,\n query,\n topK * 2,\n opts.asOf,\n opts.includeQuarantined,\n );\n // MRET-4: the vector leg's store signature is positional (no\n // dateRange parameter) — apply the same overlap semantics to its\n // hits so both legs agree before the merge.\n const from = opts.dateRange?.from !== undefined ? Date.parse(opts.dateRange.from) : null;\n const to = opts.dateRange?.to !== undefined ? Date.parse(opts.dateRange.to) : null;\n const rangedVectorHits =\n from !== null || to !== null\n ? vectorHits.filter((h) => {\n if (from !== null && Date.parse(h.record.endedAt) < from) return false;\n if (to !== null && Date.parse(h.record.startedAt) > to) return false;\n return true;\n })\n : vectorHits;\n const merged = mergeRecency(ftsHits, rangedVectorHits, weights);\n const finalHits = merged.slice(0, topK);\n span.setAttributes({\n 'memory.search.episodic.fts_count': ftsHits.length,\n 'memory.search.episodic.vector_count': vectorHits.length,\n 'memory.search.episodic.final_count': finalHits.length,\n ...(opts.asOf !== undefined ? { 'memory.search.episodic.as_of': opts.asOf } : {}),\n });\n return finalHits;\n },\n );\n }\n\n /**\n * Soft-archive an episode. Storage adapters that implement\n * `EpisodicMemoryStoreExt.archive(...)` mark the row archived in\n * place. Adapters that do not expose the extension surface a\n * friendly `TypeError` so the operator can opt the storage layer\n * in (or call `episodic.put(...)` with the archived state set\n * manually).\n */\n async archive(scope: SessionScope, episodeId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.episodic',\n scope,\n { 'memory.episodic.action': 'archive', 'memory.episodic.episode_id': episodeId },\n async () => {\n if (typeof this.#store.episodic.archive !== 'function') {\n throw new TypeError(\n '[graphorin/memory] EpisodicMemory.archive(...) requires a storage adapter that implements `episodic.archive(id)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',\n );\n }\n await this.#store.episodic.archive(episodeId, reason);\n },\n );\n }\n\n /**\n * Most-recent episodes by end time (newest first), with no embedding / FTS\n * query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent` — the default\n * `@graphorin/store-sqlite` adapter implements it. Optionally includes\n * quarantined episodes (the importance source for the reflection gate).\n */\n async listRecent(\n scope: SessionScope,\n limit: number,\n opts: { includeQuarantined?: boolean } = {},\n ): Promise<ReadonlyArray<Episode>> {\n return withMemorySpan(this.#tracer, 'memory.read.episodic', scope, {}, async (span) => {\n const store = this.#store.episodic;\n if (typeof store.listRecent !== 'function') {\n throw new Error(\n '[graphorin/memory] EpisodicMemory.listRecent requires EpisodicMemoryStoreExt.listRecent. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in.',\n );\n }\n const out = await store.listRecent(scope, limit, opts);\n span.setAttributes({ 'memory.read.episodic.count': out.length });\n return out;\n });\n }\n\n /** List the most recent episodes (no embedding required). */\n async recent(scope: SessionScope, opts: { topK?: number } = {}): Promise<ReadonlyArray<Episode>> {\n return this.listRecent(scope, opts.topK ?? 10, {});\n }\n\n /**\n * Promote a quarantined episode into default recall (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored summary and **refuses** promotion of an injection-flagged episode\n * (`QuarantinePromotionRefusedError`) unless an operator passes\n * `{ force: true }` from a trusted, non-agent context.\n */\n async validate(\n scope: SessionScope,\n episodeId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.episodic',\n scope,\n { 'memory.episodic.action': 'validate', 'memory.episodic.episode_id': episodeId },\n async (span) => {\n const store = this.#store.episodic;\n if (typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] EpisodicMemory.validate(...) requires a storage adapter that implements `episodic.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = await store.get(episodeId);\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.summary);\n if (injection.flagged) {\n span.setAttributes({ 'memory.episodic.validate.refused': true });\n throw new QuarantinePromotionRefusedError(episodeId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.episodic.validate.forced': force });\n await store.setStatus(episodeId, 'active', reason);\n },\n );\n }\n\n async #tryVectorSearch(\n scope: SessionScope,\n query: string,\n topK: number,\n asOf?: string,\n includeQuarantined?: boolean,\n ): Promise<ReadonlyArray<MemoryHit<Episode>>> {\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder === null ||\n embedderId === null ||\n typeof this.#store.episodic.searchVector !== 'function'\n ) {\n return [];\n }\n // PS-10: a search query is the `query` role for E5.\n const [vector] = await this.#embedder.embed([query], { taskType: 'query' });\n if (vector === undefined) return [];\n return this.#store.episodic.searchVector(\n scope,\n vector,\n embedderId,\n topK,\n asOf,\n includeQuarantined,\n );\n }\n}\n\nfunction mergeRecency(\n ftsHits: ReadonlyArray<MemoryHit<Episode>>,\n vectorHits: ReadonlyArray<MemoryHit<Episode>>,\n weights: EpisodeRetrievalWeights,\n): ReadonlyArray<MemoryHit<Episode>> {\n const now = Date.now();\n const aggregates = new Map<\n string,\n {\n record: Episode;\n relevance: number;\n recency: number;\n importance: number;\n score: number;\n signals: Record<string, number>;\n }\n >();\n for (const hit of ftsHits) {\n const id = hit.record.id;\n const recency = computeRecency(now, hit.record);\n const importance = hit.record.importance ?? 0;\n const relevance = normalizeRelevance(hit.score);\n const score =\n weights.recency * recency + weights.relevance * relevance + weights.importance * importance;\n aggregates.set(id, {\n record: hit.record,\n relevance,\n recency,\n importance,\n score,\n signals: { fts: relevance, recency, importance },\n });\n }\n for (const hit of vectorHits) {\n const id = hit.record.id;\n const cosine = clamp01(hit.score);\n const existing = aggregates.get(id);\n if (existing === undefined) {\n const recency = computeRecency(now, hit.record);\n const importance = hit.record.importance ?? 0;\n const score =\n weights.recency * recency + weights.relevance * cosine + weights.importance * importance;\n aggregates.set(id, {\n record: hit.record,\n relevance: cosine,\n recency,\n importance,\n score,\n signals: { vector: cosine, recency, importance },\n });\n } else {\n const blended = Math.max(existing.relevance, cosine);\n existing.relevance = blended;\n existing.score =\n weights.recency * existing.recency +\n weights.relevance * blended +\n weights.importance * existing.importance;\n existing.signals.vector = cosine;\n existing.signals['rrf-blend'] = blended;\n }\n }\n const out = Array.from(aggregates.values()).map((a) => ({\n record: a.record,\n score: a.score,\n signals: Object.freeze({ ...a.signals }),\n }));\n out.sort((a, b) => b.score - a.score);\n return out;\n}\n\nfunction computeRecency(now: number, episode: Episode): number {\n const ended = Date.parse(episode.endedAt);\n if (!Number.isFinite(ended)) return 0;\n const elapsedDays = Math.max(0, (now - ended) / (24 * 60 * 60 * 1000));\n return Math.exp(-elapsedDays / 30);\n}\n\nfunction normalizeRelevance(score: number): number {\n // MRET-5/MST-7: the store returns `score = -bm25(...)` — POSITIVE for\n // every FTS match (SQLite bm25 is always negative), so the old\n // `1 / (1 + max(0, -score))` collapsed every lexical hit to exactly\n // 1.0 and the DEC-105 triple-signal ranking degraded to\n // recency + importance. Saturating ratio: 0 at no signal, → 1 as the\n // match strengthens, graduated in between (k = 1 half-point).\n return clamp01(Math.max(0, score) / (Math.max(0, score) + 1));\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return 0;\n if (value < 0) return 0;\n if (value > 1) return 1;\n return value;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"episodic-memory.js","names":["DEFAULT_WEIGHTS: EpisodeRetrievalWeights","#store","#tracer","#embedder","#embedderIdProvider","episode: Episode","#tryVectorSearch"],"sources":["../../src/tiers/episodic-memory.ts"],"sourcesContent":["import type {\n EmbedderProvider,\n Episode,\n MemoryHit,\n MemoryOwner,\n MemoryProvenance,\n MemoryStatus,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { newMemoryId } from '../internal/id.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/**\n * Author-time episode payload. The framework derives `id`,\n * `kind: 'episodic'`, `userId`, `createdAt`, `updatedAt`, and the\n * `embedder_id` from the surrounding `EpisodicMemory.record(...)`\n * call.\n *\n * @stable\n */\nexport interface EpisodeInput {\n readonly summary: string;\n readonly startedAt: string;\n readonly endedAt: string;\n /** Optional importance score in `[0, 1]`. */\n readonly importance?: number;\n readonly sensitivity?: Sensitivity;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Trust-provenance tag (P1-4). Episodes auto-formed by the\n * consolidator pass `'extraction'` so they land quarantined; omit\n * (defaults to first-party `active`) for user-authored episodes.\n */\n readonly provenance?: MemoryProvenance;\n /**\n * Retrieval-trust state (P1-4). Defaults to `active`; the\n * consolidator records auto-formed episodes as `'quarantined'` so\n * they are excluded from action-driving recall until validated.\n */\n readonly status?: MemoryStatus;\n /** Principal dimension (D3). `'agent'` on auto-formed episodes. */\n readonly owner?: MemoryOwner;\n}\n\n/**\n * Triple-signal episode retrieval weights. Defaults match DEC-105:\n * `recency 0.3`, `relevance 0.5`, `importance 0.2`. Implementations\n * normalize the weighted sum back to `[0, 1]`.\n *\n * @stable\n */\nexport interface EpisodeRetrievalWeights {\n readonly recency: number;\n readonly relevance: number;\n readonly importance: number;\n}\n\nconst DEFAULT_WEIGHTS: EpisodeRetrievalWeights = Object.freeze({\n recency: 0.3,\n relevance: 0.5,\n importance: 0.2,\n});\n\n/**\n * Per-call options accepted by {@link EpisodicMemory.search}.\n *\n * @stable\n */\nexport interface EpisodeSearchOptions {\n readonly topK?: number;\n readonly signal?: AbortSignal;\n readonly weights?: EpisodeRetrievalWeights;\n readonly dateRange?: { readonly from?: string; readonly to?: string };\n /**\n * Point-in-time (\"as of\") read. When set, only episodes that had\n * started by this instant (`started_at <= asOf`) are returned.\n * ISO-8601. Absent ⇒ current behaviour is unchanged. P0-2.\n *\n * @stable\n */\n readonly asOf?: string;\n /**\n * Include quarantined episodes in the result set (P1-4). Defaults to\n * `false`: action-driving recall never returns quarantined rows. Set\n * `true` only for the validation / inspector path - never for\n * auto-recall fed back into the model. Auto-formed episodes (P1-2)\n * land quarantined, so this is how an operator surfaces them for\n * review.\n *\n * @stable\n */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `EpisodicMemory` - record + retrieve summarized stretches of past\n * activity. Stored embeddings power triple-signal retrieval (recency\n * × relevance × importance).\n *\n * @stable\n */\nexport class EpisodicMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n readonly #embedder: EmbedderProvider | null;\n readonly #embedderIdProvider: () => string | null;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n embedder: EmbedderProvider | null;\n embedderIdProvider: () => string | null;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#embedder = args.embedder;\n this.#embedderIdProvider = args.embedderIdProvider;\n }\n\n /** Persist an episode + its embedding (when an embedder is configured). */\n async record(scope: SessionScope, input: EpisodeInput): Promise<Episode> {\n return withMemorySpan(this.#tracer, 'memory.write.episodic', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const episode: Episode = {\n id: newMemoryId('ep'),\n kind: 'episodic',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'internal',\n summary: input.summary,\n startedAt: input.startedAt,\n endedAt: input.endedAt,\n ...(input.importance !== undefined ? { importance: input.importance } : {}),\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.provenance !== undefined ? { provenance: input.provenance } : {}),\n ...(input.status !== undefined ? { status: input.status } : {}),\n ...(input.owner !== undefined ? { owner: input.owner } : {}),\n createdAt: now,\n updatedAt: now,\n };\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder !== null &&\n embedderId !== null &&\n typeof this.#store.episodic.putWithEmbedding === 'function'\n ) {\n // PS-10: a stored episode summary is the `passage` role for E5.\n const [vector] = await this.#embedder.embed([input.summary], { taskType: 'passage' });\n if (vector !== undefined) {\n await this.#store.episodic.putWithEmbedding(episode, {\n embedding: { embedderId, vector },\n });\n } else {\n await this.#store.episodic.put(episode);\n }\n } else {\n await this.#store.episodic.put(episode);\n }\n span.setAttributes({\n 'memory.episodic.summary_length': input.summary.length,\n ...(input.importance !== undefined\n ? { 'memory.episodic.importance': input.importance }\n : {}),\n });\n return episode;\n });\n }\n\n /** Lookup a single episode by id. */\n async get(id: string): Promise<Episode | null> {\n return this.#store.episodic.get(id);\n }\n\n /**\n * Triple-signal episode retrieval (`recency × relevance ×\n * importance`). The vector signal is computed on demand when an\n * embedder is configured AND the storage adapter exposes\n * `searchVector`; otherwise the FTS5 BM25 score is fed into the\n * relevance term as a normalized fallback.\n */\n async search(\n scope: SessionScope,\n query: string,\n opts: EpisodeSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Episode>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.episodic',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const topK = opts.topK ?? 10;\n const weights = opts.weights ?? DEFAULT_WEIGHTS;\n const ftsHits = await this.#store.episodic.search(scope, {\n query,\n topK: topK * 2,\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n // MRET-4: forward the date filter - it was accepted by the\n // recall_episodes tool and silently dropped right here.\n ...(opts.dateRange !== undefined ? { dateRange: opts.dateRange } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n const vectorHits = await this.#tryVectorSearch(\n scope,\n query,\n topK * 2,\n opts.asOf,\n opts.includeQuarantined,\n );\n // MRET-4: the vector leg's store signature is positional (no\n // dateRange parameter) - apply the same overlap semantics to its\n // hits so both legs agree before the merge.\n const from = opts.dateRange?.from !== undefined ? Date.parse(opts.dateRange.from) : null;\n const to = opts.dateRange?.to !== undefined ? Date.parse(opts.dateRange.to) : null;\n const rangedVectorHits =\n from !== null || to !== null\n ? vectorHits.filter((h) => {\n if (from !== null && Date.parse(h.record.endedAt) < from) return false;\n if (to !== null && Date.parse(h.record.startedAt) > to) return false;\n return true;\n })\n : vectorHits;\n const merged = mergeRecency(ftsHits, rangedVectorHits, weights);\n const finalHits = merged.slice(0, topK);\n span.setAttributes({\n 'memory.search.episodic.fts_count': ftsHits.length,\n 'memory.search.episodic.vector_count': vectorHits.length,\n 'memory.search.episodic.final_count': finalHits.length,\n ...(opts.asOf !== undefined ? { 'memory.search.episodic.as_of': opts.asOf } : {}),\n });\n return finalHits;\n },\n );\n }\n\n /**\n * Soft-archive an episode. Storage adapters that implement\n * `EpisodicMemoryStoreExt.archive(...)` mark the row archived in\n * place. Adapters that do not expose the extension surface a\n * friendly `TypeError` so the operator can opt the storage layer\n * in (or call `episodic.put(...)` with the archived state set\n * manually).\n */\n async archive(scope: SessionScope, episodeId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.episodic',\n scope,\n { 'memory.episodic.action': 'archive', 'memory.episodic.episode_id': episodeId },\n async () => {\n if (typeof this.#store.episodic.archive !== 'function') {\n throw new TypeError(\n '[graphorin/memory] EpisodicMemory.archive(...) requires a storage adapter that implements `episodic.archive(id)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',\n );\n }\n await this.#store.episodic.archive(episodeId, reason);\n },\n );\n }\n\n /**\n * Most-recent episodes by end time (newest first), with no embedding / FTS\n * query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent` - the default\n * `@graphorin/store-sqlite` adapter implements it. Optionally includes\n * quarantined episodes (the importance source for the reflection gate).\n */\n async listRecent(\n scope: SessionScope,\n limit: number,\n opts: { includeQuarantined?: boolean } = {},\n ): Promise<ReadonlyArray<Episode>> {\n return withMemorySpan(this.#tracer, 'memory.read.episodic', scope, {}, async (span) => {\n const store = this.#store.episodic;\n if (typeof store.listRecent !== 'function') {\n throw new Error(\n '[graphorin/memory] EpisodicMemory.listRecent requires EpisodicMemoryStoreExt.listRecent. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in.',\n );\n }\n const out = await store.listRecent(scope, limit, opts);\n span.setAttributes({ 'memory.read.episodic.count': out.length });\n return out;\n });\n }\n\n /** List the most recent episodes (no embedding required). */\n async recent(scope: SessionScope, opts: { topK?: number } = {}): Promise<ReadonlyArray<Episode>> {\n return this.listRecent(scope, opts.topK ?? 10, {});\n }\n\n /**\n * Promote a quarantined episode into default recall (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored summary and **refuses** promotion of an injection-flagged episode\n * (`QuarantinePromotionRefusedError`) unless an operator passes\n * `{ force: true }` from a trusted, non-agent context.\n */\n async validate(\n scope: SessionScope,\n episodeId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.episodic',\n scope,\n { 'memory.episodic.action': 'validate', 'memory.episodic.episode_id': episodeId },\n async (span) => {\n const store = this.#store.episodic;\n if (typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] EpisodicMemory.validate(...) requires a storage adapter that implements `episodic.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = await store.get(episodeId);\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.summary);\n if (injection.flagged) {\n span.setAttributes({ 'memory.episodic.validate.refused': true });\n throw new QuarantinePromotionRefusedError(episodeId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.episodic.validate.forced': force });\n await store.setStatus(episodeId, 'active', reason);\n },\n );\n }\n\n async #tryVectorSearch(\n scope: SessionScope,\n query: string,\n topK: number,\n asOf?: string,\n includeQuarantined?: boolean,\n ): Promise<ReadonlyArray<MemoryHit<Episode>>> {\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder === null ||\n embedderId === null ||\n typeof this.#store.episodic.searchVector !== 'function'\n ) {\n return [];\n }\n // PS-10: a search query is the `query` role for E5.\n const [vector] = await this.#embedder.embed([query], { taskType: 'query' });\n if (vector === undefined) return [];\n return this.#store.episodic.searchVector(\n scope,\n vector,\n embedderId,\n topK,\n asOf,\n includeQuarantined,\n );\n }\n}\n\nfunction mergeRecency(\n ftsHits: ReadonlyArray<MemoryHit<Episode>>,\n vectorHits: ReadonlyArray<MemoryHit<Episode>>,\n weights: EpisodeRetrievalWeights,\n): ReadonlyArray<MemoryHit<Episode>> {\n const now = Date.now();\n const aggregates = new Map<\n string,\n {\n record: Episode;\n relevance: number;\n recency: number;\n importance: number;\n score: number;\n signals: Record<string, number>;\n }\n >();\n for (const hit of ftsHits) {\n const id = hit.record.id;\n const recency = computeRecency(now, hit.record);\n const importance = hit.record.importance ?? 0;\n const relevance = normalizeRelevance(hit.score);\n const score =\n weights.recency * recency + weights.relevance * relevance + weights.importance * importance;\n aggregates.set(id, {\n record: hit.record,\n relevance,\n recency,\n importance,\n score,\n signals: { fts: relevance, recency, importance },\n });\n }\n for (const hit of vectorHits) {\n const id = hit.record.id;\n const cosine = clamp01(hit.score);\n const existing = aggregates.get(id);\n if (existing === undefined) {\n const recency = computeRecency(now, hit.record);\n const importance = hit.record.importance ?? 0;\n const score =\n weights.recency * recency + weights.relevance * cosine + weights.importance * importance;\n aggregates.set(id, {\n record: hit.record,\n relevance: cosine,\n recency,\n importance,\n score,\n signals: { vector: cosine, recency, importance },\n });\n } else {\n const blended = Math.max(existing.relevance, cosine);\n existing.relevance = blended;\n existing.score =\n weights.recency * existing.recency +\n weights.relevance * blended +\n weights.importance * existing.importance;\n existing.signals.vector = cosine;\n existing.signals['rrf-blend'] = blended;\n }\n }\n const out = Array.from(aggregates.values()).map((a) => ({\n record: a.record,\n score: a.score,\n signals: Object.freeze({ ...a.signals }),\n }));\n out.sort((a, b) => b.score - a.score);\n return out;\n}\n\nfunction computeRecency(now: number, episode: Episode): number {\n const ended = Date.parse(episode.endedAt);\n if (!Number.isFinite(ended)) return 0;\n const elapsedDays = Math.max(0, (now - ended) / (24 * 60 * 60 * 1000));\n return Math.exp(-elapsedDays / 30);\n}\n\nfunction normalizeRelevance(score: number): number {\n // MRET-5/MST-7: the store returns `score = -bm25(...)` - POSITIVE for\n // every FTS match (SQLite bm25 is always negative), so the old\n // `1 / (1 + max(0, -score))` collapsed every lexical hit to exactly\n // 1.0 and the DEC-105 triple-signal ranking degraded to\n // recency + importance. Saturating ratio: 0 at no signal, → 1 as the\n // match strengthens, graduated in between (k = 1 half-point).\n return clamp01(Math.max(0, score) / (Math.max(0, score) + 1));\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return 0;\n if (value < 0) return 0;\n if (value > 1) return 1;\n return value;\n}\n"],"mappings":"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"}
|
package/dist/tiers/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EpisodeInput, EpisodeRetrievalWeights, EpisodeSearchOptions, EpisodicMemory } from "./episodic-memory.js";
|
|
2
2
|
import { FactInput, FactRememberOptions, FactSearchOptions, FusionStrategy, FusionWeights, IterativeRecallResult, IterativeSearchOptions, RememberOutcome, SemanticMemory } from "./semantic-memory.js";
|
|
3
|
+
import { BlockDefinition, BlockSpec, WorkingMemory, defineBlock } from "./working-memory.js";
|
|
3
4
|
import { InsightListOptions, InsightMemory, InsightSearchOptions, capInsightsBelowFacts } from "./insight-memory.js";
|
|
4
5
|
import { InduceOptions, ProceduralMemory, RuleActivationContext, RuleInput } from "./procedural-memory.js";
|
|
5
6
|
import { SessionCompactionPolicy, SessionCompactionResult, SessionMemory } from "./session-memory.js";
|
|
6
7
|
import { SharedMemory } from "./shared-memory.js";
|
|
7
|
-
import { BlockDefinition, BlockSpec, WorkingMemory, defineBlock } from "./working-memory.js";
|
|
8
8
|
export { type BlockDefinition, type BlockSpec, type EpisodeInput, type EpisodeRetrievalWeights, type EpisodeSearchOptions, EpisodicMemory, type FactInput, type FactRememberOptions, type FactSearchOptions, type FusionStrategy, type FusionWeights, type InduceOptions, type InsightListOptions, InsightMemory, type InsightSearchOptions, type IterativeRecallResult, type IterativeSearchOptions, ProceduralMemory, type RememberOutcome, type RuleActivationContext, type RuleInput, SemanticMemory, type SessionCompactionPolicy, type SessionCompactionResult, SessionMemory, SharedMemory, WorkingMemory, capInsightsBelowFacts, defineBlock };
|
package/dist/tiers/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { WorkingMemory, defineBlock } from "./working-memory.js";
|
|
1
2
|
import { EpisodicMemory } from "./episodic-memory.js";
|
|
2
3
|
import { InsightMemory, capInsightsBelowFacts } from "./insight-memory.js";
|
|
3
4
|
import { ProceduralMemory } from "./procedural-memory.js";
|
|
4
5
|
import { SemanticMemory } from "./semantic-memory.js";
|
|
5
6
|
import { SessionMemory } from "./session-memory.js";
|
|
6
7
|
import { SharedMemory } from "./shared-memory.js";
|
|
7
|
-
import { WorkingMemory, defineBlock } from "./working-memory.js";
|
|
8
8
|
|
|
9
9
|
export { EpisodicMemory, InsightMemory, ProceduralMemory, SemanticMemory, SessionMemory, SharedMemory, WorkingMemory, capInsightsBelowFacts, defineBlock };
|
|
@@ -9,7 +9,7 @@ interface InsightSearchOptions {
|
|
|
9
9
|
/**
|
|
10
10
|
* Include quarantined insights (P1-4). Defaults to `false`. Since
|
|
11
11
|
* reflection-synthesized insights *always* land quarantined, set this
|
|
12
|
-
* `true` to surface them for the validation / inspector path
|
|
12
|
+
* `true` to surface them for the validation / inspector path - never
|
|
13
13
|
* for auto-recall fed back into the model.
|
|
14
14
|
*/
|
|
15
15
|
readonly includeQuarantined?: boolean;
|
|
@@ -22,7 +22,7 @@ interface InsightListOptions {
|
|
|
22
22
|
readonly includeQuarantined?: boolean;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* `InsightMemory`
|
|
25
|
+
* `InsightMemory` - list / search reflection insights. A no-op (returns
|
|
26
26
|
* empty) when the storage adapter does not expose the optional
|
|
27
27
|
* `insights` surface.
|
|
28
28
|
*
|
|
@@ -54,14 +54,14 @@ declare class InsightMemory {
|
|
|
54
54
|
* Enforce the P1-1 rank ceiling: an insight may never outrank a primary
|
|
55
55
|
* fact **it cites**. For each insight hit, if any fact it cites is
|
|
56
56
|
* present in `factHits`, its score is lowered to strictly below that
|
|
57
|
-
* cited fact's score
|
|
57
|
+
* cited fact's score - so concatenating the two lists and sorting by
|
|
58
58
|
* score descending can never place the insight above the evidence it
|
|
59
59
|
* was synthesized from. Insights whose cited facts are absent from
|
|
60
60
|
* `factHits` are returned unchanged; this is a relative, not a global,
|
|
61
61
|
* cap (per the execution plan: "never outrank the primary facts they
|
|
62
62
|
* cite").
|
|
63
63
|
*
|
|
64
|
-
* Pure + deterministic
|
|
64
|
+
* Pure + deterministic - does not mutate its inputs.
|
|
65
65
|
*
|
|
66
66
|
* @stable
|
|
67
67
|
*/
|
|
@@ -4,7 +4,7 @@ import { detectMemoryInjection } from "../internal/injection-heuristics.js";
|
|
|
4
4
|
|
|
5
5
|
//#region src/tiers/insight-memory.ts
|
|
6
6
|
/**
|
|
7
|
-
* `InsightMemory`
|
|
7
|
+
* `InsightMemory` - list / search reflection insights. A no-op (returns
|
|
8
8
|
* empty) when the storage adapter does not expose the optional
|
|
9
9
|
* `insights` surface.
|
|
10
10
|
*
|
|
@@ -91,14 +91,14 @@ const RANK_CAP_EPSILON = 1e-6;
|
|
|
91
91
|
* Enforce the P1-1 rank ceiling: an insight may never outrank a primary
|
|
92
92
|
* fact **it cites**. For each insight hit, if any fact it cites is
|
|
93
93
|
* present in `factHits`, its score is lowered to strictly below that
|
|
94
|
-
* cited fact's score
|
|
94
|
+
* cited fact's score - so concatenating the two lists and sorting by
|
|
95
95
|
* score descending can never place the insight above the evidence it
|
|
96
96
|
* was synthesized from. Insights whose cited facts are absent from
|
|
97
97
|
* `factHits` are returned unchanged; this is a relative, not a global,
|
|
98
98
|
* cap (per the execution plan: "never outrank the primary facts they
|
|
99
99
|
* cite").
|
|
100
100
|
*
|
|
101
|
-
* Pure + deterministic
|
|
101
|
+
* Pure + deterministic - does not mutate its inputs.
|
|
102
102
|
*
|
|
103
103
|
* @stable
|
|
104
104
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insight-memory.js","names":["#store","#tracer"],"sources":["../../src/tiers/insight-memory.ts"],"sourcesContent":["/**\n * `InsightMemory` — the thin, read-only surface over the reflection\n * insights the consolidator synthesizes (P1-1). Insights are *derived*\n * (quarantined + `provenance: 'reflection'`), carry mandatory citations,\n * and are retrieval-ranked **below the primary facts they cite**. The\n * write + salience-management paths live in the consolidator's\n * reflection pass; this tier only lists / searches for the validation /\n * inspector path.\n *\n * Search is FTS-only — insights are a soft inspector surface, not\n * primary recall, so no embedder is wired here.\n *\n * @packageDocumentation\n */\n\nimport type { Fact, Insight, MemoryHit, SessionScope, Tracer } from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/** Per-call options accepted by {@link InsightMemory.search}. */\nexport interface InsightSearchOptions {\n readonly topK?: number;\n /**\n * Include quarantined insights (P1-4). Defaults to `false`. Since\n * reflection-synthesized insights *always* land quarantined, set this\n * `true` to surface them for the validation / inspector path — never\n * for auto-recall fed back into the model.\n */\n readonly includeQuarantined?: boolean;\n readonly signal?: AbortSignal;\n}\n\n/** Per-call options accepted by {@link InsightMemory.list}. */\nexport interface InsightListOptions {\n readonly limit?: number;\n /** Include quarantined insights (validation / inspector path). */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `InsightMemory` — list / search reflection insights. A no-op (returns\n * empty) when the storage adapter does not expose the optional\n * `insights` surface.\n *\n * @stable\n */\nexport class InsightMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n\n constructor(args: { store: MemoryStoreAdapter; tracer: Tracer }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n }\n\n /** FTS keyword search over insight text. */\n async search(\n scope: SessionScope,\n query: string,\n opts: InsightSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Insight>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.insight',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.search.insight.count': 0 });\n return [];\n }\n const hits = await store.search(scope, query, {\n topK: opts.topK ?? 10,\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n // MCON-16: retrieval reinforces (ExpeL) — a recalled insight gains\n // +1 salience, offsetting the per-reflection-pass −1 decay, so\n // used insights persist while unused ones slide to the prune\n // floor. Best-effort bookkeeping: never break the read path.\n for (const hit of hits) {\n try {\n await store.bumpSalience(hit.record.id, 1, 'retrieval');\n } catch {\n // Advisory only.\n }\n }\n span.setAttributes({ 'memory.search.insight.count': hits.length });\n return hits;\n },\n );\n }\n\n /** Most-recent insights for the scope (newest first). */\n async list(scope: SessionScope, opts: InsightListOptions = {}): Promise<ReadonlyArray<Insight>> {\n return withMemorySpan(this.#tracer, 'memory.read.insight', scope, {}, async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.read.insight.count': 0 });\n return [];\n }\n const out = await store.list(scope, {\n ...(opts.limit !== undefined ? { limit: opts.limit } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n span.setAttributes({ 'memory.read.insight.count': out.length });\n return out;\n });\n }\n\n /** Lookup a single insight by id. */\n async get(id: string): Promise<Insight | null> {\n const store = this.#store.insights;\n if (store === undefined || typeof store.get !== 'function') return null;\n return store.get(id);\n }\n\n /**\n * Promote a quarantined insight out of quarantine (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored text and **refuses** promotion of an injection-flagged insight\n * unless an operator passes `{ force: true }` from a trusted context.\n */\n async validate(\n scope: SessionScope,\n insightId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.insight',\n scope,\n { 'memory.insight.action': 'validate', 'memory.insight.insight_id': insightId },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined || typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] InsightMemory.validate(...) requires a storage adapter that implements `insights.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via InsightMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = typeof store.get === 'function' ? await store.get(insightId) : null;\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({ 'memory.insight.validate.refused': true });\n throw new QuarantinePromotionRefusedError(insightId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.insight.validate.forced': force });\n await store.setStatus(insightId, 'active', reason);\n },\n );\n }\n}\n\n/** Tie-break margin keeping a capped insight strictly below its cited fact. */\nconst RANK_CAP_EPSILON = 1e-6;\n\n/**\n * Enforce the P1-1 rank ceiling: an insight may never outrank a primary\n * fact **it cites**. For each insight hit, if any fact it cites is\n * present in `factHits`, its score is lowered to strictly below that\n * cited fact's score — so concatenating the two lists and sorting by\n * score descending can never place the insight above the evidence it\n * was synthesized from. Insights whose cited facts are absent from\n * `factHits` are returned unchanged; this is a relative, not a global,\n * cap (per the execution plan: \"never outrank the primary facts they\n * cite\").\n *\n * Pure + deterministic — does not mutate its inputs.\n *\n * @stable\n */\nexport function capInsightsBelowFacts(\n factHits: ReadonlyArray<MemoryHit<Fact>>,\n insightHits: ReadonlyArray<MemoryHit<Insight>>,\n): ReadonlyArray<MemoryHit<Insight>> {\n if (factHits.length === 0 || insightHits.length === 0) return insightHits;\n const factScoreById = new Map<string, number>();\n for (const hit of factHits) {\n const prev = factScoreById.get(hit.record.id);\n if (prev === undefined || hit.score > prev) factScoreById.set(hit.record.id, hit.score);\n }\n return insightHits.map((hit) => {\n let citedMax = Number.NEGATIVE_INFINITY;\n for (const id of hit.record.cites) {\n const score = factScoreById.get(id);\n if (score !== undefined && score > citedMax) citedMax = score;\n }\n // No cited fact present in the fused set, or already below it ⇒ keep.\n if (citedMax === Number.NEGATIVE_INFINITY || hit.score < citedMax) return hit;\n const capped = citedMax - RANK_CAP_EPSILON;\n return {\n ...hit,\n score: capped,\n signals: Object.freeze({ ...(hit.signals ?? {}), 'rank-cap': capped }),\n };\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"insight-memory.js","names":["#store","#tracer"],"sources":["../../src/tiers/insight-memory.ts"],"sourcesContent":["/**\n * `InsightMemory` - the thin, read-only surface over the reflection\n * insights the consolidator synthesizes (P1-1). Insights are *derived*\n * (quarantined + `provenance: 'reflection'`), carry mandatory citations,\n * and are retrieval-ranked **below the primary facts they cite**. The\n * write + salience-management paths live in the consolidator's\n * reflection pass; this tier only lists / searches for the validation /\n * inspector path.\n *\n * Search is FTS-only - insights are a soft inspector surface, not\n * primary recall, so no embedder is wired here.\n *\n * @packageDocumentation\n */\n\nimport type { Fact, Insight, MemoryHit, SessionScope, Tracer } from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/** Per-call options accepted by {@link InsightMemory.search}. */\nexport interface InsightSearchOptions {\n readonly topK?: number;\n /**\n * Include quarantined insights (P1-4). Defaults to `false`. Since\n * reflection-synthesized insights *always* land quarantined, set this\n * `true` to surface them for the validation / inspector path - never\n * for auto-recall fed back into the model.\n */\n readonly includeQuarantined?: boolean;\n readonly signal?: AbortSignal;\n}\n\n/** Per-call options accepted by {@link InsightMemory.list}. */\nexport interface InsightListOptions {\n readonly limit?: number;\n /** Include quarantined insights (validation / inspector path). */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `InsightMemory` - list / search reflection insights. A no-op (returns\n * empty) when the storage adapter does not expose the optional\n * `insights` surface.\n *\n * @stable\n */\nexport class InsightMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n\n constructor(args: { store: MemoryStoreAdapter; tracer: Tracer }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n }\n\n /** FTS keyword search over insight text. */\n async search(\n scope: SessionScope,\n query: string,\n opts: InsightSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Insight>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.insight',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.search.insight.count': 0 });\n return [];\n }\n const hits = await store.search(scope, query, {\n topK: opts.topK ?? 10,\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n // MCON-16: retrieval reinforces (ExpeL) - a recalled insight gains\n // +1 salience, offsetting the per-reflection-pass -1 decay, so\n // used insights persist while unused ones slide to the prune\n // floor. Best-effort bookkeeping: never break the read path.\n for (const hit of hits) {\n try {\n await store.bumpSalience(hit.record.id, 1, 'retrieval');\n } catch {\n // Advisory only.\n }\n }\n span.setAttributes({ 'memory.search.insight.count': hits.length });\n return hits;\n },\n );\n }\n\n /** Most-recent insights for the scope (newest first). */\n async list(scope: SessionScope, opts: InsightListOptions = {}): Promise<ReadonlyArray<Insight>> {\n return withMemorySpan(this.#tracer, 'memory.read.insight', scope, {}, async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.read.insight.count': 0 });\n return [];\n }\n const out = await store.list(scope, {\n ...(opts.limit !== undefined ? { limit: opts.limit } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n span.setAttributes({ 'memory.read.insight.count': out.length });\n return out;\n });\n }\n\n /** Lookup a single insight by id. */\n async get(id: string): Promise<Insight | null> {\n const store = this.#store.insights;\n if (store === undefined || typeof store.get !== 'function') return null;\n return store.get(id);\n }\n\n /**\n * Promote a quarantined insight out of quarantine (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored text and **refuses** promotion of an injection-flagged insight\n * unless an operator passes `{ force: true }` from a trusted context.\n */\n async validate(\n scope: SessionScope,\n insightId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.insight',\n scope,\n { 'memory.insight.action': 'validate', 'memory.insight.insight_id': insightId },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined || typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] InsightMemory.validate(...) requires a storage adapter that implements `insights.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via InsightMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = typeof store.get === 'function' ? await store.get(insightId) : null;\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({ 'memory.insight.validate.refused': true });\n throw new QuarantinePromotionRefusedError(insightId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.insight.validate.forced': force });\n await store.setStatus(insightId, 'active', reason);\n },\n );\n }\n}\n\n/** Tie-break margin keeping a capped insight strictly below its cited fact. */\nconst RANK_CAP_EPSILON = 1e-6;\n\n/**\n * Enforce the P1-1 rank ceiling: an insight may never outrank a primary\n * fact **it cites**. For each insight hit, if any fact it cites is\n * present in `factHits`, its score is lowered to strictly below that\n * cited fact's score - so concatenating the two lists and sorting by\n * score descending can never place the insight above the evidence it\n * was synthesized from. Insights whose cited facts are absent from\n * `factHits` are returned unchanged; this is a relative, not a global,\n * cap (per the execution plan: \"never outrank the primary facts they\n * cite\").\n *\n * Pure + deterministic - does not mutate its inputs.\n *\n * @stable\n */\nexport function capInsightsBelowFacts(\n factHits: ReadonlyArray<MemoryHit<Fact>>,\n insightHits: ReadonlyArray<MemoryHit<Insight>>,\n): ReadonlyArray<MemoryHit<Insight>> {\n if (factHits.length === 0 || insightHits.length === 0) return insightHits;\n const factScoreById = new Map<string, number>();\n for (const hit of factHits) {\n const prev = factScoreById.get(hit.record.id);\n if (prev === undefined || hit.score > prev) factScoreById.set(hit.record.id, hit.score);\n }\n return insightHits.map((hit) => {\n let citedMax = Number.NEGATIVE_INFINITY;\n for (const id of hit.record.cites) {\n const score = factScoreById.get(id);\n if (score !== undefined && score > citedMax) citedMax = score;\n }\n // No cited fact present in the fused set, or already below it ⇒ keep.\n if (citedMax === Number.NEGATIVE_INFINITY || hit.score < citedMax) return hit;\n const capped = citedMax - RANK_CAP_EPSILON;\n return {\n ...hit,\n score: capped,\n signals: Object.freeze({ ...(hit.signals ?? {}), 'rank-cap': capped }),\n };\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MemoryStoreAdapter } from "../internal/storage-adapter.js";
|
|
2
2
|
import { Trajectory, WorkflowInducer } from "../consolidator/phases/induce.js";
|
|
3
|
-
import { Rule, RunState, Sensitivity, SessionScope, Tracer } from "@graphorin/core";
|
|
3
|
+
import { MemoryHit, MemoryOwner, Rule, RunState, Sensitivity, SessionScope, Tracer } from "@graphorin/core";
|
|
4
4
|
|
|
5
5
|
//#region src/tiers/procedural-memory.d.ts
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ interface RuleInput {
|
|
|
19
19
|
* matched in `activate(...)`'s `customMatchers` argument.
|
|
20
20
|
*/
|
|
21
21
|
readonly condition?: string;
|
|
22
|
-
/** Default `'public'` per DEC-126
|
|
22
|
+
/** Default `'public'` per DEC-126 - rules are NOT user data. */
|
|
23
23
|
readonly sensitivity?: Sensitivity;
|
|
24
24
|
readonly priority?: number;
|
|
25
25
|
readonly tags?: ReadonlyArray<string>;
|
|
@@ -33,6 +33,8 @@ interface RuleInput {
|
|
|
33
33
|
readonly variables?: ReadonlyArray<string>;
|
|
34
34
|
/** Verifiable success criteria stored with the procedure (P2-2). */
|
|
35
35
|
readonly successCriteria?: ReadonlyArray<string>;
|
|
36
|
+
/** Principal dimension (D3). Absent ⇒ NULL (treated `'user'`). */
|
|
37
|
+
readonly owner?: MemoryOwner;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Options for {@link ProceduralMemory.induce}.
|
|
@@ -55,7 +57,7 @@ interface RuleActivationContext {
|
|
|
55
57
|
readonly tags?: ReadonlyArray<string>;
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
|
-
* `ProceduralMemory`
|
|
60
|
+
* `ProceduralMemory` - standing orders activated when the agent's
|
|
59
61
|
* current context matches the rule's predicate. The activation rules
|
|
60
62
|
* are deterministic so the agent runtime + ContextEngine can render
|
|
61
63
|
* the active set into the system prompt every step.
|
|
@@ -81,12 +83,12 @@ declare class ProceduralMemory {
|
|
|
81
83
|
* (MCON-2 part 4). A success increments the rule's persistent
|
|
82
84
|
* `successCount`; when `procedurePromotion.afterSuccesses` is
|
|
83
85
|
* configured and a QUARANTINED procedure reaches the threshold it is
|
|
84
|
-
* promoted through {@link validate}
|
|
86
|
+
* promoted through {@link validate} - the injection gate still
|
|
85
87
|
* applies, so a flagged text refuses promotion (surfaced as
|
|
86
88
|
* `refused: true`) no matter how many successes accumulate.
|
|
87
89
|
* Failures are observed but not persisted (no negative counter yet).
|
|
88
90
|
*
|
|
89
|
-
* Callers decide what "success" means
|
|
91
|
+
* Callers decide what "success" means - typically
|
|
90
92
|
* `checkSuccessCriteria(...)` over the procedure's stored
|
|
91
93
|
* `successCriteria`, or an operator's judgement.
|
|
92
94
|
*
|
|
@@ -127,10 +129,30 @@ declare class ProceduralMemory {
|
|
|
127
129
|
* always-active so callers do not silently lose rules.
|
|
128
130
|
*
|
|
129
131
|
* **Quarantined procedures are excluded** (P1-4 / P2-2): an induced
|
|
130
|
-
* procedure must not drive actions until validated, so activation
|
|
131
|
-
* feeds the system prompt
|
|
132
|
+
* procedure must not drive actions until validated, so activation - which
|
|
133
|
+
* feeds the system prompt - never surfaces it.
|
|
132
134
|
*/
|
|
133
135
|
activate(scope: SessionScope, context?: RuleActivationContext): Promise<ReadonlyArray<Rule>>;
|
|
136
|
+
/**
|
|
137
|
+
* Runbook content search (D3): "find the procedure for this task" -
|
|
138
|
+
* lexical recall over rule text, as opposed to predicate
|
|
139
|
+
* {@link activate}. Returns **whole validated procedures** (the full
|
|
140
|
+
* {@link Rule} incl. steps / variables / success criteria) so a match
|
|
141
|
+
* can be followed file-style rather than re-synthesized from
|
|
142
|
+
* fragments. Quarantined (unvalidated induced) procedures are
|
|
143
|
+
* excluded - they must not drive actions - unless the inspector opts
|
|
144
|
+
* in via `includeQuarantined`.
|
|
145
|
+
*
|
|
146
|
+
* Uses the storage adapter's FTS surface when available
|
|
147
|
+
* (`procedural.search`, the default `@graphorin/store-sqlite` adapter
|
|
148
|
+
* implements it via migration 028); otherwise degrades to an offline
|
|
149
|
+
* in-memory token-overlap scan over {@link list}, so custom adapters
|
|
150
|
+
* keep working without the index.
|
|
151
|
+
*/
|
|
152
|
+
search(scope: SessionScope, query: string, opts?: {
|
|
153
|
+
readonly topK?: number;
|
|
154
|
+
readonly includeQuarantined?: boolean;
|
|
155
|
+
}): Promise<ReadonlyArray<MemoryHit<Rule>>>;
|
|
134
156
|
/**
|
|
135
157
|
* Promote a quarantined (induced) procedure into `activate()` (MCON-2).
|
|
136
158
|
* Mirrors {@link SemanticMemory.validate}: re-derives the injection verdict
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"procedural-memory.d.ts","names":[],"sources":["../../src/tiers/procedural-memory.ts"],"sourcesContent":[],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"procedural-memory.d.ts","names":[],"sources":["../../src/tiers/procedural-memory.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAoCA;;;AAmBmB,UAnBF,SAAA,CAmBE;EAEI,SAAA,IAAA,EAAA,MAAA;EAEM;;;AAU7B;AAYA;AAmBA;;EAaY,SAAA,SAAA,CAAA,EAAA,MAAA;EACE;EA4BH,SAAA,WAAA,CAAA,EA/Fc,WA+Fd;EAGN,SAAA,QAAA,CAAA,EAAA,MAAA;EA8CiB,SAAA,IAAA,CAAA,EA9IJ,aA8II,CAAA,MAAA,CAAA;EAAqB;;;;;EA8CjC,SAAA,KAAA,CAAA,EAtLS,aAsLT,CAAA,MAAA,CAAA;EACG;EAAR,SAAA,SAAA,CAAA,EArLkB,aAqLlB,CAAA,MAAA,CAAA;EAuDM;EACF,SAAA,eAAA,CAAA,EA3OoB,aA2OpB,CAAA,MAAA,CAAA;EACC;EACG,SAAA,KAAA,CAAA,EA3OM,WA2ON;;;;;;;AAkBsB,UArPlB,aAAA,CAqPkB;EAoBxB;EACE,SAAA,WAAA,CAAA,EAxQY,WAwQZ;EACc;EAAd,SAAA,QAAA,CAAA,EAAA,MAAA;;;;;;;AAqFF,UApVM,qBAAA,CAoVN;EAIN,SAAA,KAAA,CAAA,EAAA,MAAA;EAAO,SAAA,IAAA,CAAA,EAtVM,aAsVN,CAAA,MAAA,CAAA;;;;;;;;;;;;;;;;cArUC,gBAAA;;;WAYF;YACC;cACE;;;;;;;;;;;;;;;;;;;uBA4BH,mDAGN;;;;;;gBA8CiB,qBAAqB,YAAY,QAAQ;;;;;;;;;;gBA4CpD,0BACK,mBACN,gBACL,QAAQ;;;;;;uBAuDF,mBACF,iBACC,gBACL,QAAQ;;gBAKS,gDAAgD;;cAalD,eAAe,QAAQ,cAAc;;;;;;;;;;;;kBAoB9C,wBACE,wBACR,QAAQ,cAAc;;;;;;;;;;;;;;;;;gBAwBhB;;;MAGN,QAAQ,cAAc,UAAU;;;;;;;;kBA0D1B;;MAIN"}
|