Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Designing a Memory Interface

Introduction

In our last lesson, we implemented a sophisticated summarization strategy, moving beyond simple message truncation to intelligently compress older parts of a conversation. We now have the low-level mechanisms for managing context: storing history, truncating it to fit a budget, and summarizing it to preserve long-term memory. However, these mechanisms are still just functions within our application logic. For a scalable, production-grade system, we need a higher level of organization.

This lesson marks the culmination of our work on memory. We will transition from implementation details to architectural design. Your goal is to design a memory interface that integrates with the agent hierarchy and orchestrator. We will abstract our memory management logic into a clean, reusable component. This is analogous to creating a dedicated data access layer (DAL) or a repository in traditional software engineering—it provides a standardized contract for how other parts of the system interact with a core service, in this case, the agent's memory. This abstraction will be crucial for building a modular and maintainable agentic infrastructure.

The Need for a Formal Memory Architecture

As you design a system with multiple agents—supervisors, workers, specialists—each with potentially different memory requirements, directly embedding memory logic into every agent becomes unmanageable. A centralized memory interface solves several key problems that arise in complex agentic systems:

  • Separation of Concerns: Agents shouldn't need to know how memory is stored, summarized, or truncated. Their job is to reason and act. A memory interface lets them simply request context or save an interaction.
  • Consistency: It ensures all agents use the same, battle-tested logic for memory operations.
  • Flexibility: It allows you to swap out memory backends (e.g., from a file-based store to a Redis or OpenSearch cluster) or change summarization strategies without rewriting every agent.
  • Support for Hierarchy: It provides a formal way to manage shared memory across a team of agents versus private, task-specific "scratchpads" for individual workers.

The article "OpenSearch as an agentic memory solution" provides an excellent industry perspective on the challenges that a formal memory system is designed to solve.

OpenSearch as an agentic memory solution: Building context-aware ...

This blog post from OpenSearch introduces the concept of "agentic memory" and the common challenges developers face.

Please read the introduction and the section on memory challenges. Then, skim the core principles listed under "Introducing OpenSearch agentic memory". This will frame our design discussion around real-world problems like token limit management, infrastructure overhead, and intelligent retrieval.

Designing the Memory Interface

A well-designed memory system isn't a single, monolithic block. It's often structured into different types, each serving a distinct purpose. For our agentic infrastructure, we can conceptualize memory in a few key layers.

This diagram illustrates a comprehensive memory architecture, distinguishing between Short-Term Memory (STM) for immediate context and Long-Term Memory (LTM), which is further broken down into semantic, episodic, and procedural types.

Let's break down these types in the context of our system:

  1. Short-Term Memory (STM) / Working Memory: This is the agent's "RAM." It's a temporary scratchpad used during a single, ongoing task. It holds the immediate conversation turns, the agent's internal thoughts (like in a ReAct loop), and the results from tool calls. In a graph-based system like LangGraph, this is often represented by the State object that gets passed between nodes. It's fast, volatile, and scoped to a single execution run.

  2. Long-Term Memory (LTM): This is the agent's "hard drive." It's persistent across sessions and provides the agent with its identity and history.

    • Episodic Memory: This is the history of past conversations. It answers the question, "What happened?" Our work with history persistence and summarization in previous lessons directly addresses this.
    • Semantic Memory: This is a repository of facts and knowledge. It answers the question, "What is true?" This is typically implemented with a vector database for Retrieval-Augmented Generation (RAG), a concept you're already familiar with.
    • Procedural Memory: This is the knowledge of how to perform tasks, primarily knowing which tools to use and how. This is implicitly encoded in an agent's system prompt and its access to the tool store.

Our memory interface should primarily focus on managing the interplay between Working Memory and Episodic Memory, as this is the core of conversational context.

The video below, which shows how the Mastra framework is built, provides a great practical example of how these different memory types are configured and used in a real agent.

Build Your Own AI Agent from Scratch (Mastra + TypeScript)

This video, "Build Your Own AI Agent from Scratch," demonstrates the configuration of multiple memory types within the Mastra framework.

Watch the following three segments to see how a developer might interact with a memory system: Episodic Memory: Notice how they configure it to remember the last 10 messages within a specific threadId. This is exactly what our episodic memory should do. Working Memory: See how they define a template for a user profile that the agent can fill in over time. This is a form of structured scratchpad. Semantic Memory: Observe the setup for semantic search using a vector database to find relevant past messages, even from different conversations.

A TypeScript Interface for Memory Management

Based on these concepts, we can now draft a TypeScript interface. This MemoryManager will be the central component that our orchestrator and agents interact with.

// Define a scope to identify which memory to access.
// This is crucial for multi-tenancy and hierarchical agents.
export interface MemoryScope {
  sessionId: string;  // Identifies a single conversation thread
  userId: string;     // Identifies the end-user
  tenantId: string;   // Identifies the client/organization
}

// Our main interface.
export interface IMemoryManager {
  /**
   * Retrieves the message history for a given scope, ready to be sent to an LLM.
   * This method is responsible for applying summarization and truncation internally.
   * @param scope The scope of the memory to retrieve.
   * @returns A promise that resolves to an array of messages.
   */
  getHistory(scope: MemoryScope): Promise<UIMessage[]>;

  /**
   * Adds a new user-assistant interaction to the history.
   * @param scope The scope to which this interaction belongs.
   * @param userMessage The user's input.
   * @param assistantMessage The agent's response.
   * @returns A promise that resolves when the interaction is saved.
   */
  addInteraction(scope: MemoryScope, userMessage: UIMessage, assistantMessage: UIMessage): Promise<void>;

  /**
   * Updates a temporary, structured scratchpad for an agent.
   * @param scope The scope, can be further narrowed by an agentId.
   * @param data The JSON-serializable data to store.
   * @returns A promise that resolves when the data is saved.
   */
  updateScratchpad(scope: MemoryScope & { agentId?: string }, data: Record<string, any>): Promise<void>;
  
  /**
   * Retrieves the content of an agent's scratchpad.
   * @param scope The scope, can be further narrowed by an agentId.
   * @returns A promise resolving to the scratchpad data.
   */
  getScratchpad(scope: MemoryScope & { agentId?: string }): Promise<Record<string, any> | null>;

  /**
   * Clears the memory for a given scope.
   * @param scope The scope of the memory to clear.
   * @returns A promise that resolves when the memory is cleared.
   */
  clear(scope: MemoryScope): Promise<void>;
}

This interface is the blueprint. A concrete class, say PostgresMemoryManager, would implement these methods using the manageHistory function from our previous lesson and a database connection for persistence. The use of MemoryScope is vital: it allows us to partition memory by tenantId, userId, and sessionId, providing the foundation for our multi-tenant architecture.

Integration with the Agent Hierarchy

With a formal interface, we can now define how it fits into our orchestrator-agent architecture. The key principle is state propagation, where the orchestrator manages the "global" state (the main conversation) and passes necessary context down to worker agents.

This diagram shows how different agents can interact with a shared Agent Engine. The engine holds the persistent memory, while each agent might maintain its own local session or working memory.

Let's consider the Manager-Worker pattern we've discussed.

Why Single Agents Fail: Building Scalable AI Teams with ... - Dev.to

This article explains how information flows in a hierarchical agent team, which is directly applicable to memory management.

Please read the sections "The 'Under the Hood' Connection" and "Task Decomposition and State Propagation". The key insight here is that agents communicate indirectly through a shared State object, which is managed by the orchestrator (the "Manager").

In our system, the MemoryManager instance becomes a part of this shared state. Here's how it works:

  1. Orchestrator's Role: When a request comes in, the orchestrator identifies the tenantId, userId, and sessionId. It instantiates our MemoryManager with this scope.
  2. Delegation to a Supervisor: The orchestrator invokes the top-level supervisor agent, passing the MemoryManager instance along with the user's request.
  3. Supervisor's Task: The supervisor uses memoryManager.getHistory() to get the conversation context. Based on this context and the new request, it decides which worker agent to delegate to.
  4. Worker's Execution: The supervisor invokes the worker, again passing the memoryManager instance.
    • For its task, the worker might need the full conversation context, so it would call memoryManager.getHistory().
    • It might also use the scratchpad for its internal reasoning: memoryManager.updateScratchpad({ ...scope, agentId: 'researcher-agent' }, { current_findings: '...' }).
  5. State Update: Once the entire process is complete, the orchestrator uses memoryManager.addInteraction() to save the final user query and agent response to the persistent episodic memory.

This approach gives us fine-grained control. The episodic history is shared and managed centrally, while individual worker agents can have their own private working memory (scratchpads) by using their unique agentId within the MemoryScope.

Durability and Long-Running Tasks

Finally, since you're building a production service, the memory system must be durable. Agent tasks can be long-running and might be interrupted. A workflow-based approach ensures that state, including memory, is persisted at each step.

Building durable Agents with Workflow DevKit & AI SDK - Peter Wielander, Vercel

This talk from Vercel on the "Workflow DevKit" is highly relevant to building durable, stateful agents, which is what our memory system enables.

Please watch these clips to understand how a workflow engine manages state: The Problem: The speaker explains why simple agent loops are not enough for production and introduces workflows for reliability. Marking Steps: Notice how use step isolates a piece of logic. Each call to our MemoryManager could be considered such a step, ensuring its inputs and outputs are saved. Resumable Streams: This demonstrates how a client can reconnect to a long-running task. Our memory system is what makes this possible by persisting the state that the agent can resume from. Sleep and Webhooks: This shows how a workflow can be paused for days and then resumed. This is only possible if the agent's state (its memory) is durably stored and can be rehydrated. The sleep function is effectively saving state to persistent storage and setting a timer.

The takeaway is that our IMemoryManager implementation must be backed by a persistent store (like a database) that is transactional and reliable. Every operation, like addInteraction, should be an atomic step in a larger, durable workflow.

Conclusion

Today, you've designed a formal memory interface, a critical piece of architectural glue for your agentic infrastructure. By abstracting away the mechanics of context management, you've paved the way for a clean, modular system where agents can be developed independently of the underlying memory technology.

Key Takeaways:

  • A dedicated memory interface separates agent logic from memory management, improving modularity and maintainability.
  • A complete memory system includes multiple types: short-term/working memory for immediate tasks and long-term memory (episodic, semantic, procedural) for persistent knowledge.
  • The MemoryScope (tenantId, userId, sessionId) is essential for partitioning memory in a multi-tenant, multi-agent environment.
  • In a hierarchical system, the orchestrator manages and propagates the memory context to worker agents, who communicate indirectly through this shared state.
  • The memory interface must be implemented with a durable, persistent backend to support resilient, long-running agent tasks.

This lesson concludes our module on Conversation and State Management. You now have a comprehensive plan for an agent's "mind." In our next module, "Multi-Tenant Architecture," we will build directly upon the MemoryScope concept you designed today. The very first step will be to design the tenant data model and store, turning the tenantId in our memory interface into a concrete entity in our system.

Can't find a good explanation? Sign up and we'll make it for you

Sign up