# How to Build a Context Graph for Agent Memory with Structured, Queryable Entities

> Learn how to build a context graph for agent memory. Semantica uses a thread-safe graph to store structured, queryable entities and relationships for advanced AI.

- Repository: [Semantica /semantica](https://github.com/semantica-agi/semantica)
- Tags: how-to-guide
- Published: 2026-08-20

---

**Semantica stores agent long-term memory in a `ContextGraph`—an in-memory, thread-safe graph structure that organizes entities as typed nodes and relationships as weighted edges, supporting temporal validity, multi-hop traversal, and cross-graph linking.**

A robust memory system for AI agents requires more than simple key-value storage; it demands a structure that captures semantic relationships, temporal bounds, and modularity. The `ContextGraph` class in the `semantica-agi/semantica` repository provides exactly this capability, implementing a queryable graph layer that agents use to retain and retrieve structured knowledge across conversation turns and tasks.

## Core Architecture and Data Model

At the foundation of the memory system are two data classes defined in [`semantica/context/context_graph.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/context/context_graph.py): **ContextNode** and **ContextEdge**. These structures store unique identifiers, semantic types, content payloads, arbitrary metadata dictionaries, and optional temporal validity windows (`valid_from` and `valid_until` ISO timestamps). The graph maintains these objects in internal dictionaries while exposing them through a high-level API.

### Thread Safety and Indexing

All mutating operations—including `add_node()`, `add_edge()`, `add_nodes()`, and `add_edges()`—acquire a re-entrant lock (`self._lock`) to guarantee thread-safe concurrent access. For fast retrieval, `ContextGraph` maintains three indices: `node_type_index` and `edge_type_index` for type-based filtering, and `_adjacency` for storing the underlying adjacency list structure.

### Optional Knowledge Graph Analytics

When initialized with `advanced_analytics=True`, the graph instantiates additional components from the `semantica/kg/*` modules. According to the source code in lines 151–159 of [`context_graph.py`](https://github.com/semantica-agi/semantica/blob/main/context_graph.py), this enables **centrality analysis**, **community detection**, and **node embeddings** via `self.kg_components`, allowing the agent to compute importance scores and semantic clusters over its memory.

## Initializing the ContextGraph

Creating a memory graph requires importing the class and optionally enabling analytics suites. The constructor accepts boolean flags that determine whether the graph calculates PageRank-style centrality, detects community structures, or generates vector embeddings for nodes.

```python
from semantica.context import ContextGraph

graph = ContextGraph(
    advanced_analytics=True,
    centrality_analysis=True,
    community_detection=True,
    node_embeddings=True,
)

```

## Populating the Graph with Structured Entities

### Bulk Insertion

For efficiency when hydrating memory from external sources, use `add_nodes()` and `add_edges()` to insert lists of dictionaries. Each dictionary must contain an `id` field and a `type` field; additional keys like `content`, `metadata`, `valid_from`, and `weight` are optional.

```python
nodes = [
    {"id": "Python", "type": "language", "content": "Python programming language"},
    {"id": "Programming", "type": "concept"},
    {"id": "AI", "type": "field", "valid_from": "2022-01-01"},
]
graph.add_nodes(nodes)

edges = [
    {"source": "Python", "target": "Programming", "type": "related_to"},
    {"source": "Python", "target": "AI", "type": "applies_to", "weight": 0.8},
]
graph.add_edges(edges)

```

### Single Entity Helpers

For incremental updates during agent execution, use the convenience methods `add_node()` and `add_edge()`. These wrap the bulk methods internally but provide a simpler signature for interactive construction.

```python
graph.add_node("ChatGPT", "agent", content="Large language model")
graph.add_edge("ChatGPT", "Python", "uses")

```

## Querying Agent Memory

### Neighbor Retrieval with Pagination

The `get_neighbors()` method performs breadth-first traversal to retrieve connected entities. It supports pagination through `skip` and `limit` parameters and respects the `hops` argument for controlling traversal depth.

```python
first_neighbors = graph.get_neighbors("Python", hops=1, limit=5, skip=0)

# → [{'id': 'Programming', 'type': 'concept', ...}, ...]

```

### Multi-Hop Weighted Traversal

For relationship-aware retrieval, `get_neighbor_distances()` (implemented in lines 210–289) returns neighbors with distance metadata, confidence decay calculations, and path reconstruction. You can filter by minimum confidence thresholds and specific relationship types.

```python
neighbors = graph.get_neighbor_distances(
    "Python",
    hops=3,
    min_confidence=0.5,
    relationship_types=["related_to", "applies_to"],
)

# Each entry contains hop, distance_band, confidence_decay, path_to_anchor

```

### Keyword Search

The `query()` method provides simple keyword search over node contents, returning ranked results with relevance scores. This is useful for fuzzy retrieval when the exact entity ID is unknown.

```python
results = graph.query("language model", limit=3)

# → [{'node': {...}, 'score': 0.667, 'content': 'ChatGPT'}, ...]

```

## Advanced Memory Features

### Temporal Validity

Nodes and edges can carry time-bound metadata. The helper methods `_parse_iso_dt_` and `_closing_valid_until_` (lines 42–73) handle ISO string parsing, while `is_active()` checks the current system time against `valid_from` and `valid_until` bounds to determine if a memory entry should be considered current.

### Cross-Graph Linking for Modular Memory

The `link_graph()` method (lines 283–327) creates a "bridge" node and edge connecting the current `ContextGraph` to another instance. This enables hierarchical or domain-specific memory modules. The `navigate_to()` method allows traversal across these bridges during query execution.

```python
other = ContextGraph()
other.add_node("Finance", "domain")
link_id = graph.link_graph(other, source_node_id="ChatGPT", target_node_id="Finance")

# Later navigation:

other_graph, entry_node = graph.navigate_to(link_id)

```

### Entity Resolution

When `extract_entities` is enabled, `ContextGraph` lazily instantiates an **EntityLinker** from [`semantica/context/entity_linker.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/context/entity_linker.py). This component resolves raw text mentions to canonical node IDs before insertion, preventing duplicate entities for the same real-world concept.

## Persistence and State Management

Agent memory can survive process restarts through JSON serialization. The `save_to_file()` method exports the entire graph structure, indices, and metadata, while `load_from_file()` reconstructs the object graph including temporal validity windows and edge weights.

```python
graph.save_to_file("/tmp/agent_memory.json")

# Later reconstruction:

new_graph = ContextGraph()
new_graph.load_from_file("/tmp/agent_memory.json")

```

## Summary

- **ContextGraph** in [`semantica/context/context_graph.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/context/context_graph.py) provides the core in-memory storage for agent memory, using **ContextNode** and **ContextEdge** objects with temporal validity support.
- All write operations are thread-safe via a re-entrant lock (`self._lock`), and fast lookups are enabled by type indices and adjacency lists.
- The query API supports BFS neighborhood retrieval (`get_neighbors`), weighted multi-hop traversal (`get_neighbor_distances`), and keyword search (`query`).
- Advanced features include optional KG analytics (centrality, embeddings), cross-graph linking via `link_graph()`, and entity resolution through **EntityLinker**.
- Full persistence is available through `save_to_file()` and `load_from_file()` for maintaining state across sessions.

## Frequently Asked Questions

### What data structure does Semantica use for agent memory?

Semantica uses the **`ContextGraph`** class, which implements an in-memory graph structure composed of **`ContextNode`** objects (representing entities, concepts, or decisions) and **`ContextEdge`** objects (representing relationships or causal links). This structure is defined in [`semantica/context/context_graph.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/context/context_graph.py) and supports typed nodes, weighted edges, and temporal validity metadata.

### How does ContextGraph handle concurrent access from multiple threads?

All mutating methods—including `add_node()`, `add_edge()`, and their bulk counterparts—acquire a re-entrant lock stored in `self._lock`. This guarantees thread-safe concurrent reads and writes without requiring external synchronization, as implemented in the core graph file.

### Can I query relationships across multiple isolated memory graphs?

Yes. The **`link_graph()`** method creates a bridge between two `ContextGraph` instances, producing a special node and edge pair that reference the external graph. You can then use **`navigate_to()`** to traverse into the linked graph during query execution, enabling modular memory architectures where different domains or tasks maintain separate but connected graph instances.

### How do I ensure old memories expire automatically?

Assign **`valid_from`** and **`valid_until`** ISO timestamp strings when creating nodes or edges. The graph stores these values and exposes an **`is_active()`** method (which internally uses `_parse_iso_dt_`) to check if the current time falls within the validity window. When querying, you can filter results based on this temporal metadata to exclude expired memories.