Mnemosyne (mnemosyne v0.1.6)

Copy Markdown View Source

Agentic memory library that models memory as a knowledge graph using reinforcement-learning primitives (episodes, trajectories, rewards, value functions).

Architecture

Mnemosyne is organized in three layers:

  1. Data Primitives - An in-memory knowledge graph with typed nodes (Episodic, Semantic, Procedural, Subgoal, Source, Tag) connected by directed links. Mutations happen through Changeset structs.

  2. Pipeline - LLM-driven extraction that turns raw observation-action sequences into structured knowledge. Episodes track steps, detect trajectory boundaries via embedding similarity, and produce changesets that grow the graph.

  3. Retrieval - Value-function-scored retrieval over the graph, combining multiple node types to produce contextually relevant memory results.

Repositories

All graph operations are scoped to a repository. A repository is an isolated graph backend with its own MemoryStore process. Open a repo via open_repo/2, then pass its repo_id as the first argument to all operations.

{:ok, _pid} = Mnemosyne.open_repo("my-repo", backend: {InMemory, persistence: {DETS, path: "repo.dets"}})

Write Path (Sessions)

Sessions are the write interface to the knowledge graph. A session is tied to a specific repo and collects observation-action pairs, groups them into trajectories, and uses LLM calls to extract semantic and procedural knowledge.

{:ok, session_id} = Mnemosyne.start_session("Learn Elixir patterns", repo: "my-repo")
:ok = Mnemosyne.append(session_id, "Read about GenServer", "Implemented a cache")
:ok = Mnemosyne.append(session_id, "Cache worked well", "Added TTL support")
:ok = Mnemosyne.close_and_commit(session_id)

Sessions follow a state machine lifecycle: :idle -> :collecting -> :extracting -> :ready -> (committed/discarded)

The :extracting state runs asynchronously under a Task.Supervisor, keeping the session process responsive. If extraction fails, the session moves to :failed and preserves the episode for retry.

Read Path (Recall)

Recall queries the knowledge graph using value functions to score and rank nodes by relevance. Session context can augment queries with the current episode's state for more targeted retrieval.

{:ok, memories} = Mnemosyne.recall("my-repo", "How to implement caching?")
{:ok, memories} = Mnemosyne.recall_in_context("my-repo", session_id, "What did I try before?")

Graph Management

Direct graph operations for inspection and bulk mutations:

graph = Mnemosyne.get_graph("my-repo")
:ok = Mnemosyne.apply_changeset("my-repo", changeset)
:ok = Mnemosyne.delete_nodes("my-repo", ["node-1", "node-2"])

Supervision

Mnemosyne runs under its own supervision tree (Mnemosyne.Supervisor). Multiple independent instances can coexist by passing a custom :supervisor name in opts. Each supervisor owns its own Registry, RepoRegistry, TaskSupervisor, RepoSupervisor, and SessionSupervisor.

Summary

Functions

Appends an observation-action pair to the current episode.

Like append/4 but returns immediately. Accepts an optional callback that receives :ok or {:error, reason} when the append finishes.

Applies a changeset to the knowledge graph asynchronously.

Closes the current episode, triggering asynchronous knowledge extraction.

Closes the episode, waits for extraction to complete, and commits the result.

Like close/2 but returns immediately with optional callback. See commit_async/3 for queuing semantics.

Closes a running memory repository.

Commits the extracted changeset to the MemoryStore.

Like commit/2 but returns immediately. When the session is busy (extracting or collecting with in-flight trajectory tasks), the commit is queued and executes when the blocking work completes.

Consolidates near-duplicate semantic nodes in the repo's graph asynchronously.

Prunes low-utility nodes from the repo's graph via decay scoring asynchronously.

Deletes nodes from the knowledge graph by their IDs asynchronously.

Discards the extraction result without committing to the knowledge graph.

Like discard/2 but returns immediately with optional callback. See commit_async/3 for queuing semantics.

Returns the current knowledge graph held by the repo's MemoryStore.

Fetches nodes linked to the given node IDs.

Fetches metadata for the given node IDs.

Fetches a single node by ID from the repo's graph.

Fetches all nodes of the given types from the repo's graph.

Fetches the most recently created memories from the repo, sorted newest first.

Lists all currently open repository IDs.

Opens a new memory repository under the supervision tree.

Retrieves relevant memories from the knowledge graph for the given query.

Retrieves memories using both the query and the session's current context.

Strips dangling link references and removes orphaned tags/intents from the repo's graph asynchronously.

Returns the current state of a session.

Like start_session/2 but for resuming an existing idle session with a new episode. Returns immediately with optional callback. See commit_async/3 for queuing semantics.

Starts a new memory session with the given goal.

Validates episodic grounding of abstract nodes in the repo's graph asynchronously.

Functions

append(session_id, observation, action, opts \\ [])

@spec append(String.t(), String.t(), String.t(), keyword()) ::
  :ok | {:error, Mnemosyne.Errors.error()}

Appends an observation-action pair to the current episode.

The observation describes what the agent perceived and the action describes what the agent did in response. Each pair becomes a step in the current trajectory. When the embedding similarity between consecutive observations drops below the threshold (0.75), a new trajectory boundary is detected automatically.

The session must be in the :collecting state.

Options

append_async(session_id, observation, action, callback \\ nil, opts \\ [])

@spec append_async(
  String.t(),
  String.t(),
  String.t(),
  (Mnemosyne.Session.append_result() -> any()) | nil,
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.error()}

Like append/4 but returns immediately. Accepts an optional callback that receives :ok or {:error, reason} when the append finishes.

Options

apply_changeset(repo_id, changeset, opts \\ [])

@spec apply_changeset(String.t(), Mnemosyne.Graph.Changeset.t(), keyword()) ::
  :ok | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}

Applies a changeset to the knowledge graph asynchronously.

Enqueues the changeset for application via the MemoryStore write lane. Returns immediately; the actual mutation happens in the background. Subscribe to Notifier events (:changeset_applied) to observe completion.

close(session_id, opts \\ [])

@spec close(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.error()}

Closes the current episode, triggering asynchronous knowledge extraction.

Moves the session from :collecting to :extracting. The extraction pipeline runs in a supervised task and processes each trajectory to extract semantic facts, procedural instructions, and compute returns. Once extraction completes, the session transitions to :ready (success) or :failed (extraction error).

Use commit/2 after extraction completes to persist the results, or close_and_commit/2 to do both in one call.

close_and_commit(session_id, opts \\ [])

@spec close_and_commit(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.error()}

Closes the episode, waits for extraction to complete, and commits the result.

Convenience function that combines close/2, polling for extraction completion, and commit/2 into a single blocking call. Handles transient extraction failures by retrying up to max_retries times.

Options

  • :max_retries - Number of retry attempts on transient extraction failures. Defaults to 2.
  • :max_polls - Maximum number of polling iterations while waiting for extraction. Defaults to 200.
  • :poll_interval - Milliseconds between polls. Defaults to 50.
  • :supervisor - Name of the Mnemosyne supervisor. Defaults to Mnemosyne.Supervisor.

Examples

:ok = Mnemosyne.close_and_commit(session_id)

:ok = Mnemosyne.close_and_commit(session_id, max_retries: 5, poll_interval: 100)

close_async(session_id, callback \\ nil, opts \\ [])

@spec close_async(String.t(), Mnemosyne.Session.op_callback(), keyword()) ::
  :ok | {:error, Mnemosyne.Errors.error()}

Like close/2 but returns immediately with optional callback. See commit_async/3 for queuing semantics.

Options

close_repo(repo_id, opts \\ [])

@spec close_repo(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}

Closes a running memory repository.

Terminates the MemoryStore process for the given repo_id.

Options

commit(session_id, opts \\ [])

@spec commit(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.error()}

Commits the extracted changeset to the MemoryStore.

Enqueues the knowledge graph changeset produced by the extraction pipeline for application to the repo's MemoryStore. The session must be in the :ready state. The changeset is applied asynchronously via the write lane; subscribe to Notifier events (:changeset_applied) to observe completion.

After committing, the session transitions back to :idle and can start a new episode.

commit_async(session_id, callback \\ nil, opts \\ [])

@spec commit_async(String.t(), Mnemosyne.Session.op_callback(), keyword()) ::
  :ok | {:error, Mnemosyne.Errors.error()}

Like commit/2 but returns immediately. When the session is busy (extracting or collecting with in-flight trajectory tasks), the commit is queued and executes when the blocking work completes.

The optional callback receives {:ok, :committed} or {:error, reason}. An :ok return means the operation was accepted, not that it succeeded.

Options

consolidate_semantics(repo_id, opts \\ [])

@spec consolidate_semantics(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}

Consolidates near-duplicate semantic nodes in the repo's graph asynchronously.

Discovers semantically similar nodes via tag-neighbor similarity and deletes the lower-scored one. Returns immediately; the consolidation runs in the background. Subscribe to Notifier events (:consolidation_completed) to observe results.

Options

decay_nodes(repo_id, opts \\ [])

@spec decay_nodes(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}

Prunes low-utility nodes from the repo's graph via decay scoring asynchronously.

Scores nodes on recency, frequency, and reward signals and removes those below the threshold. Cleans up orphaned Tags/Intents after deletion. Returns immediately; pruning runs in the background. Subscribe to Notifier events (:decay_completed) to observe results.

Options

delete_nodes(repo_id, node_ids, opts \\ [])

@spec delete_nodes(String.t(), [String.t()], keyword()) ::
  :ok | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}

Deletes nodes from the knowledge graph by their IDs asynchronously.

Enqueues the deletion via the MemoryStore write lane. Returns immediately; the actual removal happens in the background. Subscribe to Notifier events (:nodes_deleted) to observe completion.

discard(session_id, opts \\ [])

@spec discard(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.error()}

Discards the extraction result without committing to the knowledge graph.

Drops the changeset produced by extraction. Useful when the extracted knowledge is deemed low-quality or irrelevant. The session returns to :idle and can start a new episode.

discard_async(session_id, callback \\ nil, opts \\ [])

@spec discard_async(String.t(), Mnemosyne.Session.op_callback(), keyword()) ::
  :ok | {:error, Mnemosyne.Errors.error()}

Like discard/2 but returns immediately with optional callback. See commit_async/3 for queuing semantics.

Options

get_graph(repo_id, opts \\ [])

Returns the current knowledge graph held by the repo's MemoryStore.

The graph contains all committed nodes and their links. Useful for inspection, debugging, or building custom retrieval strategies.

get_linked_nodes(repo_id, node_ids, opts \\ [])

@spec get_linked_nodes(String.t(), [String.t()], keyword()) ::
  {:ok, [struct()]} | {:error, term()}

Fetches nodes linked to the given node IDs.

get_metadata(repo_id, node_ids, opts \\ [])

@spec get_metadata(String.t(), [String.t()], keyword()) ::
  {:ok, %{required(String.t()) => Mnemosyne.NodeMetadata.t()}}
  | {:error, term()}

Fetches metadata for the given node IDs.

get_node(repo_id, node_id, opts \\ [])

@spec get_node(String.t(), String.t(), keyword()) ::
  {:ok, struct() | nil} | {:error, term()}

Fetches a single node by ID from the repo's graph.

get_nodes_by_type(repo_id, types, opts \\ [])

@spec get_nodes_by_type(String.t(), [atom()], keyword()) ::
  {:ok, [struct()]} | {:error, term()}

Fetches all nodes of the given types from the repo's graph.

latest(repo_id, top_k, opts \\ [])

@spec latest(String.t(), pos_integer(), keyword()) ::
  {:ok, [{struct(), Mnemosyne.NodeMetadata.t()}]} | {:error, term()}

Fetches the most recently created memories from the repo, sorted newest first.

Returns up to top_k nodes paired with their metadata. By default fetches semantic and procedural nodes.

Options

  • :types - Node types to fetch. Defaults to [:semantic, :procedural].
  • :supervisor - Name of the Mnemosyne supervisor. Defaults to Mnemosyne.Supervisor.

Examples

{:ok, memories} = Mnemosyne.latest("my-repo", 10)
{:ok, memories} = Mnemosyne.latest("my-repo", 5, types: [:semantic])

list_repos(opts \\ [])

@spec list_repos(keyword()) :: [String.t()]

Lists all currently open repository IDs.

Options

open_repo(repo_id, opts \\ [])

@spec open_repo(
  String.t(),
  keyword()
) :: {:ok, pid()} | {:error, Mnemosyne.Errors.error()}

Opens a new memory repository under the supervision tree.

Starts a MemoryStore process registered in the RepoRegistry with the given repo_id. Each repo has its own isolated graph backend.

Options

  • :backend - Required. A {module, opts} tuple for the graph backend.
  • :supervisor - Name of the Mnemosyne supervisor. Defaults to Mnemosyne.Supervisor.
  • :config - A Mnemosyne.Config struct overriding shared defaults.
  • :llm - LLM adapter module overriding shared defaults.
  • :embedding - Embedding adapter module overriding shared defaults.

recall(repo_id, query, opts \\ [])

@spec recall(String.t(), String.t(), keyword()) ::
  {:ok, Mnemosyne.Pipeline.RecallResult.t()}
  | {:error, Mnemosyne.Errors.error()}

Retrieves relevant memories from the knowledge graph for the given query.

Runs the retrieval pipeline, which computes embeddings for the query and scores candidate nodes using value functions across all node types (episodic, semantic, procedural, subgoal, tag, source). Results are ranked and filtered by relevance.

Options

Examples

{:ok, memories} = Mnemosyne.recall("my-repo", "How to handle GenServer timeouts?")

recall_in_context(repo_id, session_id, query, opts \\ [])

@spec recall_in_context(String.t(), String.t(), String.t(), keyword()) ::
  {:ok, Mnemosyne.Pipeline.RecallResult.t()}
  | {:error, Mnemosyne.Errors.error()}

Retrieves memories using both the query and the session's current context.

Augments the query with the active episode's state (current subgoal, recent observations) to produce more contextually relevant results. If the session is not found, falls back to a plain recall/3.

Examples

{:ok, memories} = Mnemosyne.recall_in_context("my-repo", session_id, "What patterns apply here?")

repair_graph(repo_id, opts \\ [])

@spec repair_graph(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}

Strips dangling link references and removes orphaned tags/intents from the repo's graph asynchronously.

Use this after upgrading from a release whose persistence layer left back-references behind on delete, or any time the graph is suspected to carry stale link IDs. Returns immediately; repair runs in the background. Subscribe to Notifier events (:repair_completed) to observe results.

Options

session_state(session_id, opts \\ [])

Returns the current state of a session.

Possible states: :idle, :collecting, :extracting, :ready, :failed.

Returns {:error, NotFoundError} if the session ID is not registered.

start_episode_async(session_id, goal, callback \\ nil, opts \\ [])

@spec start_episode_async(
  String.t(),
  String.t(),
  Mnemosyne.Session.op_callback(),
  keyword()
) ::
  :ok | {:error, Mnemosyne.Errors.error()}

Like start_session/2 but for resuming an existing idle session with a new episode. Returns immediately with optional callback. See commit_async/3 for queuing semantics.

Options

start_session(goal, opts \\ [])

@spec start_session(
  String.t(),
  keyword()
) :: {:ok, String.t()} | {:error, term()}

Starts a new memory session with the given goal.

Creates a new Session process under the SessionSupervisor and immediately opens an episode with the provided goal. The session begins in the :collecting state, ready to receive observation-action pairs via append/4.

LLM, embedding, and config defaults are pulled from the repo's MemoryStore unless explicitly overridden in opts.

Options

  • :repo - Required. The repo ID to bind this session to.
  • :supervisor - Name of the Mnemosyne supervisor to use. Defaults to Mnemosyne.Supervisor.
  • :config - A Mnemosyne.Config struct overriding the stored defaults.
  • :llm - LLM adapter module overriding the stored default.
  • :embedding - Embedding adapter module overriding the stored default.

Examples

{:ok, session_id} = Mnemosyne.start_session("Explore caching strategies", repo: "my-repo")

validate_episodic(repo_id, opts \\ [])

@spec validate_episodic(
  String.t(),
  keyword()
) :: :ok | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}

Validates episodic grounding of abstract nodes in the repo's graph asynchronously.

Walks provenance chains from semantic/procedural nodes to source nodes and penalizes nodes whose source embeddings diverge from the abstract node's embedding. Returns immediately; validation runs in the background.