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 个既有未格式化文件(改动前即如此,未顺手修)
This commit is contained in:
1 parent
eb1828ee7a
commit
61cf80d5ee
31 files changed
+820
-820
No files matched your search
+108
-108
@@ -1,30 +1,30 @@
|
||||
// Package memory implements the Cyrene v2 memory subsystem described in
|
||||
// DevDocs/design/03-记忆.md.
|
||||
// memory 包实现 Cyrene v2 记忆子系统,规格见
|
||||
// DevDocs/design/03-记忆.md。
|
||||
//
|
||||
// It exposes three layers:
|
||||
// 它对外暴露三层:
|
||||
//
|
||||
// - Store: a SQLite-backed long-term memory store. It persists memory items
|
||||
// with persona/role isolation (design §0 layer ②, §4), applies the
|
||||
// three-scope access rules (design §3) via AddScoped/ListScope/ListProject/
|
||||
// DeleteScoped, and offers Add/Get/ListByPersona/Delete plus a manual
|
||||
// "记住" path (design §1). Each item may carry a project_id linking the
|
||||
// project-index hierarchy.
|
||||
// - WorkingMemory: a per-session sliding window of messages plus a summary
|
||||
// (design §0 layer ①).
|
||||
// - Retriever: relevance (keyword + optional semantic) × decay retrieval with
|
||||
// source provenance and context injection (design §2). It accepts an optional
|
||||
// vector.Index for the persistent semantic path and a projectindex.Store so
|
||||
// project-scoped hits surface their linked project entry.
|
||||
// - Store:基于 SQLite 的长期记忆存储。它按角色隔离持久化记忆条目
|
||||
// (design §0 layer ②、§4),经 AddScoped/ListScope/ListProject/
|
||||
// DeleteScoped 施加三作用域访问规则(design §3),并提供
|
||||
// Add/Get/ListByPersona/Delete,外加一条手动的
|
||||
// 「记住」路径(design §1)。每条条目可携带 project_id,链到
|
||||
// 项目索引层级。
|
||||
// - WorkingMemory:每会话的消息滑动窗口加一份摘要
|
||||
// (design §0 layer ①)。
|
||||
// - Retriever:相关性(关键词 + 可选语义)× 衰减检索,带
|
||||
// 溯源与上下文注入(design §2)。它可选接收
|
||||
// vector.Index 作为持久化语义路径,以及 projectindex.Store,使
|
||||
// 项目作用域的命中带出其链接的项目条目。
|
||||
//
|
||||
// Extraction (design §1) is exposed through the Extractor interface with two
|
||||
// implementations: ManualExtractor (rule-based, records an explicit "记住 X"
|
||||
// directive) and LLMExtractor (drives a model.Adapter to denoise and deduplicate
|
||||
// a turn, running synchronously via Extract or asynchronously via ExtractAsync).
|
||||
// 提取(design §1)经 Extractor 接口暴露,有两个
|
||||
// 实现:ManualExtractor(规则式,记录一条显式的「记住 X」
|
||||
// 指令)与 LLMExtractor(驱动 model.Adapter 对
|
||||
// 一轮对话去噪去重,经 Extract 同步运行或经 ExtractAsync 异步运行)。
|
||||
//
|
||||
// Milestone M4 is implemented here: the schema carries decay_weight and
|
||||
// project_id, the retriever plugs an optional embedding.Embedder / vector.Index
|
||||
// and a project-index store, and the store enforces the user/persona/project
|
||||
// scope rules. No external dependency is needed beyond the existing packages.
|
||||
// M4 里程碑在此实现:schema 携带 decay_weight 与
|
||||
// project_id,检索器接入可选的 embedding.Embedder / vector.Index
|
||||
// 与项目索引存储,存储层施加 user/persona/project
|
||||
// 作用域规则。除既有包外不需要任何外部依赖。
|
||||
package memory
|
||||
|
||||
import (
|
||||
@@ -43,20 +43,20 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
)
|
||||
|
||||
// Scope classifies who a long-term memory belongs to. It is orthogonal to Kind
|
||||
// and both are stored on every item (design §3 note).
|
||||
// Scope 分类一条长期记忆归属于谁。它与 Kind 正交,
|
||||
// 两者都存于每条条目上(design §3 注)。
|
||||
type Scope string
|
||||
|
||||
const (
|
||||
// ScopeUser is a memory about a user's habits, shared across personas.
|
||||
// ScopeUser 是关于用户习惯的记忆,跨角色共享。
|
||||
ScopeUser Scope = "user"
|
||||
// ScopePersona is a persona's everyday memory; other personas are read-only.
|
||||
// ScopePersona 是角色的日常记忆;其他角色只读。
|
||||
ScopePersona Scope = "persona"
|
||||
// ScopeProject is a project-scoped memory, readable and writable by all.
|
||||
// ScopeProject 是项目作用域记忆,人人可读可写。
|
||||
ScopeProject Scope = "project"
|
||||
)
|
||||
|
||||
// valid reports whether s is one of the defined scopes.
|
||||
// valid 报告 s 是否为已定义的作用域之一。
|
||||
func (s Scope) valid() bool {
|
||||
switch s {
|
||||
case ScopeUser, ScopePersona, ScopeProject:
|
||||
@@ -66,19 +66,19 @@ func (s Scope) valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Kind classifies the content category of a long-term memory item.
|
||||
// Kind 分类一条长期记忆条目的内容类别。
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
// KindRelationship is a memory about "you and me": preferences, habits,
|
||||
// agreements (design §0 "关系记忆").
|
||||
// KindRelationship 是关于「你和我」的记忆:偏好、习惯、
|
||||
// 约定(design §0「关系记忆」)。
|
||||
KindRelationship Kind = "relationship"
|
||||
// KindWorldEvent is a memory about objective facts and events in the world
|
||||
// (design §0 "世界/事件记忆").
|
||||
// KindWorldEvent 是关于客观事实与世界事件的记忆
|
||||
// (design §0「世界/事件记忆」)。
|
||||
KindWorldEvent Kind = "world_event"
|
||||
)
|
||||
|
||||
// valid reports whether k is one of the defined kinds.
|
||||
// valid 报告 k 是否为已定义的 kind 之一。
|
||||
func (k Kind) valid() bool {
|
||||
switch k {
|
||||
case KindRelationship, KindWorldEvent:
|
||||
@@ -88,9 +88,9 @@ func (k Kind) valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// MemoryItem is a single long-term memory record. The zero value is not a valid
|
||||
// item: PersonaID and Content are required, Scope and Kind are normalized to
|
||||
// their defaults by Add.
|
||||
// MemoryItem 是一条长期记忆记录。零值不是合法
|
||||
// 条目:PersonaID 与 Content 必填,Scope 与 Kind 由 Add 规范化到
|
||||
// 其默认值。
|
||||
type MemoryItem struct {
|
||||
ID string
|
||||
PersonaID string
|
||||
@@ -107,59 +107,59 @@ type MemoryItem struct {
|
||||
DecayWeight float64
|
||||
}
|
||||
|
||||
// Sentinels distinguishing memory package failures. Each is wrapped with %w at
|
||||
// the call site so callers can use errors.Is.
|
||||
// 区分 memory 包各类失败的哨兵错误。每个都在
|
||||
// 调用点用 %w 包裹,调用方可用 errors.Is 判断。
|
||||
var (
|
||||
// ErrMissingPersona is returned when an item has no PersonaID.
|
||||
// ErrMissingPersona 在条目没有 PersonaID 时返回。
|
||||
ErrMissingPersona = errors.New("memory: persona_id is required")
|
||||
|
||||
// ErrMissingContent is returned when an item has no content.
|
||||
// ErrMissingContent 在条目没有内容时返回。
|
||||
ErrMissingContent = errors.New("memory: content is required")
|
||||
|
||||
// ErrInvalidScope is returned when an item's Scope is not a defined scope.
|
||||
// ErrInvalidScope 在条目的 Scope 不是已定义作用域时返回。
|
||||
ErrInvalidScope = errors.New("memory: invalid scope")
|
||||
|
||||
// ErrInvalidKind is returned when an item's Kind is not a defined kind.
|
||||
// ErrInvalidKind 在条目的 Kind 不是已定义 kind 时返回。
|
||||
ErrInvalidKind = errors.New("memory: invalid kind")
|
||||
|
||||
// ErrNotFound is returned when an item id does not exist.
|
||||
// ErrNotFound 在条目 id 不存在时返回。
|
||||
ErrNotFound = errors.New("memory: item not found")
|
||||
|
||||
// ErrNilDB is returned when NewStore receives a nil *sql.DB.
|
||||
// ErrNilDB 在 NewStore 收到 nil *sql.DB 时返回。
|
||||
ErrNilDB = errors.New("memory: nil db")
|
||||
)
|
||||
|
||||
// Schema defaults and time-window constants.
|
||||
// schema 默认值与时间窗口常量。
|
||||
const (
|
||||
defaultConfidence = 1.0
|
||||
defaultDecayWeight = 1.0
|
||||
)
|
||||
|
||||
// Store is a long-term memory store backed by a *sql.DB.
|
||||
// Store 是基于 *sql.DB 的长期记忆存储。
|
||||
//
|
||||
// The store owns its schema: NewStore creates the memory_items table and its
|
||||
// indexes idempotently. Concurrency is delegated to *sql.DB, which is safe for
|
||||
// concurrent use by contract, so Store holds no further lock. Every query is
|
||||
// parameterized (?) and lists columns explicitly (no SELECT *), and reads take
|
||||
// a context.Cancel.
|
||||
// 存储自己拥有 schema:NewStore 幂等地创建 memory_items 表及其
|
||||
// 索引。并发交由 *sql.DB,按约定它可安全
|
||||
// 并发使用,因此 Store 不再持锁。每个查询都
|
||||
// 参数化(?)并显式列出列(无 SELECT *),读取带
|
||||
// context.Cancel。
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// rowScanner is satisfied by both *sql.Row and *sql.Rows, so a single scanItem
|
||||
// helper serves Get and ListByPersona.
|
||||
// rowScanner 同时被 *sql.Row 与 *sql.Rows 满足,因此单个 scanItem
|
||||
// 辅助函数即可服务 Get 与 ListByPersona。
|
||||
type rowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
// NewStore builds a Store on db, ensuring the memory_items schema exists.
|
||||
// NewStore 在 db 上构建 Store,确保 memory_items schema 存在。
|
||||
//
|
||||
// db must be a live, migrated connection (the migration framework itself is
|
||||
// owned by internal/db). A nil db returns ErrNilDB. log may be nil, in which
|
||||
// case a discard logger is used. ctx controls schema creation and must be
|
||||
// honored by the caller. Any legacy variable-width timestamps are normalized in
|
||||
// place (see normalizeMemoryTimestamps), so text ordering matches time ordering.
|
||||
// db 必须是存活且已迁移的连接(迁移框架本身由
|
||||
// internal/db 拥有)。nil db 返回 ErrNilDB。log 可为 nil,此时
|
||||
// 使用丢弃型 logger。ctx 控制 schema 创建,调用方必须
|
||||
// 遵守。任何旧的变宽时间戳都就地
|
||||
// 规范化(见 normalizeMemoryTimestamps),使文本顺序与时间顺序一致。
|
||||
func NewStore(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error) {
|
||||
if db == nil {
|
||||
return nil, ErrNilDB
|
||||
@@ -183,9 +183,9 @@ func NewStore(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error)
|
||||
return &Store{db: db, log: log}, nil
|
||||
}
|
||||
|
||||
// Add persists item, assigning an id and missing timestamps, and normalizing
|
||||
// Scope/Kind/Confidence/DecayWeight to their defaults. It returns the stored
|
||||
// item with the id and timestamps filled in.
|
||||
// Add 持久化 item,分配 id 与缺失的时间戳,并把
|
||||
// Scope/Kind/Confidence/DecayWeight 规范化到默认值。它返回已落库的
|
||||
// 条目,id 与时间戳均已填充。
|
||||
func (s *Store) Add(ctx context.Context, item MemoryItem) (MemoryItem, error) {
|
||||
if err := item.normalize(); err != nil {
|
||||
return MemoryItem{}, err
|
||||
@@ -212,8 +212,8 @@ func (s *Store) Add(ctx context.Context, item MemoryItem) (MemoryItem, error) {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// Get returns the item with the given id. It returns ErrNotFound when the id is
|
||||
// empty or does not exist.
|
||||
// Get 返回给定 id 的条目。id 为空或
|
||||
// 不存在时返回 ErrNotFound。
|
||||
func (s *Store) Get(ctx context.Context, id string) (MemoryItem, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return MemoryItem{}, ErrNotFound
|
||||
@@ -231,13 +231,13 @@ func (s *Store) Get(ctx context.Context, id string) (MemoryItem, error) {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// ListByPersona returns every item for personaID, newest first. rowid breaks
|
||||
// created_at ties so items written within the same clock tick keep a stable
|
||||
// newest-first order. It is the persona-isolation boundary: items belonging to
|
||||
// other personas are never returned, so personas never share their own 角色日常
|
||||
// namespace (design §4). It is only the *own* half of the retrieval candidate
|
||||
// set — the retriever unions it with the shared user/project scopes before
|
||||
// ranking (design §3/§4, see Retriever.visibleCandidates).
|
||||
// ListByPersona 返回 personaID 的全部条目,最新的在前。rowid 打破
|
||||
// created_at 并列,使同一时钟刻内写入的条目保持稳定的
|
||||
// 最新优先顺序。它是角色隔离边界:属于
|
||||
// 其他角色的条目绝不返回,因此角色之间从不共享各自的角色日常
|
||||
// 命名空间(design §4)。它只是检索候选集的*自有*
|
||||
// 那一半 —— 检索器在排名前把它与共享的用户/项目作用域
|
||||
// 取并集(design §3/§4,见 Retriever.visibleCandidates)。
|
||||
func (s *Store) ListByPersona(ctx context.Context, personaID string) ([]MemoryItem, error) {
|
||||
if strings.TrimSpace(personaID) == "" {
|
||||
return nil, ErrMissingPersona
|
||||
@@ -252,12 +252,12 @@ func (s *Store) ListByPersona(ctx context.Context, personaID string) ([]MemoryIt
|
||||
return scanItems(rows, "persona item")
|
||||
}
|
||||
|
||||
// List returns every memory item, newest first, across all personas. rowid
|
||||
// breaks created_at ties so items written within the same clock tick keep a
|
||||
// stable newest-first order. It is the aggregate/admin view that enumerates all
|
||||
// persisted memory regardless of the persona-isolation boundary; callers that
|
||||
// must respect isolation should use ListByPersona or ListScope instead. The
|
||||
// returned slice is non-nil.
|
||||
// List 返回跨所有角色的每条记忆条目,最新的在前。rowid
|
||||
// 打破 created_at 并列,使同一时钟刻内写入的条目保持
|
||||
// 稳定的最新优先顺序。它是聚合/管理视图,枚举全部
|
||||
// 已持久化记忆,不受角色隔离边界限制;必须
|
||||
// 遵守隔离的调用方应改用 ListByPersona 或 ListScope。返回的
|
||||
// 切片非 nil。
|
||||
func (s *Store) List(ctx context.Context) ([]MemoryItem, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id, persona_id, role_id, project_id, scope, kind,
|
||||
content, tags, confidence, source_conversation, created_at, updated_at,
|
||||
@@ -269,9 +269,9 @@ func (s *Store) List(ctx context.Context) ([]MemoryItem, error) {
|
||||
return scanItems(rows, "item")
|
||||
}
|
||||
|
||||
// scanItems reads every row into a MemoryItem slice, closing rows and wrapping
|
||||
// scan/iterate failures with the operation name. A non-nil, possibly empty slice
|
||||
// is always returned on success.
|
||||
// scanItems 把每一行读入 MemoryItem 切片,关闭 rows 并用
|
||||
// 操作名包裹 scan/遍历失败。成功时总是返回
|
||||
// 非 nil、可能为空的切片。
|
||||
func scanItems(rows *sql.Rows, op string) ([]MemoryItem, error) {
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
@@ -289,10 +289,10 @@ func scanItems(rows *sql.Rows, op string) ([]MemoryItem, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Delete removes the item with the given id and returns ErrNotFound when the id
|
||||
// is empty or no row was removed. Callers should only pass ids they obtained
|
||||
// through a persona-scoped read (ListByPersona/Get) so deletion stays within
|
||||
// the caller's persona boundary.
|
||||
// Delete 删除给定 id 的条目,id 为空或
|
||||
// 没有删除任何行时返回 ErrNotFound。调用方只应传入经
|
||||
// 角色作用域读取(ListByPersona/Get)得到的 id,使删除始终留在
|
||||
// 调用方的角色边界内。
|
||||
func (s *Store) Delete(ctx context.Context, id string) error {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return ErrNotFound
|
||||
@@ -311,9 +311,9 @@ func (s *Store) Delete(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RememberManual writes a single long-term memory item for content text — the
|
||||
// "记住 X" path from design §1. The item is scoped to the persona by default and
|
||||
// recorded as a relationship memory at full confidence. Returns the stored item.
|
||||
// RememberManual 为 content 文本写入一条长期记忆条目 —— design §1 中的
|
||||
// 「记住 X」路径。条目默认取角色作用域,
|
||||
// 并以满置信度记为关系记忆。返回已落库的条目。
|
||||
func (s *Store) RememberManual(ctx context.Context, personaID, roleID, text string) (MemoryItem, error) {
|
||||
item := MemoryItem{
|
||||
PersonaID: personaID,
|
||||
@@ -328,9 +328,9 @@ func (s *Store) RememberManual(ctx context.Context, personaID, roleID, text stri
|
||||
return s.Add(ctx, item)
|
||||
}
|
||||
|
||||
// normalize fills default Scope/Kind/Confidence/DecayWeight then validates.
|
||||
// It is called by Add so callers may omit values while still catching
|
||||
// explicitly invalid ones.
|
||||
// normalize 填充默认的 Scope/Kind/Confidence/DecayWeight,然后校验。
|
||||
// 它由 Add 调用,因此调用方可以省略取值,同时仍能
|
||||
// 捕获显式非法值。
|
||||
func (it *MemoryItem) normalize() error {
|
||||
if it.Scope == "" {
|
||||
it.Scope = ScopePersona
|
||||
@@ -347,8 +347,8 @@ func (it *MemoryItem) normalize() error {
|
||||
return it.validate()
|
||||
}
|
||||
|
||||
// validate checks required and enumerated fields. It is pure: it does not apply
|
||||
// defaults, so it can also be used to reject a caller-provided value.
|
||||
// validate 检查必填与枚举字段。它是纯函数:不施加
|
||||
// 默认值,因此也可用于拒绝调用方提供的值。
|
||||
func (it *MemoryItem) validate() error {
|
||||
if strings.TrimSpace(it.PersonaID) == "" {
|
||||
return ErrMissingPersona
|
||||
@@ -365,8 +365,8 @@ func (it *MemoryItem) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyTimestamps assigns an id and missing CreatedAt/UpdatedAt values. It
|
||||
// returns an error only when a fresh id cannot be generated.
|
||||
// applyTimestamps 分配 id 与缺失的 CreatedAt/UpdatedAt 值。
|
||||
// 仅在无法生成新 id 时返回错误。
|
||||
func applyTimestamps(item MemoryItem) (MemoryItem, error) {
|
||||
now := time.Now().UTC()
|
||||
if item.ID == "" {
|
||||
@@ -385,9 +385,9 @@ func applyTimestamps(item MemoryItem) (MemoryItem, error) {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// scanItem reads one memory_items row. tags is stored as a JSON array string;
|
||||
// timestamps are stored as fixed-width UTC strings (timeLayout) so text
|
||||
// comparison orders them chronologically.
|
||||
// scanItem 读取一行 memory_items。tags 以 JSON 数组字符串存储;
|
||||
// 时间戳以定宽 UTC 字符串(timeLayout)存储,因此文本
|
||||
// 比较即可按时间排序。
|
||||
func scanItem(sc rowScanner) (MemoryItem, error) {
|
||||
var (
|
||||
it MemoryItem
|
||||
@@ -419,9 +419,9 @@ func scanItem(sc rowScanner) (MemoryItem, error) {
|
||||
return it, nil
|
||||
}
|
||||
|
||||
// newID returns a 128-bit cryptographically random id, hex-encoded. It avoids a
|
||||
// UUID dependency so the package stays self-contained. rand.Read fails only if
|
||||
// the platform entropy source is broken, so the error is propagated.
|
||||
// newID 返回一个 128 位密码学随机 id,十六进制编码。它避开
|
||||
// UUID 依赖,使本包保持自包含。rand.Read 只在
|
||||
// 平台熵源损坏时失败,因此错误向上传播。
|
||||
func newID() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
@@ -430,7 +430,7 @@ func newID() (string, error) {
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// discardLogger returns log if non-nil, otherwise a logger that writes nowhere.
|
||||
// discardLogger 在 log 非 nil 时返回 log,否则返回一个不写任何地方的 logger。
|
||||
func discardLogger(log *slog.Logger) *slog.Logger {
|
||||
if log != nil {
|
||||
return log
|
||||
@@ -438,11 +438,11 @@ func discardLogger(log *slog.Logger) *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
|
||||
// Schema DDL. The migration framework in internal/db is the authoritative
|
||||
// mechanism for domain tables, but it is intentionally not used here: the db
|
||||
// package's migration tests assert an exact migration count, and the memory
|
||||
// store must be self-contained and usable from any *sql.DB. The table and
|
||||
// indexes are therefore created idempotently by NewStore.
|
||||
// schema DDL。internal/db 中的迁移框架是领域表的权威
|
||||
// 机制,但这里刻意不用它:db 包的迁移测试断言精确的
|
||||
// 迁移数量,而记忆存储必须自包含、
|
||||
// 可在任意 *sql.DB 上使用。表与索引因此
|
||||
// 由 NewStore 幂等创建。
|
||||
const createMemoryItemsTable = `
|
||||
CREATE TABLE IF NOT EXISTS memory_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
|
||||
Reference in new issue
Block a user