How Agents Share Knowledge Without Chaos
In traditional multi-agent systems, knowledge sharing quickly becomes a bottleneck. Agents either duplicate data everywhere or create complex point-to-point synchronization nightmares. The Shared Memory Graph pattern solves this elegantly, enabling agents to collaborate on shared knowledge without tight coupling.
The Knowledge Sharing Challenge
When multiple agents work together, they need to share:
- Discovered facts and insights
- Learned patterns and models
- State and context information
- Results and intermediate computations
Traditional approaches fail at scale:
- Shared Databases: Create contention and bottlenecks
- Message Passing: Leads to information duplication
- Direct Memory Sharing: Causes tight coupling and race conditions
Enter the Shared Memory Graph
A Shared Memory Graph is a distributed, eventually-consistent knowledge structure that agents can read and write without blocking each other. Think of it as Git for agent knowledge—distributed, versioned, and mergeable.
Core Architecture
class SharedMemoryGraph { constructor(options = {}) { this.nodes = new Map(); // Knowledge nodes this.edges = new Map(); // Relationships this.versions = new Map(); // Version tracking this.subscribers = new Map(); // Change notifications // Connect to distributed backend this.backend = new DistributedStore(options); } async addNode(id, data, metadata = {}) { const node = { id, data, metadata: { ...metadata, created: Date.now(), version: 1, author: this.agentId } }; // Local update this.nodes.set(id, node); // Distributed sync await this.backend.put(`nodes:${id}`, node); // Notify subscribers this.notifySubscribers('node.added', node); return node; } async addEdge(fromId, toId, relationship, properties = {}) { const edge = { id: `${fromId}-${relationship}-${toId}`, from: fromId, to: toId, relationship, properties, metadata: { created: Date.now(), author: this.agentId } }; // Store edge this.edges.set(edge.id, edge); await this.backend.put(`edges:${edge.id}`, edge); // Update adjacency lists await this.updateAdjacency(fromId, toId, relationship); return edge; } }
Knowledge Representation Patterns
1. Semantic Triples
// Subject-Predicate-Object representation graph.addTriple({ subject: 'document_123', predicate: 'contains_topic', object: 'machine_learning' }); graph.addTriple({ subject: 'machine_learning', predicate: 'is_subtopic_of', object: 'artificial_intelligence' }); // Query: Find all documents about AI topics const aiDocs = await graph.query({ pattern: '?doc contains_topic ?topic', where: '?topic is_subtopic_of artificial_intelligence' });
2. Property Graphs
// Rich nodes with properties const entityNode = await graph.addNode('entity_person_001', { type: 'person', name: 'John Doe', attributes: { age: 30, occupation: 'Data Scientist', skills: ['Python', 'TensorFlow', 'NATS'] } }); // Rich edges with properties await graph.addEdge( 'entity_person_001', 'entity_company_001', 'works_for', { since: '2020-01-01', position: 'Senior Data Scientist', department: 'AI Research' } );
3. Temporal Knowledge Graphs
class TemporalMemoryGraph extends SharedMemoryGraph { async addTemporalFact(fact, validFrom, validTo = null) { return this.addNode(`fact_${Date.now()}`, { ...fact, temporal: { validFrom, validTo, assertedAt: Date.now() } }); } async getFactsAt(timestamp) { return this.query({ where: { 'temporal.validFrom': { $lte: timestamp }, $or: [ { 'temporal.validTo': { $gte: timestamp } }, { 'temporal.validTo': null } ] } }); } async getFactHistory(factType, entityId) { return this.query({ where: { type: factType, entityId: entityId }, orderBy: 'temporal.validFrom', includeHistory: true }); } }
Distributed Consistency Patterns
1. Eventual Consistency with CRDTs
class CRDTMemoryGraph { constructor() { this.lwwMap = new LWWMap(); // Last-Write-Wins Map this.gCounter = new GCounter(); // Grow-only counter this.pnCounter = new PNCounter(); // Positive-Negative counter } async incrementMetric(metricId, value = 1) { // Conflict-free increment await this.pnCounter.increment(metricId, value, this.agentId); } async updateFact(factId, update) { // Last-write-wins update await this.lwwMap.set(factId, { ...update, timestamp: Date.now(), agentId: this.agentId }); } async merge(otherGraph) { // Automatic conflict resolution this.lwwMap.merge(otherGraph.lwwMap); this.gCounter.merge(otherGraph.gCounter); this.pnCounter.merge(otherGraph.pnCounter); } }
2. Consensus-Based Updates
class ConsensusMemoryGraph { async proposeUpdate(nodeId, update) { const proposal = { id: generateId(), nodeId, update, proposer: this.agentId, votes: new Map([[this.agentId, true]]) }; // Broadcast proposal await this.broadcast('graph.proposal', proposal); // Wait for consensus const decision = await this.waitForConsensus(proposal.id, { timeout: 5000, quorum: 0.51 }); if (decision.approved) { await this.applyUpdate(nodeId, update); } return decision; } async vote(proposalId, approve) { await this.publish('graph.vote', { proposalId, voter: this.agentId, approve, timestamp: Date.now() }); } }
Access Patterns and Optimization
1. Read-Heavy Optimization
class CachedMemoryGraph { constructor() { this.cache = new LRUCache({ max: 10000 }); this.bloomFilter = new BloomFilter(100000, 0.01); } async get(nodeId) { // Check cache first if (this.cache.has(nodeId)) { return this.cache.get(nodeId); } // Check bloom filter for non-existence if (!this.bloomFilter.has(nodeId)) { return null; } // Fetch from backend const node = await this.backend.get(`nodes:${nodeId}`); if (node) { this.cache.set(nodeId, node); } return node; } async prefetch(pattern) { // Predictive prefetching const likely = await this.predictAccess(pattern); const nodes = await this.backend.batchGet(likely); nodes.forEach(node => { this.cache.set(node.id, node); }); } }
2. Write-Heavy Optimization
class BatchedMemoryGraph { constructor() { this.writeBuffer = []; this.flushInterval = 100; // ms setInterval(() => this.flush(), this.flushInterval); } async addNode(id, data) { // Buffer writes this.writeBuffer.push({ op: 'addNode', id, data, timestamp: Date.now() }); // Return immediately return { id, pending: true }; } async flush() { if (this.writeBuffer.length === 0) return; const batch = this.writeBuffer.splice(0); try { await this.backend.batchWrite(batch); // Notify on success batch.forEach(op => { this.emit('written', op); }); } catch (error) { // Return to buffer on failure this.writeBuffer.unshift(...batch); } } }
Query Patterns
1. Graph Traversal Queries
class GraphQueryEngine { async findPath(startId, endId, options = {}) { const { maxDepth = 6, relationship = null, bidirectional = true } = options; // Breadth-first search const queue = [{ nodeId: startId, path: [startId], depth: 0 }]; const visited = new Set([startId]); while (queue.length > 0) { const { nodeId, path, depth } = queue.shift(); if (nodeId === endId) { return path; } if (depth >= maxDepth) continue; // Get neighbors const edges = await this.getEdges(nodeId, { relationship, direction: bidirectional ? 'both' : 'outgoing' }); for (const edge of edges) { const nextId = edge.to === nodeId ? edge.from : edge.to; if (!visited.has(nextId)) { visited.add(nextId); queue.push({ nodeId: nextId, path: [...path, nextId], depth: depth + 1 }); } } } return null; // No path found } async findSubgraph(rootId, options = {}) { const { maxDepth = 3, maxNodes = 100, relationships = null } = options; const subgraph = { nodes: new Map(), edges: new Map() }; const queue = [{ nodeId: rootId, depth: 0 }]; const visited = new Set(); while (queue.length > 0 && subgraph.nodes.size < maxNodes) { const { nodeId, depth } = queue.shift(); if (visited.has(nodeId)) continue; visited.add(nodeId); // Add node to subgraph const node = await this.getNode(nodeId); if (node) { subgraph.nodes.set(nodeId, node); } if (depth < maxDepth) { // Get connected nodes const edges = await this.getEdges(nodeId, { relationships }); edges.forEach(edge => { subgraph.edges.set(edge.id, edge); const nextId = edge.to === nodeId ? edge.from : edge.to; if (!visited.has(nextId)) { queue.push({ nodeId: nextId, depth: depth + 1 }); } }); } } return subgraph; } }
2. Pattern Matching Queries
class PatternMatcher { async matchPattern(pattern) { // Convert pattern to query // Example: "?person works_for ?company in ?location" const parsed = this.parsePattern(pattern); return this.executeQuery({ select: parsed.variables, where: parsed.conditions, joins: parsed.joins }); } async findCommunities(options = {}) { const { algorithm = 'louvain', minSize = 3, resolution = 1.0 } = options; // Load graph into memory for analysis const adjacency = await this.buildAdjacencyMatrix(); // Run community detection const communities = this.detectCommunities(adjacency, { algorithm, resolution }); // Filter by size return communities.filter(c => c.size >= minSize); } }
Agent Collaboration Patterns
1. Collaborative Knowledge Building
class CollaborativeGraph { async proposeAssertion(assertion) { // Agent proposes new knowledge const proposal = { id: generateId(), assertion, evidence: [], confidence: 0, supporters: new Set([this.agentId]) }; await this.publish('knowledge.proposal', proposal); // Other agents can support with evidence this.subscribe(`knowledge.support.${proposal.id}`, (msg) => { proposal.evidence.push(msg.evidence); proposal.supporters.add(msg.agentId); proposal.confidence = this.calculateConfidence(proposal); // Add to graph if confidence threshold met if (proposal.confidence > 0.8) { this.addVerifiedKnowledge(assertion, proposal); } }); } async challengeAssertion(nodeId, reason) { // Agents can challenge existing knowledge await this.publish('knowledge.challenge', { nodeId, challenger: this.agentId, reason, timestamp: Date.now() }); // Trigger re-evaluation await this.reevaluate(nodeId); } }
2. Distributed Learning
class LearningGraph { async shareLearnedPattern(pattern) { // Agent shares discovered pattern const node = await this.addNode('pattern_' + generateId(), { type: 'learned_pattern', pattern, confidence: pattern.confidence, examples: pattern.examples, learnedBy: this.agentId, timestamp: Date.now() }); // Other agents can validate await this.requestValidation(node.id); } async aggregateKnowledge(topic) { // Combine knowledge from multiple agents const contributions = await this.query({ where: { type: 'knowledge', topic: topic } }); // Merge and reconcile const aggregated = this.mergeKnowledge(contributions); // Store consensus view return this.addNode(`consensus_${topic}`, { type: 'consensus', topic, knowledge: aggregated, contributors: contributions.map(c => c.metadata.author), timestamp: Date.now() }); } }
Performance and Scalability
1. Sharding Strategy
class ShardedMemoryGraph { constructor(shardCount = 16) { this.shards = new Array(shardCount) .fill(null) .map(() => new MemoryShard()); } getShardIndex(nodeId) { // Consistent hashing for shard selection return hash(nodeId) % this.shards.length; } async addNode(id, data) { const shardIndex = this.getShardIndex(id); return this.shards[shardIndex].addNode(id, data); } async query(params) { // Scatter-gather across shards const promises = this.shards.map(shard => shard.query(params) ); const results = await Promise.all(promises); return this.mergeResults(results); } }
2. Indexing Strategies
class IndexedMemoryGraph { constructor() { this.indices = { type: new InvertedIndex(), relationship: new InvertedIndex(), temporal: new BTreeIndex(), spatial: new RTreeIndex(), text: new FullTextIndex() }; } async addNode(id, data) { // Add to primary store await super.addNode(id, data); // Update indices if (data.type) { this.indices.type.add(data.type, id); } if (data.text) { this.indices.text.index(id, data.text); } if (data.location) { this.indices.spatial.insert(id, data.location); } if (data.timestamp) { this.indices.temporal.insert(data.timestamp, id); } } async searchByText(query) { const nodeIds = await this.indices.text.search(query); return this.batchGet(nodeIds); } async findNearby(location, radius) { const nodeIds = this.indices.spatial.searchRadius(location, radius); return this.batchGet(nodeIds); } }
Production Deployment Considerations
-
Storage Backend Selection
- Use Redis for <100GB graphs with low latency needs
- Use Cassandra for massive graphs with high write throughput
- Use Neo4j for complex graph queries
- Use S3 + DynamoDB for cost-effective large-scale storage
-
Consistency Requirements
- Strong consistency: Use consensus protocols (Raft/Paxos)
- Eventual consistency: Use CRDTs for conflict-free updates
- Causal consistency: Use vector clocks for ordering
-
Access Patterns
- Read-heavy: Implement aggressive caching
- Write-heavy: Use write-through buffers
- Mixed: Separate read and write paths (CQRS)
-
Monitoring and Maintenance
- Track graph size and growth rate
- Monitor query performance
- Implement garbage collection for old versions
- Regular backups and snapshots
Conclusion
The Shared Memory Graph pattern enables agents to collaborate on knowledge without the complexity of traditional distributed systems. By providing a flexible, scalable foundation for knowledge representation and sharing, it allows agent systems to grow organically while maintaining performance and consistency.
The key is choosing the right consistency model, access patterns, and storage backend for your specific use case. With these building blocks, you can create agent systems that truly learn and grow together.