package memory import ( "context" "errors" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) var ( // ErrNilAdapter 在构建或使用 LLMExtractor 却未提供 // 驱动提取的 model.Adapter 时返回。 ErrNilAdapter = errors.New("memory: nil model adapter") ) // Extractor 从一次对话中推导候选记忆条目(design §1)。 // // 有两个实现: // // - LLMExtractor:让 model.Adapter 对每轮对话去噪去重, // 同步运行(Extract)或在后台 goroutine 上运行 // (ExtractAsync)。 // - ManualExtractor:确定性、规则式 —— 只记录用户 // 显式要求记住的内容(「记住 X」)。 type Extractor interface { // Extract 返回对话中值得持久化的记忆条目。ctx // 控制取消;ctx 被取消时返回错误。 Extract(ctx context.Context, conversation []model.Message) ([]MemoryItem, error) } // ManualExtractor 实现不经 LLM 的手动「记住 X」提取路径。 // 它扫描用户轮次中领起的记住指令,为每条产出一个记忆 // 条目,普通对话则不产出。它是 M4 LLM 提取器的 // 确定性对照实现。 type ManualExtractor struct { personaID string roleID string } // NewManualExtractor 构建 ManualExtractor,其提取出的条目以 // personaID/roleID 为作者。与 LLM 提取器一样,角色是 // 默认值而非权威:多角色调用方以空 // 角色构建它,并在持久化时用该轮的角色给条目盖章 // (design/22 §4.2),因此要构建与角色无关的提取器就传 ""。 func NewManualExtractor(personaID, roleID string) *ManualExtractor { return &ManualExtractor{personaID: personaID, roleID: roleID} } // Extract 为每个要求代理记住点什么的用户 // 轮次返回一条记忆条目(kind 为 relationship、作用域为 persona)。领起的 // 「记住」/「请记住」指令会从落库内容中剥离;没有该指令的轮次跳过。 // // 🔴 **档位显式置 long**(迁移 0026 / 主人 2026-10-26 对「人工动作默认 long」的裁定): // 这是**人明确要求记住**的路径,留存规则不许把它降成临时层(否则「记住我现在在 // 测通道」会被判 temp,7 天后被静默删掉)。注意这里**不能靠护栏①**( // extract_retention.go 的「显式记住一票否决」)兜底:`rememberDirective` 在上一步 // 已经把「记住」前缀剥掉了,落库正文里没有那两个词,护栏① 永远看不到它 —— // 因此必须在**产出侧**把档位写死(retention.go 的 resolveRetention 第一条分支 // 就是「调用方显式档位不许被规则推翻」)。 func (e *ManualExtractor) Extract(ctx context.Context, conversation []model.Message) ([]MemoryItem, error) { if err := ctx.Err(); err != nil { return nil, err } items := make([]MemoryItem, 0) for _, msg := range conversation { if msg.Role != model.RoleUser { continue } text := strings.TrimSpace(messageText(msg)) content, ok := rememberDirective(text) if !ok { continue } items = append(items, MemoryItem{ PersonaID: e.personaID, RoleID: e.roleID, Scope: ScopePersona, Kind: KindRelationship, Content: content, Confidence: 1.0, DecayWeight: 1.0, RetentionTier: RetentionLong, }) } return items, nil } // rememberDirective 在 text 是记住指令时剥离「记住」/「请记住」前缀。 // 它返回被记住的内容与 true;text 不是显式记住请求时 // ok=false。 func rememberDirective(text string) (string, bool) { for _, prefix := range []string{"请记住", "记住"} { if strings.HasPrefix(text, prefix) { content := strings.TrimSpace(strings.TrimPrefix(text, prefix)) if content == "" { return "", false } return content, true } } return "", false }