An Elixir implementation of task-agnostic agentic memory for LLM agents, based on the PlugMem architecture.
Mnemosyne structures raw agent interactions into a knowledge-centric memory graph, transforming verbose episodic traces into compact, reusable knowledge that any LLM agent can query at decision time.
Add mnemosyne to your list of dependencies in mix.exs:
def deps do
[
{:mnemosyne, github: "edlontech/mnemosyne"}
]
end- Episodic memory -- detailed records of experience (observation-action pairs)
- Semantic memory -- propositional knowledge ("knowing that"), factual statements distilled from episodes
- Procedural memory -- prescriptive knowledge ("knowing how"), goal-directed action strategies
Mnemosyne models memory as a three-stage pipeline:
The agent interacts with the world through sessions. Each session collects observation-action pairs into episodes, and uses LLM inference to annotate each step with subgoals, rewards, and state summaries.
When an episode closes, the structuring pipeline extracts knowledge in parallel:
- Semantic extraction -- distills factual propositions with confidence scores
- Procedural extraction -- abstracts reusable instructions with conditions and expected outcomes
- Return computation -- evaluates trajectory quality via cumulative reward signals
- Sibling linking -- creates pairwise links between semantic nodes from the same trajectory, preserving co-occurrence structure
Trajectory boundaries are detected automatically using embedding similarity (cosine threshold), splitting episodes into coherent subsequences that share a common intent.
All extracted knowledge lives in a graph managed by a pluggable GraphBackend, with seven node types:
| Node Type | Purpose |
|---|---|
| Episodic | Raw observation-action-reward tuples from interactions |
| Semantic | Factual propositions with confidence scores |
| Procedural | Instructions with conditions and expected outcomes |
| Subgoal | Decomposed objectives linking related knowledge |
| Source | Provenance links back to original episode steps |
| Intent | Goal abstractions linking related procedural nodes |
| Tag | Concept indices linking related semantic nodes |
Nodes are linked bidirectionally and indexed by type, tag, and subgoal for efficient traversal. Mutations are batched through changesets that are applied atomically.
When the agent needs memory, the retrieval pipeline:
- Computes an embedding for the query
- Scores candidate nodes using a value function that combines cosine relevance with node metadata (recency, access frequency, reward quality) via a multiplicative formula
- Returns the highest-scoring knowledge, ranked by decision relevance
Two standalone operations keep the graph clean over time:
- Semantic consolidation -- discovers near-duplicate semantic nodes that share tag-neighbors, compares their embeddings, and deletes the lower-scored duplicate when similarity exceeds a threshold
- Node decay -- scores all nodes on recency, access frequency, and reward quality (without a query), pruning those below a threshold and cleaning up orphaned Tags/Intents
Both are triggered explicitly via Mnemosyne.consolidate_semantics/2 and Mnemosyne.decay_nodes/2.
The Mnemosyne.Notifier behaviour receives events whenever the graph changes -- changeset applications, node deletions, decay/consolidation results, recall queries, and session state transitions. Plug in a Phoenix.PubSub adapter to build live graph visualizations without any Phoenix dependency in Mnemosyne itself.
Four query functions (get_node, get_nodes_by_type, get_metadata, get_linked_nodes) complement the event stream, letting consumers fetch current node state on demand.
See the Notifier guide for implementation details and a LiveView example.
graph TD
Agent[Your Agent] -->|open_repo / start_session / recall| API[Mnemosyne API]
API --> RepoSup[RepoSupervisor\nDynamicSupervisor]
RepoSup --> Store1[MemoryStore\nProject A]
RepoSup --> Store2[MemoryStore\nProject B]
API --> Session[Session\nGenStateMachine]
Store1 --> Backend1[GraphBackend]
Store2 --> Backend2[GraphBackend]
API --> Retrieval[Retrieval\nPipeline]
Retrieval --> VF[Value Function\nrelevance * recency * frequency * reward]
Retrieval -->|find_candidates\nget_linked_nodes| Backend1
Session --> Episode[Episode Pipeline]
Episode --> Structuring
subgraph Structuring [Parallel Extraction]
Semantic[Semantic\nExtraction]
Procedural[Procedural\nExtraction]
Returns[Return\nComputation]
end
Structuring -->|Changeset| Backend1
VF --> Backend1
Mnemosyne supports multiple isolated graph repositories under a single supervision tree. Each repository has its own MemoryStore process and GraphBackend instance, identified by an opaque string ID. Shared configuration (LLM, embedding adapters) is set once at supervisor startup; per-repo backend config is provided when opening a repo.
The Session is a GenStateMachine that manages the episode lifecycle:
stateDiagram-v2
[*] --> idle
idle --> collecting : start_episode
collecting --> extracting : close
extracting --> ready : extraction success
extracting --> failed : extraction error
ready --> idle : commit
failed --> extracting : commit (retry)
failed --> idle : discard
ready --> idle : discard
# Open an isolated graph repository for a project
{:ok, _pid} = Mnemosyne.open_repo("my-project",
backend: {Mnemosyne.GraphBackends.InMemory,
persistence: {Mnemosyne.GraphBackends.Persistence.DETS, path: "my-project.dets"}})
# Start a session with a goal, tied to a repo
{:ok, session} = Mnemosyne.start_session("Help user plan a trip", repo: "my-project")
# Collect observations and actions during interaction
:ok = Mnemosyne.append(session, "User says they want to visit Tokyo", "Asking about dates")
:ok = Mnemosyne.append(session, "User says next March for 2 weeks", "Suggesting itinerary")
# Close the episode and commit knowledge to the graph
:ok = Mnemosyne.close_and_commit(session)
# Later, recall relevant knowledge for a new decision
{:ok, memories} = Mnemosyne.recall("my-project", "What are the user's travel preferences?")
# List all open repos, close when done
["my-project"] = Mnemosyne.list_repos()
:ok = Mnemosyne.close_repo("my-project")Mnemosyne uses pluggable adapters for LLM, embedding, and graph storage backends. Shared config is provided when starting the supervisor; backend config is per-repo.
# Add Mnemosyne to your supervision tree
children = [
{Mnemosyne.Supervisor,
config: %Mnemosyne.Config{
llm: %{model: "gpt-4o-mini", opts: %{}},
embedding: %{model: "text-embedding-3-small", opts: %{}}
},
llm: MyApp.LLMAdapter,
embedding: MyApp.EmbeddingAdapter}
]The GraphBackend behaviour abstracts both persistence and querying behind a single interface. The built-in InMemory backend stores nodes in an Erlang map with optional DETS persistence:
# In-memory only (no persistence, useful for tests)
backend: {Mnemosyne.GraphBackends.InMemory, []}
# With DETS persistence
backend: {Mnemosyne.GraphBackends.InMemory,
persistence: {Mnemosyne.GraphBackends.Persistence.DETS, path: "memory.dets"}}Custom backends can push queries to external databases (e.g. Postgres with pgvector) by implementing the Mnemosyne.GraphBackend behaviour
The built-in adapters wrap Sycophant for LLM calls and support Bumblebee for local embeddings.
Per-pipeline-step model overrides are supported via config.overrides[step_atom], so you can use a cheaper model for subgoal inference and a stronger one for knowledge extraction.
Mnemosyne is under active development. The structuring and session management layers are functional. Retrieval and reasoning modules are partially implemented.
This project is an Elixir implementation inspired by PlugMem: A Task-Agnostic Plugin Memory Module for LLM Agents by Ke Yang et al.