package application import ( "encoding/json" "fmt" "regexp" "strings" "unicode/utf8" "img_infinite_canvas/internal/domain/design" ) const ( shortMemoryMessageLimit = 6 shortMemoryNodeLimit = 8 longMemoryArtifactLimit = 16 longMemoryDecisionLimit = 10 shortMemoryMaxRunes = 2400 longMemoryMaxRunes = 3200 ) var agentMemoryURLPattern = regexp.MustCompile(`https?://\S+`) func buildAgentMemory(project design.Project, prompt string, mode string, messages []design.AgentChatMessage) design.AgentMemory { return design.AgentMemory{ ShortTerm: buildShortMemory(project, prompt, mode, messages), LongTerm: buildLongMemory(project), } } func buildShortMemory(project design.Project, prompt string, mode string, messages []design.AgentChatMessage) string { memoryMessages := visibleAgentMemoryMessages(project.Messages) sections := []string{ "Current turn:", "- mode: " + strings.TrimSpace(mode), "- user request: " + strings.TrimSpace(prompt), } if inline := inlineInputMemory(messages); inline != "" { sections = append(sections, "Inline input:", inline) } if recent := recentMessageMemory(memoryMessages, shortMemoryMessageLimit); recent != "" { sections = append(sections, "Recent conversation:", recent) } if canvas := canvasNodeMemory(project.Nodes, shortMemoryNodeLimit); canvas != "" { sections = append(sections, "Visible canvas:", canvas) } return clampMemoryBlock(strings.Join(sections, "\n"), shortMemoryMaxRunes) } func buildLongMemory(project design.Project) string { memoryMessages := visibleAgentMemoryMessages(project.Messages) sections := []string{} if strings.TrimSpace(project.Title) != "" || strings.TrimSpace(project.Brief) != "" { sections = append(sections, "Project identity:") if strings.TrimSpace(project.Title) != "" { sections = append(sections, "- title: "+strings.TrimSpace(project.Title)) } if strings.TrimSpace(project.Brief) != "" { sections = append(sections, "- brief: "+shortenMemoryText(project.Brief, 360)) } } if decisions := durableDecisionMemory(memoryMessages, longMemoryDecisionLimit); decisions != "" { sections = append(sections, "Durable user decisions and preferences:", decisions) } if artifacts := completedArtifactMemory(project.Nodes, longMemoryArtifactLimit); artifacts != "" { sections = append(sections, "Completed canvas artifacts:", artifacts) } if research := researchMemory(memoryMessages); research != "" { sections = append(sections, "Prior web research:", research) } if len(sections) == 0 { return "none" } return clampMemoryBlock(strings.Join(sections, "\n"), longMemoryMaxRunes) } func visibleAgentMemoryMessages(messages []design.Message) []design.Message { hiddenThreads := make(map[string]bool) for _, message := range messages { if message.ThreadID == "" { continue } if isCanvasActionMemoryMessage(message) || message.Name == "text_extraction" || message.Type == textExtractionResultMessageType { hiddenThreads[message.ThreadID] = true } } visible := make([]design.Message, 0, len(messages)) for _, message := range messages { if message.ThreadID != "" && hiddenThreads[message.ThreadID] { continue } visible = append(visible, message) } return visible } func isCanvasActionMemoryMessage(message design.Message) bool { return message.Name == "canvas_action" || message.ToolHint == "canvas_action" } func inlineInputMemory(messages []design.AgentChatMessage) string { var lines []string for _, message := range messages { for _, content := range message.Contents { switch strings.ToLower(strings.TrimSpace(content.Type)) { case "text": if strings.EqualFold(strings.TrimSpace(content.TextSource), "settings") { continue } if text := strings.TrimSpace(content.Text); text != "" { text = stripImageGeneratorDirectiveLines(text) if text == "" { continue } lines = append(lines, "- text: "+shortenMemoryText(text, 280)) } case "image": if imageURL := strings.TrimSpace(content.ImageURL); imageURL != "" { size := "" if content.ImageWidth > 0 && content.ImageHeight > 0 { size = fmt.Sprintf(" (%dx%d)", content.ImageWidth, content.ImageHeight) } lines = append(lines, "- reference image"+size) } } } } return strings.Join(lines, "\n") } func recentMessageMemory(messages []design.Message, limit int) string { var lines []string start := 0 if len(messages) > limit { start = len(messages) - limit } omitted := start for _, message := range messages[start:] { if message.Role == "tool" || message.Role == "error" { continue } content := strings.TrimSpace(message.Content) if content == "" { content = strings.TrimSpace(message.Title) } if content == "" { continue } lines = append(lines, fmt.Sprintf("- %s: %s", message.Role, shortenMemoryText(content, 260))) } if omitted > 0 && len(lines) > 0 { lines = append([]string{fmt.Sprintf("- older conversation omitted: %d messages", omitted)}, lines...) } return strings.Join(lines, "\n") } func canvasNodeMemory(nodes []design.Node, limit int) string { var candidates []design.Node for _, node := range nodes { if node.Status == "generating" || node.LayerRole == "pen-stroke" { continue } candidates = append(candidates, node) } if len(candidates) > limit { candidates = candidates[len(candidates)-limit:] } var lines []string for _, node := range candidates { content := strings.TrimSpace(node.Content) if isGeneratedImageContent(content) || strings.HasPrefix(content, "/canvas-assets/") || strings.HasPrefix(content, "http") { content = "asset reference" } if content != "" { content = " | " + shortenMemoryText(content, 160) } lines = append(lines, fmt.Sprintf("- %s %q at %.0f,%.0f %.0fx%.0f%s", node.Type, node.Title, node.X, node.Y, node.Width, node.Height, content)) } return strings.Join(lines, "\n") } func completedArtifactMemory(nodes []design.Node, limit int) string { var lines []string for _, node := range nodes { if node.Status != "success" && node.Status != "ready" && node.Status != "" { continue } if node.Type != design.NodeTypeImage && node.Type != design.NodeTypeFrame && node.Type != design.NodeTypeText { continue } content := strings.TrimSpace(node.Content) if len(content) > 120 { content = shortenMemoryText(content, 120) } line := fmt.Sprintf("- %s: %s", node.Type, strings.TrimSpace(node.Title)) if content != "" && (strings.HasPrefix(content, "http") || strings.HasPrefix(content, "/canvas-assets/")) { line += " | asset reference" } lines = append(lines, line) } if len(lines) > limit { lines = lines[len(lines)-limit:] } return strings.Join(lines, "\n") } func durableDecisionMemory(messages []design.Message, limit int) string { keywords := []string{ "保留", "不要", "必须", "参考", "风格", "品牌", "颜色", "色彩", "字体", "logo", "主视觉", "电商", "官网", "keep", "avoid", "must", "reference", "style", "brand", "color", "typography", "logo", "hero", "ecommerce", } var lines []string for _, message := range messages { if message.Role != "user" { continue } content := strings.TrimSpace(message.Content) if content == "" || !containsAnyFold(content, keywords) { continue } lines = append(lines, "- "+shortenMemoryText(content, 260)) } if len(lines) > limit { lines = lines[len(lines)-limit:] } return strings.Join(lines, "\n") } func researchMemory(messages []design.Message) string { var lines []string for _, message := range messages { if message.Role != "tool" || message.Title != "Synthesize Skills" { continue } var report struct { Query string `json:"query"` Results []struct { Title string `json:"title"` URL string `json:"url"` Snippet string `json:"snippet"` } `json:"results"` } if err := json.Unmarshal([]byte(message.Content), &report); err != nil { continue } if strings.TrimSpace(report.Query) != "" { lines = append(lines, "- query: "+shortenMemoryText(report.Query, 160)) } for _, result := range report.Results { title := strings.TrimSpace(result.Title) if title == "" { continue } line := " result: " + shortenMemoryText(title, 140) if snippet := strings.TrimSpace(result.Snippet); snippet != "" { line += " | " + shortenMemoryText(snippet, 160) } lines = append(lines, line) if len(lines) >= 10 { break } } } if len(lines) > 10 { lines = lines[len(lines)-10:] } return strings.Join(lines, "\n") } func containsAnyFold(value string, keywords []string) bool { value = strings.ToLower(value) for _, keyword := range keywords { if strings.Contains(value, strings.ToLower(keyword)) { return true } } return false } func shortenMemoryText(value string, limit int) string { value = sanitizeMemoryText(value) value = strings.Join(strings.Fields(strings.TrimSpace(value)), " ") if limit <= 0 { return value } runes := []rune(value) if len(runes) <= limit { return value } return string(runes[:limit]) + "..." } func sanitizeMemoryText(value string) string { return agentMemoryURLPattern.ReplaceAllString(strings.TrimSpace(value), "[reference]") } func clampMemoryBlock(value string, limit int) string { value = strings.TrimSpace(sanitizeMemoryText(value)) if limit <= 0 || utf8.RuneCountInString(value) <= limit { return value } runes := []rune(value) clamped := strings.TrimSpace(string(runes[:limit])) if cut := strings.LastIndex(clamped, "\n"); cut > limit*3/4 { clamped = strings.TrimSpace(clamped[:cut]) } return clamped + "\n- memory window truncated; older low-signal context omitted" }