From b6e2256da67f44f4698ddad896a438b68e6faa9f Mon Sep 17 00:00:00 2001 From: John McBride Date: Wed, 11 Feb 2026 22:52:00 -0500 Subject: [PATCH] feat: memory Driver interface and "local" implementor Signed-off-by: John McBride --- api/api.go | 1 + api/config.go | 4 + api/mcp/mcp.go | 12 ++ api/mcp/memory.go | 70 +++++++++++ cmd/tapes/serve/proxy/proxy.go | 12 ++ cmd/tapes/serve/serve.go | 13 ++ pkg/config/defaults.go | 6 + pkg/config/types.go | 22 ++++ pkg/memory/driver.go | 53 +++++++++ pkg/memory/error.go | 7 ++ pkg/memory/local/local.go | 95 +++++++++++++++ pkg/memory/local/local_test.go | 164 ++++++++++++++++++++++++++ pkg/memory/local/memory_suite_test.go | 13 ++ pkg/utils/test/memory.go | 51 ++++++++ proxy/config.go | 5 + proxy/proxy.go | 1 + proxy/worker/pool.go | 24 ++++ 17 files changed, 553 insertions(+) create mode 100644 api/mcp/memory.go create mode 100644 pkg/memory/driver.go create mode 100644 pkg/memory/error.go create mode 100644 pkg/memory/local/local.go create mode 100644 pkg/memory/local/local_test.go create mode 100644 pkg/memory/local/memory_suite_test.go create mode 100644 pkg/utils/test/memory.go diff --git a/api/api.go b/api/api.go index 63c70a6..43f9a40 100644 --- a/api/api.go +++ b/api/api.go @@ -54,6 +54,7 @@ func NewServer(config Config, driver storage.Driver, dagLoader merkle.DagLoader, DagLoader: dagLoader, VectorDriver: config.VectorDriver, Embedder: config.Embedder, + MemoryDriver: config.MemoryDriver, Logger: logger, }) if err != nil { diff --git a/api/config.go b/api/config.go index 830c27f..21aecc5 100644 --- a/api/config.go +++ b/api/config.go @@ -3,6 +3,7 @@ package api import ( "github.com/papercomputeco/tapes/pkg/embeddings" + "github.com/papercomputeco/tapes/pkg/memory" "github.com/papercomputeco/tapes/pkg/vector" ) @@ -16,4 +17,7 @@ type Config struct { // Embedder for converting query text to vectors (optional, enables MCP server) Embedder embeddings.Embedder + + // MemoryDriver for fact recall (optional, enables memory_recall MCP tool) + MemoryDriver memory.Driver } diff --git a/api/mcp/mcp.go b/api/mcp/mcp.go index 0affc7a..7bef99e 100644 --- a/api/mcp/mcp.go +++ b/api/mcp/mcp.go @@ -9,6 +9,7 @@ import ( "go.uber.org/zap" "github.com/papercomputeco/tapes/pkg/embeddings" + "github.com/papercomputeco/tapes/pkg/memory" "github.com/papercomputeco/tapes/pkg/merkle" "github.com/papercomputeco/tapes/pkg/utils" "github.com/papercomputeco/tapes/pkg/vector" @@ -25,6 +26,9 @@ type Config struct { // configured VectorDriver Embedder embeddings.Embedder + // MemoryDriver for fact recall (optional, enables memory_recall tool) + MemoryDriver memory.Driver + // Noop for empty MCP server Noop bool @@ -79,6 +83,14 @@ func NewServer(c Config) (*Server, error) { Description: searchDescription, }, s.handleSearch) + // Add memory recall tool if a memory driver is configured + if c.MemoryDriver != nil { + mcp.AddTool(mcpServer, &mcp.Tool{ + Name: memoryRecallToolName, + Description: memoryRecallDescription, + }, s.handleMemoryRecall) + } + s.mcpServer = mcpServer // Create a streamable HTTP net/http handler for stateless operations diff --git a/api/mcp/memory.go b/api/mcp/memory.go new file mode 100644 index 0000000..4feb3a9 --- /dev/null +++ b/api/mcp/memory.go @@ -0,0 +1,70 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "github.com/papercomputeco/tapes/pkg/memory" +) + +var ( + memoryRecallToolName = "memory_recall" + memoryRecallDescription = "Recall facts from the tapes memory layer. Given a node hash (a position in the conversation DAG), returns extracted facts that are relevant to that position. Use this to retrieve persistent knowledge from past conversations." +) + +// MemoryRecallInput represents the input arguments for the MCP memory_recall tool. +type MemoryRecallInput struct { + Hash string `json:"hash" jsonschema:"the node hash identifying a position in the conversation DAG to recall facts for"` +} + +// MemoryRecallOutput represents the structured output of a memory recall. +type MemoryRecallOutput struct { + Facts []memory.Fact `json:"facts"` +} + +// handleMemoryRecall processes a memory recall request via MCP. +func (s *Server) handleMemoryRecall(ctx context.Context, _ *mcp.CallToolRequest, input MemoryRecallInput) (*mcp.CallToolResult, MemoryRecallOutput, error) { + if input.Hash == "" { + return &mcp.CallToolResult{ + IsError: true, + Content: []mcp.Content{ + &mcp.TextContent{Text: "hash is required"}, + }, + }, MemoryRecallOutput{}, nil + } + + facts, err := s.config.MemoryDriver.Recall(ctx, input.Hash) + if err != nil { + return &mcp.CallToolResult{ + IsError: true, + Content: []mcp.Content{ + &mcp.TextContent{Text: fmt.Sprintf("Memory recall failed: %v", err)}, + }, + }, MemoryRecallOutput{}, nil + } + + if facts == nil { + facts = []memory.Fact{} + } + + output := MemoryRecallOutput{Facts: facts} + + jsonBytes, err := json.Marshal(output) + if err != nil { + return &mcp.CallToolResult{ + IsError: true, + Content: []mcp.Content{ + &mcp.TextContent{Text: fmt.Sprintf("Failed to serialize results: %v", err)}, + }, + }, MemoryRecallOutput{}, nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(jsonBytes)}, + }, + }, output, nil +} diff --git a/cmd/tapes/serve/proxy/proxy.go b/cmd/tapes/serve/proxy/proxy.go index b8d7784..ff420b1 100644 --- a/cmd/tapes/serve/proxy/proxy.go +++ b/cmd/tapes/serve/proxy/proxy.go @@ -11,6 +11,7 @@ import ( "github.com/papercomputeco/tapes/pkg/config" embeddingutils "github.com/papercomputeco/tapes/pkg/embeddings/utils" "github.com/papercomputeco/tapes/pkg/logger" + "github.com/papercomputeco/tapes/pkg/memory/local" "github.com/papercomputeco/tapes/pkg/storage" "github.com/papercomputeco/tapes/pkg/storage/inmemory" "github.com/papercomputeco/tapes/pkg/storage/sqlite" @@ -169,6 +170,17 @@ func (c *proxyCommander) run() error { ) } + // Create local memory driver + memDriver := local.NewDriver(local.Config{ + Enabled: true, + }) + defer memDriver.Close() + config.MemoryDriver = memDriver + + c.logger.Info("memory enabled", + zap.String("provider", "local"), + ) + p, err := proxy.New(config, driver, c.logger) if err != nil { return fmt.Errorf("creating proxy: %w", err) diff --git a/cmd/tapes/serve/serve.go b/cmd/tapes/serve/serve.go index 1b76ee8..bba7303 100644 --- a/cmd/tapes/serve/serve.go +++ b/cmd/tapes/serve/serve.go @@ -19,6 +19,7 @@ import ( "github.com/papercomputeco/tapes/pkg/dotdir" embeddingutils "github.com/papercomputeco/tapes/pkg/embeddings/utils" "github.com/papercomputeco/tapes/pkg/logger" + "github.com/papercomputeco/tapes/pkg/memory/local" "github.com/papercomputeco/tapes/pkg/merkle" "github.com/papercomputeco/tapes/pkg/storage" "github.com/papercomputeco/tapes/pkg/storage/inmemory" @@ -210,6 +211,17 @@ func (c *ServeCommander) run() error { zap.String("embedding_model", c.embeddingModel), ) + // Create local memory driver + memDriver := local.NewDriver(local.Config{ + Enabled: true, + }) + defer memDriver.Close() + proxyConfig.MemoryDriver = memDriver + + c.logger.Info("memory enabled", + zap.String("provider", "local"), + ) + // Create proxy p, err := proxy.New(proxyConfig, driver, c.logger) if err != nil { @@ -228,6 +240,7 @@ func (c *ServeCommander) run() error { ListenAddr: c.apiListen, VectorDriver: proxyConfig.VectorDriver, Embedder: proxyConfig.Embedder, + MemoryDriver: memDriver, } apiServer, err := api.NewServer(apiConfig, driver, dagLoader, c.logger) if err != nil { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 72960bd..351ad1e 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -14,6 +14,8 @@ const ( defaultEmbeddingModel = "embeddinggemma" defaultEmbeddingDimensions = 768 defaultEmbeddingTarget = "http://localhost:11434" + + defaultMemoryProvider = "local" ) // NewDefaultConfig returns a Config with sane defaults for all fields. @@ -42,5 +44,9 @@ func NewDefaultConfig() *Config { Model: defaultEmbeddingModel, Dimensions: defaultEmbeddingDimensions, }, + Memory: MemoryConfig{ + Provider: defaultMemoryProvider, + Enabled: true, + }, } } diff --git a/pkg/config/types.go b/pkg/config/types.go index 14b01a0..8c85646 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -15,6 +15,7 @@ type Config struct { Client ClientConfig `toml:"client"` VectorStore VectorStoreConfig `toml:"vector_store"` Embedding EmbeddingConfig `toml:"embedding"` + Memory MemoryConfig `toml:"memory"` } // StorageConfig holds shared storage settings used by both proxy and API. @@ -56,6 +57,12 @@ type EmbeddingConfig struct { Dimensions uint `toml:"dimensions,omitempty"` } +// MemoryConfig holds memory layer settings. +type MemoryConfig struct { + Provider string `toml:"provider,omitempty"` + Enabled bool `toml:"enabled,omitempty"` +} + // configKeyInfo maps a user-facing dotted key name to a getter and setter on *Config. type configKeyInfo struct { get func(c *Config) string @@ -129,4 +136,19 @@ var configKeys = map[string]configKeyInfo{ return nil }, }, + "memory.provider": { + get: func(c *Config) string { return c.Memory.Provider }, + set: func(c *Config, v string) error { c.Memory.Provider = v; return nil }, + }, + "memory.enabled": { + get: func(c *Config) string { return strconv.FormatBool(c.Memory.Enabled) }, + set: func(c *Config, v string) error { + b, err := strconv.ParseBool(v) + if err != nil { + return fmt.Errorf("invalid value for memory.enabled: %w", err) + } + c.Memory.Enabled = b + return nil + }, + }, } diff --git a/pkg/memory/driver.go b/pkg/memory/driver.go new file mode 100644 index 0000000..3896f0c --- /dev/null +++ b/pkg/memory/driver.go @@ -0,0 +1,53 @@ +// Package memory provides a pluggable memory layer for the tapes system. +// +// Memory drivers extract durable facts from conversation nodes and recall them +// on demand. Facts are distilled, persistent knowledge derived from +// conversations — not raw messages. +// +// The [Driver] interface is intentionally minimal: Store extracts facts from +// nodes, Recall retrieves facts relevant to a DAG position, and Close releases +// resources. Memory is one-way; backend systems manage their own lifecycle and +// eviction policies. +// +// Short-term context (e.g., sliding windows over recent nodes) is a +// proxy-level concern handled via the storage driver's Ancestry method and is +// not part of the memory interface. +// +// Drivers are pluggable via configuration: +// +// [memory] +// provider = "local" # or "cognee", "graph" +package memory + +import ( + "context" + + "github.com/papercomputeco/tapes/pkg/merkle" +) + +// Driver handles storage and recall of conversation memory. +// Implementers extract durable facts from conversation nodes and recall them +// given a position in the DAG. +type Driver interface { + // Store persists one or more nodes into memory. This is the forcing + // function for driver implementors to extract facts from conversation + // nodes. Called asynchronously by the proxy worker pool after a + // conversation turn is stored in the DAG. + Store(ctx context.Context, nodes []*merkle.Node) error + + // Recall retrieves facts relevant to a position in the DAG tree. + // The hash identifies a node (typically the current leaf), and the + // driver returns facts relevant to that branch/position. + Recall(ctx context.Context, hash string) ([]Fact, error) + + // Close releases driver resources. + Close() error +} + +// Fact represents a distilled, durable piece of knowledge extracted from +// conversations. Facts are the output of the memory layer — not raw messages, +// but persistent knowledge that may be relevant across branches and sessions. +type Fact struct { + // Content is the extracted fact text. + Content string `json:"content"` +} diff --git a/pkg/memory/error.go b/pkg/memory/error.go new file mode 100644 index 0000000..98aa4fd --- /dev/null +++ b/pkg/memory/error.go @@ -0,0 +1,7 @@ +package memory + +import "errors" + +// ErrNotConfigured is returned when memory operations are attempted +// but no memory driver has been configured. +var ErrNotConfigured = errors.New("memory not configured") diff --git a/pkg/memory/local/local.go b/pkg/memory/local/local.go new file mode 100644 index 0000000..468e7ff --- /dev/null +++ b/pkg/memory/local/local.go @@ -0,0 +1,95 @@ +// Package local provides an in-memory implementation of the memory.Driver interface. +// +// Facts are extracted from stored nodes and keyed by their originating node +// hash. Recall returns all facts associated with a given hash. This is a +// simple local-dev story — production backends (e.g., Cognee) will use +// sophisticated ML pipelines to extract and recall cross-branch knowledge. +package local + +import ( + "context" + "sync" + + "github.com/papercomputeco/tapes/pkg/memory" + "github.com/papercomputeco/tapes/pkg/merkle" +) + +// Config holds configuration for the local memory driver. +type Config struct { + // Enabled controls whether the driver stores and recalls facts. + // When false, Store is a no-op and Recall returns nil. + Enabled bool +} + +// Driver implements memory.Driver using in-process data structures. +type Driver struct { + config Config + + mu sync.RWMutex + + // facts maps node hash -> extracted facts from that node. + facts map[string][]memory.Fact +} + +// NewDriver creates a local in-memory memory driver. +func NewDriver(config Config) *Driver { + return &Driver{ + config: config, + facts: make(map[string][]memory.Fact), + } +} + +// Store extracts text content from nodes and persists them as facts. +// Each node's text content becomes a fact keyed by the node's hash. +func (d *Driver) Store(_ context.Context, nodes []*merkle.Node) error { + if len(nodes) == 0 { + return nil + } + + if !d.config.Enabled { + return nil + } + + d.mu.Lock() + defer d.mu.Unlock() + + for _, node := range nodes { + text := node.Bucket.ExtractText() + if text == "" { + continue + } + + d.facts[node.Hash] = append(d.facts[node.Hash], memory.Fact{ + Content: text, + }) + } + + return nil +} + +// Recall retrieves facts associated with the given node hash. +// Returns nil if no facts exist for the hash or if the driver is disabled. +func (d *Driver) Recall(_ context.Context, hash string) ([]memory.Fact, error) { + if !d.config.Enabled { + return nil, nil + } + + d.mu.RLock() + defer d.mu.RUnlock() + + facts, ok := d.facts[hash] + if !ok { + return nil, nil + } + + // Return a copy to avoid callers mutating internal state. + result := make([]memory.Fact, len(facts)) + copy(result, facts) + + return result, nil +} + +// Close is a no-op for the in-memory driver. +func (d *Driver) Close() error { + return nil +} diff --git a/pkg/memory/local/local_test.go b/pkg/memory/local/local_test.go new file mode 100644 index 0000000..afcd49f --- /dev/null +++ b/pkg/memory/local/local_test.go @@ -0,0 +1,164 @@ +package local + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/papercomputeco/tapes/pkg/llm" + "github.com/papercomputeco/tapes/pkg/memory" + "github.com/papercomputeco/tapes/pkg/merkle" +) + +func newTestNode(role, text string, parent *merkle.Node) *merkle.Node { + bucket := merkle.Bucket{ + Type: "message", + Role: role, + Content: []llm.ContentBlock{{Type: "text", Text: text}}, + Model: "test-model", + Provider: "test-provider", + } + return merkle.NewNode(bucket, parent) +} + +var _ = Describe("Local Memory Driver", func() { + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + }) + + Describe("NewDriver", func() { + It("returns a non-nil driver", func() { + d := NewDriver(Config{Enabled: true}) + Expect(d).NotTo(BeNil()) + Expect(d.facts).NotTo(BeNil()) + }) + }) + + Describe("Store", func() { + It("extracts facts from nodes", func() { + d := NewDriver(Config{Enabled: true}) + + user := newTestNode("user", "The capital of France is Paris", nil) + err := d.Store(ctx, []*merkle.Node{user}) + Expect(err).NotTo(HaveOccurred()) + + Expect(d.facts).To(HaveKey(user.Hash)) + Expect(d.facts[user.Hash]).To(HaveLen(1)) + Expect(d.facts[user.Hash][0].Content).To(Equal("The capital of France is Paris")) + }) + + It("stores multiple nodes", func() { + d := NewDriver(Config{Enabled: true}) + + n1 := newTestNode("user", "Hello", nil) + n2 := newTestNode("assistant", "Hi there!", n1) + + err := d.Store(ctx, []*merkle.Node{n1, n2}) + Expect(err).NotTo(HaveOccurred()) + + Expect(d.facts).To(HaveLen(2)) + Expect(d.facts).To(HaveKey(n1.Hash)) + Expect(d.facts).To(HaveKey(n2.Hash)) + }) + + It("handles empty node slice", func() { + d := NewDriver(Config{Enabled: true}) + err := d.Store(ctx, []*merkle.Node{}) + Expect(err).NotTo(HaveOccurred()) + Expect(d.facts).To(BeEmpty()) + }) + + It("skips nodes with no text content", func() { + d := NewDriver(Config{Enabled: true}) + + node := &merkle.Node{ + Hash: "emptyhash", + Bucket: merkle.Bucket{ + Type: "message", + Role: "user", + Content: []llm.ContentBlock{{Type: "image", ImageURL: "http://example.com/img.png"}}, + }, + } + err := d.Store(ctx, []*merkle.Node{node}) + Expect(err).NotTo(HaveOccurred()) + + Expect(d.facts).To(BeEmpty()) + }) + + It("is a no-op when disabled", func() { + d := NewDriver(Config{Enabled: false}) + + node := newTestNode("user", "some text", nil) + err := d.Store(ctx, []*merkle.Node{node}) + Expect(err).NotTo(HaveOccurred()) + + Expect(d.facts).To(BeEmpty()) + }) + }) + + Describe("Recall", func() { + It("returns facts for a stored node hash", func() { + d := NewDriver(Config{Enabled: true}) + + node := newTestNode("user", "Go was created at Google", nil) + err := d.Store(ctx, []*merkle.Node{node}) + Expect(err).NotTo(HaveOccurred()) + + facts, err := d.Recall(ctx, node.Hash) + Expect(err).NotTo(HaveOccurred()) + Expect(facts).To(HaveLen(1)) + Expect(facts[0].Content).To(Equal("Go was created at Google")) + }) + + It("returns nil for unknown hash", func() { + d := NewDriver(Config{Enabled: true}) + + facts, err := d.Recall(ctx, "nonexistent") + Expect(err).NotTo(HaveOccurred()) + Expect(facts).To(BeNil()) + }) + + It("returns nil when disabled", func() { + d := NewDriver(Config{Enabled: false}) + + facts, err := d.Recall(ctx, "anything") + Expect(err).NotTo(HaveOccurred()) + Expect(facts).To(BeNil()) + }) + + It("returns a copy so callers cannot mutate internal state", func() { + d := NewDriver(Config{Enabled: true}) + + node := newTestNode("user", "original fact", nil) + _ = d.Store(ctx, []*merkle.Node{node}) + + facts, err := d.Recall(ctx, node.Hash) + Expect(err).NotTo(HaveOccurred()) + Expect(facts).To(HaveLen(1)) + + // Mutate the returned slice + facts[0].Content = "mutated" + + // Internal state should be unchanged + internal, err := d.Recall(ctx, node.Hash) + Expect(err).NotTo(HaveOccurred()) + Expect(internal[0].Content).To(Equal("original fact")) + }) + }) + + Describe("interface compliance", func() { + It("satisfies memory.Driver", func() { + var _ memory.Driver = NewDriver(Config{}) + }) + }) + + Describe("Close", func() { + It("is a no-op and returns nil", func() { + d := NewDriver(Config{}) + Expect(d.Close()).To(Succeed()) + }) + }) +}) diff --git a/pkg/memory/local/memory_suite_test.go b/pkg/memory/local/memory_suite_test.go new file mode 100644 index 0000000..1b83363 --- /dev/null +++ b/pkg/memory/local/memory_suite_test.go @@ -0,0 +1,13 @@ +package local + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMemory(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Local Memory Suite") +} diff --git a/pkg/utils/test/memory.go b/pkg/utils/test/memory.go new file mode 100644 index 0000000..61d1b75 --- /dev/null +++ b/pkg/utils/test/memory.go @@ -0,0 +1,51 @@ +package testutils + +import ( + "context" + + "github.com/papercomputeco/tapes/pkg/memory" + "github.com/papercomputeco/tapes/pkg/merkle" +) + +// MockMemoryDriver is a test memory driver that records calls and returns +// configurable results. +type MockMemoryDriver struct { + // StoredNodes accumulates all nodes passed to Store. + StoredNodes []*merkle.Node + + // RecallResults is returned by Recall for any hash. + RecallResults []memory.Fact + + // FailStore causes Store to return an error. + FailStore bool + + // FailRecall causes Recall to return an error. + FailRecall bool +} + +// NewMockMemoryDriver creates a new mock memory driver. +func NewMockMemoryDriver() *MockMemoryDriver { + return &MockMemoryDriver{ + StoredNodes: make([]*merkle.Node, 0), + RecallResults: make([]memory.Fact, 0), + } +} + +func (m *MockMemoryDriver) Store(_ context.Context, nodes []*merkle.Node) error { + if m.FailStore { + return memory.ErrNotConfigured + } + m.StoredNodes = append(m.StoredNodes, nodes...) + return nil +} + +func (m *MockMemoryDriver) Recall(_ context.Context, _ string) ([]memory.Fact, error) { + if m.FailRecall { + return nil, memory.ErrNotConfigured + } + return m.RecallResults, nil +} + +func (m *MockMemoryDriver) Close() error { + return nil +} diff --git a/proxy/config.go b/proxy/config.go index f5ae6a5..2c81f8a 100644 --- a/proxy/config.go +++ b/proxy/config.go @@ -2,6 +2,7 @@ package proxy import ( "github.com/papercomputeco/tapes/pkg/embeddings" + "github.com/papercomputeco/tapes/pkg/memory" "github.com/papercomputeco/tapes/pkg/vector" ) @@ -24,4 +25,8 @@ type Config struct { // Embedder is an optional embedder for generating embeddings. // Required if VectorDriver is set. Embedder embeddings.Embedder + + // MemoryDriver is an optional memory driver for context-driven recall. + // If nil, memory is disabled and the proxy operates without context injection. + MemoryDriver memory.Driver } diff --git a/proxy/proxy.go b/proxy/proxy.go index fd031cb..a6a443f 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -61,6 +61,7 @@ func New(config Config, driver storage.Driver, logger *zap.Logger) (*Proxy, erro Driver: driver, VectorDriver: config.VectorDriver, Embedder: config.Embedder, + MemoryDriver: config.MemoryDriver, Logger: logger, }) if err != nil { diff --git a/proxy/worker/pool.go b/proxy/worker/pool.go index 9545bd8..1771d06 100644 --- a/proxy/worker/pool.go +++ b/proxy/worker/pool.go @@ -16,6 +16,7 @@ import ( "github.com/papercomputeco/tapes/pkg/embeddings" "github.com/papercomputeco/tapes/pkg/llm" + "github.com/papercomputeco/tapes/pkg/memory" "github.com/papercomputeco/tapes/pkg/merkle" "github.com/papercomputeco/tapes/pkg/storage" "github.com/papercomputeco/tapes/pkg/vector" @@ -45,6 +46,10 @@ type Config struct { // A configured Embedder is required if VectorDriver is set. Embedder embeddings.Embedder + // MemoryDriver is the optional memory driver for storing conversation + // context into short-term and long-term memory. + MemoryDriver memory.Driver + // NumWorkers is the number of background workers in the pool. NumWorkers uint @@ -155,6 +160,14 @@ func (p *Pool) processJob(job Job) { ) p.storeEmbeddings(ctx, newNodes) } + + // If the memory driver is configured, store nodes into memory + if p.config.MemoryDriver != nil && len(newNodes) > 0 { + p.logger.Debug("storing nodes in memory", + zap.Int("new_node_count", len(newNodes)), + ) + p.storeMemory(ctx, newNodes) + } } // storeConversationTurn stores a request-response pair in the merkle dag. @@ -270,3 +283,14 @@ func (p *Pool) storeEmbeddings(ctx context.Context, nodes []*merkle.Node) { ) } } + +// storeMemory feeds newly inserted nodes to the memory driver. +// Errors are logged but not returned to avoid failing the main storage operation. +func (p *Pool) storeMemory(ctx context.Context, nodes []*merkle.Node) { + if err := p.config.MemoryDriver.Store(ctx, nodes); err != nil { + p.logger.Warn("failed to store nodes in memory", + zap.Error(err), + zap.Int("node_count", len(nodes)), + ) + } +}