Create and Search Linked Memory Graphs
Memory links connect related memory-like records while preserving historical records. Use them when a newer memory supersedes or refines an older one, or when two memories support, duplicate, or contradict one another. Automatic memory linking creates these links as part of automatic memory extraction; manual linking remains available when an application already knows the relationship.
This guide automatically creates a small graph of pizza preferences, shows how to manage links explicitly when needed, and retrieves graph context with search.
Set Up a Linked-Memory Client
Create a normal OracleAgentMemory client. On the first run, use
SchemaPolicy.CREATE_IF_NECESSARY so the SDK creates all managed database
objects, including the memory-relation store and property graph.
import oracledb
from oracleagentmemory.core import (
MemoryExtractionConfig,
OracleAgentMemory,
OracleSearchResultFormatConfig,
SchemaPolicy,
)
from oracleagentmemory.core.embedders.embedder import Embedder
embedder = Embedder(model="YOUR_EMBEDDING_MODEL")
db_pool = oracledb.SessionPool(
user="YOUR DB USER",
password="YOUR DB PASSWORD",
dsn="localhost:1521/...",
)
memory_store_id = "T_GRAPH_MEMORY"
memory = OracleAgentMemory(
connection=db_pool,
embedder=embedder,
schema_policy=SchemaPolicy.CREATE_IF_NECESSARY,
memory_store_id=memory_store_id,
memory_extraction_config=MemoryExtractionConfig(extract_memories=False),
)
user_id = "graph-demo-user"
Link Memories Automatically During Extraction
For most applications, enable automatic extraction and let the SDK identify
links as it extracts new memories from add_messages(). The default link
mode, POST_EXTRACTION, extracts memories first, retrieves a bounded set of
existing candidates for each new memory, and uses one additional LLM request
to decide whether a typed link should be created. This gives link resolution
its own context and is the recommended mode when link quality matters.
Configure the mode through MemoryExtractionConfig at the client or thread
level. The following thread extracts after every added message and resolves
links after extraction. The OracleAgentMemory client must also be
configured with an extraction LLM.
from oracleagentmemory.core import (
MemoryExtractionConfig,
MemoryLinkExtractionMode,
)
preference_thread = memory.create_thread(
thread_id="pizza-preferences",
user_id=user_id,
memory_extraction_config=MemoryExtractionConfig(
extract_memories=True,
memory_extraction_frequency=1,
memory_link_extraction_mode=MemoryLinkExtractionMode.POST_EXTRACTION,
),
)
preference_thread.add_messages(
[{"role": "user", "content": "I now prefer sourdough pizza."}]
)
For example, when the thread already contains a memory that the user prefers
thin crust, the linker can extract the new sourdough preference and create a
supersedes link to the older memory. Lifecycle link types such as
supersedes, refines, and duplicates retain the older memory as
history and mark it invalid; graph search can still return it as linked
context.
DURING_EXTRACTION asks the extraction LLM to identify links in the same
request that extracts memories, which avoids the additional link-resolution
request. Its candidates are bounded by the extraction search. Set
memory_link_extraction_mode=MemoryLinkExtractionMode.DISABLED to turn off
automatic linking. Neither automatic mode attempts to discover every possible
relationship in the store, so use explicit links for a relationship that must
be recorded.
Create Linked Memories Manually
Create memories normally, then connect them with link_records(). A relation
is directed: its source points to its target. supersedes, refines, and
duplicates make the target invalid; supports and contradicts leave
both endpoint memories valid.
Select a Link Type
For memory evolution, the source is normally the newer memory and the target is the existing memory. Select the link type that describes that relationship.
| Link type | Use when the source memory… | Effect on target memory |
|---|---|---|
supersedes |
Replaces the target as the current information. For example, a new preference replaces an earlier preference. | Becomes invalid but remains available as history. |
refines |
Retains the target’s information while adding detail or precision. For example, a specific hiking preference refines a general one. | Becomes invalid but remains available as history. |
duplicates |
Has the same meaning as the target. The source is the preferred copy. | Becomes invalid to prevent duplicate direct results. |
supports |
Provides evidence for the target. For example, avoiding meat and fish supports a vegetarian preference. | Remains unchanged. |
contradicts |
Conflicts with the target, but the SDK cannot determine which memory is correct. | Remains unchanged. |
Graph search follows each link in either direction. When it traverses from the
target to the source, it displays the corresponding reverse label, such as
is_superseded_by or is_refined_by.
thin_crust_id = memory.add_memory(
"The user's preferred pizza style is thin crust.",
memory_id="pizza-thin-crust",
memory_type="preference",
user_id=user_id,
)
sourdough_id = memory.add_memory(
"The user now prefers sourdough pizza.",
memory_id="pizza-sourdough",
memory_type="preference",
user_id=user_id,
)
neapolitan_id = memory.add_memory(
"The user's preferred pizza style is sourdough, especially Neapolitan sourdough.",
memory_id="pizza-neapolitan-sourdough",
memory_type="preference",
user_id=user_id,
)
supersedes_link_id = memory.link_records(
source_record_id=sourdough_id,
source_record_type="preference",
target_record_id=thin_crust_id,
target_record_type="preference",
relation_type="supersedes",
)
memory.link_records(
source_record_id=neapolitan_id,
source_record_type="preference",
target_record_id=sourdough_id,
target_record_type="preference",
relation_type="refines",
)
Search follows a link in either direction while retaining its stored direction in the result.
Edit and Delete Links
Use the relation ID returned by link_records() to update a relation. Omitted
fields remain unchanged. Updating a relation type recalculates the endpoint
statuses, so a former lifecycle link no longer leaves a memory invalid when
its replacement type does not have lifecycle effects.
Delete a relation by its relation ID, or by its complete directed endpoint tuple: source ID and type, target ID and type, and relation type. Deletion also recalculates affected endpoint statuses.
#Changing a lifecycle link recalculates the status of both endpoint memories.
memory.update_record_link(
supersedes_link_id,
relation_type="supports",
metadata={"reviewed_by": "preference-service"},
)
#Delete by the stable relation ID.
memory.delete_record_link(relation_id=supersedes_link_id)
#Or delete by the complete directed relation triple.
memory.delete_record_link(
source_record_id=neapolitan_id,
source_record_type="preference",
target_record_id=sourdough_id,
target_record_type="preference",
relation_type="refines",
)
Search Through Linked Memories
Set num_hops to attach linked context to every direct memory result.
0 is the default and returns direct results only; values from 1 through
5 follow that many links. max_linked_results limits the total linked
memories attached to each direct result across all hops. It defaults to 100;
set it to 0 to return direct results without linked context.
Scope, metadata, record-type, and expiration filters apply to linked memories.
include_invalid_results applies only to top-level direct results. Linked
memory context can include invalid historical records regardless of that option.
For positive num_hops values, the SDK follows links in both directions and
returns the linked context as the deterministic shortest-path tree described in
the following section.
results = memory.search(
"What pizza should I recommend?",
user_id=user_id,
max_results=5,
num_hops=2,
max_linked_results=20,
include_invalid_results=False,
)
Each result contains a recursive linked-results tree. The SDK formats that tree using the shortest available path to each linked memory. The traversal prevents repeated memories in a path and selects one deterministic shortest path when alternate paths reach the same memory.
Format Graph Context for a Prompt
Each SearchResult exposes format_content() to render a direct result
and its linked memories as structured prompt text. By default, it uses the
include_invalid_results setting of the search that produced the result. A
supplied formatting configuration overrides its explicitly set options. When
invalid content is disabled, invalid linked memories omit their content.
Invalid records that lead to a valid linked memory retain their status and link
context; invalid-only branches are omitted.
Use OracleSearchResultFormatConfig to tailor the rendered tree. This
example includes invalid historical content while removing timestamps, roles,
and statuses for a more compact prompt. You can also include metadata, thread,
user, or agent identifiers and estimated relevance. Every rendered relation label
describes the displayed parent-to-child direction, even though
RecordRelation retains its stored direction.
format_config = OracleSearchResultFormatConfig(
include_invalid_results=True,
show_timestamp=False,
show_role=False,
show_status=False,
)
for result in results:
print(result.format_content(format_config))
Full Code
The complete example is included in this guide for you to copy and run.
#Copyright © 2026 Oracle and/or its affiliates.
#This software is under the Apache License 2.0
#(LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or Universal Permissive License
#(UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option.
#Oracle Agent Memory Code Example - Create and Search Linked Memory Graphs
#-------------------------------------------------------------------------
##Create a graph memory client
import oracledb
from oracleagentmemory.core import (
MemoryExtractionConfig,
OracleAgentMemory,
OracleSearchResultFormatConfig,
SchemaPolicy,
)
from oracleagentmemory.core.embedders.embedder import Embedder
embedder = Embedder(model="YOUR_EMBEDDING_MODEL")
db_pool = oracledb.SessionPool(
user="YOUR DB USER",
password="YOUR DB PASSWORD",
dsn="localhost:1521/...",
)
memory_store_id = "T_GRAPH_MEMORY"
memory = OracleAgentMemory(
connection=db_pool,
embedder=embedder,
schema_policy=SchemaPolicy.CREATE_IF_NECESSARY,
memory_store_id=memory_store_id,
memory_extraction_config=MemoryExtractionConfig(extract_memories=False),
)
user_id = "graph-demo-user"
##Create linked memories
thin_crust_id = memory.add_memory(
"The user's preferred pizza style is thin crust.",
memory_id="pizza-thin-crust",
memory_type="preference",
user_id=user_id,
)
sourdough_id = memory.add_memory(
"The user now prefers sourdough pizza.",
memory_id="pizza-sourdough",
memory_type="preference",
user_id=user_id,
)
neapolitan_id = memory.add_memory(
"The user's preferred pizza style is sourdough, especially Neapolitan sourdough.",
memory_id="pizza-neapolitan-sourdough",
memory_type="preference",
user_id=user_id,
)
supersedes_link_id = memory.link_records(
source_record_id=sourdough_id,
source_record_type="preference",
target_record_id=thin_crust_id,
target_record_type="preference",
relation_type="supersedes",
)
memory.link_records(
source_record_id=neapolitan_id,
source_record_type="preference",
target_record_id=sourdough_id,
target_record_type="preference",
relation_type="refines",
)
##Edit and delete links
#Changing a lifecycle link recalculates the status of both endpoint memories.
memory.update_record_link(
supersedes_link_id,
relation_type="supports",
metadata={"reviewed_by": "preference-service"},
)
#Delete by the stable relation ID.
memory.delete_record_link(relation_id=supersedes_link_id)
#Or delete by the complete directed relation triple.
memory.delete_record_link(
source_record_id=neapolitan_id,
source_record_type="preference",
target_record_id=sourdough_id,
target_record_type="preference",
relation_type="refines",
)
#Recreate the example links for the search example.
memory.link_records(sourdough_id, "preference", thin_crust_id, "preference", "supersedes")
memory.link_records(neapolitan_id, "preference", sourdough_id, "preference", "refines")
##Search linked memories
results = memory.search(
"What pizza should I recommend?",
user_id=user_id,
max_results=5,
num_hops=2,
max_linked_results=20,
include_invalid_results=False,
)
##Format graph context for a prompt
format_config = OracleSearchResultFormatConfig(
include_invalid_results=True,
show_timestamp=False,
show_role=False,
show_status=False,
)
for result in results:
print(result.format_content(format_config))