q
File size: 3,984 Bytes
3365e13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// core/memory.js — persistent USER MEMORY (LibreChat memory semantics, substrate-native).
// Memories are key/value facts (key validated /^[a-z_]+$/, value a complete sentence) stored as
// κ-objects with pointers in the boot index. They inject into the system block as the
// "# Existing memory:" section, and the model maintains them itself through two LOCAL tools
// (set_memory / delete_memory) armed alongside MCP tools in the agentic loop — no server,
// every write content-addressed and verifiable.

const LC = { lc: "https://librechat.ai/ns#" };
export const KEY_RE = /^[a-z_]+$/;

export function makeMemory(chatStore, { tokenLimit = 2000 } = {}) {
  const { store, getIndex, newId } = chatStore;
  const putIndex = async (idx) => { const b = new TextEncoder().encode(JSON.stringify(idx)); return store.backend.putRaw ? store.backend.putRaw("index:org.hologram.HoloQ", b) : store.backend.put("index:org.hologram.HoloQ", b); };
  const tokensOf = (s) => Math.ceil((s || "").length / 4);   // a fair, dependency-free estimate

  async function list() { return (await getIndex()).memories || []; }

  async function set(key, value) {
    key = String(key || "").toLowerCase().trim();
    if (!KEY_RE.test(key)) return { ok: false, error: "invalid key — use lowercase letters and underscores" };
    const idx = await getIndex();
    idx.memories = idx.memories || [];
    const used = idx.memories.filter((m) => m.key !== key).reduce((n, m) => n + (m.tokenCount || 0), 0);
    const tc = tokensOf(value);
    if (used + tc > tokenLimit) return { ok: false, error: `memory full: ${used}+${tc} > ${tokenLimit} tokens` };
    const obj = await store.makeObject({
      type: ["schema:Statement", "prov:Entity"], context: [LC],
      "lc:key": key, "lc:value": String(value || ""), "lc:tokenCount": tc,
      "schema:dateModified": new Date().toISOString(),
    });
    const i = idx.memories.findIndex((m) => m.key === key);
    const ptr = { key, kappa: obj.id, value: String(value || ""), tokenCount: tc, updated_at: new Date().toISOString() };
    if (i >= 0) idx.memories[i] = ptr; else idx.memories.push(ptr);
    await putIndex(idx);
    return { ok: true, key, kappa: obj.id };
  }

  async function remove(key) {
    const idx = await getIndex();
    idx.memories = (idx.memories || []).filter((m) => m.key !== key);
    await putIndex(idx);
    return { ok: true };
  }

  // The system-block injection ("# Existing memory:" — the LibreChat convention).
  async function injection() {
    const mems = await list();
    if (!mems.length) return "";
    const used = mems.reduce((n, m) => n + (m.tokenCount || 0), 0);
    return `# Memory Status:\nCurrent memory usage: ${used} tokens\nToken limit: ${tokenLimit} tokens\nRemaining capacity: ${Math.max(0, tokenLimit - used)} tokens\n\n# Existing memory:\n` +
      mems.map((m) => `- ${m.key}: ${m.value}`).join("\n");
  }

  // The two LOCAL tools the agentic loop arms (same shape as MCP hub tools).
  function localTools() {
    return [
      {
        def: { name: "set_memory", description: "Remember a fact about the user across conversations. Only when the user explicitly asks (\"remember that…\"). key: lowercase_with_underscores; value: one complete sentence.", inputSchema: { type: "object", properties: { key: { type: "string" }, value: { type: "string" } }, required: ["key", "value"] } },
        serverName: "memory", call: async (a) => { const r = await set(a.key, a.value); return { text: JSON.stringify(r), isError: !r.ok }; },
      },
      {
        def: { name: "delete_memory", description: "Forget a remembered fact, by key. Only when the user explicitly asks.", inputSchema: { type: "object", properties: { key: { type: "string" } }, required: ["key"] } },
        serverName: "memory", call: async (a) => { const r = await remove(a.key); return { text: JSON.stringify(r), isError: false }; },
      },
    ];
  }

  return { list, set, remove, injection, localTools, tokenLimit };
}