Help us shape the future of Go! Take the 2026 Go Developer Survey

memorykit

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

memorykit

memorykit provides governed, caller-owned long-term memory for GoAgents. V1 shares memory between Agents only inside one authorized project Scope. It keeps storage, authorization, model providers, worker scheduling, and shutdown under the Host's control; it is not a standalone service.

Scope and trust boundary

A Scope is {tenant_id, subject_type, subject_id}. The Host V1 constructs only subject_type=project from an authenticated tenant identity and a trusted project route. HTTP bodies and memory tool schemas cannot supply or override tenant, subject, or write permission.

SubjectUser exists only as a future-shaped storage/domain value. The Host V1 does not expose user-Scope routes or composition and rejects attempts to inject subject_type=user.

Retrieved memory is untrusted contextual data. Only active, currently effective, same-Scope records may enter model messages or memory tool results. Memory text cannot add tools, grant write permission, or change Scope.

Required configuration

There are no permissive production defaults. A Host must explicitly provide:

  • validated Limits, including a version and positive bounds for keys, content, metadata, sources, and list size;
  • a versioned RecallPolicy with exact, full-text, and vector budgets, RRF parameters, item/token/query limits, a deadline, and the embedding profile and dimension;
  • a deterministic contract-compatible Embedder for the configured profile;
  • a fail-closed project authorizer at the Host boundary;
  • a content validator for direct writes and extracted candidates;
  • caller-owned lifecycle, recall, embedding, and extraction-job Stores;
  • explicit clocks, worker identities, lease/retry limits, and worker scheduling.

Do not run a production composition with a nil authorizer, an allow-all fallback, or a partially initialized policy.

PostgreSQL and pgvector

pgstore.Open uses the pgx driver, takes a DSN, acquires a schema advisory lock, applies the versioned migration, runs CREATE EXTENSION IF NOT EXISTS vector, and checks a vector value round trip. The database role therefore needs permission to install/use pgvector during initial setup. V1 is verified against PostgreSQL 16 and pgvector 0.8.2.

One local setup is:

docker run --name memorykit-postgres --rm \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_DB=memorykit_test \
  -p 5432:5432 \
  pgvector/pgvector:0.8.2-pg16-bookworm

The Store is caller-owned. pgstore.Open owns and closes only the pool it creates; Host shutdown must stop intake/workers before closing it.

Recall paths

Automatic recall uses the Agent context projector. It searches before a model call, enforces one configured deadline and item/token budget, fuses exact, PostgreSQL full-text, and pgvector ranks, and inserts a bounded memory view immediately before the current user message.

Full-text recall splits the query with memorykit.SearchTerms: words match the simple tsvector, and runs of Chinese or Japanese characters, which the default PostgreSQL parser does not segment, become overlapping two-character grams matched as substrings. A memory matches if it shares any term; only memories matching more than half as many terms as the best match are kept, and results rank by matched terms. memorystore and pgstore apply the same rule, checked by storetest.RunFullTextRecallConformance. The query is evaluated per scope without the GIN index, which is fine at project-memory scale.

On-demand recall uses request-scoped search_memory and read_memory tools. Their schemas contain no Scope fields. remember_project_memory is registered only when trusted Host metadata grants explicit write intent. A model write takes effect immediately only for a new kind/key or to replace the model's own untouched earlier write. If the current active record was written, reviewed, or corrected by anyone else, the write is stored as a candidate for review and the existing record stays in effect.

A typed recoverable embedding/vector failure degrades to the remaining exact/full-text channels and records only content-free channel/ID metadata. If the entire Recall Store returns a typed recoverable failure, the Agent Projector injects no memory, records a content-free degradation marker, and continues through the next projector/main task path. The same degradation applies when Recall's own deadline expires while the caller's context is still alive. The Projector keeps only the last MaxQueryRunes runes of an oversized query instead of failing the turn. Invalid Scope or authorization, policy, candidate shape, and Store integrity still fail closed. Raw query text, content, vectors, bearer headers, provider payloads, and Artifact bodies are not valid log/event/error fields.

Write paths

  • A trusted explicit user/tool write creates an active record immediately after authorization and content validation.
  • A deterministic trusted projection may create active memory without a model.
  • Model extraction can only propose candidate records. Candidates remain hidden from recall until an authorized reviewer activates them.
  • Candidate extraction jobs are durable and leased with bounded retries. Deterministic job/candidate identities make retries idempotent.

Explicit write failure is truthful: the tool returns 项目记忆未保存, marks the result as an error, and sets a request-scoped content-free failure flag. Hosts compose the ToolProvider's output validator so any later model success claim is rejected with the same fixed error before an output is persisted. A later successful write in that run does not clear the failure flag. An extraction enqueue failure does not roll back an already successful Agent run; it records a content-free degradation event for operator follow-up.

Lifecycle and operations

  • Migration: opening pgstore applies only the supported schema version and fails closed on an unavailable extension or incompatible version.
  • Embedding rebuild: delete the affected embedding rows (or let changed content hashes become pending), then run EmbeddingWorker until PendingEmbeddings is empty. Exact/full-text recall remains available while vector rows are absent.
  • Activation: changes a reviewed candidate to active and supersedes an existing active record with the same Scope/kind/key.
  • Correction: creates a new revision, replaces content/sources, and invalidates the old embedding by content hash.
  • Forget: changes an active record to inactive. It immediately leaves all recall paths but retains governed content, sources, and revision audit.
  • Erase: keeps an inactive identity/audit tombstone while scrubbing content, content hash, sources, embeddings, and content-bearing revision snapshots.
  • Audit: revisions record lifecycle action, actor, reason, version, and timestamps. Extraction-job completion/final failure retains only a content-free idempotency/operational tombstone.

Verification

Unit tests:

go test -race ./...
go vet ./...

The PostgreSQL suite is mandatory evidence only when the required flag is set; missing DSN, database, or pgvector then fails instead of skipping:

MEMORYKIT_REQUIRE_POSTGRES=1 \
MEMORYKIT_POSTGRES_TEST_DSN='postgres://postgres:postgres@localhost:5432/memorykit_test?sslmode=disable' \
go test -count=1 -race ./...

cd ../examples/host-api
MEMORYKIT_REQUIRE_POSTGRES=1 \
MEMORYKIT_POSTGRES_TEST_DSN='postgres://postgres:postgres@localhost:5432/memorykit_test?sslmode=disable' \
go test -count=1 -race -run '^TestHostMemoryPostgresBlackBox$' ./...

Explicit non-goals

V1 has no network memory service, production memory UI, approximate-nearest- neighbor index, live Agent-state copying, automatic candidate promotion, or universal latency SLA. Recall enforces the caller's configured deadline/budget; deployment-specific latency objectives remain a Host responsibility.

Documentation

Overview

Package memorykit defines the versioned domain contract for project memory.

The project-memory-v1 normalization policy preserves identities: keys are neither lowercased nor Unicode-normalized, and content is stored byte-for-byte.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidScope        = errors.New("invalid memory scope")
	ErrInvalidMemory       = errors.New("invalid memory")
	ErrNotFound            = errors.New("memory not found")
	ErrConflict            = errors.New("memory version conflict")
	ErrNoExtractionJob     = errors.New("no claimable extraction job")
	ErrInvalidRecallResult = errors.New("invalid memory recall result")
	ErrRecallDependency    = errors.New("memory recall dependency failure")
)

Functions

func CountSearchTermHits

func CountSearchTermHits(key, content string, words, grams []string) int

CountSearchTermHits counts how many query terms a memory matches: words against the memory's own words, grams as substrings of its text.

func ExtractionCandidateID

func ExtractionCandidateID(jobID string, draft CandidateDraft) (string, error)

ExtractionCandidateID derives a candidate's ID from its job and content, so a retry that extracts the same draft, in any position, replays the same create instead of colliding with a different draft.

func IsRecoverable

func IsRecoverable(err error) bool

func SearchTerms

func SearchTerms(text string) (words, grams []string)

SearchTerms splits recall text into the terms every RecallStore matches the same way. Words are lowercase runs of letters and digits, as PostgreSQL's 'simple' text-search configuration produces them. Chinese and Japanese are written without spaces, so their runs become overlapping two-character grams (a lone character stays a unigram) that stores match as substrings.

func ValidateCandidateDraft

func ValidateCandidateDraft(draft CandidateDraft, limits Limits) error

ValidateCandidateDraft delegates field validation to the create contract while fixing the lifecycle state to candidate and using validation-only identities.

func ValidateCandidateQuery

func ValidateCandidateQuery(query CandidateQuery, limits Limits) error

ValidateCandidateQuery validates both shared filters and channel-specific inputs.

func ValidateCommand

func ValidateCommand(command VersionedCommand, limits Limits) error

func ValidateCorrectRequest

func ValidateCorrectRequest(request CorrectRequest, limits Limits) error

func ValidateCreateRequest

func ValidateCreateRequest(request CreateRequest, limits Limits) error

func ValidateListQuery

func ValidateListQuery(query ListQuery, limits Limits) error

func ValidateMemoryID

func ValidateMemoryID(id string) error

ValidateMemoryID accepts only the canonical lowercase UUID text form. Storage implementations must not normalize alternate representations implicitly.

func ValidatePendingEmbeddingQuery

func ValidatePendingEmbeddingQuery(query PendingEmbeddingQuery, limits Limits) error

func ValidatePutEmbeddingRequest

func ValidatePutEmbeddingRequest(request PutEmbeddingRequest, limits Limits) error

func ValidateRevisionQuery

func ValidateRevisionQuery(query RevisionQuery, limits Limits) error

func ValidateSources

func ValidateSources(sources []Source, limits Limits) error

Types

type BackendError

type BackendError struct {
	Op          string
	Recoverable bool
	Err         error
}

func (*BackendError) Error

func (e *BackendError) Error() string

func (*BackendError) Unwrap

func (e *BackendError) Unwrap() error

type Candidate

type Candidate struct {
	Memory     Memory
	Sources    []Source
	Channel    Channel
	Rank       int
	Similarity float64
}

type CandidateDraft

type CandidateDraft struct {
	Kind       Kind      `json:"kind"`
	Key        string    `json:"key"`
	Content    string    `json:"content"`
	ValidFrom  time.Time `json:"valid_from,omitempty"`
	ValidUntil time.Time `json:"valid_until,omitempty"`
	Importance int       `json:"importance,omitempty"`
	Confidence float64   `json:"confidence,omitempty"`
}

CandidateDraft is untrusted extracted content that has not been persisted or activated.

type CandidateExtractor

type CandidateExtractor interface {
	Extract(context.Context, ExtractionRequest) ([]CandidateDraft, error)
}

type CandidateQuery

type CandidateQuery struct {
	Scope               Scope
	Text                string
	Keys                []string
	Kinds               []Kind
	QueryVector         []float32
	Now                 time.Time
	ExactLimit          int
	FullTextLimit       int
	VectorLimit         int
	MinVectorSimilarity float64
	EmbeddingProfileID  string
	EmbeddingDimensions int
}

type CandidateSet

type CandidateSet struct {
	Exact, FullText, Vector []Candidate
}

type Channel

type Channel string
const (
	ChannelExact    Channel = "exact"
	ChannelFullText Channel = "full_text"
	ChannelVector   Channel = "vector"
)

type ChannelError

type ChannelError struct {
	Channel Channel
	Err     error
}

func (*ChannelError) Error

func (e *ChannelError) Error() string

func (*ChannelError) Unwrap

func (e *ChannelError) Unwrap() error

type ContentValidator

type ContentValidator interface {
	ValidateMemoryContent(context.Context, string) error
}

type CorrectRequest

type CorrectRequest struct {
	Command               VersionedCommand
	Content               string
	ValidFrom, ValidUntil time.Time
	Importance            int
	Confidence            float64
	Sources               []Source
}

type CreateRequest

type CreateRequest struct {
	ID                                           string
	Scope                                        Scope
	Kind                                         Kind
	Key                                          string
	Status                                       Status
	Content                                      string
	ValidFrom, ValidUntil                        time.Time
	Importance                                   int
	Confidence                                   float64
	SourceAgentID, Actor, Reason, IdempotencyKey string
	Sources                                      []Source
	Now                                          time.Time
}

type EmbedRequest

type EmbedRequest struct {
	ProfileID string
	Texts     []string
}

type Embedder

type Embedder interface {
	Embed(context.Context, EmbedRequest) ([][]float32, error)
}

type EmbeddingInput

type EmbeddingInput struct {
	MemoryID    string
	Scope       Scope
	Content     string
	ContentHash string
	Version     int64
}

type EmbeddingStore

type EmbeddingStore interface {
	PendingEmbeddings(context.Context, PendingEmbeddingQuery) ([]EmbeddingInput, error)
	PutEmbedding(context.Context, PutEmbeddingRequest) error
}

type EmbeddingWorker

type EmbeddingWorker struct {
	// contains filtered or unexported fields
}

func NewEmbeddingWorker

func NewEmbeddingWorker(config EmbeddingWorkerConfig) (*EmbeddingWorker, error)

func (*EmbeddingWorker) RunOnce

func (w *EmbeddingWorker) RunOnce(ctx context.Context) (int, error)

RunOnce processes at most one Store-ordered batch. Host owns scheduling and retries.

type EmbeddingWorkerConfig

type EmbeddingWorkerConfig struct {
	Store      EmbeddingStore
	Embedder   Embedder
	ProfileID  string
	Dimensions int
	BatchSize  int
}

func (EmbeddingWorkerConfig) Validate

func (c EmbeddingWorkerConfig) Validate() error

Validate rejects incomplete worker configuration before background work starts.

type EvalCase

type EvalCase struct {
	ID           string   `json:"id"`
	Query        string   `json:"query"`
	ExpectedIDs  []string `json:"expected_ids"`
	ForbiddenIDs []string `json:"forbidden_ids"`
}

type EvalCorpus

type EvalCorpus struct {
	Version  string       `json:"version"`
	Memories []EvalMemory `json:"memories"`
	Cases    []EvalCase   `json:"cases"`
}

type EvalMemory

type EvalMemory struct {
	ID        string    `json:"id"`
	ProjectID string    `json:"project_id"`
	Status    string    `json:"status"`
	Kind      string    `json:"kind"`
	Key       string    `json:"key"`
	Content   string    `json:"content"`
	Vector    []float32 `json:"vector"`
}

type EvalReport

type EvalReport struct {
	Cases              int
	ExpectedFound      int
	ExpectedTotal      int
	ForbiddenInjected  int
	ForbiddenTotal     int
	RecallAtBudget     float64
	FalseInjectionRate float64
}

func Evaluate

func Evaluate(results map[string][]string, cases []EvalCase) (EvalReport, error)

Evaluate scores result IDs by exact set membership. Duplicate returned IDs count once, and missing result entries are treated as empty result sets.

type ExtractionJob

type ExtractionJob struct {
	ID            string
	Scope         Scope
	Source        Source
	SourceAgentID string
	ExtractorID   string
	Status        ExtractionJobStatus
	Attempts      int
	LeaseOwner    string
	LeaseUntil    time.Time
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

type ExtractionJobStatus

type ExtractionJobStatus string
const (
	ExtractionPending   ExtractionJobStatus = "pending"
	ExtractionLeased    ExtractionJobStatus = "leased"
	ExtractionCompleted ExtractionJobStatus = "completed"
	ExtractionFailed    ExtractionJobStatus = "failed"
)

type ExtractionJobStore

type ExtractionJobStore interface {
	EnqueueExtraction(context.Context, ExtractionJob) error
	ClaimExtraction(context.Context, string, time.Duration, int, time.Time) (ExtractionJob, error)
	CompleteExtraction(context.Context, string, string, int, time.Time) error
	FailExtraction(context.Context, string, string, int, string, int, time.Time) error
}

type ExtractionRequest

type ExtractionRequest struct {
	Scope                            Scope
	Source                           Source
	SourceAgentID, ExtractorID, Text string
	// Now is the extraction job's creation time, not the wall clock. It is
	// stable across retries, so drafts that default to it replay idempotently.
	Now time.Time
}

type ExtractionWorker

type ExtractionWorker struct {
	// contains filtered or unexported fields
}

func NewExtractionWorker

func NewExtractionWorker(config ExtractionWorkerConfig) (*ExtractionWorker, error)

func (*ExtractionWorker) RunOnce

func (w *ExtractionWorker) RunOnce(ctx context.Context) (bool, error)

RunOnce owns at most one lease. Model-derived output is always persisted as candidate, regardless of confidence or extractor behavior.

type ExtractionWorkerConfig

type ExtractionWorkerConfig struct {
	Jobs            ExtractionJobStore
	Memories        LifecycleStore
	Reader          SourceReader
	Extractor       CandidateExtractor
	ValidateContent ContentValidator
	Limits          Limits
	WorkerID        string
	LeaseDuration   time.Duration
	MaxAttempts     int
	Now             func() time.Time
}

func (ExtractionWorkerConfig) Validate

func (c ExtractionWorkerConfig) Validate() error

type Kind

type Kind string
const (
	KindFact       Kind = "fact"
	KindDecision   Kind = "decision"
	KindConstraint Kind = "constraint"
	KindLesson     Kind = "lesson"
)

func (Kind) IsValid

func (k Kind) IsValid() bool

type Limits

type Limits struct {
	Version             string
	MaxKeyRunes         int
	MaxContentRunes     int
	MaxMetadataRunes    int
	MaxSourcesPerMemory int
	MaxListItems        int
}

func (Limits) Validate

func (l Limits) Validate() error

Validate checks that callers explicitly configure every project-memory-v1 limit.

type ListQuery

type ListQuery struct {
	Scope  Scope
	Kind   Kind
	Status Status
	Key    string
	Limit  int
}

type Memory

type Memory struct {
	ID, Key, Content, SourceAgentID, CreatedBy, IdempotencyKey string
	Scope                                                      Scope
	Kind                                                       Kind
	Status                                                     Status
	ValidFrom, ValidUntil                                      time.Time
	Importance                                                 int
	Confidence                                                 float64
	Version                                                    int64
	CreatedAt, UpdatedAt                                       time.Time
}

func ProjectTrusted

func ProjectTrusted(
	ctx context.Context,
	store LifecycleStore,
	validator ContentValidator,
	limits Limits,
	projection TrustedProjection,
) (Memory, error)

ProjectTrusted maps an already-approved deterministic event directly to an active memory. It never invokes a model or converts candidate state.

type PendingEmbeddingQuery

type PendingEmbeddingQuery struct {
	ProfileID string
	Limit     int
}

type PutEmbeddingRequest

type PutEmbeddingRequest struct {
	MemoryID    string
	Scope       Scope
	ProfileID   string
	ContentHash string
	Dimensions  int
	Vector      []float32
	EmbeddedAt  time.Time
}

type RecallConfig

type RecallConfig struct {
	Store       RecallStore
	Embedder    Embedder
	CountTokens TokenCounter
	Policy      RecallPolicy
}

type RecallItem

type RecallItem struct {
	Memory  Memory
	Sources []Source
}

type RecallPolicy

type RecallPolicy struct {
	Version                                string
	ExactLimit, FullTextLimit, VectorLimit int
	RRFK                                   float64
	MinVectorSimilarity                    float64
	MaxItems, MaxTokens                    int
	MaxQueryRunes, MaxKeys                 int
	Deadline                               time.Duration
	EmbeddingProfileID                     string
	EmbeddingDimensions                    int
}

func (RecallPolicy) Validate

func (p RecallPolicy) Validate() error

Validate rejects partial policies so recall behavior is fixed at startup.

type RecallRequest

type RecallRequest struct {
	Scope Scope
	Text  string
	Keys  []string
	Kinds []Kind
	Now   time.Time
}

type RecallResult

type RecallResult struct {
	Items            []RecallItem
	DegradedChannels []Channel
	PolicyVersion    string
}

type RecallStore

type RecallStore interface {
	SearchCandidates(context.Context, CandidateQuery) (CandidateSet, error)
}

type Recaller

type Recaller struct {
	// contains filtered or unexported fields
}

func NewRecaller

func NewRecaller(config RecallConfig) (*Recaller, error)

func (*Recaller) MaxQueryRunes

func (r *Recaller) MaxQueryRunes() int

MaxQueryRunes exposes only the immutable query bound captured at construction.

func (*Recaller) Recall

func (r *Recaller) Recall(ctx context.Context, request RecallRequest) (RecallResult, error)

type Revision

type Revision struct {
	MemoryID      string
	Version       int64
	Action        RevisionAction
	Actor, Reason string
	Snapshot      Memory
	ContentErased bool
	CreatedAt     time.Time
}

type RevisionAction

type RevisionAction string
const (
	RevisionCreate    RevisionAction = "create"
	RevisionActivate  RevisionAction = "activate"
	RevisionCorrect   RevisionAction = "correct"
	RevisionSupersede RevisionAction = "supersede"
	RevisionForget    RevisionAction = "forget"
	RevisionErase     RevisionAction = "erase"
	RevisionDismiss   RevisionAction = "dismiss"
)

type RevisionQuery

type RevisionQuery struct {
	Scope         Scope
	MemoryID      string
	BeforeVersion int64
	Limit         int
}

type Scope

type Scope struct {
	TenantID    string
	SubjectType SubjectType
	SubjectID   string
}

func (Scope) Validate

func (s Scope) Validate() error

Validate checks that a scope identifies exactly one supported subject.

type Source

type Source struct{ Kind, Ref, EvidenceHash string }

type SourceReader

type SourceReader interface {
	ReadSource(context.Context, Source) (string, error)
}

type Status

type Status string
const (
	StatusCandidate Status = "candidate"
	StatusActive    Status = "active"
	StatusInactive  Status = "inactive"
)

func (Status) IsValid

func (s Status) IsValid() bool

type Store

type Store interface {
	LifecycleStore
	RecallStore
	EmbeddingStore
}

type SubjectType

type SubjectType string
const (
	SubjectProject SubjectType = "project"
	SubjectUser    SubjectType = "user"
)

type TokenCounter

type TokenCounter func(string) int

type TrustedProjection

type TrustedProjection struct {
	Create  CreateRequest
	EventID string
}

type VersionedCommand

type VersionedCommand struct {
	Scope           Scope
	ID              string
	ExpectedVersion int64
	Actor, Reason   string
	Now             time.Time
}

Directories

Path Synopsis
Package storetest defines reusable conformance tests for lifecycle stores.
Package storetest defines reusable conformance tests for lifecycle stores.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL