agent-working-memory 0.6.1 → 0.7.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/README.md +12 -8
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +1 -0
- package/dist/api/routes.js.map +1 -1
- package/dist/core/hebbian.d.ts +25 -1
- package/dist/core/hebbian.d.ts.map +1 -1
- package/dist/core/hebbian.js +71 -3
- package/dist/core/hebbian.js.map +1 -1
- package/dist/engine/activation.d.ts +22 -4
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +136 -73
- package/dist/engine/activation.js.map +1 -1
- package/dist/mcp.js +9 -1
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +17 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +73 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/engram.d.ts +1 -0
- package/dist/types/engram.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/routes.ts +2 -0
- package/src/core/hebbian.ts +84 -3
- package/src/engine/activation.ts +136 -70
- package/src/mcp.ts +69 -60
- package/src/storage/sqlite.ts +77 -0
- package/src/types/engram.ts +1 -0
package/src/engine/activation.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { randomUUID } from 'node:crypto';
|
|
23
23
|
import { baseLevelActivation, softplus } from '../core/decay.js';
|
|
24
|
-
import { strengthenAssociation, CoActivationBuffer } from '../core/hebbian.js';
|
|
24
|
+
import { strengthenAssociation, CoActivationBuffer, ValidationGatedBuffer } from '../core/hebbian.js';
|
|
25
25
|
import { embed, cosineSimilarity } from '../core/embeddings.js';
|
|
26
26
|
import { rerank } from '../core/reranker.js';
|
|
27
27
|
import { expandQuery } from '../core/query-expander.js';
|
|
@@ -163,10 +163,12 @@ function jaccard(a: Set<string>, b: Set<string>): number {
|
|
|
163
163
|
export class ActivationEngine {
|
|
164
164
|
private store: EngramStore;
|
|
165
165
|
private coActivationBuffer: CoActivationBuffer;
|
|
166
|
+
readonly validationGate: ValidationGatedBuffer;
|
|
166
167
|
|
|
167
168
|
constructor(store: EngramStore) {
|
|
168
169
|
this.store = store;
|
|
169
170
|
this.coActivationBuffer = new CoActivationBuffer(50);
|
|
171
|
+
this.validationGate = new ValidationGatedBuffer();
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
/**
|
|
@@ -181,14 +183,18 @@ export class ActivationEngine {
|
|
|
181
183
|
const abstentionThreshold = query.abstentionThreshold ?? 0;
|
|
182
184
|
const adaptive = resolveAdaptiveParams(query);
|
|
183
185
|
|
|
186
|
+
// Resolve workspace scope: if workspace is set, search across all agents in that workspace
|
|
187
|
+
const agentIds = query.workspace
|
|
188
|
+
? this.store.getWorkspaceAgentIds(query.agentId, query.workspace)
|
|
189
|
+
: [query.agentId];
|
|
190
|
+
const isWorkspaceScoped = agentIds.length > 1;
|
|
191
|
+
|
|
184
192
|
// Phase -1: Coref expansion — if query has pronouns, append recent entity names
|
|
185
|
-
// Helps conversational recall where "she/he/they/it" refers to a named entity.
|
|
186
193
|
let queryContext = query.context;
|
|
187
194
|
const pronounPattern = /\b(she|he|they|her|his|him|their|it|that|this|there)\b/i;
|
|
188
195
|
if (pronounPattern.test(queryContext)) {
|
|
189
|
-
// Pull recent entity names from the agent's most-accessed memories
|
|
190
196
|
try {
|
|
191
|
-
const recentEntities = this.store.
|
|
197
|
+
const recentEntities = this.store.getEngramsByAgents(agentIds, 'active')
|
|
192
198
|
.sort((a, b) => b.accessCount - a.accessCount)
|
|
193
199
|
.slice(0, 10)
|
|
194
200
|
.flatMap(e => e.tags.filter(t => t.length >= 3 && !/^(session-|low-|D\d)/.test(t)))
|
|
@@ -228,9 +234,9 @@ export class ActivationEngine {
|
|
|
228
234
|
// Two-pass BM25: (1) keyword-stripped query for precision, (2) expanded query for recall.
|
|
229
235
|
const keywordQuery = Array.from(tokenize(query.context)).join(' ');
|
|
230
236
|
const bm25Keyword = keywordQuery.length > 2
|
|
231
|
-
? this.store.
|
|
237
|
+
? this.store.searchBM25WithRankMultiAgent(agentIds, keywordQuery, limit * 3)
|
|
232
238
|
: [];
|
|
233
|
-
const bm25Expanded = this.store.
|
|
239
|
+
const bm25Expanded = this.store.searchBM25WithRankMultiAgent(agentIds, searchContext, limit * 3);
|
|
234
240
|
|
|
235
241
|
// Merge: take the best BM25 score per engram from either pass
|
|
236
242
|
const bm25ScoreMap = new Map<string, number>();
|
|
@@ -246,8 +252,8 @@ export class ActivationEngine {
|
|
|
246
252
|
engram, bm25Score: bm25ScoreMap.get(id) ?? 0,
|
|
247
253
|
}));
|
|
248
254
|
|
|
249
|
-
const allActive = this.store.
|
|
250
|
-
|
|
255
|
+
const allActive = this.store.getEngramsByAgents(
|
|
256
|
+
agentIds,
|
|
251
257
|
query.includeStaging ? undefined : 'active',
|
|
252
258
|
query.includeRetracted ?? false
|
|
253
259
|
);
|
|
@@ -402,7 +408,7 @@ export class ActivationEngine {
|
|
|
402
408
|
// Take top 5 feedback terms and re-search
|
|
403
409
|
const extraTerms = Array.from(feedbackTerms).slice(0, 5).join(' ');
|
|
404
410
|
if (extraTerms) {
|
|
405
|
-
const feedbackBM25 = this.store.
|
|
411
|
+
const feedbackBM25 = this.store.searchBM25WithRankMultiAgent(agentIds, `${searchContext} ${extraTerms}`, limit * 2);
|
|
406
412
|
for (const r of feedbackBM25) {
|
|
407
413
|
if (!candidateMap.has(r.engram.id)) {
|
|
408
414
|
candidateMap.set(r.engram.id, r.engram);
|
|
@@ -632,13 +638,21 @@ export class ActivationEngine {
|
|
|
632
638
|
|
|
633
639
|
const activatedIds = results.map(r => r.engram.id);
|
|
634
640
|
|
|
635
|
-
// Side effects: touch, co-activate, Hebbian
|
|
641
|
+
// Side effects: touch, co-activate, defer Hebbian to validation gate (skip for internal/system calls)
|
|
636
642
|
if (!query.internal) {
|
|
637
643
|
for (const id of activatedIds) {
|
|
638
644
|
this.store.touchEngram(id);
|
|
639
645
|
}
|
|
640
646
|
this.coActivationBuffer.pushBatch(activatedIds);
|
|
641
|
-
|
|
647
|
+
// Validation-gated Hebbian: defer strengthening until feedback arrives
|
|
648
|
+
const pairs = this.coActivationBuffer.getCoActivatedPairs(10_000);
|
|
649
|
+
const seen = new Set<string>();
|
|
650
|
+
const uniquePairs: [string, string][] = [];
|
|
651
|
+
for (const [a, b] of pairs) {
|
|
652
|
+
const key = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
653
|
+
if (!seen.has(key)) { seen.add(key); uniquePairs.push([a, b]); }
|
|
654
|
+
}
|
|
655
|
+
this.validationGate.addPending(activatedIds, uniquePairs);
|
|
642
656
|
|
|
643
657
|
// Log activation event for eval
|
|
644
658
|
const latencyMs = performance.now() - startTime;
|
|
@@ -658,10 +672,27 @@ export class ActivationEngine {
|
|
|
658
672
|
}
|
|
659
673
|
|
|
660
674
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
675
|
+
* Multi-graph traversal (MAGMA-inspired).
|
|
676
|
+
*
|
|
677
|
+
* Instead of one beam search over all edge types, runs independent traversals
|
|
678
|
+
* per graph type with specialized scoring, then fuses the boosts.
|
|
679
|
+
*
|
|
680
|
+
* Four sub-graphs:
|
|
681
|
+
* - Semantic (connection + hebbian edges) → standard weight-based walk
|
|
682
|
+
* - Temporal (temporal edges) → recency-weighted (favor recent connections)
|
|
683
|
+
* - Causal (causal edges) → full weight walk (causal links are high-value)
|
|
684
|
+
* - Entity (bridge edges) → entity-tag-weighted walk
|
|
685
|
+
*
|
|
686
|
+
* Each sub-graph contributes independently to the final graph boost,
|
|
687
|
+
* weighted by configurable per-graph weights.
|
|
664
688
|
*/
|
|
689
|
+
private static readonly GRAPH_WEIGHTS = {
|
|
690
|
+
semantic: 0.40, // connection + hebbian
|
|
691
|
+
temporal: 0.20, // temporal edges
|
|
692
|
+
causal: 0.25, // causal edges (high-value signal)
|
|
693
|
+
entity: 0.15, // bridge edges
|
|
694
|
+
};
|
|
695
|
+
|
|
665
696
|
private graphWalk(
|
|
666
697
|
scored: { engram: Engram; score: number; phaseScores: PhaseScores; associations: Association[] }[],
|
|
667
698
|
maxDepth: number,
|
|
@@ -670,85 +701,120 @@ export class ActivationEngine {
|
|
|
670
701
|
): void {
|
|
671
702
|
const scoreMap = new Map(scored.map(s => [s.engram.id, s]));
|
|
672
703
|
const MAX_TOTAL_BOOST = 0.25;
|
|
673
|
-
const BEAM_WIDTH = beamWidth;
|
|
674
704
|
|
|
675
|
-
//
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
705
|
+
// Define which edge types belong to each sub-graph
|
|
706
|
+
const graphTypes: Record<string, string[]> = {
|
|
707
|
+
semantic: ['connection', 'hebbian'],
|
|
708
|
+
temporal: ['temporal'],
|
|
709
|
+
causal: ['causal'],
|
|
710
|
+
entity: ['bridge'],
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// Run independent traversals per sub-graph, accumulate boosts
|
|
714
|
+
const boostAccum = new Map<string, number>(); // engramId → total boost
|
|
680
715
|
|
|
681
|
-
|
|
682
|
-
|
|
716
|
+
for (const [graphName, edgeTypes] of Object.entries(graphTypes)) {
|
|
717
|
+
const graphWeight = ActivationEngine.GRAPH_WEIGHTS[graphName as keyof typeof ActivationEngine.GRAPH_WEIGHTS];
|
|
718
|
+
const subBeamWidth = Math.max(3, Math.ceil(beamWidth * graphWeight));
|
|
683
719
|
|
|
684
|
-
|
|
685
|
-
const
|
|
720
|
+
// Seed beam
|
|
721
|
+
const beam = scored
|
|
722
|
+
.filter(item => item.phaseScores.textMatch >= 0.15)
|
|
723
|
+
.sort((a, b) => b.score - a.score)
|
|
724
|
+
.slice(0, subBeamWidth);
|
|
725
|
+
|
|
726
|
+
const explored = new Set<string>();
|
|
727
|
+
|
|
728
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
729
|
+
const nextBeam: typeof beam = [];
|
|
686
730
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
731
|
+
for (const item of beam) {
|
|
732
|
+
if (explored.has(item.engram.id)) continue;
|
|
733
|
+
explored.add(item.engram.id);
|
|
690
734
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
: this.store.getAssociationsFor(item.engram.id);
|
|
735
|
+
const associations = item.associations.length > 0
|
|
736
|
+
? item.associations
|
|
737
|
+
: this.store.getAssociationsFor(item.engram.id);
|
|
695
738
|
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
? assoc.toEngramId
|
|
699
|
-
: assoc.fromEngramId;
|
|
739
|
+
// Filter to only edges of this sub-graph type
|
|
740
|
+
const relevantEdges = associations.filter(a => edgeTypes.includes(a.type));
|
|
700
741
|
|
|
701
|
-
|
|
742
|
+
for (const assoc of relevantEdges) {
|
|
743
|
+
const neighborId = assoc.fromEngramId === item.engram.id
|
|
744
|
+
? assoc.toEngramId
|
|
745
|
+
: assoc.fromEngramId;
|
|
702
746
|
|
|
703
|
-
|
|
704
|
-
|
|
747
|
+
if (explored.has(neighborId)) continue;
|
|
748
|
+
const neighbor = scoreMap.get(neighborId);
|
|
749
|
+
if (!neighbor) continue;
|
|
705
750
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const relevanceFloor = depth === 0 ? 0.1 : 0.05;
|
|
709
|
-
if (neighbor.phaseScores.textMatch < relevanceFloor) continue;
|
|
751
|
+
const relevanceFloor = depth === 0 ? 0.1 : 0.05;
|
|
752
|
+
if (neighbor.phaseScores.textMatch < relevanceFloor) continue;
|
|
710
753
|
|
|
711
|
-
|
|
712
|
-
|
|
754
|
+
// Path score with graph-type-specific weighting
|
|
755
|
+
const normalizedWeight = Math.min(assoc.weight, 5.0) / 5.0;
|
|
756
|
+
let pathScore = item.score * normalizedWeight * Math.pow(hopPenalty, depth + 1);
|
|
713
757
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const pathScore = item.score * normalizedWeight * Math.pow(hopPenalty, depth + 1);
|
|
758
|
+
// Causal edges get a 2x boost — they represent verified reasoning chains
|
|
759
|
+
if (graphName === 'causal') pathScore *= 2.0;
|
|
717
760
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
761
|
+
// Weight by sub-graph importance
|
|
762
|
+
const boost = Math.min(pathScore * graphWeight, 0.15);
|
|
763
|
+
if (boost > 0.001) {
|
|
764
|
+
boostAccum.set(neighborId, (boostAccum.get(neighborId) ?? 0) + boost);
|
|
765
|
+
nextBeam.push(neighbor);
|
|
766
|
+
}
|
|
723
767
|
}
|
|
724
768
|
}
|
|
769
|
+
|
|
770
|
+
if (nextBeam.length === 0) break;
|
|
771
|
+
beam.length = 0;
|
|
772
|
+
beam.push(...nextBeam
|
|
773
|
+
.sort((a, b) => b.score - a.score)
|
|
774
|
+
.slice(0, subBeamWidth)
|
|
775
|
+
);
|
|
725
776
|
}
|
|
777
|
+
}
|
|
726
778
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
779
|
+
// Apply fused boosts to scored items
|
|
780
|
+
for (const [engramId, totalBoost] of boostAccum) {
|
|
781
|
+
const item = scoreMap.get(engramId);
|
|
782
|
+
if (!item) continue;
|
|
783
|
+
const capped = Math.min(totalBoost, MAX_TOTAL_BOOST - item.phaseScores.graphBoost);
|
|
784
|
+
if (capped > 0.001) {
|
|
785
|
+
item.score += capped;
|
|
786
|
+
item.phaseScores.graphBoost += capped;
|
|
787
|
+
}
|
|
734
788
|
}
|
|
735
789
|
}
|
|
736
790
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
791
|
+
/**
|
|
792
|
+
* Resolve validation-gated Hebbian update for a specific engram.
|
|
793
|
+
* Called by memory_feedback — only strengthens when retrieval was useful.
|
|
794
|
+
* This prevents hub toxicity from noisy co-retrieval (Kairos-inspired).
|
|
795
|
+
*/
|
|
796
|
+
resolveHebbianFeedback(engramId: string, useful: boolean): number {
|
|
797
|
+
const { pairs, signal } = this.validationGate.resolveFeedback(engramId, useful);
|
|
798
|
+
let updated = 0;
|
|
745
799
|
|
|
800
|
+
for (const [a, b] of pairs) {
|
|
746
801
|
const existing = this.store.getAssociation(a, b) ?? this.store.getAssociation(b, a);
|
|
747
802
|
const currentWeight = existing?.weight ?? 0.1;
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
803
|
+
|
|
804
|
+
if (signal > 0) {
|
|
805
|
+
// Positive feedback → strengthen
|
|
806
|
+
const newWeight = strengthenAssociation(currentWeight, signal);
|
|
807
|
+
this.store.upsertAssociation(a, b, newWeight, 'hebbian');
|
|
808
|
+
this.store.upsertAssociation(b, a, newWeight, 'hebbian');
|
|
809
|
+
} else {
|
|
810
|
+
// Negative feedback → slight weakening (decay by signal magnitude)
|
|
811
|
+
const newWeight = Math.max(0.001, currentWeight * (1 + signal)); // signal is -0.3
|
|
812
|
+
this.store.upsertAssociation(a, b, newWeight, 'hebbian');
|
|
813
|
+
this.store.upsertAssociation(b, a, newWeight, 'hebbian');
|
|
814
|
+
}
|
|
815
|
+
updated++;
|
|
751
816
|
}
|
|
817
|
+
return updated;
|
|
752
818
|
}
|
|
753
819
|
|
|
754
820
|
private explain(phases: PhaseScores, engram: Engram, associations: Association[]): string {
|
package/src/mcp.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { readFileSync } from 'node:fs';
|
|
28
28
|
import { resolve } from 'node:path';
|
|
29
|
-
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
29
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
30
30
|
|
|
31
31
|
// Load .env file if present (no external dependency)
|
|
32
32
|
try {
|
|
@@ -113,64 +113,64 @@ consolidationScheduler.start();
|
|
|
113
113
|
// Coordination DB handle — set when AWM_COORDINATION=true, used by memory_write for decision propagation
|
|
114
114
|
let coordDb: import('better-sqlite3').Database | null = null;
|
|
115
115
|
|
|
116
|
-
const server = new McpServer({
|
|
117
|
-
name: 'agent-working-memory',
|
|
118
|
-
version: '0.6.0',
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
server.registerResource(
|
|
122
|
-
'awm-overview',
|
|
123
|
-
'awm://server/overview',
|
|
124
|
-
{
|
|
125
|
-
title: 'AWM Overview',
|
|
126
|
-
description: 'AgentWorkingMemory MCP server metadata and discovery notes',
|
|
127
|
-
mimeType: 'text/markdown',
|
|
128
|
-
},
|
|
129
|
-
async () => ({
|
|
130
|
-
contents: [{
|
|
131
|
-
uri: 'awm://server/overview',
|
|
132
|
-
text: [
|
|
133
|
-
'# Agent Working Memory',
|
|
134
|
-
'',
|
|
135
|
-
`Agent: ${AGENT_ID}`,
|
|
136
|
-
`DB: ${DB_PATH}`,
|
|
137
|
-
`Coordination: ${process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1' ? 'enabled' : 'disabled'}`,
|
|
138
|
-
'',
|
|
139
|
-
'This MCP server primarily exposes tools such as `memory_restore`, `memory_recall`, `memory_write`, and task/checkpoint operations.',
|
|
140
|
-
'The resources below exist so generic MCP clients can discover the server through `resources/list` and `resources/templates/list`.',
|
|
141
|
-
].join('\n'),
|
|
142
|
-
mimeType: 'text/markdown',
|
|
143
|
-
}],
|
|
144
|
-
})
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
server.registerResource(
|
|
148
|
-
'awm-memory-template',
|
|
149
|
-
new ResourceTemplate('awm://memory/{id}', { list: undefined }),
|
|
150
|
-
{
|
|
151
|
-
title: 'AWM Memory By ID',
|
|
152
|
-
description: 'Metadata resource template for a memory identifier',
|
|
153
|
-
mimeType: 'text/markdown',
|
|
154
|
-
},
|
|
155
|
-
async (_uri, variables) => ({
|
|
156
|
-
contents: [{
|
|
157
|
-
uri: `awm://memory/${variables.id ?? ''}`,
|
|
158
|
-
text: [
|
|
159
|
-
'# AWM Memory Reference',
|
|
160
|
-
'',
|
|
161
|
-
`Requested memory id: ${variables.id ?? ''}`,
|
|
162
|
-
'',
|
|
163
|
-
'Use the AWM memory tools for actual retrieval and mutation:',
|
|
164
|
-
'- `memory_recall` for cognitive retrieval',
|
|
165
|
-
'- `memory_restore` for session state',
|
|
166
|
-
'- `memory_feedback`, `memory_retract`, `memory_supersede` for memory maintenance',
|
|
167
|
-
].join('\n'),
|
|
168
|
-
mimeType: 'text/markdown',
|
|
169
|
-
}],
|
|
170
|
-
})
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
// --- Auto-classification for memory types ---
|
|
116
|
+
const server = new McpServer({
|
|
117
|
+
name: 'agent-working-memory',
|
|
118
|
+
version: '0.6.0',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
server.registerResource(
|
|
122
|
+
'awm-overview',
|
|
123
|
+
'awm://server/overview',
|
|
124
|
+
{
|
|
125
|
+
title: 'AWM Overview',
|
|
126
|
+
description: 'AgentWorkingMemory MCP server metadata and discovery notes',
|
|
127
|
+
mimeType: 'text/markdown',
|
|
128
|
+
},
|
|
129
|
+
async () => ({
|
|
130
|
+
contents: [{
|
|
131
|
+
uri: 'awm://server/overview',
|
|
132
|
+
text: [
|
|
133
|
+
'# Agent Working Memory',
|
|
134
|
+
'',
|
|
135
|
+
`Agent: ${AGENT_ID}`,
|
|
136
|
+
`DB: ${DB_PATH}`,
|
|
137
|
+
`Coordination: ${process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1' ? 'enabled' : 'disabled'}`,
|
|
138
|
+
'',
|
|
139
|
+
'This MCP server primarily exposes tools such as `memory_restore`, `memory_recall`, `memory_write`, and task/checkpoint operations.',
|
|
140
|
+
'The resources below exist so generic MCP clients can discover the server through `resources/list` and `resources/templates/list`.',
|
|
141
|
+
].join('\n'),
|
|
142
|
+
mimeType: 'text/markdown',
|
|
143
|
+
}],
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.registerResource(
|
|
148
|
+
'awm-memory-template',
|
|
149
|
+
new ResourceTemplate('awm://memory/{id}', { list: undefined }),
|
|
150
|
+
{
|
|
151
|
+
title: 'AWM Memory By ID',
|
|
152
|
+
description: 'Metadata resource template for a memory identifier',
|
|
153
|
+
mimeType: 'text/markdown',
|
|
154
|
+
},
|
|
155
|
+
async (_uri, variables) => ({
|
|
156
|
+
contents: [{
|
|
157
|
+
uri: `awm://memory/${variables.id ?? ''}`,
|
|
158
|
+
text: [
|
|
159
|
+
'# AWM Memory Reference',
|
|
160
|
+
'',
|
|
161
|
+
`Requested memory id: ${variables.id ?? ''}`,
|
|
162
|
+
'',
|
|
163
|
+
'Use the AWM memory tools for actual retrieval and mutation:',
|
|
164
|
+
'- `memory_recall` for cognitive retrieval',
|
|
165
|
+
'- `memory_restore` for session state',
|
|
166
|
+
'- `memory_feedback`, `memory_retract`, `memory_supersede` for memory maintenance',
|
|
167
|
+
].join('\n'),
|
|
168
|
+
mimeType: 'text/markdown',
|
|
169
|
+
}],
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// --- Auto-classification for memory types ---
|
|
174
174
|
|
|
175
175
|
function classifyMemoryType(content: string): 'episodic' | 'semantic' | 'procedural' | 'unclassified' {
|
|
176
176
|
const lower = content.toLowerCase();
|
|
@@ -378,6 +378,7 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
|
|
|
378
378
|
use_reranker: z.boolean().optional().default(true).describe('Use cross-encoder re-ranking for better relevance (default true)'),
|
|
379
379
|
use_expansion: z.boolean().optional().default(true).describe('Expand query with synonyms for better recall (default true)'),
|
|
380
380
|
memory_type: z.enum(['episodic', 'semantic', 'procedural']).optional().describe('Filter by memory type (omit to search all types)'),
|
|
381
|
+
workspace: z.string().optional().describe('Search across all agents in this workspace (hive mode). Omit for agent-scoped recall only.'),
|
|
381
382
|
},
|
|
382
383
|
async (params) => {
|
|
383
384
|
const queryText = params.query ?? params.context;
|
|
@@ -389,6 +390,8 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
|
|
|
389
390
|
}],
|
|
390
391
|
};
|
|
391
392
|
}
|
|
393
|
+
// Use workspace from param, env var, or omit for agent-scoped
|
|
394
|
+
const workspace = params.workspace ?? process.env.AWM_WORKSPACE ?? undefined;
|
|
392
395
|
const results = await activationEngine.activate({
|
|
393
396
|
agentId: AGENT_ID,
|
|
394
397
|
context: queryText,
|
|
@@ -398,6 +401,7 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
|
|
|
398
401
|
useReranker: params.use_reranker,
|
|
399
402
|
useExpansion: params.use_expansion,
|
|
400
403
|
memoryType: params.memory_type,
|
|
404
|
+
workspace,
|
|
401
405
|
});
|
|
402
406
|
|
|
403
407
|
// Auto-checkpoint: track recall
|
|
@@ -456,10 +460,13 @@ Always call this after using a recalled memory so the system learns what's valua
|
|
|
456
460
|
store.updateConfidence(engram.id, engram.confidence + delta);
|
|
457
461
|
}
|
|
458
462
|
|
|
463
|
+
// Validation-gated Hebbian: resolve pending co-activation pairs for this engram
|
|
464
|
+
const hebbianUpdated = activationEngine.resolveHebbianFeedback(params.engram_id, params.useful);
|
|
465
|
+
|
|
459
466
|
return {
|
|
460
467
|
content: [{
|
|
461
468
|
type: 'text' as const,
|
|
462
|
-
text: `Feedback: ${params.useful ? '+useful' : '-not useful'}`,
|
|
469
|
+
text: `Feedback: ${params.useful ? '+useful' : '-not useful'}${hebbianUpdated > 0 ? ` (${hebbianUpdated} association${hebbianUpdated > 1 ? 's' : ''} ${params.useful ? 'strengthened' : 'weakened'})` : ''}`,
|
|
463
470
|
}],
|
|
464
471
|
};
|
|
465
472
|
}
|
|
@@ -673,6 +680,7 @@ Use this at the start of every session or after compaction to pick up where you
|
|
|
673
680
|
minScore: 0.05,
|
|
674
681
|
useReranker: true,
|
|
675
682
|
useExpansion: true,
|
|
683
|
+
workspace: process.env.AWM_WORKSPACE ?? undefined,
|
|
676
684
|
});
|
|
677
685
|
recalledMemories = results.map(r => ({
|
|
678
686
|
id: r.engram.id,
|
|
@@ -983,6 +991,7 @@ This ensures your state is saved before you start, and primes recall with releva
|
|
|
983
991
|
minScore: 0.05,
|
|
984
992
|
useReranker: true,
|
|
985
993
|
useExpansion: true,
|
|
994
|
+
workspace: process.env.AWM_WORKSPACE ?? undefined,
|
|
986
995
|
});
|
|
987
996
|
|
|
988
997
|
if (results.length > 0) {
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -329,6 +329,83 @@ export class EngramStore {
|
|
|
329
329
|
return (this.db.prepare(query).all(...params) as any[]).map(r => this.rowToEngram(r));
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Get engrams across multiple agents (workspace-scoped recall).
|
|
334
|
+
* Used when workspace mode is enabled for hive memory sharing.
|
|
335
|
+
*/
|
|
336
|
+
getEngramsByAgents(agentIds: string[], stage?: EngramStage, includeRetracted: boolean = false): Engram[] {
|
|
337
|
+
if (agentIds.length === 0) return [];
|
|
338
|
+
if (agentIds.length === 1) return this.getEngramsByAgent(agentIds[0], stage, includeRetracted);
|
|
339
|
+
|
|
340
|
+
const placeholders = agentIds.map(() => '?').join(',');
|
|
341
|
+
let query = `SELECT * FROM engrams WHERE agent_id IN (${placeholders})`;
|
|
342
|
+
const params: any[] = [...agentIds];
|
|
343
|
+
|
|
344
|
+
if (stage) {
|
|
345
|
+
query += ' AND stage = ?';
|
|
346
|
+
params.push(stage);
|
|
347
|
+
}
|
|
348
|
+
if (!includeRetracted) {
|
|
349
|
+
query += ' AND retracted = 0';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return (this.db.prepare(query).all(...params) as any[]).map(r => this.rowToEngram(r));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* BM25 search across multiple agents (workspace-scoped).
|
|
357
|
+
*/
|
|
358
|
+
searchBM25WithRankMultiAgent(agentIds: string[], query: string, limit: number = 10): { engram: Engram; bm25Score: number }[] {
|
|
359
|
+
if (agentIds.length === 0) return [];
|
|
360
|
+
if (agentIds.length === 1) return this.searchBM25WithRank(agentIds[0], query, limit);
|
|
361
|
+
|
|
362
|
+
const sanitized = query
|
|
363
|
+
.replace(/[^\w\s]/g, '')
|
|
364
|
+
.split(/\s+/)
|
|
365
|
+
.filter(w => w.length > 1)
|
|
366
|
+
.map(w => `"${w}"`)
|
|
367
|
+
.join(' OR ');
|
|
368
|
+
|
|
369
|
+
if (!sanitized) return [];
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const placeholders = agentIds.map(() => '?').join(',');
|
|
373
|
+
const rows = this.db.prepare(`
|
|
374
|
+
SELECT e.*, rank FROM engrams e
|
|
375
|
+
JOIN engrams_fts ON e.rowid = engrams_fts.rowid
|
|
376
|
+
WHERE engrams_fts MATCH ? AND e.agent_id IN (${placeholders}) AND e.retracted = 0
|
|
377
|
+
ORDER BY rank
|
|
378
|
+
LIMIT ?
|
|
379
|
+
`).all(sanitized, ...agentIds, limit) as any[];
|
|
380
|
+
|
|
381
|
+
return rows.map(r => ({
|
|
382
|
+
engram: this.rowToEngram(r),
|
|
383
|
+
bm25Score: Math.abs(r.rank ?? 0) / (1 + Math.abs(r.rank ?? 0)),
|
|
384
|
+
}));
|
|
385
|
+
} catch {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get all distinct agent IDs that share a workspace (requires coord_agents table).
|
|
392
|
+
* Returns just the queried agentId if coordination tables don't exist.
|
|
393
|
+
*/
|
|
394
|
+
getWorkspaceAgentIds(agentId: string, workspace: string): string[] {
|
|
395
|
+
try {
|
|
396
|
+
const rows = this.db.prepare(
|
|
397
|
+
`SELECT DISTINCT id FROM coord_agents WHERE workspace = ? AND status != 'dead'`
|
|
398
|
+
).all(workspace) as Array<{ id: string }>;
|
|
399
|
+
const ids = rows.map(r => r.id);
|
|
400
|
+
// Ensure the querying agent is always included
|
|
401
|
+
if (!ids.includes(agentId)) ids.push(agentId);
|
|
402
|
+
return ids;
|
|
403
|
+
} catch {
|
|
404
|
+
// No coordination tables — fall back to single agent
|
|
405
|
+
return [agentId];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
332
409
|
/**
|
|
333
410
|
* Touch an engram: increment access count, update last_accessed, and
|
|
334
411
|
* nudge confidence upward. Each retrieval is weak evidence the memory
|
package/src/types/engram.ts
CHANGED
|
@@ -186,6 +186,7 @@ export interface ActivationQuery {
|
|
|
186
186
|
internal?: boolean; // Skip access count increment, Hebbian update, and event logging (for system calls)
|
|
187
187
|
memoryType?: MemoryType; // Filter by memory type (episodic, semantic, procedural)
|
|
188
188
|
mode?: QueryMode; // Pipeline mode — 'auto' by default
|
|
189
|
+
workspace?: string; // Search across all agents in this workspace (hive mode). If unset, agent-scoped only.
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
/**
|