package memorytools import ( "context" "fmt" "strings" "sync" "time" "github.com/enmanuel/agents/pkg/memory" "github.com/enmanuel/agents/tools" ) // MemoryStore is the subset of memory.Store needed by memory tools. type MemoryStore interface { SaveFact(ctx context.Context, fact memory.Fact) error RecallFacts(ctx context.Context, agentID, subject string, key *string) ([]memory.Fact, error) DeleteFacts(ctx context.Context, agentID, subject string, key *string) error } // WindowClearer allows tools to clear the conversation window for a room. type WindowClearer interface { ClearWindow(roomID string) } // RoomContext is a thread-safe holder for the current room ID. // Set by the runtime before each event handling; read by memory_clear_context. type RoomContext struct { mu sync.RWMutex roomID string } // Set updates the current room ID. func (rc *RoomContext) Set(roomID string) { rc.mu.Lock() rc.roomID = roomID rc.mu.Unlock() } // Get returns the current room ID. func (rc *RoomContext) Get() string { rc.mu.RLock() defer rc.mu.RUnlock() return rc.roomID } // NewMemorySave creates a tool that saves a fact to long-term memory. func NewMemorySave(agentID string, store MemoryStore) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "memory_save", Description: "Save a fact to long-term memory. Use this to remember important information about users, topics, or preferences.", Parameters: []tools.Param{ {Name: "subject", Type: "string", Description: "The subject this fact is about (e.g. a username, a topic)", Required: true}, {Name: "key", Type: "string", Description: "The fact key (e.g. 'favorite_language', 'timezone')", Required: true}, {Name: "value", Type: "string", Description: "The fact value to store", Required: true}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { subject := tools.GetString(args, "subject") key := tools.GetString(args, "key") value := tools.GetString(args, "value") if subject == "" || key == "" || value == "" { return tools.Result{Err: fmt.Errorf("memory_save: subject, key, and value are required")} } err := store.SaveFact(ctx, memory.Fact{ AgentID: agentID, Subject: subject, Key: key, Value: value, }) if err != nil { return tools.Result{Err: fmt.Errorf("memory_save: %w", err)} } return tools.Result{Output: fmt.Sprintf("saved: %s.%s = %s", subject, key, value)} }, } } // NewMemoryRecall creates a tool that retrieves facts from long-term memory. func NewMemoryRecall(agentID string, store MemoryStore) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "memory_recall", Description: "Recall facts from long-term memory about a subject. Omit key to get all facts for the subject.", Parameters: []tools.Param{ {Name: "subject", Type: "string", Description: "The subject to recall facts about", Required: true}, {Name: "key", Type: "string", Description: "Optional specific fact key to recall", Required: false}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { subject := tools.GetString(args, "subject") if subject == "" { return tools.Result{Err: fmt.Errorf("memory_recall: subject is required")} } var keyPtr *string if k := tools.GetString(args, "key"); k != "" { keyPtr = &k } facts, err := store.RecallFacts(ctx, agentID, subject, keyPtr) if err != nil { return tools.Result{Err: fmt.Errorf("memory_recall: %w", err)} } if len(facts) == 0 { return tools.Result{Output: fmt.Sprintf("no facts found for subject %q", subject)} } var sb strings.Builder for _, f := range facts { fmt.Fprintf(&sb, "%s.%s = %s\n", f.Subject, f.Key, f.Value) } return tools.Result{Output: sb.String()} }, } } // NewMemoryForget creates a tool that deletes facts from long-term memory. func NewMemoryForget(agentID string, store MemoryStore) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "memory_forget", Description: "Delete facts from long-term memory. Omit key to delete all facts for the subject.", Parameters: []tools.Param{ {Name: "subject", Type: "string", Description: "The subject whose facts to delete", Required: true}, {Name: "key", Type: "string", Description: "Optional specific fact key to delete; omit to delete all", Required: false}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { subject := tools.GetString(args, "subject") if subject == "" { return tools.Result{Err: fmt.Errorf("memory_forget: subject is required")} } var keyPtr *string if k := tools.GetString(args, "key"); k != "" { keyPtr = &k } err := store.DeleteFacts(ctx, agentID, subject, keyPtr) if err != nil { return tools.Result{Err: fmt.Errorf("memory_forget: %w", err)} } if keyPtr != nil { return tools.Result{Output: fmt.Sprintf("forgot %s.%s", subject, *keyPtr)} } return tools.Result{Output: fmt.Sprintf("forgot all facts about %s", subject)} }, } } // NewMemoryClearContext creates a tool that clears the conversation window. func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "memory_clear_context", Description: "Clear the conversation context window. Useful to start fresh. Omit room_id to clear the current room.", Parameters: []tools.Param{ {Name: "room_id", Type: "string", Description: "Optional room ID to clear; defaults to current room", Required: false}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { roomID := tools.GetString(args, "room_id") if roomID == "" { roomID = roomCtx.Get() } if roomID == "" { return tools.Result{Err: fmt.Errorf("memory_clear_context: no room_id provided and no current room")} } clearer.ClearWindow(roomID) return tools.Result{Output: fmt.Sprintf("conversation context cleared for room %s", roomID)} }, } } // NewMemorySummary creates a tool that saves an important summary to long-term memory. func NewMemorySummary(agentID string, store MemoryStore) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "memory_summary", Description: "Save an important summary or takeaway from the current conversation to long-term memory.", Parameters: []tools.Param{ {Name: "text", Type: "string", Description: "The summary text to save", Required: true}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { text := tools.GetString(args, "text") if text == "" { return tools.Result{Err: fmt.Errorf("memory_summary: text is required")} } key := time.Now().UTC().Format("2006-01-02T15:04:05") err := store.SaveFact(ctx, memory.Fact{ AgentID: agentID, Subject: "_summary", Key: key, Value: text, }) if err != nil { return tools.Result{Err: fmt.Errorf("memory_summary: %w", err)} } return tools.Result{Output: "summary saved"} }, } }