Spaces:
Runtime error
Runtime error
File size: 5,534 Bytes
e92be04 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | /**
* P2PCLAW Agent Memory Service
* ==============================
* Persistent cross-session memory for autonomous agents.
* Implements the AgentMemory class from §3.5/§4.4 of the guide.
*
* Storage: Gun.js path "memories/{agentId}/{key}"
* Search: SparseEmbeddingStore (TF-IDF bigram hashing, no external model)
* Pattern: remember/recall/search — persists across server restarts
*/
import { db } from "../config/gun.js";
import { gunSafe } from "../utils/gunUtils.js";
import { SparseEmbeddingStore } from "./sparse-memory.js";
const MAX_MEMORY_KEYS = 200; // per agent, to prevent unbounded growth
export class AgentMemory {
/**
* @param {string} agentId - The agent's unique ID.
*/
constructor(agentId) {
this.agentId = agentId;
this.store = new SparseEmbeddingStore();
this.node = db.get("memories").get(agentId);
this._localMap = new Map(); // write-through cache — instant reads, no Gun.js round-trip needed
}
/**
* Store a key-value in the agent's persistent memory.
* Optionally provide `text` for semantic search indexing.
*
* @param {string} key - Memory key (e.g. 'current_investigation', 'last_paper').
* @param {*} value - Any JSON-serialisable value.
* @param {string} [text] - Optional text for semantic embedding (for search).
*/
async remember(key, value, text = null) {
const serialized = JSON.stringify(value);
const entry = gunSafe({
key,
value: serialized,
timestamp: Date.now(),
has_embedding: !!text,
});
// Write-through: update local Map immediately so recall is instant
this._localMap.set(key, value);
this.node.get(key).put(entry);
if (text) {
this.store.storeText(key, text);
}
return this;
}
/**
* Recall a single memory by key.
* Checks the in-process write-through cache first, then Gun.js.
* @returns {Promise<*|null>} Parsed value or null if not found.
*/
async recall(key) {
// Fast path: in-process write-through cache
if (this._localMap.has(key)) return this._localMap.get(key);
// Slow path: Gun.js (persisted across restarts)
return new Promise(resolve => {
this.node.get(key).once(data => {
if (!data || !data.value) return resolve(null);
try {
const parsed = JSON.parse(data.value);
this._localMap.set(key, parsed); // populate cache from Gun
resolve(parsed);
} catch {
resolve(data.value); // raw string fallback
}
});
});
}
/**
* Load all memories from Gun.js on agent reconnect.
* Merges Gun.js data into the in-process write-through cache.
* Returns a flat object: { key: value, ... }
*/
async recallAll() {
// Start with whatever is in the write-through cache
const memories = Object.fromEntries(this._localMap);
// Merge in Gun.js data (catches entries from previous server instances)
await new Promise(resolve => {
this.node.map().once((data, key) => {
if (!data || !data.value || data.deleted) return;
try {
const parsed = JSON.parse(data.value);
memories[key] = parsed;
this._localMap.set(key, parsed); // backfill cache
} catch {
memories[key] = data.value;
this._localMap.set(key, data.value);
}
});
setTimeout(resolve, 1500);
});
return memories;
}
/**
* Semantic search across memories that were stored with `text`.
* Returns top-K keys ranked by cosine similarity.
*/
searchSimilar(queryText, topK = 5) {
return this.store.searchSimilarText(queryText, topK);
}
/**
* Forget (delete) a specific memory key.
*/
forget(key) {
// Gun.js doesn't support true delete — we mark as deleted
this.node.get(key).put(gunSafe({ key, value: null, timestamp: Date.now(), deleted: true }));
this._localMap.delete(key);
this.store.embeddings.delete(key);
}
/** Memory stats. */
stats() {
return {
agentId: this.agentId,
storeSize: this.store.size,
storeMemory: this.store.memoryStats(),
};
}
}
// ── In-process cache: one AgentMemory instance per agentId ────────
const _memoryCache = new Map(); // agentId → AgentMemory
export function getAgentMemory(agentId) {
if (!_memoryCache.has(agentId)) {
_memoryCache.set(agentId, new AgentMemory(agentId));
}
return _memoryCache.get(agentId);
}
/**
* Save a key-value to an agent's persistent memory.
*/
export async function saveMemory(agentId, key, value, text = null) {
const mem = getAgentMemory(agentId);
await mem.remember(key, value, text);
return { agentId, key, saved: true };
}
/**
* Load all memories for an agent.
*/
export async function loadMemory(agentId) {
const mem = getAgentMemory(agentId);
const memories = await mem.recallAll();
return { agentId, memories, count: Object.keys(memories).length };
}
|