From b6171e1c1956e3880b7a2ba24478ff4e5c5874e0 Mon Sep 17 00:00:00 2001 From: Mars Date: Sat, 21 Feb 2026 22:22:41 -0500 Subject: [PATCH 1/2] feat(web): memory management in web ui --- interface/src/api/client.ts | 58 +++++ interface/src/routes/AgentMemories.tsx | 283 +++++++++++++++++++++++-- src/api/memories.rs | 226 ++++++++++++++++++++ src/api/server.rs | 8 +- 4 files changed, 559 insertions(+), 16 deletions(-) diff --git a/interface/src/api/client.ts b/interface/src/api/client.ts index 837861d77..857282435 100644 --- a/interface/src/api/client.ts +++ b/interface/src/api/client.ts @@ -378,6 +378,32 @@ export interface MemoriesSearchParams { memory_type?: MemoryType; } +export interface MemoryCreateRequest { + agent_id: string; + content: string; + memory_type?: MemoryType; + importance?: number; +} + +export interface MemoryUpdateRequest { + agent_id: string; + memory_id: string; + content?: string; + memory_type?: MemoryType; + importance?: number; +} + +export interface MemoryWriteResponse { + success: boolean; + memory: MemoryItem; +} + +export interface MemoryDeleteResponse { + success: boolean; + forgotten: boolean; + message: string; +} + export type CortexEventType = | "bulletin_generated" | "bulletin_failed" @@ -961,6 +987,38 @@ export const api = { if (params.sort) search.set("sort", params.sort); return fetchJson(`/agents/memories?${search}`); }, + createMemory: async (request: MemoryCreateRequest) => { + const response = await fetch(`${API_BASE}/agents/memories`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + return response.json() as Promise; + }, + updateMemory: async (request: MemoryUpdateRequest) => { + const response = await fetch(`${API_BASE}/agents/memories`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + return response.json() as Promise; + }, + deleteMemory: async (agentId: string, memoryId: string) => { + const search = new URLSearchParams({ agent_id: agentId, memory_id: memoryId }); + const response = await fetch(`${API_BASE}/agents/memories?${search}`, { + method: "DELETE", + }); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + return response.json() as Promise; + }, searchMemories: (agentId: string, query: string, params: MemoriesSearchParams = {}) => { const search = new URLSearchParams({ agent_id: agentId, q: query }); if (params.limit) search.set("limit", String(params.limit)); diff --git a/interface/src/routes/AgentMemories.tsx b/interface/src/routes/AgentMemories.tsx index 3aba1ec27..bea1db5b0 100644 --- a/interface/src/routes/AgentMemories.tsx +++ b/interface/src/routes/AgentMemories.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useVirtualizer } from "@tanstack/react-virtual"; import { AnimatePresence, motion } from "framer-motion"; import { @@ -13,10 +13,23 @@ import { CortexChatPanel } from "@/components/CortexChatPanel"; import { MemoryGraph } from "@/components/MemoryGraph"; import { Button, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Slider, + TextArea, ToggleGroup, SearchInput, FilterButton, @@ -27,6 +40,28 @@ import { HugeiconsIcon } from "@hugeicons/react"; type ViewMode = "list" | "graph"; +interface MemoryFormData { + content: string; + memory_type: MemoryType; + importance: number; +} + +function emptyFormData(): MemoryFormData { + return { + content: "", + memory_type: "fact", + importance: 0.6, + }; +} + +function memoryToFormData(memory: MemoryItem): MemoryFormData { + return { + content: memory.content, + memory_type: memory.memory_type, + importance: memory.importance, + }; +} + const SORT_OPTIONS: { value: MemorySort; label: string }[] = [ { value: "recent", label: "Recent" }, { value: "importance", label: "Importance" }, @@ -71,6 +106,7 @@ interface AgentMemoriesProps { } export function AgentMemories({ agentId }: AgentMemoriesProps) { + const queryClient = useQueryClient(); const [viewMode, setViewMode] = useState("list"); const [searchQuery, setSearchQuery] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); @@ -78,6 +114,11 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { const [typeFilter, setTypeFilter] = useState(null); const [expandedId, setExpandedId] = useState(null); const [chatOpen, setChatOpen] = useState(true); + const [editorOpen, setEditorOpen] = useState(false); + const [editingMemory, setEditingMemory] = useState(null); + const [formData, setFormData] = useState(emptyFormData()); + const [formError, setFormError] = useState(null); + const [deleteConfirmMemoryId, setDeleteConfirmMemoryId] = useState(null); const parentRef = useRef(null); @@ -128,6 +169,96 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { const isLoading = isSearching ? searchQueryResult.isLoading : listQuery.isLoading; const isError = isSearching ? searchQueryResult.isError : listQuery.isError; + const refreshMemories = () => { + queryClient.invalidateQueries({ queryKey: ["memories", agentId] }); + queryClient.invalidateQueries({ queryKey: ["memories-search", agentId] }); + }; + + const createMutation = useMutation({ + mutationFn: () => { + return api.createMemory({ + agent_id: agentId, + content: formData.content, + memory_type: formData.memory_type, + importance: formData.importance, + }); + }, + onSuccess: () => { + refreshMemories(); + setEditorOpen(false); + setEditingMemory(null); + setFormData(emptyFormData()); + setFormError(null); + }, + onError: () => setFormError("Failed to create memory"), + }); + + const updateMutation = useMutation({ + mutationFn: () => { + if (!editingMemory) throw new Error("No memory selected"); + return api.updateMemory({ + agent_id: agentId, + memory_id: editingMemory.id, + content: formData.content, + memory_type: formData.memory_type, + importance: formData.importance, + }); + }, + onSuccess: () => { + refreshMemories(); + setEditorOpen(false); + setEditingMemory(null); + setFormData(emptyFormData()); + setFormError(null); + }, + onError: () => setFormError("Failed to update memory"), + }); + + const deleteMutation = useMutation({ + mutationFn: (memoryId: string) => api.deleteMemory(agentId, memoryId), + onSuccess: () => { + refreshMemories(); + setDeleteConfirmMemoryId(null); + if (expandedId === deleteConfirmMemoryId) { + setExpandedId(null); + } + }, + }); + + const openCreateDialog = () => { + setEditingMemory(null); + setFormData(emptyFormData()); + setFormError(null); + setEditorOpen(true); + }; + + const openEditDialog = (memory: MemoryItem) => { + setEditingMemory(memory); + setFormData(memoryToFormData(memory)); + setFormError(null); + setEditorOpen(true); + }; + + const saveMemory = () => { + const trimmedContent = formData.content.trim(); + if (!trimmedContent) { + setFormError("Content is required"); + return; + } + + if (formData.importance < 0 || formData.importance > 1) { + setFormError("Importance must be between 0 and 1"); + return; + } + + setFormError(null); + if (editingMemory) { + updateMutation.mutate(); + } else { + createMutation.mutate(); + } + }; + const virtualizer = useVirtualizer({ count: memories.length, getScrollElement: () => parentRef.current, @@ -174,6 +305,10 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { + + {/* View mode toggle */} {/* Table header */} -
+
Type {isSearching ? "Content / Score" : "Content"} Importance - Source Created
@@ -277,7 +411,7 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { + +
+
ID: {memory.id} Accessed: {memory.access_count}x Last accessed: {formatTimeAgo(memory.last_accessed_at)} Updated: {formatTimeAgo(memory.updated_at)} - {memory.channel_id && ( - Channel: {memory.channel_id} - )}
@@ -355,6 +495,119 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { )} + + !open && setEditorOpen(false)}> + + + {editingMemory ? "Edit memory" : "Add memory"} + +
+
+ +