Skip to content

Latest commit

 

History

History
142 lines (106 loc) · 5.81 KB

adapter-authoring.md

File metadata and controls

142 lines (106 loc) · 5.81 KB

Authoring a Memora adapter

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 an agent_id matches an identity_proof.

Third-party adapters import only pkg/adapter and pkg/types — they do not need to depend on any internal/ package.

Hello-world VectorStore

// 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.

Capability advertisement

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 an X-Memora-Hybrid-Degraded: true response header.
  • LedgerCapabilities.SupportsQuery=false — GET /v1/.../ledger returns 501 with capability_unavailable.

Always answer truthfully. The server's behavior degrades gracefully when capabilities are missing, but it relies on your honesty.

Schema migrations

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.

Watermark semantics

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_watermark

Return 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.

Closed edge_type enum

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.

Testing your adapter

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 — UpdateMemory with stale watermark returns ErrCAS.
  • Unique-live-triple — GraphLink of the same (source, target, edge_type) twice returns ErrAlreadyExists.
  • Cascade Forget — Forget(memory) followed by Neighbors(other) does not return the forgotten memory.
  • Capability advertisement — every flag you advertise as true actually works; every flag advertised as false actually fails cleanly.

Build tags

If your adapter only makes sense in a commercial/cloud deployment, gate the registration with the cloud build tag:

//go:build cloud

package vec

OSS builds (make memora-oss) won't include the package; Cloud builds (make memora-cloud, or go build -tags cloud) will.