agent-working-memory 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +2 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/routes.d.ts +53 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +388 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/decay.d.ts +36 -0
- package/dist/core/decay.d.ts.map +1 -0
- package/dist/core/decay.js +38 -0
- package/dist/core/decay.js.map +1 -0
- package/dist/core/embeddings.d.ts +33 -0
- package/dist/core/embeddings.d.ts.map +1 -0
- package/dist/core/embeddings.js +76 -0
- package/dist/core/embeddings.js.map +1 -0
- package/dist/core/hebbian.d.ts +38 -0
- package/dist/core/hebbian.d.ts.map +1 -0
- package/dist/core/hebbian.js +74 -0
- package/dist/core/hebbian.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/query-expander.d.ts +24 -0
- package/dist/core/query-expander.d.ts.map +1 -0
- package/dist/core/query-expander.js +58 -0
- package/dist/core/query-expander.js.map +1 -0
- package/dist/core/reranker.d.ts +25 -0
- package/dist/core/reranker.d.ts.map +1 -0
- package/dist/core/reranker.js +75 -0
- package/dist/core/reranker.js.map +1 -0
- package/dist/core/salience.d.ts +30 -0
- package/dist/core/salience.d.ts.map +1 -0
- package/dist/core/salience.js +81 -0
- package/dist/core/salience.js.map +1 -0
- package/dist/engine/activation.d.ts +38 -0
- package/dist/engine/activation.d.ts.map +1 -0
- package/dist/engine/activation.js +516 -0
- package/dist/engine/activation.js.map +1 -0
- package/dist/engine/connections.d.ts +31 -0
- package/dist/engine/connections.d.ts.map +1 -0
- package/dist/engine/connections.js +74 -0
- package/dist/engine/connections.js.map +1 -0
- package/dist/engine/consolidation-scheduler.d.ts +31 -0
- package/dist/engine/consolidation-scheduler.d.ts.map +1 -0
- package/dist/engine/consolidation-scheduler.js +115 -0
- package/dist/engine/consolidation-scheduler.js.map +1 -0
- package/dist/engine/consolidation.d.ts +62 -0
- package/dist/engine/consolidation.d.ts.map +1 -0
- package/dist/engine/consolidation.js +368 -0
- package/dist/engine/consolidation.js.map +1 -0
- package/dist/engine/eval.d.ts +22 -0
- package/dist/engine/eval.d.ts.map +1 -0
- package/dist/engine/eval.js +79 -0
- package/dist/engine/eval.js.map +1 -0
- package/dist/engine/eviction.d.ts +29 -0
- package/dist/engine/eviction.d.ts.map +1 -0
- package/dist/engine/eviction.js +86 -0
- package/dist/engine/eviction.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/retraction.d.ts +32 -0
- package/dist/engine/retraction.d.ts.map +1 -0
- package/dist/engine/retraction.js +77 -0
- package/dist/engine/retraction.js.map +1 -0
- package/dist/engine/staging.d.ts +33 -0
- package/dist/engine/staging.d.ts.map +1 -0
- package/dist/engine/staging.js +63 -0
- package/dist/engine/staging.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +532 -0
- package/dist/mcp.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/sqlite.d.ts +116 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +750 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/types/agent.d.ts +30 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +23 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/checkpoint.d.ts +50 -0
- package/dist/types/checkpoint.d.ts.map +1 -0
- package/dist/types/checkpoint.js +8 -0
- package/dist/types/checkpoint.js.map +1 -0
- package/dist/types/engram.d.ts +165 -0
- package/dist/types/engram.d.ts.map +1 -0
- package/dist/types/engram.js +8 -0
- package/dist/types/engram.js.map +1 -0
- package/dist/types/eval.d.ts +84 -0
- package/dist/types/eval.d.ts.map +1 -0
- package/dist/types/eval.js +11 -0
- package/dist/types/eval.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +55 -0
- package/src/api/index.ts +1 -0
- package/src/api/routes.ts +528 -0
- package/src/cli.ts +260 -0
- package/src/core/decay.ts +61 -0
- package/src/core/embeddings.ts +82 -0
- package/src/core/hebbian.ts +91 -0
- package/src/core/index.ts +3 -0
- package/src/core/query-expander.ts +64 -0
- package/src/core/reranker.ts +99 -0
- package/src/core/salience.ts +95 -0
- package/src/engine/activation.ts +577 -0
- package/src/engine/connections.ts +101 -0
- package/src/engine/consolidation-scheduler.ts +123 -0
- package/src/engine/consolidation.ts +443 -0
- package/src/engine/eval.ts +100 -0
- package/src/engine/eviction.ts +99 -0
- package/src/engine/index.ts +6 -0
- package/src/engine/retraction.ts +98 -0
- package/src/engine/staging.ts +72 -0
- package/src/index.ts +100 -0
- package/src/mcp.ts +635 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/sqlite.ts +893 -0
- package/src/types/agent.ts +65 -0
- package/src/types/checkpoint.ts +44 -0
- package/src/types/engram.ts +194 -0
- package/src/types/eval.ts +98 -0
- package/src/types/index.ts +4 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Salience Filter — decides what's worth remembering.
|
|
3
|
+
*
|
|
4
|
+
* Codex feedback incorporated:
|
|
5
|
+
* - Persists raw feature scores for auditability
|
|
6
|
+
* - Returns reason codes for explainability
|
|
7
|
+
* - Thresholds are tunable per agent
|
|
8
|
+
* - Deterministic heuristics first, LLM augmentation optional
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Weights for the salience scoring formula.
|
|
12
|
+
*/
|
|
13
|
+
const WEIGHTS = {
|
|
14
|
+
surprise: 0.3,
|
|
15
|
+
decision: 0.25,
|
|
16
|
+
causalDepth: 0.25,
|
|
17
|
+
resolutionEffort: 0.2,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Rule-based salience scorer with full audit trail.
|
|
21
|
+
*/
|
|
22
|
+
export function evaluateSalience(input, activeThreshold = 0.4, stagingThreshold = 0.2) {
|
|
23
|
+
const features = {
|
|
24
|
+
surprise: input.surprise ?? 0,
|
|
25
|
+
decisionMade: input.decisionMade ?? false,
|
|
26
|
+
causalDepth: input.causalDepth ?? 0,
|
|
27
|
+
resolutionEffort: input.resolutionEffort ?? 0,
|
|
28
|
+
eventType: input.eventType ?? 'observation',
|
|
29
|
+
};
|
|
30
|
+
const reasonCodes = [];
|
|
31
|
+
// Score components
|
|
32
|
+
const surpriseScore = WEIGHTS.surprise * features.surprise;
|
|
33
|
+
const decisionScore = WEIGHTS.decision * (features.decisionMade ? 1.0 : 0);
|
|
34
|
+
const causalScore = WEIGHTS.causalDepth * features.causalDepth;
|
|
35
|
+
const effortScore = WEIGHTS.resolutionEffort * features.resolutionEffort;
|
|
36
|
+
if (features.surprise > 0.5)
|
|
37
|
+
reasonCodes.push('high_surprise');
|
|
38
|
+
if (features.decisionMade)
|
|
39
|
+
reasonCodes.push('decision_point');
|
|
40
|
+
if (features.causalDepth > 0.5)
|
|
41
|
+
reasonCodes.push('causal_insight');
|
|
42
|
+
if (features.resolutionEffort > 0.5)
|
|
43
|
+
reasonCodes.push('high_effort_resolution');
|
|
44
|
+
// Event type bonus
|
|
45
|
+
let typeBonus = 0;
|
|
46
|
+
switch (features.eventType) {
|
|
47
|
+
case 'decision':
|
|
48
|
+
typeBonus = 0.15;
|
|
49
|
+
reasonCodes.push('event:decision');
|
|
50
|
+
break;
|
|
51
|
+
case 'friction':
|
|
52
|
+
typeBonus = 0.2;
|
|
53
|
+
reasonCodes.push('event:friction');
|
|
54
|
+
break;
|
|
55
|
+
case 'surprise':
|
|
56
|
+
typeBonus = 0.25;
|
|
57
|
+
reasonCodes.push('event:surprise');
|
|
58
|
+
break;
|
|
59
|
+
case 'causal':
|
|
60
|
+
typeBonus = 0.2;
|
|
61
|
+
reasonCodes.push('event:causal');
|
|
62
|
+
break;
|
|
63
|
+
case 'observation': break;
|
|
64
|
+
}
|
|
65
|
+
const score = Math.min(surpriseScore + decisionScore + causalScore + effortScore + typeBonus, 1.0);
|
|
66
|
+
let disposition;
|
|
67
|
+
if (score >= activeThreshold) {
|
|
68
|
+
disposition = 'active';
|
|
69
|
+
reasonCodes.push('disposition:active');
|
|
70
|
+
}
|
|
71
|
+
else if (score >= stagingThreshold) {
|
|
72
|
+
disposition = 'staging';
|
|
73
|
+
reasonCodes.push('disposition:staging');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
disposition = 'discard';
|
|
77
|
+
reasonCodes.push('disposition:discard');
|
|
78
|
+
}
|
|
79
|
+
return { score, disposition, features, reasonCodes };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=salience.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"salience.js","sourceRoot":"","sources":["../../src/core/salience.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation Pipeline — the core retrieval engine.
|
|
3
|
+
*
|
|
4
|
+
* 10-phase cognitive retrieval pipeline:
|
|
5
|
+
* 0. Query expansion (flan-t5-small synonym generation)
|
|
6
|
+
* 1. Vector embedding (MiniLM 384d)
|
|
7
|
+
* 2. Parallel retrieval (FTS5/BM25 + vector pool)
|
|
8
|
+
* 3. Scoring (BM25, Jaccard, z-score vector, entity-bridge boost)
|
|
9
|
+
* 4. Rocchio pseudo-relevance feedback (expand + re-search BM25)
|
|
10
|
+
* 5. ACT-R temporal decay
|
|
11
|
+
* 6. Hebbian boost (co-activation strength)
|
|
12
|
+
* 7. Composite scoring with confidence gating
|
|
13
|
+
* 8. Beam search graph walk (depth 2, hop penalty)
|
|
14
|
+
* 9. Cross-encoder reranking (ms-marco-MiniLM, adaptive blend)
|
|
15
|
+
* 10. Abstention gate
|
|
16
|
+
*
|
|
17
|
+
* Logs activation events for eval metrics.
|
|
18
|
+
*/
|
|
19
|
+
import type { ActivationResult, ActivationQuery } from '../types/index.js';
|
|
20
|
+
import type { EngramStore } from '../storage/sqlite.js';
|
|
21
|
+
export declare class ActivationEngine {
|
|
22
|
+
private store;
|
|
23
|
+
private coActivationBuffer;
|
|
24
|
+
constructor(store: EngramStore);
|
|
25
|
+
/**
|
|
26
|
+
* Activate — retrieve the most cognitively relevant engrams for a context.
|
|
27
|
+
*/
|
|
28
|
+
activate(query: ActivationQuery): Promise<ActivationResult[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Beam search graph walk — replaces naive BFS.
|
|
31
|
+
* Scores paths (not just nodes), uses query-dependent edge filtering,
|
|
32
|
+
* and supports deeper exploration with focused beams.
|
|
33
|
+
*/
|
|
34
|
+
private graphWalk;
|
|
35
|
+
private updateHebbianWeights;
|
|
36
|
+
private explain;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=activation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activation.d.ts","sourceRoot":"","sources":["../../src/engine/activation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAQH,OAAO,KAAK,EACF,gBAAgB,EAAE,eAAe,EAC1C,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAsCxD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,kBAAkB,CAAqB;gBAEnC,KAAK,EAAE,WAAW;IAK9B;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAuYnE;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAuEjB,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,OAAO;CAchB"}
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation Pipeline — the core retrieval engine.
|
|
3
|
+
*
|
|
4
|
+
* 10-phase cognitive retrieval pipeline:
|
|
5
|
+
* 0. Query expansion (flan-t5-small synonym generation)
|
|
6
|
+
* 1. Vector embedding (MiniLM 384d)
|
|
7
|
+
* 2. Parallel retrieval (FTS5/BM25 + vector pool)
|
|
8
|
+
* 3. Scoring (BM25, Jaccard, z-score vector, entity-bridge boost)
|
|
9
|
+
* 4. Rocchio pseudo-relevance feedback (expand + re-search BM25)
|
|
10
|
+
* 5. ACT-R temporal decay
|
|
11
|
+
* 6. Hebbian boost (co-activation strength)
|
|
12
|
+
* 7. Composite scoring with confidence gating
|
|
13
|
+
* 8. Beam search graph walk (depth 2, hop penalty)
|
|
14
|
+
* 9. Cross-encoder reranking (ms-marco-MiniLM, adaptive blend)
|
|
15
|
+
* 10. Abstention gate
|
|
16
|
+
*
|
|
17
|
+
* Logs activation events for eval metrics.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { baseLevelActivation, softplus } from '../core/decay.js';
|
|
21
|
+
import { strengthenAssociation, CoActivationBuffer } from '../core/hebbian.js';
|
|
22
|
+
import { embed, cosineSimilarity } from '../core/embeddings.js';
|
|
23
|
+
import { rerank } from '../core/reranker.js';
|
|
24
|
+
import { expandQuery } from '../core/query-expander.js';
|
|
25
|
+
/**
|
|
26
|
+
* Common English stopwords — filtered from similarity calculations.
|
|
27
|
+
* These words carry no semantic signal for memory retrieval.
|
|
28
|
+
*/
|
|
29
|
+
const STOPWORDS = new Set([
|
|
30
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had',
|
|
31
|
+
'her', 'was', 'one', 'our', 'out', 'has', 'have', 'been', 'from', 'that',
|
|
32
|
+
'this', 'with', 'they', 'will', 'each', 'make', 'like', 'then', 'than',
|
|
33
|
+
'them', 'some', 'what', 'when', 'where', 'which', 'who', 'how', 'use',
|
|
34
|
+
'into', 'does', 'also', 'just', 'more', 'over', 'such', 'only', 'very',
|
|
35
|
+
'about', 'after', 'being', 'between', 'could', 'during', 'before',
|
|
36
|
+
'should', 'would', 'their', 'there', 'these', 'those', 'through',
|
|
37
|
+
'because', 'using', 'other',
|
|
38
|
+
]);
|
|
39
|
+
function tokenize(text) {
|
|
40
|
+
return new Set(text.toLowerCase()
|
|
41
|
+
.split(/\s+/)
|
|
42
|
+
.filter(w => w.length > 2 && !STOPWORDS.has(w)));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Jaccard similarity between two word sets: |intersection| / |union|
|
|
46
|
+
*/
|
|
47
|
+
function jaccard(a, b) {
|
|
48
|
+
if (a.size === 0 || b.size === 0)
|
|
49
|
+
return 0;
|
|
50
|
+
let intersection = 0;
|
|
51
|
+
for (const w of a) {
|
|
52
|
+
if (b.has(w))
|
|
53
|
+
intersection++;
|
|
54
|
+
}
|
|
55
|
+
const union = a.size + b.size - intersection;
|
|
56
|
+
return union > 0 ? intersection / union : 0;
|
|
57
|
+
}
|
|
58
|
+
export class ActivationEngine {
|
|
59
|
+
store;
|
|
60
|
+
coActivationBuffer;
|
|
61
|
+
constructor(store) {
|
|
62
|
+
this.store = store;
|
|
63
|
+
this.coActivationBuffer = new CoActivationBuffer(50);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Activate — retrieve the most cognitively relevant engrams for a context.
|
|
67
|
+
*/
|
|
68
|
+
async activate(query) {
|
|
69
|
+
const startTime = performance.now();
|
|
70
|
+
const limit = query.limit ?? 10;
|
|
71
|
+
const minScore = query.minScore ?? 0.01; // Default: filter out zero-relevance results
|
|
72
|
+
const useReranker = query.useReranker ?? true;
|
|
73
|
+
const useExpansion = query.useExpansion ?? true;
|
|
74
|
+
const abstentionThreshold = query.abstentionThreshold ?? 0;
|
|
75
|
+
// Phase 0: Query expansion — add related terms to improve BM25 recall
|
|
76
|
+
let searchContext = query.context;
|
|
77
|
+
if (useExpansion) {
|
|
78
|
+
try {
|
|
79
|
+
searchContext = await expandQuery(query.context);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Expansion unavailable — use original query
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Phase 1: Embed original query for vector similarity (original, not expanded)
|
|
86
|
+
let queryEmbedding = null;
|
|
87
|
+
try {
|
|
88
|
+
queryEmbedding = await embed(query.context);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Embedding unavailable — fall back to text-only matching
|
|
92
|
+
}
|
|
93
|
+
// Phase 2: Parallel retrieval — BM25 with rank scores + all active engrams
|
|
94
|
+
// Use expanded query for BM25 (more terms = better keyword recall)
|
|
95
|
+
const bm25Ranked = this.store.searchBM25WithRank(query.agentId, searchContext, limit * 3);
|
|
96
|
+
const bm25ScoreMap = new Map(bm25Ranked.map(r => [r.engram.id, r.bm25Score]));
|
|
97
|
+
const allActive = this.store.getEngramsByAgent(query.agentId, query.includeStaging ? undefined : 'active', query.includeRetracted ?? false);
|
|
98
|
+
// Merge candidates (deduplicate)
|
|
99
|
+
const candidateMap = new Map();
|
|
100
|
+
for (const r of bm25Ranked)
|
|
101
|
+
candidateMap.set(r.engram.id, r.engram);
|
|
102
|
+
for (const e of allActive)
|
|
103
|
+
candidateMap.set(e.id, e);
|
|
104
|
+
const candidates = Array.from(candidateMap.values());
|
|
105
|
+
if (candidates.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
// Tokenize query once
|
|
108
|
+
const queryTokens = tokenize(query.context);
|
|
109
|
+
// Phase 3a: Compute raw cosine similarities for adaptive normalization
|
|
110
|
+
const rawCosineSims = new Map();
|
|
111
|
+
if (queryEmbedding) {
|
|
112
|
+
for (const engram of candidates) {
|
|
113
|
+
if (engram.embedding) {
|
|
114
|
+
rawCosineSims.set(engram.id, cosineSimilarity(queryEmbedding, engram.embedding));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Compute distribution stats for model-agnostic normalization
|
|
119
|
+
const simValues = Array.from(rawCosineSims.values());
|
|
120
|
+
const simMean = simValues.length > 0
|
|
121
|
+
? simValues.reduce((a, b) => a + b, 0) / simValues.length : 0;
|
|
122
|
+
const rawStdDev = simValues.length > 1
|
|
123
|
+
? Math.sqrt(simValues.reduce((sum, s) => sum + (s - simMean) ** 2, 0) / simValues.length) : 0.15;
|
|
124
|
+
// Floor stddev at 0.10 to prevent z-score inflation with small candidate pools
|
|
125
|
+
const simStdDev = Math.max(rawStdDev, 0.10);
|
|
126
|
+
// Phase 3b: Score each candidate with per-phase breakdown
|
|
127
|
+
const scored = candidates.map(engram => {
|
|
128
|
+
const ageDays = (Date.now() - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
129
|
+
const associations = this.store.getAssociationsFor(engram.id);
|
|
130
|
+
// --- Text relevance (keyword signals) ---
|
|
131
|
+
// Signal 1: BM25 continuous score (0-1, from FTS5 rank)
|
|
132
|
+
const bm25Score = bm25ScoreMap.get(engram.id) ?? 0;
|
|
133
|
+
// Signal 2: Jaccard similarity with stopword filtering
|
|
134
|
+
const conceptTokens = tokenize(engram.concept);
|
|
135
|
+
const contentTokens = tokenize(engram.content);
|
|
136
|
+
const conceptJaccard = jaccard(queryTokens, conceptTokens);
|
|
137
|
+
const contentJaccard = jaccard(queryTokens, contentTokens);
|
|
138
|
+
const jaccardScore = 0.6 * conceptJaccard + 0.4 * contentJaccard;
|
|
139
|
+
// Signal 3: Concept exact match bonus (up to 0.3)
|
|
140
|
+
const conceptOverlap = conceptTokens.size > 0
|
|
141
|
+
? [...conceptTokens].filter(w => queryTokens.has(w)).length / conceptTokens.size
|
|
142
|
+
: 0;
|
|
143
|
+
const conceptBonus = conceptOverlap * 0.3;
|
|
144
|
+
const keywordMatch = Math.min(Math.max(bm25Score, jaccardScore) + conceptBonus, 1.0);
|
|
145
|
+
// --- Vector similarity (semantic signal) ---
|
|
146
|
+
// Two-stage: absolute floor prevents noise, then z-score ranks within matches.
|
|
147
|
+
// Stage 1: Raw cosine must exceed mean + 1 stddev (absolute relevance gate)
|
|
148
|
+
// Stage 2: Z-score maps relative position to 0-1 for ranking quality
|
|
149
|
+
let vectorMatch = 0;
|
|
150
|
+
const rawSim = rawCosineSims.get(engram.id);
|
|
151
|
+
if (rawSim !== undefined) {
|
|
152
|
+
const zScore = (rawSim - simMean) / simStdDev;
|
|
153
|
+
// Gate: must be at least 1 stddev above mean to be considered a match
|
|
154
|
+
if (zScore > 1.0) {
|
|
155
|
+
// Map z=1..3 → 0..1 linearly
|
|
156
|
+
vectorMatch = Math.min(1, (zScore - 1.0) / 2.0);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Combined text match: best of keyword and vector signals
|
|
160
|
+
const textMatch = Math.max(keywordMatch, vectorMatch);
|
|
161
|
+
// --- Temporal signals ---
|
|
162
|
+
// ACT-R decay — confidence-modulated
|
|
163
|
+
// High-confidence memories (confirmed useful via feedback) decay slower.
|
|
164
|
+
// Default exponent: 0.5. At confidence 0.8+: 0.3 (much slower decay).
|
|
165
|
+
const decayExponent = 0.5 - 0.2 * Math.max(0, (engram.confidence - 0.5) / 0.5);
|
|
166
|
+
const decayScore = baseLevelActivation(engram.accessCount, ageDays, decayExponent);
|
|
167
|
+
// Hebbian boost from associations — capped to prevent popular memories
|
|
168
|
+
// from dominating regardless of query relevance
|
|
169
|
+
const rawHebbian = associations.length > 0
|
|
170
|
+
? associations.reduce((sum, a) => sum + a.weight, 0) / associations.length
|
|
171
|
+
: 0;
|
|
172
|
+
const hebbianBoost = Math.min(rawHebbian, 0.5);
|
|
173
|
+
// Centrality signal — well-connected memories (high weighted degree)
|
|
174
|
+
// get a small boost. This makes consolidation edges matter for retrieval.
|
|
175
|
+
// Log-scaled to prevent hub domination: 10 edges ≈ 0.05 boost, 50 ≈ 0.08
|
|
176
|
+
const weightedDegree = associations.reduce((sum, a) => sum + a.weight, 0);
|
|
177
|
+
const centralityBoost = associations.length > 0
|
|
178
|
+
? Math.min(0.1, 0.03 * Math.log1p(weightedDegree))
|
|
179
|
+
: 0;
|
|
180
|
+
// Confidence gate — multiplicative quality signal
|
|
181
|
+
const confidenceGate = engram.confidence;
|
|
182
|
+
// Feedback bonus — memories confirmed useful via explicit feedback get a
|
|
183
|
+
// direct additive boost. Models how a senior dev "just knows" certain things
|
|
184
|
+
// are important. Confidence > 0.6 means at least 2+ positive feedbacks.
|
|
185
|
+
// Scales: conf 0.6→0.03, 0.7→0.06, 0.8→0.09, 1.0→0.15
|
|
186
|
+
const feedbackBonus = engram.confidence > 0.55
|
|
187
|
+
? Math.min(0.15, 0.3 * Math.max(0, engram.confidence - 0.5))
|
|
188
|
+
: 0;
|
|
189
|
+
// --- Composite score: relevance-gated additive ---
|
|
190
|
+
// Text relevance must be present for temporal signals to contribute.
|
|
191
|
+
// Without text relevance, a memory shouldn't activate regardless of recency.
|
|
192
|
+
// Temporal contribution scales with text relevance (weak match = weak temporal boost).
|
|
193
|
+
const temporalNorm = Math.min(softplus(decayScore + hebbianBoost), 3.0) / 3.0;
|
|
194
|
+
const relevanceGate = textMatch > 0.1 ? textMatch : 0.0; // Proportional gate
|
|
195
|
+
const composite = (0.6 * textMatch + 0.4 * temporalNorm * relevanceGate + centralityBoost * relevanceGate + feedbackBonus * relevanceGate) * confidenceGate;
|
|
196
|
+
const phaseScores = {
|
|
197
|
+
textMatch,
|
|
198
|
+
vectorMatch,
|
|
199
|
+
decayScore,
|
|
200
|
+
hebbianBoost,
|
|
201
|
+
graphBoost: 0, // Filled in phase 5
|
|
202
|
+
confidenceGate,
|
|
203
|
+
composite,
|
|
204
|
+
rerankerScore: 0, // Filled in phase 7
|
|
205
|
+
};
|
|
206
|
+
return { engram, score: composite, phaseScores, associations };
|
|
207
|
+
});
|
|
208
|
+
// Phase 3.5: Rocchio pseudo-relevance feedback — expand query with top result terms
|
|
209
|
+
// then re-search BM25 to find candidates that keyword search missed
|
|
210
|
+
const preSorted = scored.sort((a, b) => b.score - a.score);
|
|
211
|
+
const topForFeedback = preSorted.slice(0, 3).filter(r => r.phaseScores.textMatch > 0.1);
|
|
212
|
+
if (topForFeedback.length > 0) {
|
|
213
|
+
const feedbackTerms = new Set();
|
|
214
|
+
for (const item of topForFeedback) {
|
|
215
|
+
const tokens = tokenize(item.engram.content);
|
|
216
|
+
for (const t of tokens) {
|
|
217
|
+
if (!queryTokens.has(t) && t.length >= 4)
|
|
218
|
+
feedbackTerms.add(t);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Take top 5 feedback terms and re-search
|
|
222
|
+
const extraTerms = Array.from(feedbackTerms).slice(0, 5).join(' ');
|
|
223
|
+
if (extraTerms) {
|
|
224
|
+
const feedbackBM25 = this.store.searchBM25WithRank(query.agentId, `${searchContext} ${extraTerms}`, limit * 2);
|
|
225
|
+
for (const r of feedbackBM25) {
|
|
226
|
+
if (!candidateMap.has(r.engram.id)) {
|
|
227
|
+
candidateMap.set(r.engram.id, r.engram);
|
|
228
|
+
// Score the new candidate
|
|
229
|
+
const engram = r.engram;
|
|
230
|
+
const ageDays = (Date.now() - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
231
|
+
const associations = this.store.getAssociationsFor(engram.id);
|
|
232
|
+
const cTokens = tokenize(engram.concept);
|
|
233
|
+
const ctTokens = tokenize(engram.content);
|
|
234
|
+
const cJac = jaccard(queryTokens, cTokens);
|
|
235
|
+
const ctJac = jaccard(queryTokens, ctTokens);
|
|
236
|
+
const jSc = 0.6 * cJac + 0.4 * ctJac;
|
|
237
|
+
const cOvlp = cTokens.size > 0 ? [...cTokens].filter(w => queryTokens.has(w)).length / cTokens.size : 0;
|
|
238
|
+
const km = Math.min(Math.max(r.bm25Score, jSc) + cOvlp * 0.3, 1.0);
|
|
239
|
+
let vm = 0;
|
|
240
|
+
const rs = rawCosineSims.get(engram.id) ?? (queryEmbedding && engram.embedding ? cosineSimilarity(queryEmbedding, engram.embedding) : 0);
|
|
241
|
+
if (rs) {
|
|
242
|
+
const z = (rs - simMean) / simStdDev;
|
|
243
|
+
if (z > 1.0)
|
|
244
|
+
vm = Math.min(1, (z - 1.0) / 2.0);
|
|
245
|
+
}
|
|
246
|
+
const tm = Math.max(km, vm);
|
|
247
|
+
const ds = baseLevelActivation(engram.accessCount, ageDays);
|
|
248
|
+
const rh = associations.length > 0 ? Math.min(associations.reduce((s, a) => s + a.weight, 0) / associations.length, 0.5) : 0;
|
|
249
|
+
const tn = Math.min(softplus(ds + rh), 3.0) / 3.0;
|
|
250
|
+
const rg = tm > 0.1 ? tm : 0.0;
|
|
251
|
+
const comp = (0.6 * tm + 0.4 * tn * rg) * engram.confidence;
|
|
252
|
+
scored.push({ engram, score: comp, phaseScores: { textMatch: tm, vectorMatch: vm, decayScore: ds, hebbianBoost: rh, graphBoost: 0, confidenceGate: engram.confidence, composite: comp, rerankerScore: 0 }, associations });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Phase 3.7: Entity-Bridge boost — boost scored candidates that share entity tags
|
|
258
|
+
// with the most query-relevant result. Only bridge from the single best text-match
|
|
259
|
+
// to avoid pulling in unrelated entities from tangentially-matching results.
|
|
260
|
+
{
|
|
261
|
+
// Find the result with the highest textMatch (most query-relevant, not just highest score)
|
|
262
|
+
// Gate: only bridge when anchor has meaningful text relevance (> 0.15)
|
|
263
|
+
// Adaptive: scale bridge boost inversely with candidate pool size to prevent
|
|
264
|
+
// over-boosting in large memory pools where many items share entity tags
|
|
265
|
+
const sortedByTextMatch = scored
|
|
266
|
+
.filter(r => r.phaseScores.textMatch > 0.15)
|
|
267
|
+
.sort((a, b) => b.phaseScores.textMatch - a.phaseScores.textMatch);
|
|
268
|
+
// Bridge from top 2 text-matched results (IDF handles weighting)
|
|
269
|
+
const bridgeAnchors = sortedByTextMatch.slice(0, 2);
|
|
270
|
+
if (bridgeAnchors.length > 0) {
|
|
271
|
+
const entityTags = new Set();
|
|
272
|
+
const anchorIds = new Set(bridgeAnchors.map(r => r.engram.id));
|
|
273
|
+
for (const item of bridgeAnchors) {
|
|
274
|
+
for (const tag of item.engram.tags) {
|
|
275
|
+
const t = tag.toLowerCase();
|
|
276
|
+
// Skip non-entity tags: turn IDs, session tags, dialogue IDs, generic speaker labels
|
|
277
|
+
if (/^t\d+$/.test(t) || t.startsWith('session-') || t.startsWith('dia_') || t.length < 3)
|
|
278
|
+
continue;
|
|
279
|
+
if (/^speaker\d*$/.test(t))
|
|
280
|
+
continue; // Generic speaker labels are too broad
|
|
281
|
+
entityTags.add(t);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Document frequency filter: remove tags appearing in >30% of items (too common)
|
|
285
|
+
// This prevents speaker names in 2-person conversations from being used as bridges
|
|
286
|
+
if (entityTags.size > 0 && scored.length > 10) {
|
|
287
|
+
const tagFreqs = new Map();
|
|
288
|
+
for (const item of scored) {
|
|
289
|
+
const seen = new Set();
|
|
290
|
+
for (const tag of item.engram.tags) {
|
|
291
|
+
const t = tag.toLowerCase();
|
|
292
|
+
if (entityTags.has(t) && !seen.has(t)) {
|
|
293
|
+
seen.add(t);
|
|
294
|
+
tagFreqs.set(t, (tagFreqs.get(t) ?? 0) + 1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const maxFreq = scored.length * 0.30;
|
|
299
|
+
for (const [tag, freq] of tagFreqs) {
|
|
300
|
+
if (freq > maxFreq)
|
|
301
|
+
entityTags.delete(tag);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (entityTags.size > 0) {
|
|
305
|
+
for (const item of scored) {
|
|
306
|
+
if (anchorIds.has(item.engram.id))
|
|
307
|
+
continue;
|
|
308
|
+
const engramTags = new Set(item.engram.tags.map((t) => t.toLowerCase()));
|
|
309
|
+
let sharedEntities = 0;
|
|
310
|
+
for (const et of entityTags) {
|
|
311
|
+
if (engramTags.has(et))
|
|
312
|
+
sharedEntities++;
|
|
313
|
+
}
|
|
314
|
+
if (sharedEntities > 0) {
|
|
315
|
+
// Flat bridge boost per shared entity
|
|
316
|
+
const bridgeBoost = Math.min(sharedEntities * 0.15, 0.4);
|
|
317
|
+
item.score += bridgeBoost;
|
|
318
|
+
item.phaseScores.composite += bridgeBoost;
|
|
319
|
+
item.phaseScores.graphBoost += bridgeBoost;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Phase 4+5: Graph walk — boost engrams connected to high-scoring ones
|
|
326
|
+
// Only walk from engrams that had text relevance (composite > 0 pre-walk)
|
|
327
|
+
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
328
|
+
const topN = sorted.slice(0, limit * 3);
|
|
329
|
+
this.graphWalk(topN, 2, 0.3);
|
|
330
|
+
// Phase 6: Initial filter and sort for re-ranking pool
|
|
331
|
+
const pool = topN
|
|
332
|
+
.filter(r => r.score >= minScore)
|
|
333
|
+
.sort((a, b) => b.score - a.score);
|
|
334
|
+
// Phase 7: Cross-encoder re-ranking — scores (query, passage) pairs directly
|
|
335
|
+
// Widens the pool to find relevant results that keyword matching missed
|
|
336
|
+
const rerankPool = pool.slice(0, Math.max(limit * 3, 30));
|
|
337
|
+
if (useReranker && rerankPool.length > 0) {
|
|
338
|
+
try {
|
|
339
|
+
const passages = rerankPool.map(r => `${r.engram.concept}: ${r.engram.content}`);
|
|
340
|
+
const rerankResults = await rerank(query.context, passages);
|
|
341
|
+
// Adaptive reranker blend (Codex recommendation):
|
|
342
|
+
// When BM25/text signals are strong, trust them more; when weak, lean on reranker.
|
|
343
|
+
const bm25Max = Math.max(...rerankPool.map(r => r.phaseScores.textMatch));
|
|
344
|
+
const rerankWeight = Math.min(0.7, Math.max(0.3, 0.3 + 0.4 * (1 - bm25Max)));
|
|
345
|
+
const compositeWeight = 1 - rerankWeight;
|
|
346
|
+
for (const rr of rerankResults) {
|
|
347
|
+
const item = rerankPool[rr.index];
|
|
348
|
+
item.phaseScores.rerankerScore = rr.score;
|
|
349
|
+
item.score = compositeWeight * item.phaseScores.composite + rerankWeight * rr.score;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Re-ranker unavailable — keep original scores
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Phase 8a: Semantic drift penalty — if no candidate has meaningful vector match
|
|
357
|
+
// (none exceeded 1 stddev above mean), the query is likely off-topic.
|
|
358
|
+
if (queryEmbedding && rerankPool.length > 0) {
|
|
359
|
+
const maxVectorSim = Math.max(...rerankPool.map(r => r.phaseScores.vectorMatch));
|
|
360
|
+
if (maxVectorSim < 0.05) {
|
|
361
|
+
// Query is semantically distant from everything — apply drift penalty
|
|
362
|
+
for (const item of rerankPool) {
|
|
363
|
+
item.score *= 0.5;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Phase 8b: Entropy gating — if top-5 reranker scores are flat (low variance),
|
|
368
|
+
// the reranker can't distinguish relevant from irrelevant. Abstain.
|
|
369
|
+
if (abstentionThreshold > 0 && rerankPool.length >= 3) {
|
|
370
|
+
const topRerankerScores = rerankPool
|
|
371
|
+
.map(r => r.phaseScores.rerankerScore)
|
|
372
|
+
.sort((a, b) => b - a)
|
|
373
|
+
.slice(0, 5);
|
|
374
|
+
const maxScore = topRerankerScores[0];
|
|
375
|
+
const meanScore = topRerankerScores.reduce((s, v) => s + v, 0) / topRerankerScores.length;
|
|
376
|
+
const variance = topRerankerScores.reduce((s, v) => s + (v - meanScore) ** 2, 0) / topRerankerScores.length;
|
|
377
|
+
// Abstain if: top score below threshold OR scores are flat (low discrimination)
|
|
378
|
+
if (maxScore < abstentionThreshold || (maxScore < 0.5 && variance < 0.01)) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Phase 9: Final sort, limit, explain
|
|
383
|
+
const results = rerankPool
|
|
384
|
+
.sort((a, b) => b.score - a.score)
|
|
385
|
+
.slice(0, limit)
|
|
386
|
+
.map(r => ({
|
|
387
|
+
engram: r.engram,
|
|
388
|
+
score: r.score,
|
|
389
|
+
phaseScores: r.phaseScores,
|
|
390
|
+
why: this.explain(r.phaseScores, r.engram, r.associations),
|
|
391
|
+
associations: r.associations,
|
|
392
|
+
}));
|
|
393
|
+
const activatedIds = results.map(r => r.engram.id);
|
|
394
|
+
// Side effects: touch, co-activate, Hebbian update (skip for internal/system calls)
|
|
395
|
+
if (!query.internal) {
|
|
396
|
+
for (const id of activatedIds) {
|
|
397
|
+
this.store.touchEngram(id);
|
|
398
|
+
}
|
|
399
|
+
this.coActivationBuffer.pushBatch(activatedIds);
|
|
400
|
+
this.updateHebbianWeights();
|
|
401
|
+
// Log activation event for eval
|
|
402
|
+
const latencyMs = performance.now() - startTime;
|
|
403
|
+
this.store.logActivationEvent({
|
|
404
|
+
id: randomUUID(),
|
|
405
|
+
agentId: query.agentId,
|
|
406
|
+
timestamp: new Date(),
|
|
407
|
+
context: query.context,
|
|
408
|
+
resultsReturned: results.length,
|
|
409
|
+
topScore: results.length > 0 ? results[0].score : 0,
|
|
410
|
+
latencyMs,
|
|
411
|
+
engramIds: activatedIds,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return results;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Beam search graph walk — replaces naive BFS.
|
|
418
|
+
* Scores paths (not just nodes), uses query-dependent edge filtering,
|
|
419
|
+
* and supports deeper exploration with focused beams.
|
|
420
|
+
*/
|
|
421
|
+
graphWalk(scored, maxDepth, hopPenalty) {
|
|
422
|
+
const scoreMap = new Map(scored.map(s => [s.engram.id, s]));
|
|
423
|
+
const MAX_TOTAL_BOOST = 0.25; // Slightly higher cap for beam search (deeper paths earn it)
|
|
424
|
+
const BEAM_WIDTH = 15;
|
|
425
|
+
// Seed the beam with high-scoring, text-relevant items
|
|
426
|
+
const beam = scored
|
|
427
|
+
.filter(item => item.phaseScores.textMatch >= 0.15)
|
|
428
|
+
.sort((a, b) => b.score - a.score)
|
|
429
|
+
.slice(0, BEAM_WIDTH);
|
|
430
|
+
// Track which engrams have been explored (avoid cycles)
|
|
431
|
+
const explored = new Set();
|
|
432
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
433
|
+
const nextBeam = [];
|
|
434
|
+
for (const item of beam) {
|
|
435
|
+
if (explored.has(item.engram.id))
|
|
436
|
+
continue;
|
|
437
|
+
explored.add(item.engram.id);
|
|
438
|
+
// Get associations — for depth > 0, fetch from store if not in scored set
|
|
439
|
+
const associations = item.associations.length > 0
|
|
440
|
+
? item.associations
|
|
441
|
+
: this.store.getAssociationsFor(item.engram.id);
|
|
442
|
+
for (const assoc of associations) {
|
|
443
|
+
const neighborId = assoc.fromEngramId === item.engram.id
|
|
444
|
+
? assoc.toEngramId
|
|
445
|
+
: assoc.fromEngramId;
|
|
446
|
+
if (explored.has(neighborId))
|
|
447
|
+
continue;
|
|
448
|
+
const neighbor = scoreMap.get(neighborId);
|
|
449
|
+
if (!neighbor)
|
|
450
|
+
continue;
|
|
451
|
+
// Query-dependent edge filtering: neighbor must have SOME relevance
|
|
452
|
+
// (textMatch > 0.05 for deeper hops, relaxed from 0.1)
|
|
453
|
+
const relevanceFloor = depth === 0 ? 0.1 : 0.05;
|
|
454
|
+
if (neighbor.phaseScores.textMatch < relevanceFloor)
|
|
455
|
+
continue;
|
|
456
|
+
// Skip if neighbor already at boost cap
|
|
457
|
+
if (neighbor.phaseScores.graphBoost >= MAX_TOTAL_BOOST)
|
|
458
|
+
continue;
|
|
459
|
+
// Path score: source score * edge weight * hop penalty^(depth+1)
|
|
460
|
+
const normalizedWeight = Math.min(assoc.weight, 5.0) / 5.0;
|
|
461
|
+
const pathScore = item.score * normalizedWeight * Math.pow(hopPenalty, depth + 1);
|
|
462
|
+
const boost = Math.min(pathScore, 0.15, MAX_TOTAL_BOOST - neighbor.phaseScores.graphBoost);
|
|
463
|
+
if (boost > 0.001) {
|
|
464
|
+
neighbor.score += boost;
|
|
465
|
+
neighbor.phaseScores.graphBoost += boost;
|
|
466
|
+
nextBeam.push(neighbor);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Prune beam for next depth level
|
|
471
|
+
if (nextBeam.length === 0)
|
|
472
|
+
break;
|
|
473
|
+
beam.length = 0;
|
|
474
|
+
beam.push(...nextBeam
|
|
475
|
+
.sort((a, b) => b.score - a.score)
|
|
476
|
+
.slice(0, BEAM_WIDTH));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
updateHebbianWeights() {
|
|
480
|
+
const pairs = this.coActivationBuffer.getCoActivatedPairs(10_000);
|
|
481
|
+
// Deduplicate pairs to prevent repeated strengthening
|
|
482
|
+
const seen = new Set();
|
|
483
|
+
for (const [a, b] of pairs) {
|
|
484
|
+
const key = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
485
|
+
if (seen.has(key))
|
|
486
|
+
continue;
|
|
487
|
+
seen.add(key);
|
|
488
|
+
const existing = this.store.getAssociation(a, b) ?? this.store.getAssociation(b, a);
|
|
489
|
+
const currentWeight = existing?.weight ?? 0.1;
|
|
490
|
+
const newWeight = strengthenAssociation(currentWeight);
|
|
491
|
+
this.store.upsertAssociation(a, b, newWeight, 'hebbian');
|
|
492
|
+
this.store.upsertAssociation(b, a, newWeight, 'hebbian');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
explain(phases, engram, associations) {
|
|
496
|
+
const parts = [];
|
|
497
|
+
parts.push(`composite=${phases.composite.toFixed(3)}`);
|
|
498
|
+
if (phases.textMatch > 0)
|
|
499
|
+
parts.push(`text=${phases.textMatch.toFixed(2)}`);
|
|
500
|
+
if (phases.vectorMatch > 0)
|
|
501
|
+
parts.push(`vector=${phases.vectorMatch.toFixed(2)}`);
|
|
502
|
+
parts.push(`decay=${phases.decayScore.toFixed(2)}`);
|
|
503
|
+
if (phases.hebbianBoost > 0)
|
|
504
|
+
parts.push(`hebbian=${phases.hebbianBoost.toFixed(2)}`);
|
|
505
|
+
if (phases.graphBoost > 0)
|
|
506
|
+
parts.push(`graph=${phases.graphBoost.toFixed(2)}`);
|
|
507
|
+
if (phases.rerankerScore > 0)
|
|
508
|
+
parts.push(`reranker=${phases.rerankerScore.toFixed(2)}`);
|
|
509
|
+
parts.push(`conf=${phases.confidenceGate.toFixed(2)}`);
|
|
510
|
+
parts.push(`access=${engram.accessCount}`);
|
|
511
|
+
if (associations.length > 0)
|
|
512
|
+
parts.push(`edges=${associations.length}`);
|
|
513
|
+
return parts.join(' | ');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
//# sourceMappingURL=activation.js.map
|