// Package memory 定义记忆端口(N3 WP5)。宿主注入 Store 实现;循环在每次 // 请求前把 Recall 结果以系统消息注入请求副本(不落盘、不累计——与 // reminders 同款的 request-scoped 语义);nil = 不启用。 // // 注入文案与产品既有 [记忆] 形态对齐(迁移前后同文): // // [记忆] // - (typ) content // // 排序确定性:按 (Type, Content) 字典序(同一 Recall 结果集在任何 // 消费方下渲染一致)。 package memory import ( "context" "fmt" "sort" "strings" "time" ) // Entry 是一条记忆。Scope 为 user 或 project。 type Entry struct { ID string Scope string ProjectID string Type string Content string Source string Confidence float64 CreatedAt time.Time ExpiresAt *time.Time } // RecallQuery 是一次召回。Scope 空 = 不限(含 user/project);Limit ≤0 = 20。 type RecallQuery struct { Scope string Limit int } // Store 是记忆端口。实现方自担 owner/项目上下文(循环不传租户态)。 type Store interface { Recall(ctx context.Context, q RecallQuery) ([]Entry, error) Append(ctx context.Context, e Entry) error } // MaxRecallEntries 是注入的条目上限(与产品既有形态一致)。 const MaxRecallEntries = 20 // Block 把一次 Recall 结果渲染成注入文本;空结果返回空串(调用方不注入)。 // 过期与空内容条目在渲染时剔除(与产品既有 InjectMemory 同语义)。 func Block(entries []Entry, now time.Time) string { live := make([]Entry, 0, len(entries)) for _, e := range entries { if e.ExpiresAt != nil && !e.ExpiresAt.After(now) { continue } if strings.TrimSpace(e.Content) == "" { continue } live = append(live, e) } if len(live) == 0 { return "" } sort.Slice(live, func(i, j int) bool { if live[i].Type != live[j].Type { return live[i].Type < live[j].Type } return live[i].Content < live[j].Content }) var b strings.Builder b.WriteString("[记忆]\n") n := 0 for _, e := range live { fmt.Fprintf(&b, "- (%s) %s\n", e.Type, strings.TrimSpace(e.Content)) n++ if n >= MaxRecallEntries { break } } return b.String() }