Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
}
12 changes: 12 additions & 0 deletions api/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions api/mcp/memory.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For query-based backends (e.g., Cognee), there is a possible mismatch here since they expect queries, not hashes. Consider resolving the hash via DagLoader to derive a query and pass that to the driver.

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
}
12 changes: 12 additions & 0 deletions cmd/tapes/serve/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions cmd/tapes/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions pkg/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -42,5 +44,9 @@ func NewDefaultConfig() *Config {
Model: defaultEmbeddingModel,
Dimensions: defaultEmbeddingDimensions,
},
Memory: MemoryConfig{
Provider: defaultMemoryProvider,
Enabled: true,
},
}
}
22 changes: 22 additions & 0 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
},
},
}
53 changes: 53 additions & 0 deletions pkg/memory/driver.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could pass a context string here as well so it is compatible with Cognee


// 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"`
}
7 changes: 7 additions & 0 deletions pkg/memory/error.go
Original file line number Diff line number Diff line change
@@ -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")
Loading