Skip to content

Agent memory ​

Agent memory enables long-term personalization and organizational knowledge sharing beyond single-session chat history. The SDK provides two distinct memory scopes: user memory for private, per-user preferences and organization memory for shared, tenant-wide facts.

Memory is automatically integrated into agent workflows through dependency injection and dedicated events.

Two memory scopes ​

User memory is private to individual users and automatically extracted from conversation messages by the LLM. It stores personal preferences, working styles, and individual context—things like "User prefers concise code examples in Python." Retrieval is vector-based semantic search.

Organization memory is shared across all users in a tenant or namespace. Unlike user memory, it requires explicit documentation rather than automatic inference. It stores company policies, project details, and team conventions—things like "We deploy to production on Fridays." Retrieval works the same way.

Both scopes retrieve from the vector store only. MemorySearchResult.relations still exists on the model but is always empty.

Memory in chat agents ​

Chat blueprints do not write the steps below. They call the Memory capability: Memory.recall(query) returns a block per scope that Conversation.compose merges into the model's system instructions, and Memory.remember(...) returns the storage request, which belongs ahead of the completion in the list the answering step returns so it is published before the run ends. See Capabilities and Tools. The explicit workflow below is the pattern for a custom agent that wires memory by hand.

Memory workflow pattern ​

Both memory types follow a common four-step workflow:

The pattern retrieves relevant memories, injects them into the chat history as a system message, generates a memory-aware response, and persists new learnings.

User memory pattern ​

User memory learns personal preferences automatically from conversations. The agent extracts facts about the user's working style without requiring explicit documentation. Use this pattern for conversational agents that should adapt to individual user preferences over time—code assistants, personal productivity agents, custom assistants.

Reference implementation: playground/minimal_workflow/user_memory_workflow/

The termination constraint

If memory persistence depends on LLM output, the LLM step must not return StopEvent. When as_stop_step=True, the workflow terminates immediately with an LLMStopEvent — the memory storage step never executes.

python
# INCORRECT: LLMStopEvent terminates before storage
@step()
async def respond(self, ..., displayer: EventDisplayer) -> LLMStopEvent:
    return await displayer.display_llm_stream(..., as_stop_step=True)  # Workflow ends here

@step()
async def store(self, llm: LLMStopEvent, ...) -> StoreMemoryEvent:
    ...  # Never executes — StopEvent already terminated the run

# CORRECT: LLMEvent allows downstream steps
@step()
async def respond(self, ..., displayer: EventDisplayer) -> LLMEvent:
    return await displayer.display_llm_stream(..., as_stop_step=False)

@step()
async def store(self, llm: LLMEvent, memory: AgentMemory) -> StoreUserMemoryEvent:
    await memory.add_user_memory(messages=llm.chat_messages, ...)
    return StoreUserMemoryEvent(...)

@step()
async def stop_step(self, _: StoreUserMemoryEvent) -> StopEvent:
    return StopEvent()

See The dangling stop violation for the general rule.

Complete example ​

python
from swiss_ai_hub.agent.agents.agent import Agent
from swiss_ai_hub.agent.workflow.decorators.step import step
from swiss_ai_hub.core.generative_ai.memory.agent_memory import AgentMemory
from swiss_ai_hub.core.generative_ai.chat_history.extend_chat_history_with_user_memory import (
    extend_chat_history_with_user_memory,
)
from swiss_ai_hub.core.events.agent.user.user_message_event import UserMessageEvent
from swiss_ai_hub.core.events.agent.semantic.llm.llm_event import LLMEvent
from swiss_ai_hub.core.events.agent.control.stop.stop_event import StopEvent
from swiss_ai_hub.core.events.agent.memory.retrieve.retrieve_user_memory_event import RetrieveUserMemoryEvent
from swiss_ai_hub.core.events.agent.memory.history.add_user_memory_to_chat_history_event import AddUserMemoryToChatHistoryEvent
from swiss_ai_hub.core.events.agent.memory.store.store_user_memory_event import StoreUserMemoryEvent
from swiss_ai_hub.core.topics.agents.agent_instance_topic import AgentInstanceTopic
from swiss_ai_hub.core.displayers.event_displayer import EventDisplayer
from swiss_ai_hub.core.i18n.locale_handler import LocaleHandler

class UserMemoryAgent(Agent):
    """
    Memory-enhanced conversational agent that retrieves and persists user memories.

    Use this agent for personalized conversations requiring long-term context
    beyond single session chat history.
    """

    @step()
    async def retrieve_memory_step(
        self,
        event: UserMessageEvent,
        memory: AgentMemory,
    ) -> RetrieveUserMemoryEvent:
        """Searches user memories to provide personalized context."""
        memory_search_result = await memory.search_user_memory(
            query=event.user_query,
            user_id=event.user.id
        )
        return RetrieveUserMemoryEvent.from_memory_search_result(
            memory_search_result=memory_search_result
        )

    @step()
    async def add_memory_to_chat_history_step(
        self,
        user_message_event: UserMessageEvent,
        memory_event: RetrieveUserMemoryEvent,
        t: LocaleHandler
    ) -> AddUserMemoryToChatHistoryEvent:
        """Prepends memories as system message to guide LLM responses."""
        extended_chat_history = extend_chat_history_with_user_memory(
            chat_history=user_message_event.messages,
            memories=memory_event.memories,
            relations=memory_event.relations,
            user=user_message_event.user,
            t=t,
        )
        return AddUserMemoryToChatHistoryEvent(extended_history=extended_chat_history)

    @step()
    async def respond_with_memory_step(
        self,
        event: AddUserMemoryToChatHistoryEvent,
        agent_config: UserMemoryAgentConfig,
        displayer: EventDisplayer,
    ) -> LLMEvent:
        """Generates response using memory-enhanced chat history."""
        async with agent_config.llm.cost_reporting_llm(displayer) as llm:
            return await displayer.display_llm_stream(
                agent_config.llm,
                llm,
                event.extended_history,
                as_stop_step=False
            )

    @step()
    async def update_memory_step(
        self,
        user_message_event: UserMessageEvent,
        llm_event: LLMEvent,
        memory: AgentMemory,
        topic: AgentInstanceTopic,
    ) -> StoreUserMemoryEvent:
        """Persists conversation learnings to long-term memory."""
        memory_added = await memory.add_user_memory(
            messages=llm_event.chat_messages,
            user_id=user_message_event.user.id,
            thread_id=topic.thread_id,
            display_id=topic.display_id,
            run_id=topic.run_id,
        )
        return StoreUserMemoryEvent.from_memory_added_object(
            memory_added=memory_added
        )

    @step()
    async def stop_step(self, _: StoreUserMemoryEvent) -> StopEvent:
        """Marks workflow completion."""
        return StopEvent()
python
from swiss_ai_hub.core.agents.agent_config import AgentConfig
from swiss_ai_hub.core.generative_ai.resources.models.llm.llm_config import LLMConfig

class UserMemoryAgentConfig(AgentConfig):
    llm: LLMConfig

Key components ​

AgentMemory injection ​

The AgentMemory object is automatically injected into steps via dependency injection:

python
@step()
async def retrieve_memory_step(
    self,
    event: UserMessageEvent,
    memory: AgentMemory,  # Injected automatically
) -> RetrieveUserMemoryEvent:
    memory_search_result = await memory.search_user_memory(
        query=event.user_query,
        user_id=event.user.id
    )
    return RetrieveUserMemoryEvent.from_memory_search_result(
        memory_search_result=memory_search_result
    )

Memory retrieval ​

search_user_memory() performs semantic search against the user's private memory store. It takes the search query (typically the user's current message), the user ID, and an optional limit (default: 100). It returns a MemorySearchResult containing the matching memories.

Chat history extension ​

The extend_chat_history_with_user_memory() helper inserts memories as a system message:

python
extended_chat_history = extend_chat_history_with_user_memory(
    chat_history=user_message_event.messages,
    memories=memory_event.memories,
    relations=memory_event.relations,
    user=user_message_event.user,
    t=t,  # LocaleHandler for i18n
)

LLMs treat system messages as authoritative background information, so memories are presented as optional context that the LLM may or may not use based on relevance. The memories are inserted after existing system messages (agent personality/behavior) but before user messages.

Memory persistence ​

add_user_memory() uses an LLM to extract learnings from the conversation:

python
memory_added = await memory.add_user_memory(
    messages=llm_event.chat_messages,  # Full conversation including LLM response
    user_id=user_message_event.user.id,
    thread_id=topic.thread_id,      # Swiss AI Agent Protocol context
    display_id=topic.display_id,    # Swiss AI Agent Protocol context
    run_id=topic.run_id,            # Swiss AI Agent Protocol context
)

The LLM analyzes the conversation and extracts facts like "User prefers Python over JavaScript" without storing the entire conversation.

Organization memory pattern ​

Organization memory stores explicit, shared organizational knowledge. Unlike user memory (which is inferred), organization memory requires users to intentionally document facts. Use this pattern for agents managing shared organizational context—team conventions, project documentation, company policies, or technical facts that all users should know.

Reference implementation: playground/minimal_workflow/organization_memory_workflow/

Complete example ​

python
from swiss_ai_hub.agent.agents.agent import Agent
from swiss_ai_hub.agent.workflow.decorators.step import step
from swiss_ai_hub.core.generative_ai.memory.agent_memory import AgentMemory
from swiss_ai_hub.core.generative_ai.chat_history.extend_chat_history_with_organization_memory import (
    extend_chat_history_with_organization_memory,
)
from swiss_ai_hub.core.events.agent.user.user_message_event import UserMessageEvent
from swiss_ai_hub.core.events.agent.semantic.llm.llm_stop_event import LLMStopEvent
from swiss_ai_hub.core.events.agent.memory.store.store_organization_memory_event import StoreOrganizationMemoryEvent
from swiss_ai_hub.core.events.agent.memory.retrieve.retrieve_organization_memory_event import RetrieveOrganizationMemoryEvent
from swiss_ai_hub.core.events.agent.memory.history.add_organization_memory_to_chat_history_event import AddOrganizationMemoryToChatHistoryEvent
from swiss_ai_hub.core.topics.agents.agent_instance_topic import AgentInstanceTopic
from swiss_ai_hub.core.displayers.event_displayer import EventDisplayer
from swiss_ai_hub.core.i18n.locale_handler import LocaleHandler

class OrganizationMemoryAgent(Agent):
    """
    Organization memory management agent that stores and retrieves
    explicit organizational facts.

    Key Differences from UserMemoryAgent:
    - Input: Explicit facts (user provides clean memory text) vs. inferred from chat
    - Scope: Organization-wide (shared) vs. user-private
    - Namespace: Supports department-level scoping via tenant_namespace
    """

    @step()
    async def store_organization_memory_step(
        self,
        event: UserMessageEvent,
        memory: AgentMemory,
        topic: AgentInstanceTopic,
        agent_config: OrganizationMemoryAgentConfig,
    ) -> StoreOrganizationMemoryEvent:
        """Stores the user's query as an explicit organizational fact."""
        memory_added = await memory.add_organization_memory(
            memory=event.user_query,  # Direct storage - user query is the fact itself
            user_id=event.user.id,
            thread_id=topic.thread_id,
            display_id=topic.display_id,
            run_id=topic.run_id,
            tenant_id=agent_config.tenant_id,
            tenant_namespace=agent_config.tenant_namespace,
        )
        return StoreOrganizationMemoryEvent.from_memory_added_object(
            memory_added=memory_added
        )

    @step()
    async def retrieve_organization_memory_step(
        self,
        event: UserMessageEvent,
        memory: AgentMemory,
        agent_config: OrganizationMemoryAgentConfig,
    ) -> RetrieveOrganizationMemoryEvent:
        """Searches organization memories to provide shared org context."""
        memory_search_result = await memory.search_organization_memory(
            query=event.user_query,
            tenant_id=agent_config.tenant_id,
            tenant_namespace=agent_config.tenant_namespace,
            user_id=event.user.id,
        )
        return RetrieveOrganizationMemoryEvent.from_memory_search_result(
            memory_search_result=memory_search_result
        )

    @step()
    async def add_memory_to_chat_history_step(
        self,
        user_message_event: UserMessageEvent,
        memory_event: RetrieveOrganizationMemoryEvent,
        t: LocaleHandler
    ) -> AddOrganizationMemoryToChatHistoryEvent:
        """Prepends organization memories as system message."""
        extended_chat_history = extend_chat_history_with_organization_memory(
            chat_history=user_message_event.messages,
            memories=memory_event.memories,
            t=t,
        )
        return AddOrganizationMemoryToChatHistoryEvent(extended_history=extended_chat_history)

    @step()
    async def respond_with_memory_step(
        self,
        event: AddOrganizationMemoryToChatHistoryEvent,
        agent_config: OrganizationMemoryAgentConfig,
        displayer: EventDisplayer,
    ) -> LLMStopEvent:
        """Generates response using memory-enhanced chat history."""
        async with agent_config.llm.cost_reporting_llm(displayer) as llm:
            return await displayer.display_llm_stream(
                agent_config.llm,
                llm,
                event.extended_history,
                as_stop_step=True
            )
python
from swiss_ai_hub.core.agents.agent_config import AgentConfig
from swiss_ai_hub.core.generative_ai.resources.models.llm.llm_config import LLMConfig

class OrganizationMemoryAgentConfig(AgentConfig):
    """Configuration for OrganizationMemoryAgent.

    Defines the LLM and the tenant context (ID and namespace) for memory scoping.
    """
    llm: LLMConfig
    tenant_id: str
    tenant_namespace: str

Key differences from user memory ​

Explicit storage (no inference) ​

Organization memory is stored directly as provided by the user:

python
memory_added = await memory.add_organization_memory(
    memory=event.user_query,  # Direct - no LLM extraction
    # ... context fields ...
)

Organization memories affect all users, so explicit documentation ensures accuracy and intentionality. This prevents accidental policy creation from casual conversation.

Tenant scoping ​

Organization memory supports multi-tenant and department-level isolation:

python
memory_search_result = await memory.search_organization_memory(
    query=event.user_query,
    tenant_id=agent_config.tenant_id,           # Organization boundary
    tenant_namespace=agent_config.tenant_namespace,  # Department boundary
    user_id=event.user.id,
)

The namespace parameter scopes memories to departments. "Engineering" might contain technical documentation and deployment procedures, "Sales" might contain product pricing and customer segments, and None indicates global tenant knowledge.

Shared visibility ​

Retrieved memories are visible to all users in the tenant/namespace, not just the user who created them.

Memory events ​

The memory system provides six specialized events for workflow control:

Event typePurpose
RetrieveUserMemoryEventContains retrieved user memories
RetrieveOrganizationMemoryEventContains retrieved organization memories
AddUserMemoryToChatHistoryEventContains chat history with user memory injected
AddOrganizationMemoryToChatHistoryEventContains chat history with org memory injected
StoreUserMemoryEventConfirms user memory persistence
StoreOrganizationMemoryEventConfirms organization memory persistence

Memory retrieval and storage automatically emit display events for observability. These appear in the Swiss AI Agent Protocol trace and require no special handling.

Combining user and organization memory ​

For agents that need both memory types, combine the workflows:

python
class HybridMemoryAgent(Agent):
    @step()
    async def retrieve_user_memory_step(
        self, event: UserMessageEvent, memory: AgentMemory
    ) -> RetrieveUserMemoryEvent:
        # Retrieve personal preferences
        result = await memory.search_user_memory(query=event.user_query, user_id=event.user.id)
        return RetrieveUserMemoryEvent.from_memory_search_result(result)

    @step()
    async def retrieve_org_memory_step(
        self, event: UserMessageEvent, memory: AgentMemory, config: AgentConfig
    ) -> RetrieveOrganizationMemoryEvent:
        # Retrieve organizational facts
        result = await memory.search_organization_memory(
            query=event.user_query,
            tenant_id=config.tenant_id,
            tenant_namespace=config.tenant_namespace,
            user_id=event.user.id
        )
        return RetrieveOrganizationMemoryEvent.from_memory_search_result(result)

    @step()
    async def combine_memories_step(
        self,
        event: UserMessageEvent,
        user_mem: RetrieveUserMemoryEvent,
        org_mem: RetrieveOrganizationMemoryEvent,
        t: LocaleHandler
    ) -> CombinedMemoryEvent:
        # Extend with both memory types
        chat_history = extend_chat_history_with_user_memory(
            chat_history=event.messages,
            memories=user_mem.memories,
            relations=user_mem.relations,
            user=event.user,
            t=t
        )
        chat_history = extend_chat_history_with_organization_memory(
            chat_history=chat_history,  # Already has user memory
            memories=org_mem.memories,
            t=t
        )
        return CombinedMemoryEvent(extended_history=chat_history)

The order matters: user memories are added first (more general context), then organization memories (specific facts).

Advanced usage ​

Filtering memory retrieval ​

Narrow memory searches by agent or thread:

python
@step()
async def retrieve_memory_step(
    self, event: UserMessageEvent, memory: AgentMemory, topic: AgentInstanceTopic
) -> RetrieveUserMemoryEvent:
    result = await memory.search_user_memory(
        query=event.user_query,
        user_id=event.user.id,
        agent_id=topic.agent_id,      # Only memories from this agent
        thread_id=topic.thread_id,    # Only memories from this conversation
    )
    return RetrieveUserMemoryEvent.from_memory_search_result(result)

Thread-specific filtering supports "recall what we discussed in this conversation" use cases. Agent-specific filtering keeps a code assistant from seeing memories created by a RAG agent.

Custom memory extraction ​

The AgentMemory class automatically customizes extraction based on agent class:

python
class SpecializedMemoryAgent(Agent):
    @step()
    async def update_memory_step(
        self, user_message_event: UserMessageEvent, llm_event: LLMEvent, memory: AgentMemory, topic: AgentInstanceTopic
    ) -> StoreUserMemoryEvent:
        # AgentMemory automatically customizes extraction based on agent class
        memory_added = await memory.add_user_memory(
            messages=llm_event.chat_messages,
            user_id=user_message_event.user.id,
            thread_id=topic.thread_id,
            display_id=topic.display_id,
            run_id=topic.run_id,
        )
        # AgentMemory includes agent context automatically via self.agent_id
        return StoreUserMemoryEvent.from_memory_added_object(memory_added)

Code assistants extract technical preferences, RAG agents extract domain interests—all automatically based on agent type.

Config-driven memory ​

Production agents often make memory features optional via configuration flags. Use preconditions to gate memory steps based on config, preventing race conditions with optional events:

python
from swiss_ai_hub.agent.workflow.decorators.precondition import precondition

@precondition()
def check_memory_ready(
    user_event: UserMessageEvent,
    user_memory: RetrieveUserMemoryEvent | None,
    org_memory: RetrieveOrganizationMemoryEvent | None,
    config: AgentConfig,
) -> bool:
    if config.enable_user_memory and user_memory is None:
        return False
    if config.enable_org_memory and org_memory is None:
        return False
    return config.enable_user_memory or config.enable_org_memory

@precondition()
def check_storage_complete(
    llm: LLMEvent,
    stored: StoreUserMemoryEvent | None,
    config: AgentConfig,
) -> bool:
    if config.enable_memory_storage and stored is None:
        return False
    return True

The check_memory_ready precondition blocks the history extension step until all enabled memory types have been retrieved. The check_storage_complete precondition blocks the final stop step until storage completes (if enabled). This prevents the optional parameter trap where steps execute prematurely with None values.

Observability ​

All memory operations are automatically traced in the observability dashboard. Retrieval traces show the query, returned memories, and relevance scores. Storage traces show extracted memories, relationships, and metadata. Chat history extension displays the system message with memory content.

All memories store full Swiss AI Agent Protocol context: agent_id (which agent created the memory), thread_id (which conversation thread), display_id (UI display context), run_id (workflow execution ID), and user_id (who the memory belongs to or who documented it). This enables complete auditability—you can trace back to which conversation taught the agent a particular preference.

Storage events also carry llm_model_name, the model that extracted the memories, so a stored fact can be attributed to the model that produced it. It is empty for organization memory, which stores the text you pass verbatim and runs no model.

Choosing the extraction model ​

Extraction and reconciliation are several model calls per stored conversation, and they are short, mechanical tasks — so they do not need the agent's answer model. AgentMemory takes an optional llm_model_name; unset, it uses the platform-wide default (MEM0_LLM_NAME). The dispatcher supplies it from the agent config when injecting AgentMemory, reading AgentConfig.memory_llm_model_name.

That property is the platform's hook: it returns None on the base class, and a blueprint that offers a memory-model picker overrides it to point at its own field. RAGAgentConfig does exactly that, exposing memory_llm inside its UserMemoryConfig so administrators pick the model per profile.

python
class MyAgentConfig(AgentConfig):
    memory_llm: Annotated[str | ModelSelect | None, Field(description="Model that extracts memories.")] = None

    @property
    @override
    def memory_llm_model_name(self) -> str | None:
        # A form-mode element or a blank submission both mean "use the platform default".
        return self.memory_llm if isinstance(self.memory_llm, str) and self.memory_llm else None

Only the extraction model is per-agent. Embedding and reranking stay deployment-wide, because memories written with one embedding model cannot be searched with another.

The RAG blueprints do not write user memory themselves — they delegate it to the MemoryWriterAgent so the run finalizes as soon as the answer is ready. The chosen model travels there on the start event (origin_memory_llm), which is what keeps extraction on the profile's own model rather than the platform default.

Best practices ​

Use user memory for preferences ("User prefers brief responses") and organization memory for facts ("We deploy on Fridays"). Let user memory infer from conversation while explicitly documenting organization memory. Always retrieve memories at workflow start so memory context guides the entire response, and store new learnings at workflow end after the LLM response is included.

Memory retrieval adds roughly 100ms latency. Use the limit parameter to avoid overwhelming context, and filter by agent or thread when appropriate to reduce irrelevant memories.

User memory is GDPR-compliant—users can view, edit, and delete all their memories. Organization memory requires access control since changes affect all users. Every memory tracks who created it and when for auditability, and all memory data stays on Swiss infrastructure.

Next steps

Explore the complete examples in playground/minimal_workflow/user_memory_workflow/ and playground/minimal_workflow/organization_memory_workflow/. Review memory events in Langfuse after running a memory-enhanced agent. Try building a hybrid agent combining both memory types, or experiment with namespace scoping for department-level isolation.

Built with ❤️ in Switzerland 🇨🇭