# LangGraph Integration Guide

Use mcp-memory-service as the persistent memory backend for LangGraph agents and StateGraphs.

## Key Differentiator: Cross-Graph Shared Memory

LangGraph's built-in `MemorySaver` is **graph-local and ephemeral** — memory is lost between runs and cannot be shared between different StateGraphs.

mcp-memory-service provides **persistent shared memory** across all graphs, runs, and even separate processes:

```
Graph A (Researcher)  ──┐
                        ├──→ mcp-memory-service ←──→ All graphs share one store
Graph B (Writer)     ──┘
Graph C (Reviewer)  ──┘
```

## Setup

```bash
pip install mcp-memory-service httpx
MCP_ALLOW_ANONYMOUS_ACCESS=true memory server --http
```

## Memory Tools for ReAct Agents

Define memory as `@tool` functions for use in a ReAct agent:

```python
import httpx
from langchain_core.tools import tool

MEMORY_URL = "http://localhost:8000"

@tool
async def search_memory(query: str) -> str:
    """Search long-term memory for relevant context."""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{MEMORY_URL}/api/memories/search",
            json={"query": query, "limit": 5},
        )
        memories = response.json()["memories"]
        if not memories:
            return "No relevant memories found."
        return "\n".join(f"- {m['content']}" for m in memories)

@tool
async def store_memory(content: str, tags: list[str] = None) -> str:
    """Store a new memory for future retrieval."""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{MEMORY_URL}/api/memories",
            json={"content": content, "tags": tags or []},
        )
        result = response.json()
        return f"Stored memory: {result.get('content_hash', 'unknown')}"
```

Use in a ReAct agent:

```python
from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-sonnet-4-6")

agent = create_react_agent(
    llm,
    tools=[search_memory, store_memory],
    state_modifier="You have access to long-term memory. Search memory before answering. Store important findings.",
)
```

## Memory Node in StateGraph

Add explicit memory retrieve/store nodes to a StateGraph:

```python
import httpx
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_anthropic import ChatAnthropic

MEMORY_URL = "http://localhost:8000"

class AgentState(TypedDict):
    messages: Annotated[list, "message history"]
    memory_context: str
    agent_id: str


async def retrieve_memory_node(state: AgentState) -> dict:
    """Retrieve relevant memory before calling the LLM."""
    last_message = state["messages"][-1]
    query = last_message.content if hasattr(last_message, "content") else str(last_message)

    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{MEMORY_URL}/api/memories/search",
            json={
                "query": query,
                "limit": 5,
                "tags": [f"agent:{state['agent_id']}"],  # Scope to this agent
            },
        )
        memories = response.json().get("memories", [])

    if memories:
        context = "Relevant memory:\n" + "\n".join(f"- {m['content']}" for m in memories)
    else:
        context = ""

    return {"memory_context": context}


async def llm_node(state: AgentState) -> dict:
    """Call LLM with memory context injected into system message."""
    llm = ChatAnthropic(model="claude-sonnet-4-6")

    system_parts = ["You are a helpful assistant."]
    if state.get("memory_context"):
        system_parts.append(state["memory_context"])

    messages = [SystemMessage(content="\n\n".join(system_parts))] + state["messages"]
    response = await llm.ainvoke(messages)
    return {"messages": state["messages"] + [response]}


async def store_memory_node(state: AgentState) -> dict:
    """Store important information from LLM response."""
    last_response = state["messages"][-1]
    content = last_response.content if hasattr(last_response, "content") else str(last_response)

    async with httpx.AsyncClient() as client:
        await client.post(
            f"{MEMORY_URL}/api/memories",
            json={
                "content": content[:500],  # Store a summary
                "tags": [f"agent:{state['agent_id']}", "llm-response"],
                "memory_type": "observation",
            },
            headers={"X-Agent-ID": state["agent_id"]},
        )

    return {}


# Build the graph
graph = StateGraph(AgentState)
graph.add_node("retrieve_memory", retrieve_memory_node)
graph.add_node("llm", llm_node)
graph.add_node("store_memory", store_memory_node)

graph.set_entry_point("retrieve_memory")
graph.add_edge("retrieve_memory", "llm")
graph.add_edge("llm", "store_memory")
graph.add_edge("store_memory", END)

agent = graph.compile()
```

## Cross-Graph Memory Sharing

Two independent StateGraphs sharing the same memory store:

```python
# Researcher graph — tags memories with agent:researcher
researcher_result = await researcher_agent.ainvoke({
    "messages": [HumanMessage(content="Research API rate limits")],
    "memory_context": "",
    "agent_id": "researcher",
})

# Writer graph — retrieves memories from researcher
async with httpx.AsyncClient() as client:
    response = await client.post(
        f"{MEMORY_URL}/api/memories/search",
        json={
            "query": "API rate limits",
            "tags": ["agent:researcher"],  # Read from researcher's memory
        },
    )
    shared_context = response.json()["memories"]

writer_result = await writer_agent.ainvoke({
    "messages": [HumanMessage(content="Write a summary of API limits")],
    "memory_context": "\n".join(m["content"] for m in shared_context),
    "agent_id": "writer",
})
```

## Incremental Conversation Summaries

Use `conversation_id` to store incremental summaries without semantic deduplication:

```python
import uuid

conversation_id = str(uuid.uuid4())

async def store_turn_summary(turn: int, summary: str):
    async with httpx.AsyncClient() as client:
        await client.post(
            f"{MEMORY_URL}/api/memories",
            json={
                "content": f"Turn {turn}: {summary}",
                "tags": ["conversation-summary"],
                "conversation_id": conversation_id,  # Bypasses dedup for this convo
                "memory_type": "note",
            },
        )
```
