Memory System
The Memory System (@cycgraph/memory) provides a temporal knowledge graph with xMemory-inspired hierarchical organization. It gives agents persistent, queryable memory that survives across workflow runs — not just the ephemeral WorkflowState.memory that exists within a single execution.
The memory package is standalone with zero orchestrator dependencies. It works with any application or as the memory layer inside @cycgraph/orchestrator via the memoryRetriever option.
Architecture
Section titled “Architecture”Messages (raw conversation turns) | EpisodeSegmenterEpisodes (topic-coherent message groups) | SemanticExtractorSemanticFacts (atomic knowledge units) | ThemeClustererThemes (high-level clusters)Parallel to the hierarchy, a knowledge graph stores entities (nodes) and relationships (edges) with temporal validity windows. Retrieval combines both paths: top-down hierarchical search and BFS subgraph extraction.
Memory hierarchy levels
Section titled “Memory hierarchy levels”| Level | Type | Description |
|---|---|---|
| 0 | Messages | Raw conversation turns |
| 1 | Episodes | Groups of messages about one topic |
| 2 | SemanticFacts | Atomic facts distilled from episodes |
| 3 | Themes | Clusters of related facts |
Queries start at the theme level and drill down only as needed, reducing token usage by up to 50% compared to flat retrieval.
Knowledge graph
Section titled “Knowledge graph”Entities and relationships form a directed graph with temporal awareness:
- Entities — people, organizations, concepts, tools, locations
- Relationships — directed, weighted edges with
valid_from/valid_untilwindows - Temporal invalidation — old facts are soft-deleted (invalidated), not removed
- Provenance tracking — every record knows its origin (agent, tool, human, system, derived)
import { InMemoryMemoryStore } from '@cycgraph/memory';import type { Entity, Relationship } from '@cycgraph/memory';
const store = new InMemoryMemoryStore();
const aliceId = crypto.randomUUID();const acmeId = crypto.randomUUID();
await store.putEntity({ id: aliceId, name: 'Alice', entity_type: 'person', attributes: { role: 'engineer' }, provenance: { source: 'agent', created_at: new Date() }, created_at: new Date(), updated_at: new Date(),});
await store.putEntity({ id: acmeId, name: 'Acme Corp', entity_type: 'organization', attributes: {}, provenance: { source: 'agent', created_at: new Date() }, created_at: new Date(), updated_at: new Date(),});
await store.putRelationship({ id: crypto.randomUUID(), source_id: aliceId, target_id: acmeId, relation_type: 'work_at', weight: 1.0, attributes: {}, valid_from: new Date('2024-01-01'), provenance: { source: 'agent', created_at: new Date() },});Fact extraction
Section titled “Fact extraction”Three extractors convert episodes into atomic facts:
SimpleSemanticExtractor
Section titled “SimpleSemanticExtractor”Minimal extraction: one fact per episode (the topic). Use for bootstrapping or when extraction quality doesn’t matter.
RuleBasedExtractor
Section titled “RuleBasedExtractor”Pattern-based extraction producing 3-10 facts per episode. Detects entities (capitalized names, @handles, camelCase, ACRONYMS) and relationships (work_at, manage, depend_on, and ~20 other base verbs with automatic inflection). Entity matching uses word boundaries to prevent false positives (e.g., “Smith” won’t match inside “Blacksmith”). No LLM required.
import { RuleBasedExtractor } from '@cycgraph/memory';
const extractor = new RuleBasedExtractor({ minSentenceLength: 20 });const facts = await extractor.extract(episode);
// Standalone entity extractionconst entities = extractor.extractEntities('Alice Smith works at Acme Corp');// [{ name: 'Alice Smith', type: 'person' }, { name: 'Acme Corp', type: 'organization' }]LLMExtractor
Section titled “LLMExtractor”LLM-backed extraction for maximum quality. Uses an injectable LLMProvider interface (bring your own LLM). Falls back to RuleBasedExtractor on failure.
import { LLMExtractor } from '@cycgraph/memory';import type { LLMProvider } from '@cycgraph/memory';
const provider: LLMProvider = { complete: async (prompt) => { /* call your LLM */ return response; },};
const extractor = new LLMExtractor({ provider, maxFactsPerEpisode: 20 });const facts = await extractor.extract(episode);Theme clustering
Section titled “Theme clustering”SimpleThemeClusterer
Section titled “SimpleThemeClusterer”Greedy single-pass assignment: each fact joins the most similar existing theme (by embedding cosine similarity) or creates a new one.
ConsolidatingThemeClusterer
Section titled “ConsolidatingThemeClusterer”Two-pass clustering that prevents theme proliferation:
- Assignment pass — same greedy assignment as
SimpleThemeClusterer - Merge pass — pairwise cosine similarity between all theme centroids; themes above
mergeThresholdare merged, centroids recomputed
import { ConsolidatingThemeClusterer } from '@cycgraph/memory';
const clusterer = new ConsolidatingThemeClusterer({ assignmentThreshold: 0.7, // min similarity to join existing theme mergeThreshold: 0.85, // merge themes above this similarity maxThemes: 50, // soft cap});
const themes = await clusterer.cluster(facts, existingThemes);Retrieval
Section titled “Retrieval”Hierarchical retrieval (embedding-based)
Section titled “Hierarchical retrieval (embedding-based)”Top-down search: match themes by embedding similarity, expand to facts, apply temporal filters, expand to episodes, collect entities and relationships.
import { retrieveMemory } from '@cycgraph/memory';
const result = await retrieveMemory(store, index, { embedding: queryVector, limit: 20, min_similarity: 0.5, valid_at: new Date(), // only currently-valid facts changed_since: lastQueryTime, // only recent changes});// result.themes, result.facts, result.episodes, result.entities, result.relationshipsEntity-based retrieval
Section titled “Entity-based retrieval”When you have specific entity IDs, retrieval uses BFS subgraph extraction:
const result = await retrieveMemory(store, index, { entity_ids: [aliceId, bobId], max_hops: 2, limit: 20,});Temporal filtering
Section titled “Temporal filtering”import { isValidAt, filterValid } from '@cycgraph/memory';
isValidAt(relationship, new Date()); // within [valid_from, valid_until)?
const validFacts = filterValid(allFacts, { valid_at: new Date(), changed_since: lastSync, include_invalidated: false,});Memory consolidation
Section titled “Memory consolidation”Over time, memory accumulates duplicates, outdated facts, and contradictions. The consolidation system manages the lifecycle:
MemoryConsolidator
Section titled “MemoryConsolidator”Prunes and deduplicates memory records to stay within budget:
import { MemoryConsolidator } from '@cycgraph/memory';
const consolidator = new MemoryConsolidator(store, index, { maxFacts: 1000, // prune lowest-scoring facts over this count maxEpisodes: 100, // prune oldest episodes over this count decayHalfLifeDays: 30, // time-based relevance decay dedupThreshold: 0.9, // cosine similarity for near-duplicate detection deleteMode: 'soft', // 'soft' (invalidate) or 'hard' (delete) batchSize: 1000, // paginated fact loading (avoids OOM on large stores) logger: { warn: console.warn }, // optional structured logging});
const report = await consolidator.consolidate();// report.factsDeduped — near-duplicates merged// report.factsDecayed — low-relevance facts pruned// report.episodesPruned — old episodes removed// report.themesCleanedUp — themes with updated fact_ids// report.themesRemoved — empty themes deletedConsolidation cascades to themes: when facts are pruned, the themes that referenced them have their fact_ids updated and their embeddings recomputed. Themes with zero remaining facts are deleted.
ConflictDetector
Section titled “ConflictDetector”Identifies contradictory, negating, or superseding facts:
import { ConflictDetector } from '@cycgraph/memory';
const detector = new ConflictDetector(store, index, { autoResolveSupersession: true, embeddingThreshold: 0.8, policy: 'negation-invalidates-positive',});
const conflicts = await detector.detectConflicts();
// Auto-resolve with configured policyconst resolution = await detector.autoResolveAll(conflicts);Three conflict types:
| Type | Detection | Confidence |
|---|---|---|
negation | One fact contains negation words, high word overlap | 0.8 |
supersession | Same entities, similar content, >N days apart (configurable via supersessionDayThreshold, default 1) | 0.9 |
semantic_contradiction | High embedding similarity, shared entities, low text overlap | 0.3-0.7 (scaled by fact length) |
Three resolution policies:
| Policy | Behavior |
|---|---|
supersede-on-newer | Always keep the newer fact |
negation-invalidates-positive | Keep the negation (the correction), use temporal order for supersession, skip semantic contradictions |
manual-review | Return all conflicts unresolved |
Storage backends
Section titled “Storage backends”| Backend | Package | Use Case |
|---|---|---|
InMemoryMemoryStore | @cycgraph/memory | Testing and lightweight use |
InMemoryMemoryIndex | @cycgraph/memory | Brute-force cosine similarity |
DrizzleMemoryStore | @cycgraph/orchestrator-postgres | Production Postgres |
DrizzleMemoryIndex | @cycgraph/orchestrator-postgres | pgvector HNSW indexes |
// Production setupimport { DrizzleMemoryStore, DrizzleMemoryIndex } from '@cycgraph/orchestrator-postgres';
const store = new DrizzleMemoryStore();const index = new DrizzleMemoryIndex();Orchestrator integration
Section titled “Orchestrator integration”Inject memory retrieval into GraphRunner via the memoryRetriever option:
import { GraphRunner } from '@cycgraph/orchestrator';import { InMemoryMemoryStore, InMemoryMemoryIndex, retrieveMemory } from '@cycgraph/memory';
const store = new InMemoryMemoryStore();const index = new InMemoryMemoryIndex();
const memoryRetriever = async (query, options) => { const result = await retrieveMemory(store, index, { entity_ids: query.entityIds, limit: options?.maxFacts ?? 20, }); return { facts: result.facts.map(f => ({ content: f.content, validFrom: f.valid_from })), entities: result.entities.map(e => ({ name: e.name, type: e.entity_type })), themes: result.themes.map(t => ({ label: t.label })), };};
const runner = new GraphRunner(graph, state, { memoryRetriever });Next steps
Section titled “Next steps”- Workflow State — ephemeral per-run memory vs persistent knowledge graph
- Context Engine — compress memory payloads before prompt injection
- Using Memory — practical guide for integrating memory into workflows
- Persistence — how workflow state is persisted alongside memory