# Jido.SimpleMem
`jido_simplemem` is a developer-focused SimpleMem-style memory plugin and
runtime for [Jido](https://github.com/agentjido/jido) agents.
It is built around the same core ideas as upstream
[SimpleMem](https://github.com/aiming-lab/SimpleMem): buffered dialogue
ingestion, semantic compression, synthesis, and intent-aware retrieval. This
package adapts that model to the Jido plugin/action lifecycle and to an Elixir
runtime backed by [LanceDB](https://github.com/lancedb/lancedb). The default
runtime path uses
[jido_action](https://github.com/agentjido/jido_action),
[ReqLLM](https://github.com/agentjido/req_llm).
At a glance:
- `Jido.SimpleMem.Plugin` gives Jido agents passive memory hooks
- `Jido.SimpleMem` exposes the buffered lifecycle directly
- LanceDB is the only storage and indexing backend
- retrieval is hybrid and LLM-planned
## What This Package Is
Use this package when you want a Jido agent to:
- accumulate dialogue in windows instead of writing every turn immediately
- compress overlapping turns into standalone long-term memories
- ask grounded follow-up questions against stored memory
- inspect, explain, and delete stored memories directly
This package is not a generic multi-backend memory abstraction. It is a
single-tier, LanceDB-backed runtime with a SimpleMem-style workflow.
## Quick Start
The main integration path is the plugin.
```elixir
defmodule MyApp.MemoryAgent do
alias Jido.SimpleMem.Actions.{Finalize, PostTurn, PreTurn}
use Jido.Agent,
name: "memory_agent",
plugins: [
{Jido.SimpleMem.Plugin,
%{
window_size: 4,
overlap_size: 1
}}
]
def chat(agent, user_input) do
{agent, _} =
cmd(agent, {PreTurn, %{user_input: user_input, context_result_key: :memory_context}})
response = build_response(user_input, agent.state.memory_context)
{agent, _} =
cmd(agent, {PostTurn, %{user_input: user_input, assistant_response: response}})
{:ok, agent, response}
end
def flush(agent) do
{agent, _} = cmd(agent, {Finalize, %{}})
{:ok, agent}
end
defp build_response(_user_input, memory_context) do
if is_binary(memory_context) and memory_context != "" do
"I found relevant memory context."
else
"I don't have anything in memory yet."
end
end
end
```
Required environment:
```bash
export JIDO_SIMPLEMEM_LLM_MODEL="openai:gpt-5-mini"
export JIDO_SIMPLEMEM_EMBEDDING_MODEL="openai:text-embedding-3-small"
export OPENAI_API_KEY="..."
```
Embedding dimensions are inferred. Keep one embedding size per Lance store. If
you switch to an embedding model with a different vector size, clear the store
or re-embed the existing data first.
If you want a runnable example, see
[examples/simple_memory_agent.ex](examples/simple_memory_agent.ex)
and
[examples/simple_memory_demo.exs](examples/simple_memory_demo.exs).
## Public Surfaces
### Plugin
`Jido.SimpleMem.Plugin` exposes these actions:
- `pre_turn`
- `post_turn`
- `finalize`
- `ask`
- `get_all_memories`
- `delete_memory`
Recommended flow:
1. Call `pre_turn` before generating a response.
2. Add the returned memory context to your model prompt.
3. Generate the assistant response.
4. Call `post_turn` with the user input and assistant response.
5. Call `finalize` at session end or before switching `session_id`.
### Direct Runtime API
`Jido.SimpleMem` exposes the same lifecycle without going through a plugin:
- `add_dialogue/4`
- `add_dialogues/3`
- `finalize/2`
- `ask/3`
- `get_all_memories/2`
- `delete_memory/3`
- `explain/3`
Example:
```elixir
{:ok, _} =
Jido.SimpleMem.add_dialogues(agent, [
%{speaker: "user", content: "My name is Alice Chen"},
%{speaker: "user", content: "I live in Portland"},
%{speaker: "user", content: "I prefer concise answers"}
])
{:ok, _} = Jido.SimpleMem.finalize(agent)
{:ok, result} = Jido.SimpleMem.ask(agent, "Where does Alice Chen live?")
```
## How The Package Works
The default runtime is buffered and LLM-first:
1. Dialogue is appended to a persisted session buffer.
2. Once a window fills, the builder sends that window to the configured LLM.
3. The LLM extracts structured memory candidates.
4. A synthesis pass consolidates overlapping facts within the current session.
5. Extracted entries are normalized into `MemoryUnit` structs.
6. LanceDB persists those units and indexes them for semantic, lexical, and structured retrieval.
7. `ask/3` plans retrieval with the LLM, executes hybrid search, and can run reflection rounds.
8. `Answerer` produces a grounded answer from the selected records.
Important behavior:
- writes are buffered, not immediate
- `finalize` is still caller-controlled
- `tokens_before_finalize` is a helper, not a replacement for explicit flushes
- plugin auto-capture uses the same ingestion path as direct writes
- auto-capture failures are logged, emitted via telemetry, and returned as typed plugin errors
## Repository Layout
The repository is organized by subsystem:
- `lib/jido/simple_mem/domain`: core data structures such as `Dialogue`,
`MemoryUnit`, and embedding helpers
- `lib/jido/simple_mem/pipeline`: extraction, synthesis, planning, retrieval,
explanation, and answer generation
- `lib/jido/simple_mem/runtime`: shared runtime/config resolution,
supervision, and job handling
- `lib/jido/simple_mem/plugin`: plugin integration and Jido actions
- `lib/jido/simple_mem/store`: LanceDB storage adapter and Python worker bridge
- `test/support`: test fixtures, fake clients, and target builders
- `examples`: minimal agent/demo flows
## Storage And Runtime Notes
LanceDB is the only backend.
Because there is no official Elixir LanceDB SDK, the package runs a supervised
Python worker over `Port`. That worker is responsible for:
- durable memory storage
- durable session buffer storage
- vector search
- Tantivy-backed keyword search
- structured metadata filtering
Default store path:
```bash
export JIDO_SIMPLEMEM_LANCE_PATH=".jido/simplemem.lance"
```
The embedding dimension is pinned per store. You can switch embedding models,
but not between models with different vector sizes against the same Lance store
without clearing or re-embedding that store.
## Configuration
Common runtime settings:
- `window_size`
- `overlap_size`
- `enable_parallel_retrieval`
- `max_retrieval_workers`
- `retrieval_limit`
- `context_token_budget`
- `tokens_before_finalize`
- `reflection_enabled`
- `max_reflection_rounds`
- `session_id`
- `namespace`
- `store` and `store_opts`
- `llm_client` and `llm_client_opts`
- `embedding_client` and `embedding_client_opts`
Required environment variables:
- `JIDO_SIMPLEMEM_LLM_MODEL`
- `JIDO_SIMPLEMEM_EMBEDDING_MODEL`
- provider API key env vars for the configured models, such as `OPENAI_API_KEY`
Optional environment variables:
- `JIDO_SIMPLEMEM_EXTRACTION_MODEL`
- `JIDO_SIMPLEMEM_PLANNING_MODEL`
- `JIDO_SIMPLEMEM_SYNTHESIS_MODEL`
- `JIDO_SIMPLEMEM_ANSWER_MODEL`
- `JIDO_SIMPLEMEM_BASE_URL`
- `JIDO_SIMPLEMEM_API_KEY`
- `JIDO_SIMPLEMEM_RECEIVE_TIMEOUT_MS`
- `JIDO_SIMPLEMEM_POOL_TIMEOUT_MS`
- `JIDO_SIMPLEMEM_LANCE_PATH`
- `JIDO_SIMPLEMEM_PYTHON_EXECUTABLE`
- `JIDO_SIMPLEMEM_UV_EXECUTABLE`
- `JIDO_SIMPLEMEM_WORKER_START_TIMEOUT_MS`
- `JIDO_SIMPLEMEM_ENABLE_LIVE_LANCE_TESTS`
If stage-specific models are not set, they fall back to
`JIDO_SIMPLEMEM_LLM_MODEL`.
For a custom OpenAI-compatible endpoint:
```bash
export JIDO_SIMPLEMEM_BASE_URL="https://your-endpoint.example.com/v1"
export JIDO_SIMPLEMEM_API_KEY="..."
export JIDO_SIMPLEMEM_LLM_MODEL="openai/gpt-5-mini"
export JIDO_SIMPLEMEM_EMBEDDING_MODEL="openai/text-embedding-3-small"
```
## Development And Testing
Run the default suite:
```bash
mix test
```
`mix test` excludes `:integration` by default.
Run live integration tests explicitly:
```bash
JIDO_SIMPLEMEM_ENABLE_LIVE_LANCE_TESTS=1 mix test --include integration
```
Useful checks:
```bash
mix format
mix test
mix release.check
mix xref callers Jido.SimpleMem.Runtime
```
## Additional Docs
- [Buffered SimpleMem Lifecycle](docs/explanations/default-memory-policy.md)
- [Lance Worker Architecture](docs/architecture/lance-worker.md)
- [ADR-0001: Single-Tier SimpleMem](docs/decisions/ADR-0001-single-tier-simplemem.md)
- [ADR-0002: SimpleMem Parity Refactor](docs/decisions/ADR-0002-simplemem-parity-refactor.md)
- [Releasing](RELEASING.md)