Skip to content

ConversationMemory

ConversationMemory automatically preserves and appends conversation history across successive node invocations.

By default, an agent_node is stateless: each invocation is isolated. Attaching ConversationMemory causes previous conversational turns to be remembered and prepended to new user inputs automatically.

import railtracks as rt
from railtracks.prebuilt.middleware import ConversationMemory

# ConversationMemory is node-level: it preserves and appends conversation
# history across repeated invocations automatically.
memory = ConversationMemory()
ChatAgent = rt.agent_node(
    name="chat-demo",
    llm=rt.llm.OpenAILLM("gpt-6-luna"),
    middleware=[memory],
)

flow = rt.Flow("ChatFlow", entry_point=ChatAgent)
# flow.invoke("What is your name?")
# flow.invoke("What did I just ask?")  -> Agent remembers Turn 1!

How It Works

  1. On the first turn, the user message is sent to the model normally, and the resulting message history is cached in the active session context (rt.context) under memory.context_key.
  2. By default, each ConversationMemory instance generates a unique, isolated session context key (e.g. conversation_history_a1b2c3d4). This ensures multiple agents running in the same flow each have their own independent conversation memory by default.
  3. On subsequent turns, the prior message history is retrieved from the session context, the incoming user input is appended, and the accumulated history is passed to the agent.
  4. The session context variable is automatically updated after every turn, accumulating multi-turn conversation context.

Multi-Agent Isolation & Configuration

  • Automatic Per-Instance Isolation: Each ConversationMemory() instance has its own unique session context key by default. Two agents in the same flow maintain completely independent memory stores with zero extra configuration.
  • Custom Context Key: Pass an explicit context_key to assign a known session variable name:
    memory = ConversationMemory(context_key="researcher_memory")
    
  • Shared Memory Between Agents: If you want multiple agents to share a common conversation history, pass the same explicit context_key or instance:
    shared_memory = ConversationMemory(context_key="team_chat")
    AgentA = rt.agent_node("AgentA", llm=model, middleware=[shared_memory])
    AgentB = rt.agent_node("AgentB", llm=model, middleware=[shared_memory])
    
  • Seeding Prior History: To start a conversation from existing turns, give the instance an explicit context_key and set that key in the flow or session context. A default instance's generated key is not addressable from outside, so seeding always needs an explicit key:
    memory = ConversationMemory(context_key="team_chat")
    flow = rt.Flow("chat", entry_point=ChatAgent, context={"team_chat": prior_history})
    
  • Context Inspection After Flow Completion: flow.invoke() and flow.ainvoke() return only the flow's final result. To inspect context after an invocation finishes, use flow.connect(), which returns a FlowConnection:
    conn = flow.connect()
    result = await conn.ainvoke("Follow up question")
    
    # Inspect conversation history from the completed run's context:
    history = conn.context.get(memory.context_key)
    
  • Avoid Passing History Manually: Do not pass prior MessageHistory as user input when ConversationMemory is attached, as the middleware automatically accumulates and prepends history across turns.
  • Max Messages: Pass max_messages=10 to prune history to the most recent N messages and avoid exceeding model context windows. Both None (the default) and 0 mean no limit; a negative value raises ValueError.
  • One Conversation Per Instance: A memory instance models a single sequential conversation. Invoking the same instance concurrently (for example asyncio.gather over one agent) makes both turns read the same prior history, so the later write wins and the other turn is lost. Give each concurrent branch its own ConversationMemory, or, if the branches must share one history, put Lock outermost so the read and write are serialized:
    ChatAgent = rt.agent_node("Chat", llm=model, middleware=[Lock(), ConversationMemory()])
    
  • Clearing Memory: Call memory.clear() to wipe the stored history from both the instance and the active session context. Called after a run has finished there is no active context left to reach, so the instance copy is dropped but a FlowConnection still open on that run keeps reading the pre-clear value through conn.context.