# `Mnemosyne`
[🔗](https://github.com/edlontech/mnemosyne/blob/main/lib/mnemosyne.ex#L1)

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.

# `append`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `append_async`

```elixir
@spec append_async(
  String.t(),
  String.t(),
  String.t(),
  (Mnemosyne.Session.append_result() -&gt; 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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `apply_changeset`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `close_repo`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `commit`

```elixir
@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`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `consolidate_semantics`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `decay_nodes`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `delete_nodes`

```elixir
@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`

```elixir
@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`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `get_graph`

```elixir
@spec get_graph(
  String.t(),
  keyword()
) ::
  Mnemosyne.Graph.t() | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}
```

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`

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

Fetches nodes linked to the given node IDs.

# `get_metadata`

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

Fetches metadata for the given node IDs.

# `get_node`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Lists all currently open repository IDs.

## Options

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `open_repo`

```elixir
@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`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

## Examples

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

# `recall_in_context`

```elixir
@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`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `session_state`

```elixir
@spec session_state(
  String.t(),
  keyword()
) ::
  Mnemosyne.Session.state()
  | {:error, Mnemosyne.Errors.Framework.NotFoundError.t()}
```

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`

```elixir
@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

  * `:supervisor` - Name of the Mnemosyne supervisor. Defaults to `Mnemosyne.Supervisor`.

# `start_session`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
