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))