Vector memory retrieves independent pieces of knowledge. A support case is more than one piece of knowledge.
It has a beginning, ordered actions, observations, a resolution, and an outcome. If those parts are flattened into one isolated sentence, the agent loses the shape of the experience.
This project stores that complete experience as episodic memory.
First: What Is Episodic Memory?
Episodic memory answers:
What happened in a similar past case?
The unit of memory is one complete episode:
issue
↓
actions taken in order
↓
observations returned
↓
resolution
↓
outcome
For this project, an episode is a handled customer-support case stored as a structured PostgreSQL row.
Vector Search Is the Mechanism, Not the Memory Type
Both vector and episodic memory use embeddings, but they store different things.
VECTOR MEMORY EPISODIC MEMORY
independent note complete case
"refunds take five days" CASE-101
issue
actions
observations
resolution
outcome
The episode summary is embedded for discovery. The full structured row remains the source of truth after retrieval.
query vector → candidate episode ID → complete PostgreSQL record
The Episode Has a Lifecycle
An episode begins open and becomes immutable when complete.
start_episode
↓
record_action + record_observation
↓
complete_episode
↓
build summary
↓
embed once
↓
atomic database completion
Completion requires at least one action and one observation. This prevents an empty case shell from being presented later as useful experience.
Completed episodes cannot be changed. That protects the historical record from silently drifting after it has informed later cases.
What the Project Stores
The main object keeps both structured events and terminal state:
@dataclass(frozen=True, slots=True)
class Episode:
id: str
episode_key: str
customer_id: str | None
issue_type: str
description: str
episode_summary: str | None
actions: tuple[EpisodeAction, ...]
observations: tuple[EpisodeObservation, ...]
resolution: str | None
outcome: EpisodeOutcome | None
status: EpisodeStatus
metadata: dict[str, JSONValue]
created_at: datetime
completed_at: datetime | None
Actions and observations have independent sequence numbers. PostgreSQL stores them as ordered JSONB while retaining fields that can be filtered relationally.
Observable Events, Not Hidden Reasoning
An action records what the system did:
sequence: 1
action_type: tool_call
tool_name: check_transaction
input_summary: transaction identifier
result_summary: transaction failed
An observation records what came back:
sequence: 1
source: check_transaction
content: transaction failed; amount deducted
important: true
The project stores tool calls, results, resolution, and outcome. It does not persist private chain-of-thought. Future retrieval needs evidence about what happened, not an unverifiable narrative of hidden reasoning.
One Summary, One Embedding
The project does not embed every action as it arrives. It waits until the episode reaches a terminal state, then builds one compact search representation:
parts = [
f"Issue: {episode.description}",
f"Issue type: {episode.issue_type}",
f"Actions: {action_text}",
f"Key observations: {observation_text}",
f"Resolution: {resolution}",
f"Outcome: {outcome.value}",
]
This separates two responsibilities:
episode_summary → semantic discovery
structured row → exact retrieved evidence
The summary can evolve as a search representation in a future migration without replacing the original actions and observations.
Outcomes Control Retrieval
Supported terminal outcomes are:
RESOLVED
ESCALATED
FAILED
ABANDONED
Default guidance search retrieves completed, resolved episodes only.
resolved → available as successful prior experience
failed → excluded unless explicitly requested for analysis
open → never retrieved as a completed example
That default prevents a failed attempt from quietly becoming recommended guidance. Analysts can still request failed episodes deliberately.
Why PostgreSQL and pgvector Share the Job
PostgreSQL stores the structured case and pgvector searches its embedding.
relational fields → exact IDs, filters, lifecycle
JSONB → ordered actions and observations
vector column → semantic candidate search
transaction → atomic completion
The schema also includes a partial HNSW cosine index for completed, resolved cases, matching the common retrieval path.
One operational boundary handles backup, migration, authorization, and deletion for both the record and its search representation.
What Actually Happened?
I ran the real EpisodicMemoryManager, PostgreSQL store, migrations, and pgvector search against the isolated Docker test database. Embeddings came from the deterministic provider used by the tests.
The trace stored one payment episode and one unrelated account episode, then searched for a charged-card checkout failure:
stored: CASE-001 completed resolved
actions: 1 observations: 1
summary first line: Issue: checkout payment failed and card was charged
Issue type: payment_failure
Actions: 1. tool_call via check_transaction → transaction failed
Key observations: 1. check_transaction: transaction failed; amount deducted
Resolution: Confirmed automatic refund
Outcome: resolved
1. CASE-001 score=0.6211
issue=checkout payment failed and card was charged
2. CASE-002 score=0.4596
issue=customer cannot update account profile address
stats: EpisodeStats(total=2, open=0, completed=2,
resolved=2, escalated=0,
failed=0, abandoned=0)
The vector found the candidate. The returned result still contained the complete case, including its exact action and observation.
How a Later Case Uses the Episode
Before handling a new issue, the application searches similar resolved cases:
new issue
↓
query embedding
↓
pgvector search
↓
complete past episodes
↓
current policy + past evidence + current tools
Past experience can suggest useful checks. It cannot prove that the new case has the same cause.
The agent must still use current tools for customer-specific status, amount, cause, and timeline.
The Trust Boundary Is Explicit
Past episodes are formatted as UNTRUSTED CONTEXT DATA: PAST SUPPORT EPISODES and passed as user-role context. Trusted support rules stay in the Responses API instructions field.
TRUSTED
current support policy
UNTRUSTED
current user issue
retrieved past cases
current tool results
The label matters because an episode may contain text originally supplied by a user or external system. Retrieval does not promote that text into policy.
Application Code Enforces Critical Rules
The model participates in the support loop, but code guards high-value invariants:
check_refundmust run beforeissue_refund- transaction, gateway, and refund checks must finish before resolution
- every handled case ends with
resolve_caseorescalate_case - the refund fixture is duplicate-safe
- current tool output supplies customer facts and ETA
The resulting episode records the actual enforced sequence rather than only the final natural-language answer.
Memory and No-Memory Become Comparable
The CLI supports a baseline:
/chat <issue>
/chat --no-memory <same issue>
Both paths use the same current policy and tools, and both record the actual new episode. The second path skips prior-episode retrieval.
This enables observable comparisons:
- which tools were selected
- in what order
- how many calls occurred
- whether the case resolved or escalated
- whether the final answer remained grounded
The comparison should not claim access to hidden reasoning.
What the Tests Prove
I started the repository's PostgreSQL/pgvector container and ran the complete integration suite:
43 passed in 0.53s
The tests cover lifecycle validation, JSONB ordering, summary and embedding timing, persistence, rollback, resolved-only retrieval, explicit failed-case retrieval, filters, ranking, top-k, query-versus-summary embeddings, tool safety, trust separation, and the complete agent loop.
These are real database tests with a deterministic embedding provider. They do not call OpenAI.
What Episodic Memory Cannot Decide
A successful historical case is evidence, not policy.
what happened before
≠
what must happen now
If many resolved episodes suggest a better process, that signal should enter offline analysis and review. It should not silently rewrite the operational procedure used by the agent.
That distinction leads to the final memory type: procedural memory.
What I Took Away
The valuable part of episodic memory is not merely storing a transcript. It preserves a complete, queryable experience with an explicit outcome.
search with a compact summary
retrieve the full structured episode
verify the current case independently
That structure makes the memory useful for later guidance, analysis, and evaluation while keeping current evidence authoritative.
Source Code
The complete project includes PostgreSQL migrations, pgvector search, structured episode lifecycle, deterministic support tools, Responses API orchestration, a Rich terminal UI, Docker setup, and integration tests.
GITHUB / SOURCE CODEExplore the episodic-memory implementationsriraman-10 / applied-ai-concepts / memory / 04-episodic-memory