DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Give Your AI Assistant Long-Term Memory With perag
  • Persistent Memory for AI Agents Using LangChain's Deep Agents
  • How to Build and Optimize AI Models for Real-World Applications
  • Stateful AI: Streaming Long-Term Agent Memory With Amazon Kinesis

Trending

  • Spec-Driven Development Renamed an Old Problem; It Didn't Solve It
  • How to Build a Solid Test Pipeline in the Era of Agentic AI Development
  • The Rise of Agentic SRE: Humans, Agents, and Reliability
  • Why MCP Servers Lose Session State Behind Load Balancers
  1. DZone
  2. Data Engineering
  3. AI/ML
  4. Building On-Call Agents That Learn Using LangChain Memory

Building On-Call Agents That Learn Using LangChain Memory

On-call agents forget everything between incidents. Here's a three-layer memory architecture: episodic, semantic, and working, with real LangChain code.

By 
Prakshal Doshi user avatar
Prakshal Doshi
·
Jul. 28, 26 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
107 Views

Join the DZone community and get the full member experience.

Join For Free

Every on-call AI agent has the same dirty secret: it starts each incident completely blank. It doesn't remember that the payments service crashed last Tuesday for the same reason. It doesn't know your team always checks the database connection pool first. It doesn't recall that rollbacks on this service take 20 minutes, not 5.

Humans build this knowledge over months of being paged at 2 a.m. Current agents throw it away at the end of every session.

That's the long-horizon memory problem. And it's the gap between an agent that's useful in a demo and one that actually gets better over time.

The Three Memory Types You Actually Need

Research on agentic memory converges on three distinct types, each doing a different job. Skip one and your agent has a specific failure mode.

Episodic memory: What happened in past incidents. The raw "we saw this before" recall.

Semantic memory: What the agent has learned from those episodes. Distilled patterns, not raw events.

Working memory: What the agent is actively tracking in the current incident. Scoped, temporary, cleared when the incident closes.

Most implementations conflate all three into one context window. That's why they degrade. Episodic recall bloats the context, semantic patterns get buried, and the agent loses track of the current incident in the noise.

The Architecture

The memory router is the key piece most implementations miss. It decides what from episodic and semantic stores is actually relevant to the current incident, not everything, just what's useful right now.

Implementation

1. Working Memory, Current Incident Context

Working memory is the simplest piece. It tracks the active incident state and gets cleared when the incident resolves.

Python
 
from langchain.memory import ConversationBufferWindowMemory
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional

@dataclass
class WorkingMemory:
    incident_id: str
    service: str
    started_at: datetime
    observations: list[str] = field(default_factory=list)
    actions_taken: list[str] = field(default_factory=list)
    current_hypothesis: Optional[str] = None

    def add_observation(self, obs: str):
        self.observations.append(f"[{datetime.utcnow().isoformat()}] {obs}")

    def add_action(self, action: str, result: str):
        self.actions_taken.append(f"{action} -> {result}")

    def to_prompt_context(self) -> str:
        return f"""
        Current Incident: {self.incident_id}
        Service: {self.service}
        Running for: {(datetime.utcnow() - self.started_at).seconds // 60} minutes
        Observations so far: {chr(10).join(self.observations[-5:])}
        Actions taken: {chr(10).join(self.actions_taken[-3:])}
        Current hypothesis: {self.current_hypothesis or "None yet"}
        """

    def close(self) -> dict:
        """Serialize for episodic storage when incident resolves."""
        return {
            "incident_id": self.incident_id,
            "service": self.service,
            "duration_minutes": (datetime.utcnow() - self.started_at).seconds // 60,
            "observations": self.observations,
            "actions_taken": self.actions_taken,
            "resolved_hypothesis": self.current_hypothesis
        }

The close() method is the handoff point. When the incident resolves, working memory gets serialized into episodic storage. That's how the agent accumulates experience over time.

2. Episodic Memory, Past Incident Store

Episodic memory stores resolved incidents as structured records. The agent can search them by service, error signature, or time window.

Python
 
from langchain.memory import ConversationSummaryBufferMemory
from langchain_core.language_models import BaseChatModel
import json
from pathlib import Path

class EpisodicMemoryStore:
    def __init__(self, store_path: str, llm: BaseChatModel):
        self.store_path = Path(store_path)
        self.store_path.mkdir(exist_ok=True)
        self.llm = llm

    def save_incident(self, incident: dict):
        """Save a resolved incident to episodic store."""
        path = self.store_path / f"{incident['incident_id']}.json"
        path.write_text(json.dumps(incident, indent=2))

    def recall_similar(self, service: str, error_hint: str, limit: int = 3) -> list[dict]:
        """Retrieve past incidents for the same service."""
        incidents = []
        for f in self.store_path.glob("*.json"):
            data = json.loads(f.read_text())
            if data.get("service") == service:
                incidents.append(data)

        # Sort by recency and cap results
        incidents.sort(
            key=lambda x: x.get("duration_minutes", 999)
        )
        return incidents[:limit]

    def summarize_for_prompt(self, incidents: list[dict]) -> str:
        """Compress past incidents into a prompt-friendly summary."""
        if not incidents:
            return "No past incidents found for this service."

        raw = json.dumps(incidents, indent=2)
        summary = self.llm.invoke(f"""
        Summarize these past incidents for an on-call agent.
        Focus on: what triggered each incident, what actions resolved it,
        and how long it took. Be concise - 3-4 bullet points max.

        Incidents:
        {raw}
        """)
        return summary.content

The summarize_for_prompt method is doing real work here. Raw incident JSON dumped into a prompt bloats context fast. Summarizing it keeps the episodic recall tight and relevant.

3. Semantic Memory, Learned Patterns

Semantic memory is where the agent stores what it has learned from episodes - not raw events, but distilled patterns. Think of it as the runbook that writes itself.

Python
 
from langchain.memory import ConversationSummaryMemory

class SemanticMemoryStore:
    def __init__(self, llm: BaseChatModel):
        self.llm = llm
        self.patterns: dict[str, list[str]] = {}  # service -> learned patterns

    def extract_and_store(self, incident: dict):
        """Extract a reusable pattern from a resolved incident."""
        service = incident["service"]

        pattern_prompt = f"""
        An on-call agent just resolved this incident:
        Service: {service}
        Actions that worked: {incident.get("actions_taken", [])}
        Root cause: {incident.get("resolved_hypothesis", "unknown")}
        Duration: {incident.get("duration_minutes")} minutes

        Extract ONE reusable pattern for future incidents on this service.
        Format: "When [symptom], check [thing] because [reason]. Fix: [action]."
        One sentence only.
        """
        pattern = self.llm.invoke(pattern_prompt).content.strip()

        if service not in self.patterns:
            self.patterns[service] = []
        self.patterns[service].append(pattern)

        # Cap patterns per service to avoid bloat
        self.patterns[service] = self.patterns[service][-10:]

    def get_patterns(self, service: str) -> str:
        patterns = self.patterns.get(service, [])
        if not patterns:
            return "No learned patterns yet for this service."
        return "Learned patterns for this service:\n" + "\n".join(
            f"- {p}" for p in patterns
        )

The cap at 10 patterns per service is intentional. Unlimited pattern accumulation is its own problem. Old patterns from a different version of the service become noise. Trim aggressively.

4. The Memory Router

This is the piece that ties everything together. Before the agent starts working on an incident, the router assembles what's actually relevant from all three stores.

Python
 
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.language_models import BaseChatModel

class MemoryRouter:
    def __init__(
        self,
        episodic: EpisodicMemoryStore,
        semantic: SemanticMemoryStore,
        llm: BaseChatModel
    ):
        self.episodic = episodic
        self.semantic = semantic
        self.llm = llm

    def assemble_context(
        self,
        working: WorkingMemory,
        error_hint: str
    ) -> str:
        # Pull relevant episodic memories
        past_incidents = self.episodic.recall_similar(
            service=working.service,
            error_hint=error_hint,
            limit=3
        )
        episodic_summary = self.episodic.summarize_for_prompt(past_incidents)

        # Pull learned patterns
        semantic_patterns = self.semantic.get_patterns(working.service)

        # Assemble the full memory context for the agent prompt
        return f"""
        === MEMORY CONTEXT ===

        {working.to_prompt_context()}

        Past incidents on {working.service}:
        {episodic_summary}

        {semantic_patterns}

        === END MEMORY CONTEXT ===
        Use the above context to inform your investigation. 
        Prioritize patterns that match the current symptoms.
        Do not repeat actions that failed in past incidents unless you have new information.
        """

    def on_incident_close(self, working: WorkingMemory):
        """Called when incident resolves - persist to episodic and semantic stores."""
        record = working.close()
        self.episodic.save_incident(record)
        self.semantic.extract_and_store(record)

The last instruction in the assembled context - "do not repeat actions that failed in past incidents" - is subtle but important. Without it, the agent will try the same failed fix it tried last time, because that's what its training data says to try for this type of error.

5. Wiring It Into the Agent

Python
 
from langchain.agents import AgentExecutor, create_react_agent
from langchain_anthropic import ChatAnthropic

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

# Initialize stores
episodic_store = EpisodicMemoryStore(store_path="./incident_memory", llm=llm)
semantic_store = SemanticMemoryStore(llm=llm)
router = MemoryRouter(episodic_store, semantic_store, llm)

def handle_incident(incident_id: str, service: str, alert: str):
    # Open working memory for this incident
    working = WorkingMemory(
        incident_id=incident_id,
        service=service,
        started_at=datetime.utcnow()
    )

    # Assemble memory context
    memory_context = router.assemble_context(working, error_hint=alert)

    # Build the agent prompt with memory injected
    full_prompt = f"""
    You are an on-call SRE agent. Use your memory context to investigate and resolve this incident.

    {memory_context}

    Current alert: {alert}

    Investigate and resolve this incident. If you recognize a pattern from past incidents, try that resolution first.
    """

    agent = create_react_agent(llm, tools, prompt_template)
    executor = AgentExecutor(
        agent=agent,
        tools=tools,
        max_iterations=10,
        max_execution_time=120,
        handle_parsing_errors=True
    )

    try:
        result = executor.invoke({"input": full_prompt})
        working.current_hypothesis = result["output"]
    finally:
        # Always persist - even if agent errored, save what we observed
        router.on_incident_close(working)

    return result

The finally block matters. If the agent errors out mid-investigation, you still want to save whatever observations it made. The next incident benefits from partial information.

What This Looks Like After 30 Incidents

After the agent handles 30 incidents on the payments service, the semantic store might look like:

Plain Text
 
Learned patterns for payments-api:
- When connection timeout > 5s, check RDS connection pool because pool exhaustion
  causes cascade. Fix: increase max_connections in parameter group.
- When error rate spikes post-deploy, check feature flags because incomplete
  rollouts leave partial code paths active. Fix: disable new flag, redeploy.
- When latency p99 > 2s but p50 normal, check for slow queries in aurora logs
  because outlier queries drag tail latency. Fix: identify and kill long-running queries.

That's a runbook that wrote itself from real incidents. The agent surfaces these patterns before starting its investigation - not as hard rules, but as "try this first" signals.

What This Doesn't Solve

Episodic memory is only as good as the incidents that feed it. If the agent consistently misdiagnoses root causes, it stores bad patterns and compounds the error. You need a feedback loop - human confirmation of root cause before a pattern gets persisted.

The in-memory semantic store used here doesn't survive process restarts. For production, back it with a simple key-value store or SQLite. The pattern extraction logic stays the same.

And context windows are still finite. Even with summarization, a service with hundreds of incidents needs smarter retrieval than recency sorting. That's where vector search earns its place - but that's a separate article.

Research project context: This work was developed as a personal research project, not affiliated with any employer. If you're building incident memory for on-call agents and have hit the pattern-decay problem specifically, drop it in the comments.

AI Build (game engine) Memory (storage engine)

Opinions expressed by DZone contributors are their own.

Related

  • Give Your AI Assistant Long-Term Memory With perag
  • Persistent Memory for AI Agents Using LangChain's Deep Agents
  • How to Build and Optimize AI Models for Real-World Applications
  • Stateful AI: Streaming Long-Term Agent Memory With Amazon Kinesis

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook