/** * Engram Memory Store * * A {@link MemoryStore} implementation backed by Engram's REST API. * Engram provides shared team memory for AI agents — facts committed by one * agent are visible to all others in the workspace. * * Run: * npx tsx examples/integrations/with-engram/research-team.ts * * Prerequisites: * - Engram server running at http://localhost:7474 (or custom baseUrl) * - ENGRAM_INVITE_KEY env var (or passed via constructor) */ import type { MemoryEntry, MemoryStore } from '../../../src/types.js' // --------------------------------------------------------------------------- // Engram fact shape (as returned by the API) // --------------------------------------------------------------------------- interface EngramFact { fact_id: string lineage_id: string content: string scope: string agent_id?: string committed_at: string } // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- export interface EngramStoreOptions { /** Engram server URL. Defaults to `http://localhost:7474`. */ baseUrl?: string /** Workspace invite key. Falls back to `ENGRAM_INVITE_KEY` env var. */ inviteKey?: string /** Default confidence for commits. Defaults to `0.9`. */ confidence?: number } // --------------------------------------------------------------------------- // EngramMemoryStore // --------------------------------------------------------------------------- export class EngramMemoryStore implements MemoryStore { private readonly baseUrl: string private readonly inviteKey: string private readonly confidence: number constructor(options: EngramStoreOptions = {}) { this.baseUrl = (options.baseUrl ?? 'http://localhost:7474').replace(/\/+$/, '') this.inviteKey = options.inviteKey ?? process.env.ENGRAM_INVITE_KEY ?? '' this.confidence = options.confidence ?? 0.9 } // --------------------------------------------------------------------------- // MemoryStore interface // --------------------------------------------------------------------------- /** * Store a value under `key` by committing a fact with `scope=key`. * Uses `operation: "update"` so repeated writes to the same key supersede * the previous value rather than creating duplicates. */ async set(key: string, value: string, metadata?: Record): Promise { await this.post('/api/commit', { scope: key, content: value, confidence: this.confidence, agent_id: metadata?.agent ?? undefined, operation: 'update', }) } /** * Retrieve the most recent fact for `key` (scope). * Returns `null` when no matching fact exists. */ async get(key: string): Promise { const url = `${this.baseUrl}/api/facts?scope=${encodeURIComponent(key)}&limit=1` const res = await fetch(url, { headers: this.headers() }) if (!res.ok) return null const facts: EngramFact[] = await res.json() if (facts.length === 0) return null return this.toMemoryEntry(facts[0]) } /** * List all facts in the workspace (up to 200). * Each fact is mapped to a {@link MemoryEntry} using `scope` as the key. */ async list(): Promise { const url = `${this.baseUrl}/api/facts?limit=200` const res = await fetch(url, { headers: this.headers() }) if (!res.ok) return [] const facts: EngramFact[] = await res.json() return facts.map((f) => this.toMemoryEntry(f)) } /** * Retire the most recent fact for `key` (scope) by its lineage ID. * * Engram's `delete` operation requires `corrects_lineage` — it retires a * specific lineage rather than deleting by scope. We look up the latest * fact first to obtain its `lineage_id`, then issue the delete. * * No-op when no fact exists for the key. */ async delete(key: string): Promise { // Look up the latest fact to get its lineage_id. const entry = await this.getFact(key) if (!entry) return await this.post('/api/commit', { scope: key, content: `Retired by MemoryStore.delete("${key}")`, confidence: this.confidence, operation: 'delete', corrects_lineage: entry.lineage_id, }) } /** * No-op. Engram preserves full audit history by design — bulk erasure is * not supported and would violate the append-only contract. */ async clear(): Promise { // Intentional no-op: Engram preserves audit history. } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- private headers(): Record { return { Authorization: `Bearer ${this.inviteKey}`, 'Content-Type': 'application/json', } } /** * Fetch the most recent raw fact for a scope. * Used internally by `delete()` to obtain the `lineage_id`. */ private async getFact(scope: string): Promise { const url = `${this.baseUrl}/api/facts?scope=${encodeURIComponent(scope)}&limit=1` const res = await fetch(url, { headers: this.headers() }) if (!res.ok) return null const facts: EngramFact[] = await res.json() return facts.length > 0 ? facts[0] : null } private async post(path: string, body: Record): Promise { const res = await fetch(`${this.baseUrl}${path}`, { method: 'POST', headers: this.headers(), body: JSON.stringify(body), }) if (!res.ok) { const text = await res.text().catch(() => '') throw new Error(`Engram ${path} failed (${res.status}): ${text}`) } } private toMemoryEntry(fact: EngramFact): MemoryEntry { return { key: fact.scope, value: fact.content, metadata: { fact_id: fact.fact_id, lineage_id: fact.lineage_id, agent_id: fact.agent_id, }, createdAt: new Date(fact.committed_at), } } }