Files
Cyrene_v2/internal/runtime/memory/working.go
T
AskaEth 61cf80d5ee
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/memory(31 文件,纯替换)
- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后,
  逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致
- 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文
- 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
2026-10-04 13:32:54 +08:00

148 lines
4.0 KiB
Go

package memory
import (
"strconv"
"strings"
"sync"
"unicode/utf8"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// 工作记忆的窗口大小与摘要常量。
const (
// defaultWorkingWindow 是 NewWorkingMemory 以非正 size
// 调用时滑动窗口默认保留的消息数。
defaultWorkingWindow = 20
// summaryLastTextMax 是朴素摘要中所含末条消息文本
// 的最大 rune 数,用于保持自摘要紧凑。
summaryLastTextMax = 80
)
// WorkingMemory 是每会话的短期记忆:当前对话消息的
// 滑动窗口加一份摘要(design §0 layer ①)。它是
// 代理推理所依据的即时上下文。
//
// 窗口有界:超出上限的添加会淘汰最旧的消息。
// Summarize 返回经 SetSummary 设置的摘要,或按需计算的
// 朴素结构化摘要。LLM 生成的摘要推迟到 M4;管线
// 已就位,摘要器可调用 SetSummary。
//
// WorkingMemory 并发安全;Add/SetSummary 以写锁串行,
// Window/Summarize 在读锁下读取。
type WorkingMemory struct {
mu sync.RWMutex
window []model.Message
max int
summary string
}
// NewWorkingMemory 构建一个工作记忆,滑动窗口最多
// maxMessages 条消息。非正 maxMessages 选取 defaultWorkingWindow。
func NewWorkingMemory(maxMessages int) *WorkingMemory {
if maxMessages <= 0 {
maxMessages = defaultWorkingWindow
}
return &WorkingMemory{max: maxMessages}
}
// Add 把 msg 追加到窗口,并在窗口超出上限时
// 淘汰最旧的消息。
func (w *WorkingMemory) Add(msg model.Message) {
w.mu.Lock()
defer w.mu.Unlock()
w.window = append(w.window, msg)
if len(w.window) > w.max {
overflow := len(w.window) - w.max
copy(w.window, w.window[overflow:])
w.window = w.window[:w.max]
}
}
// Window 返回当前消息窗口的深拷贝,最旧的在前。
// 返回的切片与每条消息的 content 部分都与内部
// 状态脱离,因此调用方可以随意修改。
func (w *WorkingMemory) Window() []model.Message {
w.mu.RLock()
defer w.mu.RUnlock()
out := make([]model.Message, len(w.window))
for i, m := range w.window {
out[i] = cloneMessage(m)
}
return out
}
// cloneMessage 返回 m 的副本,其 content 部分与源脱离。
// model.Content 的每个字段都是值类型,因此复制 parts
// 切片就足够了。
func cloneMessage(m model.Message) model.Message {
parts := make([]model.Content, len(m.Content))
copy(parts, m.Content)
m.Content = parts
return m
}
// SetSummary 记录一条显式摘要(例如 M4 中由 LLM 产出)。
func (w *WorkingMemory) SetSummary(s string) {
w.mu.Lock()
defer w.mu.Unlock()
w.summary = s
}
// Summarize 在已设置摘要时返回存储的摘要,否则返回由当前
// 窗口推导的朴素结构化摘要。它从不修改状态,因此
// 重复调用返回与调用时窗口一致的值。
func (w *WorkingMemory) Summarize() string {
w.mu.RLock()
defer w.mu.RUnlock()
if w.summary != "" {
return w.summary
}
return w.naiveSummaryLocked()
}
// naiveSummaryLocked 构造 "{n} 条消息, {role} {count}, ...; 最近: {text}"。
// 它要求持有 w.mu(读锁或写锁)。
func (w *WorkingMemory) naiveSummaryLocked() string {
if len(w.window) == 0 {
return ""
}
counts := make(map[model.Role]int)
last := ""
for _, m := range w.window {
counts[m.Role]++
last = messageText(m)
}
var b strings.Builder
b.WriteString(strconv.Itoa(len(w.window)))
b.WriteString(" 条消息")
for _, role := range []model.Role{model.RoleSystem, model.RoleUser, model.RoleAssistant, model.RoleTool} {
if c := counts[role]; c > 0 {
b.WriteString(", ")
b.WriteString(string(role))
b.WriteByte(' ')
b.WriteString(strconv.Itoa(c))
}
}
if last != "" {
b.WriteString("; 最近: ")
b.WriteString(truncateRunes(last, summaryLastTextMax))
}
return b.String()
}
// truncateRunes 把 s 截到最多 n 个 rune。Unicode 安全:它绝不把
// 多字节 rune 切一半。
func truncateRunes(s string, n int) string {
if utf8.RuneCountInString(s) <= n {
return s
}
runes := []rune(s)
return string(runes[:n])
}