package session import ( "bufio" "encoding/json" "os" "agent-native/runtime/llm" ) // Stats 是转录文件的轻量元数据(扫描即得,无副作用)。 type Stats struct { Lines int Bytes int64 } // Stats 统计当前转录行数与字节数(空行不计)。 func (s *Store) Stats() Stats { s.mu.Lock() defer s.mu.Unlock() st := Stats{} f, err := os.Open(s.path) if err != nil { return st } defer f.Close() sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, 64*1024), 8*1024*1024) for sc.Scan() { if len(sc.Bytes()) == 0 { continue } st.Lines++ } if info, err := f.Stat(); err == nil { st.Bytes = info.Size() } return st } // SaveMeta 把会话元数据写进 .meta.json(创建时间/模型等由调用方定义)。 func (s *Store) SaveMeta(meta map[string]string) error { s.mu.Lock() defer s.mu.Unlock() raw, err := json.MarshalIndent(meta, "", " ") if err != nil { return err } tmp := s.path + ".meta.json.tmp" if err := os.WriteFile(tmp, raw, 0o644); err != nil { return err } return os.Rename(tmp, s.path+".meta.json") } // ReadMeta 读取会话元数据;不存在时返回 nil(新会话)。 func (s *Store) ReadMeta() map[string]string { raw, err := os.ReadFile(s.path + ".meta.json") if err != nil { return nil } var meta map[string]string if err := json.Unmarshal(raw, &meta); err != nil { return nil } return meta } // Compact 把转录改写为「系统消息 + 摘要消息 + 最近 keepLastN 条」。 // 摘要由 summarize 生成(由适配层注入,通常是一次无工具模型调用); // 改写原子落盘(临时文件 + rename)。 // 返回是否发生了压缩(消息数不足/已是紧凑形态时返回 false 且不动文件)。 func (s *Store) Compact(keepLastN int, summarize func(messages []llm.Message) (string, error)) (bool, error) { messages, err := s.Load() if err != nil { return false, err } if len(messages) <= keepLastN+1 { // 系统消息 + 尾部,无可压缩头部 return false, nil } var system *llm.Message rest := messages if len(rest) > 0 && rest[0].Role == llm.RoleSystem { copyOf := rest[0] system = ©Of rest = rest[1:] } tailStart := len(rest) - keepLastN if tailStart <= 0 { return false, nil } head := rest[:tailStart] tail := rest[tailStart:] summary, err := summarize(head) if err != nil { return false, err } summaryMsg := llm.Message{ Role: llm.RoleUser, Content: "[conversation summary]\n" + summary + "\n[recent messages follow]", } var rebuilt []llm.Message if system != nil { rebuilt = append(rebuilt, *system) } rebuilt = append(rebuilt, summaryMsg) rebuilt = append(rebuilt, tail...) s.mu.Lock() defer s.mu.Unlock() tmp := s.path + ".compact.tmp" f, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { return false, err } w := bufio.NewWriter(f) for _, m := range rebuilt { raw, err := json.Marshal(m) if err != nil { _ = f.Close() _ = os.Remove(tmp) return false, err } w.Write(raw) w.WriteByte('\n') } if err := w.Flush(); err != nil { _ = f.Close() _ = os.Remove(tmp) return false, err } _ = f.Close() if err := os.Rename(tmp, s.path); err != nil { _ = os.Remove(tmp) return false, err } return true, nil }