Shared Memory Graph: How Agents Share Knowledge

ArtCafe Team
March 5, 2025
12 min read min read
Memory GraphArchitectureKnowledge Sharing
Back to Blog

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

  1. 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
  2. Consistency Requirements

    • Strong consistency: Use consensus protocols (Raft/Paxos)
    • Eventual consistency: Use CRDTs for conflict-free updates
    • Causal consistency: Use vector clocks for ordering
  3. Access Patterns

    • Read-heavy: Implement aggressive caching
    • Write-heavy: Use write-through buffers
    • Mixed: Separate read and write paths (CQRS)
  4. 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.