Episodic Memory
EpisodicMemory is a durable log of specific past runs — what was asked, what the agent answered, which tools it used, and how well it went — with similarity + recency recall of relevant prior experience before a new run. It is the third memory concept, and the one that keeps the specifics.
| Type | API | Keeps |
|---|---|---|
| Semantic | Agent.memory | raw key/value facts (tool results) |
| Procedural | ProceduralMemory | distilled how-to rules (helpful / harmful bullets) |
| Episodic | EpisodicMemory | the actual past runs (query, answer, outcome, score) |
Like ProceduralMemory, it layers on any MemoryProvider as a ring buffer of JSON episodes (episodic:{namespace}:{i} + a __meta__ key).
The automatic cycle
Set Agent.episodic_memory and both halves happen on their own.
from deepcrew import Agent, EpisodicMemory, run_agent
from deepcrew.memory import FileMemoryProvider
episodic = EpisodicMemory(FileMemoryProvider("episodes.json"), namespace="support-bot")
agent = Agent(
name="assistant",
model="gemini/gemini-3.5-flash-lite",
episodic_memory=episodic,
)
# run 1 - nothing to recall; the run is recorded afterwards
await run_agent(agent, [{"role": "user", "content": "How do I rotate an API key?"}])
# run 2 - the earlier episode is recalled into context, then this run is recorded too
r = await run_agent(agent, [{"role": "user", "content": "Is key rotation zero-downtime?"}])
print(r.episodes_recalled) # >= 1
- Recall happens in the runner before the LLM sees anything: relevant episodes are ranked and injected as a
## Relevant past experiencesystem block.AgentResult.episodes_recalledreports how many. - Record happens after the run. In a refinement loop, one episode is recorded per iteration, with its
outcome/scoretaken from that iteration's verifier feedback.
Episode
@dataclass
class Episode:
query: str
response: str
outcome: Literal["success", "failure", "partial", "unknown"] = "unknown"
score: float | None = None
tools_used: list[str] = field(default_factory=list)
reflection: str | None = None # set when reflect_model is configured
tags: list[str] = field(default_factory=list)
timestamp: str = "" # ISO-8601 UTC, auto-filled
agent_id: str = ""
EpisodicMemory
recall() returns.Methods: await record(episode), await recall(query, *, top_k=None) (Jaccard keyword overlap + recency tie-breaker; no-overlap episodes are not returned; dependency-free), render(episodes), await reflect(episode), await clear().
Optional LLM reflection
episodic = EpisodicMemory(backend, reflect_model="gemini/gemini-3.5-flash-lite")
Each recorded episode gets a one-sentence reflection. render() prefers the reflection over the raw response, so recall stays compact. Off by default — no extra call unless you opt in. Reflection failures are swallowed (the episode is still recorded, reflection=None). The same option exists on ProceduralMemory(reflect_model=...) for its curation step.
Events
EventType.EPISODE_RECALLED ({count}) and EPISODE_RECORDED ({outcome, score}) are emitted on the stream. Like MEMORY_STORE / MEMORY_RETRIEVE, they are not in the chat() or standard() StreamPolicy presets — use verbose() to see them.
Common pitfalls
- Recall needs topical overlap. A totally unrelated new query recalls nothing — that is intentional. Widen recall with
tagson your episodes. - The ring buffer overwrites. Past
max_episodes, the oldest episode is gone. Raisemax_episodesor prune a persistent backend deliberately. reflect_modelcosts one call per record. In a 5-iteration loop that is 5 calls. Leave it off unless the lesson quality is worth it.- Namespaces are manual. Two agents sharing a backend but not a
namespacekeep separate logs; give them the same one to share experience.