LemonMemory. Provider behaviour
(lemon_memory v0.1.0)
View Source
Behaviour for searchable memory providers.
A provider is a place an agent's past work can be written to and searched.
LemonMemory.Providers.Local (SQLite FTS) is built in; additional providers —
a vector store, a wiki, an issue tracker — register at runtime through
LemonMemory.Providers.register_provider/1 and are queried through the same
fan-out.
Providers receive the same scoped search options as LemonMemory.SessionSearch
and return LemonMemory.Document structs.
Contract
LemonPlatformTest.ProviderCase turns each of these into a test.
search/2returns a plain list ofLemonMemory.Documentstructs. Not{:ok, docs}, notnil, not a stream.LemonMemory.Providersdiscards any result that is not a list of documents and treats it as a provider failure.search/2must not raise for user input. Queries come from agents and from people: quotes, wildcards, boolean operators, emoji, ten-thousand character pastes. Full-text engines reject many of those outright, so sanitise rather than pass through —LemonMemory.Storestrips FTS5 metacharacters and AND-joins the remaining terms. Return[]for a query you cannot serve.Unknown options are ignored, not rejected. The platform adds keys to
search_opts/0over time and older providers must keep working.put/2returns:okor{:error, reason}. It runs on the run-finalisation path, so it should be cheap; buffer internally if your store is slow.
LemonMemory.Providers isolates provider failures and timeouts — each call
runs in a task with a per-provider :timeout_ms, exceptions and exits are
rescued and logged. That is a safety net for the agent, not a licence for the
provider: a provider that blocks for its whole timeout on every query makes
every memory search that slow.
Known gap: search has no error channel
search/2 returns [Document.t()] with no error branch, so "no results"
and "my backend is unreachable" are the same answer and the platform cannot
tell them apart or report degradation. Anything richer has to be added to
this behaviour first; until then, providers should log their own failures.
Summary
Types
Options passed to put/2. Carries :provider_id; otherwise provider-defined.
Which slice of memory a search covers.
Options passed to search/2.
Types
@type put_opts() :: keyword()
Options passed to put/2. Carries :provider_id; otherwise provider-defined.
@type scope() :: :session | :agent | :workspace | :all
Which slice of memory a search covers.
:session and :agent and :workspace are scoped by the matching
:scope_key; :all is unscoped.
@type search_opts() :: keyword()
Options passed to search/2.
:scope—scope/0, defaults to:session:scope_key— the session key, agent id or workspace key to scope to. When a scoped search arrives without one, return[]rather than widening the search to everything.:limit— how many documents the caller wants. The registry re-trims after merging providers, so returning more is tolerated, not encouraged.:provider_id— the id this provider was registered under, injected byLemonMemory.Providers.
Providers must ignore keys they do not recognise.
Callbacks
@callback put(LemonMemory.Document.t(), put_opts()) :: :ok | {:error, term()}
Store a memory document.
Called on the run-finalisation path, once per finished run, for every enabled
provider whose scopes include the document's scope. Returns :ok or
{:error, reason} — reason is logged, never surfaced to the agent, and must
not be raised.
@callback search(query :: binary(), search_opts()) :: [LemonMemory.Document.t()]
Search this provider and return matching documents.
Best-effort by contract: return [] for an empty result, an unparseable
query, or an unreachable backend. Must not raise.