Memora has five pluggable persistence contracts plus an identity
contract — each defined in pkg/adapter:
MetadataStore— tabular data (workspaces, collections, memories, cells, agents, watermarks).ContentStore— content bytes (memory and cell bodies).GraphStore— context graph (edges, traversal, statistics).VectorStore— embedding index.LedgerStore— append-only audit log.IdentityProvider— verifies anagent_idmatches anidentity_proof.
Third-party adapters import only pkg/adapter and pkg/types —
they do not need to depend on any internal/ package.
// In your-org/memora-acme-vec/vec.go
package vec
import (
"context"
"github.com/axiom-studio/memora/pkg/adapter"
)
func init() {
adapter.RegisterVector("acme", func() adapter.VectorStore { return &Store{} })
}
type Store struct { /* ... */ }
func (s *Store) Open(ctx context.Context, cfg adapter.VectorConfig) error { /* ... */ return nil }
func (s *Store) Close() error { return nil }
func (s *Store) Ping(ctx context.Context) error { return nil }
func (s *Store) Capabilities() adapter.VectorCapabilities {
return adapter.VectorCapabilities{SupportsANN: true, MaxDimensions: 4096}
}
func (s *Store) PutVector(ctx context.Context, p adapter.VectorPut) error { /* ... */ return nil }
func (s *Store) PutVectorsBatch(ctx context.Context, puts []adapter.VectorPut) error { /* ... */ return nil }
func (s *Store) Query(ctx context.Context, q adapter.VectorQuery) ([]adapter.VectorHit, error) {
/* ... */
return nil, nil
}
func (s *Store) DeleteVectors(ctx context.Context, keys []adapter.VectorKey) error { /* ... */ return nil }DeleteVectors takes two kinds of key. A key with a CellID removes that cell's vector. A key with only WorkspaceID and MemoryID removes every vector of that memory in that workspace; Forget uses it, so a store that ignores it leaves a forgotten memory's embeddings behind. adapter.SplitVectorKeys separates the two and refuses keys that name neither. compliance.VectorDeleteContract checks the behaviour. A store that also implements adapter.ModelAwareVectorStore lists the memories it holds vectors for with VectorMemories, which memora-core reembed uses to remove the vectors of memories that no longer exist.
Then in your memora-core build, add a blank import:
import _ "github.com/your-org/memora-acme-vec"…and the deployer can select your driver with
--vector-driver=acme or MEMORA_VECTOR_DRIVER=acme.
The server checks Capabilities() at request-handling time and
returns a friendly capability error (HTTP 501 / MCP error -32603)
rather than attempting an unsupported operation. Examples:
VectorCapabilities.SupportsHybridFilter=false— Recall hybrid mode falls back to keyword-only ranking and adds anX-Memora-Hybrid-Degraded: trueresponse header.LedgerCapabilities.SupportsQuery=false—GET /v1/.../ledgerreturns 501 withcapability_unavailable.
Always answer truthfully. The server's behavior degrades gracefully when capabilities are missing, but it relies on your honesty.
Adapters own their migration lifecycle. The SQLite adapter embeds
its SQL files via //go:embed migrations/*.sql and runs them
idempotently at Open() time, tracking applied versions in
memora_schema_migrations. Postgres / etc. adapters follow the same
pattern.
Every write must produce a fresh wmk_<ULID> watermark and append to
memora_watermark_history. The CAS contract is:
newWmk, err := store.UpdateMemory(ctx, workspaceID, memID, expectedWmk, mem)
// if expectedWmk != head: return ErrCAS + current head_watermarkReturn types.ErrCAS on conflict — the HTTP layer maps that to 412.
Every method that addresses a row by ID also takes the caller's
workspaceID and must scope its query by it. A row that exists in another
workspace must look exactly like a missing row: return types.ErrNotFound,
never ErrCAS, so a foreign ID cannot be confirmed or have its watermark
read.
DeleteCellsFrom(ctx, workspaceID, memoryID, fromSeq) removes a Memory's
cells with seq >= fromSeq and returns their cell_ids in seq order. The
service calls it when an edit re-chunks to fewer cells, then drops those
cells' vectors and content, so an adapter that skips it keeps serving text
the user removed. compliance.DeleteCellsFromContract checks it.
Edge types are a closed enum (pkg/types/edge.go). Don't accept
unknown types — types.ValidEdgeType(s) is the canonical check.
Custom edge metadata goes in Edge.PropertiesJSON.
The compliance test suite (pkg/adapter/compliance/) is the canonical
acceptance bar. v0.1 ships a minimal contract suite; the full suite
with the Context Graph block lands in v0.5 per the OSS roadmap.
In the meantime, here's the minimum you should test:
- Roundtrip per write verb (Imprint/Update/Patch/Append/Forget).
- CAS conflict —
UpdateMemorywith stale watermark returnsErrCAS. - Unique-live-triple —
GraphLinkof the same(source, target, edge_type)twice returnsErrAlreadyExists. - Cascade Forget —
Forget(memory)followed byNeighbors(other)does not return the forgotten memory. - Capability advertisement — every flag you advertise as
trueactually works; every flag advertised asfalseactually fails cleanly.
If your adapter only makes sense in a commercial/cloud deployment,
gate the registration with the cloud build tag:
//go:build cloud
package vecOSS builds (make memora-oss) won't include the package; Cloud builds
(make memora-cloud, or go build -tags cloud) will.