memory

package
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 26, 2026 License: None detected not legal advice Imports: 0 Imported by: 0

Documentation

Overview

Package memory manages the Flyto agent's long-term memory: per-session memories, cross-session recall, extraction from transcripts, relevance scoring, and optional sync to external stores.

Consumer integration surfaces fall into three shapes. For the full taxonomy see `docs/api-reference.md` section "API 消费形态 / API Consumption Patterns".

Synchronous callback — form 3:

  • Store: persistent storage backend (consumers can plug filesystem, DB, or remote store); engine calls CRUD synchronously
  • MemoryExtractor: extracts durable memories from a transcript; engine calls Extract synchronously after turn boundaries
  • RelevanceScorer: ranks memory candidates for a given prompt; engine calls Score synchronously before prompt assembly
  • SyncAdapter: push/pull memories to external systems (Notion, GitHub gist, etc.); engine calls Sync synchronously when configured
  • MemorySelector: AI-driven memory selection; consumer can plug model
  • Backupper: on-demand or scheduled snapshot to backup medium

Pull — form 2:

  • Store.List / Get: consumer querying current memory state (beyond the engine's use, SaaS admin UIs list/inspect memories via Store)

Package memory 管理 Flyto agent 的长期记忆: 每会话记忆 / 跨会话召回 / 从对话记录提取 / 相关性打分 / 可选的外部 store 同步.

消费者接入面分三种形态. 完整分类见 `docs/api-reference.md` "API 消费形态 / API Consumption Patterns" 章节.

同步回调 (callback) —— 形态三:

  • Store: 持久化后端 (消费者可换文件系统 / DB / 远端); 引擎同步调 CRUD
  • MemoryExtractor: 从对话记录抽取持久记忆; 引擎在 turn 边界后同步调 Extract
  • RelevanceScorer: 对记忆候选按当前 prompt 排序; 引擎在拼 prompt 前同步调 Score
  • SyncAdapter: 与外部系统 (Notion / GitHub gist 等) 同步; 配置后引擎同步调 Sync
  • MemorySelector: AI 驱动记忆选取; 消费者可换模型
  • Backupper: 按需或定时快照到备份介质

调取 (pull) —— 形态二:

  • Store.List / Get: 消费者主动查询当前记忆状态 (引擎之外, SaaS admin UI 经 Store 列举/查看记忆)

Package memory 的 YAML frontmatter 解析器.

手写解析,不引入第三方 YAML 库. Frontmatter 格式:文件以 "---" 开头,到下一个 "---" 结束, 中间每行是 "key: value" 格式.

版本兼容(INF-6): Frontmatter 加入 `version` 字段,用于未来 breaking change 的迁移路径. 当前所有文件写入 version: 1.旧文件(无 version 字段)读取时默认为 1.

Package memory 实现记忆系统.

对应原项目中同名模块的功能. 每条记忆是一个独立的 markdown 文件(带 YAML frontmatter), 存储在 ~/.flyto/projects/<project-hash>/memory/ 目录下.

4 种记忆类型:

  • User(用户画像):记住用户的偏好和习惯
  • Feedback(行为指导):根据用户反馈调整行为
  • Project(项目上下文):项目结构,技术栈,约定
  • Reference(外部指针):指向外部资源的链接/摘要

核心流程:

  • Save: 将记忆写为 markdown 文件(frontmatter + body)
  • List: 扫描目录,解析 frontmatter,按 mtime 排序
  • FindRelevant: 基于文本相似度评分选出相关记忆
  • Delete: 删除记忆文件
  • UpdateIndex: 更新 MEMORY.md 索引文件

Package memory 的相关性评分模块.

原项目在 findRelevantMemories 中调用模型来评估记忆和查询的相关性. Go 版本暂时用基于 token 重叠的简单文本相似度代替,避免每次检索都调用 API. 后续可以替换为嵌入向量或模型评估.

Package memory 的记忆目录扫描器.

递归扫描指定目录下的 .md 文件,只读前 30 行提取 frontmatter(性能优化), 按 mtime 排序返回.

Package memory 的相关性评分器接口及实现.

从包级函数 Score() 提升为接口 RelevanceScorer, 支持不同场景注入不同的评分策略.

Package memory 的 AI 记忆选择器.

对应早期实现的 AI 选择逻辑: 用模型从记忆列表中选出与当前查询最相关的 ≤5 条记忆.

升华改进(ELEVATED): 与 RelevanceScorer 的区别--

  • RelevanceScorer: 单条打分(0.0~1.0),适合排序
  • MemorySelector: 批量选择,一次 AI 调用完成,支持额外过滤参数

替代方案:<复用 RelevanceScorer 逐条打分再排序> - 否决: AI 打分成本高,逐条打分 token 消耗是批量选择的数倍.

Package memory 的 MemoryType 分层注册表.

升华改进(ELEVATED): 从 4 个硬编码 const 升级为分层注册制. 编程场景内置 4 种类型,仓储/法律/医疗等场景注册自己的类型. 分层继承支持管理层→运营→加盟仓的组织架构. 替代方案:硬编码 const 枚举(原始设计,只支持 4 种类型).

分层设计的精妙之处(CLEVER): 管理层定义 org 级类型(sla_rule)→ 只读继承给运营和加盟仓 运营定义 team 级类型(exception_pattern)→ 继承给加盟仓 加盟仓定义 local 级类型(warehouse_layout)→ 仅本仓可见 加盟仓可以覆盖上级类型(local 优先级高于 parent)

使用示例:

// 管理层
orgReg := NewTypeRegistry()
orgReg.Register(&MemoryTypeInfo{Name: "sla_rule", Scope: "org", ...})

// 运营团队继承 org
teamReg := NewTypeRegistry(WithParent(orgReg))
teamReg.Register(&MemoryTypeInfo{Name: "exception_pattern", Scope: "team", ...})

// 加盟仓继承全部
localReg := NewTypeRegistry(WithParent(teamReg))
localReg.Register(&MemoryTypeInfo{Name: "warehouse_layout", Scope: "local", ...})

Index

Constants

View Source
const SELECT_MEMORIES_SYSTEM_PROMPT = `` /* 972-byte string literal not displayed */

SELECT_MEMORIES_SYSTEM_PROMPT 是记忆选择的系统提示词.

不翻译为中文的原因: 模型对英文指令的理解更精准(大多数模型的预训练语料以英文为主), 且 Flyto Agent 本身是多语言工具,保留英文可以服务于多语言用户场景.

Variables

View Source
var ErrSyncConflict = &syncConflictError{}

ErrSyncConflict 是 ConflictFail 策略下检测到冲突时返回的错误. 调用方可以用 errors.Is(err, memory.ErrSyncConflict) 判断.

Functions

func AgeDuration

func AgeDuration(modTime time.Time) time.Duration

AgeDuration 返回记忆的年龄(当前时刻 - 最后修改时间). modTime 为零值时返回 0.

func AgeString

func AgeString(d time.Duration) string

AgeString 将年龄 duration 格式化为人类可读字符串.

输出粒度:

  • < 2 小时:显示分钟("45 minutes")
  • 2 小时 - 48 小时:显示小时("6 hours")
  • > 48 小时:显示天("3 days")

精妙之处(CLEVER): 选择"合适粒度"而非始终显示最精细单位-- "2883 minutes" 比 "2 days" 更难理解. 48 小时边界(而非 24 小时)防止"1 day 1 hour"这类边界歧义: 25 小时显示 "25 hours" 而非 "1 day",更准确.

func FormatFrontmatter

func FormatFrontmatter(fm Frontmatter, body string) string

FormatFrontmatter 将 frontmatter 和正文组合成完整的 markdown 内容.

输出格式:

---
name: xxx
description: xxx
type: xxx
version: 1
---
正文内容

升华改进(ELEVATED): 早期方案不写 version 字段,此处新增. 向后兼容:旧版本引擎读到 version 字段时,ParseFrontmatter 的 switch 会静默忽略未知 key(走到 default,不做任何事)-- 实际上由于我们现在处理了 "version" key,旧引擎需要重新编译才能正确解析. 但 JSON 层面旧引擎读不到 version 字段也不会报错(只是 fm.Version == 0). 替代方案:<只在 v2 时才写 version 字段,v1 不写> - 否决原因:如果 v1 文件不写版本,升级到 v2 时无法区分"v1 文件"和"v0 旧文件".

func FreshnessNote

func FreshnessNote(modTime time.Time, threshold time.Duration) string

FreshnessNote 将新鲜度警告包裹为 <system-reminder> 标签格式.

返回空字符串表示不需要警告(ShouldWarn == false). 调用方可直接注入返回值到消息流,空字符串安全可忽略.

精妙之处(CLEVER): 返回值可以直接拼接到消息内容中, 无需调用方做 if note != "" 的判断--空字符串拼接无害. 当然调用方在意性能时仍可先判断再调用.

func FreshnessText

func FreshnessText(modTime time.Time, threshold time.Duration) string

FreshnessText 返回针对该条记忆的完整新鲜度警告文本.

仅在 ShouldWarn(modTime, threshold) == true 时调用才有意义. 调用方应先 ShouldWarn 判断,再决定是否调用本函数.

文本结构参考早期实现的注入风格:

  1. 说明事实(年龄)
  2. 解释含义(记忆是点时刻观测值)
  3. 给出行动建议(主动核实)

升华改进(ELEVATED): 早期实现 无任何新鲜度提示文本,只在 MEMORY.md 里加了 "_(last updated N days ago)_" 注记,完全靠模型自己推断是否过时. 我们加显式的自然语言警告,让模型知道"应该主动核实". 替代方案:<只在 MEMORY.md 里加注记,不加运行时提示> - 否决原因:MEMORY.md 是索引,不保证每轮都被读取;运行时注入才能保证覆盖.

func IndexAnnotation

func IndexAnnotation(modTime time.Time, threshold time.Duration) string

IndexAnnotation 返回适合嵌入 MEMORY.md 行末的紧凑年龄注记.

格式:" _(last updated N days/hours ago)_"(含前导空格) 返回空字符串表示不需要注记(ShouldWarn == false).

精妙之处(CLEVER): 前导空格是有意为之-- 调用方可以无条件 sb.WriteString(IndexAnnotation(...)) 而不需要 先检查返回值是否为空再决定是否加空格. 格式参考早期实现的 "_(last updated N days ago)_" 斜体 markdown.

func Score

func Score(query, description string) float64

Score 计算查询和描述之间的相关性分数(向后兼容的包装函数). 历史包袱(LEGACY): 保留导出签名,内部转发到 textScore. 新代码应使用 RelevanceScorer 接口.

func ShouldWarn

func ShouldWarn(modTime time.Time, threshold time.Duration) bool

ShouldWarn 判断给定修改时间的记忆是否需要新鲜度警告.

threshold == 0 时总是返回 true(医疗/金融等强制警告场景). modTime 为零值时返回 false(新建记忆尚未落盘,无需警告).

func TruncateIndex

func TruncateIndex(content string) string

TruncateIndex 对 MEMORY.md 内容应用双重截断限制(200行 / 25KB).

超出任一限制时,截断并在末尾追加 WARNING 告知模型内容不完整.

升华改进(ELEVATED): 早期实现 只做行数截断(200行),无字节限制. 行数限制对"每条记忆描述很长"的场景失效--200 行 × 200 字节/行 = 40KB. 我们的双重限制保证注入体积在合理范围内. 替代方案:<只保留行数限制,与早期方案对齐> - 否决原因:SDK 嵌入场景下,调用方会同时传自己的 system prompt, context window 压力更大,需要更严格的限制.

Types

type AIMemorySelector

type AIMemorySelector struct {
	// contains filtered or unexported fields
}

AIMemorySelector 是基于模型调用的记忆选择器.

func NewAIMemorySelector

func NewAIMemorySelector(fn ModelQueryFunc) *AIMemorySelector

NewAIMemorySelector 创建 AI 记忆选择器.

func (*AIMemorySelector) Select

func (s *AIMemorySelector) Select(ctx context.Context, query string, headers []MemoryHeader, opts SelectOpts) ([]MemoryHeader, error)

Select 实现 MemorySelector 接口.

流程:

  1. 过滤 opts.AlreadySurfaced(先于 AI 调用,节省 token)
  2. 过滤后若 headers 为空,直接返回 nil, nil
  3. 构建 validFilenames set(只接受这些文件名作为 AI 回复)
  4. 调用 formatMemoryManifest 构建清单
  5. 若 opts.RecentTools 非空,追加 "\n\nRecently used tools: tool1, tool2"
  6. 构建 user prompt: "Query: {query}\n\nAvailable memories:\n{manifest}{toolsSection}"
  7. 调用 queryFn(ctx, SELECT_MEMORIES_SYSTEM_PROMPT, userPrompt)
  8. 解析 JSON 回复 {"selected_memories": ["file1.md", "file2.md"]}
  9. 过滤不在 validFilenames 中的条目

10. 按原 headers 顺序重建结果(保持 mtime 倒序) 11. 截断到 opts.Limit

AI 调用失败时 (契约于 2026-04-14 反转):

  • ctx 已取消:返回 (nil, nil), 正常中止不算错误.
  • 其他错误 (queryFn 返回 error / JSON 解析失败): 返回 (nil, err). 调用方 (Store.FindRelevant) 负责 fallback 到 SelectRelevant 并传 s.scorer. 之前版本在此内部调 SelectRelevant(..., nil), 静默忽略 Store 的 WithScorer 配置, 形成潜伏 bug - 已修复.

type Backupper

type Backupper interface {
	// Backup 在写入前备份指定路径的文件.
	// 如果文件不存在,实现应静默跳过(新建文件无需备份).
	// path 是绝对路径;ctx 用于超时/取消控制.
	Backup(ctx context.Context, path string) error
}

Backupper is the pre-write backup interface.

Shape: synchronous callback. Store calls Snapshot synchronously before mutating on-disk memory files, letting the consumer implementation capture a version or rollback point.

形态: 同步回调. Store 在改动磁盘记忆文件前同步调 Snapshot, 让消费者 实现捕获版本或回滚点.

Backupper 是写入前备份接口.

升华改进(ELEVATED): 用接口而非具体类型(*engine.FileHistory)打破循环依赖-- memory 包不能 import engine 包(engine 已经 import memory). engine.FileHistory 实现此接口,memory.fileStore 通过 WithFileHistory 注入. 这和 observer 的消费思路一致:通过接口解耦,让 engine 注入具体实现.

精妙之处(CLEVER): 只暴露一个方法--Backup(ctx, path) error. fileStore 不关心备份的内部实现(内容寻址,SHA256,最大快照数), 只关心"在写入 path 前,帮我备份一下". 替代方案:传入 *engine.FileHistory(循环依赖,直接否决). 替代方案:传入 func(ctx, path) error(函数类型,但接口更可组合,可 mock).

type CompositeScorer

type CompositeScorer struct {
	// contains filtered or unexported fields
}

CompositeScorer 将多个评分器加权组合.

升华改进(ELEVATED): 支持叠加而非替换(宪法第 8 条). 编程评分器 + 仓储评分器可以共存,加权融合. 替代方案:单评分器(锁定单场景).

使用示例:

composite := NewCompositeScorer(
    WeightedScorer{Scorer: &TextScorer{}, Weight: 0.7},
    WeightedScorer{Scorer: &WarehouseScorer{}, Weight: 0.3},
)

func NewCompositeScorer

func NewCompositeScorer(scorers ...WeightedScorer) *CompositeScorer

NewCompositeScorer 创建一个新的组合评分器.

func (*CompositeScorer) Add

func (cs *CompositeScorer) Add(ws WeightedScorer)

Add 动态添加一个加权评分器.

func (*CompositeScorer) Name

func (cs *CompositeScorer) Name() string

func (*CompositeScorer) Remove

func (cs *CompositeScorer) Remove(name string) bool

Remove 按名称移除评分器.返回是否找到并移除了.

func (*CompositeScorer) Score

func (cs *CompositeScorer) Score(query string, header *MemoryHeader) float64

Score 计算加权平均分数(权重归一化).

type ConflictPolicy

type ConflictPolicy int

ConflictPolicy 定义 Push 时遇到写冲突的处理策略.

冲突场景:两个 session 同时修改了同一记忆文件, 后 Push 的一方发现服务器版本已被修改(HTTP 412 / git diverge).

const (
	// ConflictLocalWins 本地版本覆盖服务器版本.
	//
	// 适用:CLI 单用户,用户正在活跃编辑,不希望 teammate 的远端更新覆盖自己的工作.
	// 早期实现 的行为:412 时刷新 serverChecksums 再 push,等价于本地覆盖.
	// 注意:另一方的修改会丢失(静默),不适用于高并发写场景.
	ConflictLocalWins ConflictPolicy = iota

	// ConflictServerWins 服务器版本覆盖本地版本.
	//
	// 适用:HTTP API 无状态模式--"本地"是临时容器,一旦冲突说明本地已过时,
	// 应丢弃本地,以远端为准.
	// 语义:Pull 成功后 Push,若 Pull 前有未提交的本地更改则丢弃.
	ConflictServerWins

	// ConflictMerge 三路合并,冲突时产生冲突标记(<<<< HEAD).
	//
	// 适用:Git 后端,多人协作,需要完整历史记录.
	// Git 的 rebase/merge 处理大多数非交叉修改,只有真正冲突才产生标记.
	// AI 可以读取并解决冲突标记,不需要人工介入.
	ConflictMerge

	// ConflictFail 发生冲突时返回错误,由调用方决定如何处理.
	//
	// 适用:需要强一致性的场景(金融,医疗),任何冲突都需要显式确认.
	// 调用方可以提示用户,重试,或放弃本次写入.
	ConflictFail
)

func (ConflictPolicy) String

func (p ConflictPolicy) String() string

String 返回策略名称,方便日志和错误消息.

type DefaultCodeExtractor

type DefaultCodeExtractor struct{}

DefaultCodeExtractor 编程场景的默认记忆提取器.

提取策略:

  • 每 5 轮对话检查一次(避免频繁提取浪费 API 调用)
  • 关注:项目结构,代码规范,技术决策,用户偏好
  • 输出格式:YAML frontmatter + markdown 正文
  • 最多 5 轮(提取任务通常 1-2 轮就够了)

func (*DefaultCodeExtractor) AllowedTools

func (e *DefaultCodeExtractor) AllowedTools() []string

AllowedTools 返回编程场景提取器允许使用的工具.

升华改进(ELEVATED): 只允许文件系统工具 + 搜索工具,不允许 Bash,Agent 等. 提取器的职责是"读取和记录",不应该有执行代码或创建子 agent 的能力. Edit/Write 只用于写 memory 目录下的文件(由 SubAgentConfig.MemoryDirRestrict 进一步限制). 替代方案:允许所有工具(权限过宽,提取器可能意外修改代码文件).

func (*DefaultCodeExtractor) BuildPrompt

func (e *DefaultCodeExtractor) BuildPrompt(existingMemories []*Entry, newMessageCount int) string

BuildPrompt 构建编程场景的记忆提取提示词.

升华改进(ELEVATED): 相比早期方案 Go 实现,补入了两项早期实现 的关键设计:

  1. "most recent ~N messages" 精准定位--SubAgent 只看新消息,不重复分析旧内容;
  2. 并行读写策略--turn 1 所有 Read 并行,turn 2 所有 Write/Edit 并行, 节省 2x token 往返(早期实现 buildExtractAutoOnlyPrompt 第 39 行明确写出).

替代方案:不传 newMessageCount,只说 "review conversation history"-- SubAgent 可能重复分析已提取内容,浪费 token 且写重复记忆.

func (*DefaultCodeExtractor) MaxTurns

func (e *DefaultCodeExtractor) MaxTurns() int

MaxTurns 返回最大轮数. 提取任务通常 1-2 轮就完成,5 轮是安全上限.

func (*DefaultCodeExtractor) Name

func (e *DefaultCodeExtractor) Name() string

func (*DefaultCodeExtractor) ShouldExtract

func (e *DefaultCodeExtractor) ShouldExtract(turnCount, lastExtractTurn int) bool

ShouldExtract 判断是否应触发提取. 精妙之处(CLEVER): 每 5 轮检查一次,而非每轮都检查. 频繁提取浪费 API 调用且收益递减(短间隔内的对话内容往往在同一个主题上). 5 轮是经验值:大约对应一个完整的"提问→探索→实现→验证"子任务周期.

type Entry

type Entry struct {
	Name        string    `json:"name"`
	Description string    `json:"description"`
	Type        Type      `json:"type"`
	Content     string    `json:"content"`
	Path        string    `json:"path,omitempty"` // 文件路径(文件存储)
	ModTime     time.Time `json:"mod_time,omitempty"`
}

Entry 是一条记忆记录.

type ExternalScorer

type ExternalScorer struct {
	// contains filtered or unexported fields
}

ExternalScorer 桥接外部进程实现的评分器(跨语言支持).

升华改进(ELEVATED): 仓储团队可以用 Python 写评分器, 通过 stdin/stdout JSON 通信.引擎内部看到的仍然是 Go 接口. 替代方案:要求所有评分器用 Go 实现(锁定语言生态).

使用示例(Python 端):

# warehouse_scorer.py
import json, sys
for line in sys.stdin:
    req = json.loads(line)
    score = my_scoring_logic(req["query"], req["name"], req["type"])
    print(json.dumps({"score": score}))

使用示例(Go 端):

scorer, _ := NewExternalScorer(ctx, ExternalScorerOptions{
    Name:     "warehouse",
    Command:  "python3",
    Args:     []string{"warehouse_scorer.py"},
    Executor: execenv.DefaultExecutor{},
})
composite.Add(WeightedScorer{Scorer: scorer, Weight: 0.3})

通信协议(JSON Lines):

请求:{"query": "数据库配置", "name": "db_config", "description": "...", "type": "project"}
响应:{"score": 0.85}

Score 失败时返回 0.0(不阻断主流程).

func NewExternalScorer

func NewExternalScorer(ctx context.Context, opts ExternalScorerOptions) (*ExternalScorer, error)

NewExternalScorer 创建一个外部进程评分器.

ctx 是评分器的生命周期上下文: 从构造到 Close 整段时间内有效, ctx cancel 会终止子进程 (由 Executor 实现负责转换为 Kill). 长驻进程场景里 ctx 属 于 scorer 实例本身, 而不是单次 Score 调用.

opts.Executor 必填, nil 会 panic. 严格 DI 契约 (M1 方案 β).

func (*ExternalScorer) Close

func (s *ExternalScorer) Close() error

Close 关闭外部进程.

语义: 先关 stdin (触发子进程 EOF 自主退出), 再 Wait 回收进程资源. Process interface 刻意不暴露 "进程是否已启动" 状态 (红线 1 不暴露 *os.Process), 所以只要 s.proc 非 nil (即 NewExternalScorer 成功返回) 就可以直接 Wait. Wait 的幂等性 / 重复调用保护由 Executor 实现负责.

func (*ExternalScorer) Name

func (s *ExternalScorer) Name() string

func (*ExternalScorer) Score

func (s *ExternalScorer) Score(query string, header *MemoryHeader) float64

Score 向外部进程发送请求并读取评分结果. 精妙之处(CLEVER): 失败时返回 0.0 而非 error--评分器是可选增强, 不应因外部进程故障阻断整个记忆检索流程.

type ExternalScorerOptions

type ExternalScorerOptions struct {
	// Name 是评分器名称, 用于日志和 CompositeScorer.Remove.
	Name string

	// Command 是外部进程的可执行路径 (python3 / node / 自定义 binary).
	Command string

	// Args 是传给 Command 的参数列表, 不含 Command 本身.
	Args []string

	// Executor 是子进程启动抽象 (M1 方案 β 严格 DI). 必填, nil 即 panic.
	// 本地 CLI 传 execenv.DefaultExecutor{}, 云端 SaaS 由 platform 层传
	// sandbox.Backend. ClassPluginTool 告诉 backend "这是第三方 plugin 代码,
	// 零信任隔离" — 和 plugin shell tool 同策略.
	Executor execenv.Executor
}

ExternalScorerOptions 是 NewExternalScorer 的构造参数.

对齐 GitSyncOptions 风格 (opts struct DI), 便于未来扩容 (如 WorkDir, 自定义 Env) 不破 API.

type FileStoreOption

type FileStoreOption func(*fileStore)

FileStoreOption 是 NewFileStoreWithOptions 的配置选项.

func WithFileHistory

func WithFileHistory(bk Backupper) FileStoreOption

WithFileHistory 注入写入前备份器. 设置后,Save() 在覆盖已有记忆文件前自动调用 Backup()--确保记忆文件可回滚.

升华改进(ELEVATED): 记忆文件是 Agent 的长期知识库,一旦覆盖无法恢复. 通过 Backupper 接口注入备份能力,与 engine.FileHistory 解耦(打破循环依赖). 典型用法(在 Engine.New() 中):

memory.WithFileHistory(e.fileHistory)

精妙之处(CLEVER): 只在文件已存在时备份(新建无需备份)-- os.Stat 检查存在性,避免对新文件做无意义的备份调用. 备份失败不阻断 Save--fail-open 原则:记忆写入比备份更重要, 但会通过 observer 记录告警,让监控系统感知到备份异常. 替代方案:备份失败阻断写入(过于保守,备份目录满时会让整个记忆系统不可用). 替代方案:直接注入 *engine.FileHistory(循环依赖,否决).

func WithFreshness

func WithFreshness(cfg FreshnessConfig) FileStoreOption

WithFreshness 启用记忆新鲜度警告功能.

升华改进(ELEVATED): 早期实现 硬编码 30 天阈值且不可配置. 通过 functional option 注入 FreshnessConfig,各场景可自定义阈值:

  • CLI 默认:WithFreshness(memory.DefaultFreshnessConfig()) → 24h
  • 仓储场景:TypeOverrides["project"] = 2h
  • 医疗场景:GlobalThreshold = 0(总是警告)

不调用此选项 = 不启用新鲜度功能,向后兼容. 替代方案:<在 Config 里加 FreshnessDays int> - 否决原因:int 无法表达 sub-day 阈值,也无法按类型差异化配置.

func WithMemorySelector

func WithMemorySelector(sel MemorySelector) FileStoreOption

WithMemorySelector 注入 AI 记忆选择器. 设置后,FindRelevant 优先使用 AI 选择而非文本相似度评分. 回退策略:AI 调用失败时自动降级为文本评分.

func WithObserver

func WithObserver(obs flyto.EventObserver) FileStoreOption

WithObserver 设置可观测性接口. 升华改进(ELEVATED): 通过 functional option 注入 observer, 与 WithScorer/WithTypeRegistry 风格一致,不破坏现有构造函数. 替代方案:加 SetObserver 方法(但 Store 接口返回的是接口类型,调用方拿不到具体类型调 SetObserver).

func WithScorer

func WithScorer(scorer RelevanceScorer) FileStoreOption

WithScorer 设置自定义评分器.

func WithSecretGuard

func WithSecretGuard(g security.SecretGuard) FileStoreOption

WithSecretGuard 设置秘密扫描器. 设置后,Save() 在写入前扫描记忆内容--阻止将 API key 等敏感信息写入记忆文件.

升华改进(ELEVATED): 记忆文件存储在 ~/.flyto/projects/<hash>/memory/, 是用户数据,但也是 Agent 持久化的内容,可能被 Agent 自动写入敏感信息. 通过 SecretGuard 保护记忆层是纵深防御的一部分. 替代方案:<只保护 FileWrite/FileEdit 工具> - 否决原因:Memory.Save() 是独立写入路径, 绕过了 FileWrite 工具,不在工具层保护范围内.

func WithStrictSymlink() FileStoreOption

WithStrictSymlink 启用严格符号链接保护模式. 启用后,Save/Delete 在检测到符号链接时拒绝操作(返回错误),而非仅记录日志.

升华改进(ELEVATED): 这是可选的"强化模式"--默认宽松以兼容 NFS/Docker/WSL, 高安全场景(CI 环境,服务端部署)可启用严格模式. 精妙之处(CLEVER): 宽松/严格双模式比单一策略更灵活-- 开发者本地环境有可能用符号链接组织目录结构,强制拒绝会破坏工作流; 服务端环境则需要硬拒绝符号链接防止路径逃逸. 反向思维:严格模式是否多余?若攻击者能在 ~/.flyto/ 创建符号链接, 他们已有足够权限直接读写目标文件,符号链接只是多一跳. 答:在 setuid/容器边界场景下,进程权限高于文件系统权限,符号链接依然危险. 替代方案:始终拒绝符号链接(过于激进,NFS 挂载普遍用符号链接).

func WithSyncAdapter

func WithSyncAdapter(adapter SyncAdapter, cfg SyncConfig) FileStoreOption

WithSyncAdapter 设置同步适配器和策略配置.

升华改进(ELEVATED): 早期实现把同步硬编码为 Anthropic OAuth + GitHub repo,无法在私有部署或 API 无状态模式使用. 我们通过 SyncAdapter 接口 + SyncConfig 组合,让调用方完全控制同步行为:

  • CLI 模式传 GitSyncAdapter + DefaultSyncConfig(session 开始 Pull 一次,本地胜)
  • API 模式传 HTTPSyncAdapter + APISyncConfig(1*time.Minute)(TTL 缓存,服务器胜)
  • 离线/不需要同步:不调用此选项(默认 NoopSyncAdapter,IsAvailable=false,零 overhead)

替代方案:<在 Store 接口上暴露 Sync() 方法,由调用方手动调用> - 否决原因:调用方需要知道何时调用 Sync,而引擎更清楚读写时机(DRY 原则). 反向思维:自动同步会不会在意外时机触发? 是的,这就是 PullPolicy 存在的原因:PullNever + 手动调用 adapter 是 fallback 方案.

func WithTypeRegistry

func WithTypeRegistry(reg *MemoryTypeRegistry) FileStoreOption

WithTypeRegistry 设置自定义类型注册表.

type FreshnessConfig

type FreshnessConfig struct {
	// GlobalThreshold 全局新鲜度阈值.
	// 记忆年龄超过此值时触发警告.
	// 0 = 总是触发警告(任何年龄的记忆都提示可能过时).
	// 典型值:24 * time.Hour(默认),2 * time.Hour(仓储),0(医疗强制)
	GlobalThreshold time.Duration

	// TypeOverrides 按记忆类型覆盖阈值.
	// key 是 Type 字符串("user"/"feedback"/"project"/"reference").
	// 未出现在此 map 中的类型使用 GlobalThreshold.
	//
	// 示例:
	//   TypeOverrides: map[string]time.Duration{
	//     "project": 12 * time.Hour,  // 项目上下文半天就可能过时
	//     "user":    7 * 24 * time.Hour, // 用户偏好一周才变
	//   }
	TypeOverrides map[string]time.Duration
}

FreshnessConfig 配置记忆新鲜度检测策略.

精妙之处(CLEVER): 用"零值禁用"约定统一 nil 和 zero-value 的语义-- GlobalThreshold == 0 表示"总是警告"(不是"从不警告"), 这对医疗/金融场景(任何过时记忆都需警告)自然成立. 调用方传 nil 时,引擎不初始化 FreshnessConfig,不产生任何警告-- nil 与 GlobalThreshold==0 是两种不同语义,需通过指针区分. 在 engine.Config 中用 *FreshnessConfig,nil = 不启用新鲜度功能.

func DefaultFreshnessConfig

func DefaultFreshnessConfig() FreshnessConfig

DefaultFreshnessConfig 返回适合通用场景的默认配置.

24 小时阈值的选择依据:

  • 大多数项目上下文每天最多更新一次
  • 用户偏好变化周期更长,24 小时保守而合理
  • 比早期实现 的 30 天更积极,但不会因频繁警告降低信噪比

func (FreshnessConfig) ThresholdFor

func (c FreshnessConfig) ThresholdFor(t Type) time.Duration

ThresholdFor 返回指定记忆类型的新鲜度阈值. TypeOverrides 中有覆盖则返回覆盖值,否则返回 GlobalThreshold.

type Frontmatter

type Frontmatter struct {
	Name        string // 记忆名称(唯一标识)
	Description string // 一行描述,用于相关性检索
	Type        Type   // 记忆类型:user/feedback/project/reference
	// Version 是 frontmatter 格式 schema 版本(从 1 开始).
	// 0 = 未设置(旧文件),调用方应规范化为 1.
	Version int
}

Frontmatter 是记忆文件的元数据头部. 对应原项目中每个 .md 文件的 YAML frontmatter 部分.

版本兼容(INF-6):

Version 字段记录 frontmatter 格式版本.
旧文件(无 version 行)解析后 Version == 0,ScanMemoryDir/ParseFrontmatter
调用方将其规范化为 1(对应 frontmatterCurrentVersion).

func ParseFrontmatter

func ParseFrontmatter(content string) (fm Frontmatter, body string, ok bool)

ParseFrontmatter 从 markdown 内容中解析出 frontmatter 和正文.

设计决策:手写解析而非引入 gopkg.in/yaml.v3,因为 frontmatter 格式非常简单(几个 key),不值得引入依赖. 解析逻辑:找到两个 "---" 分隔符之间的行,按 "key: value" 拆分.

返回值:

  • fm: 解析出的 frontmatter,如果无法解析则返回零值
  • body: frontmatter 之后的正文内容
  • ok: 是否成功解析出 frontmatter

注意:fm.Version == 0 表示旧文件(无 version 字段),调用方应规范化为 1.

type FrontmatterMigrateFunc

type FrontmatterMigrateFunc func(*Frontmatter) error

FrontmatterMigrateFunc 是一个 frontmatter 迁移函数的类型. 输入是版本 N 的 Frontmatter,函数就地修改为版本 N+1 的格式.

约定:

  • 幂等:重复调用不产生副作用
  • 无损:不丢失原有字段的语义信息
  • 不修改 Version 字段:migrateFrontmatter 统一递增

type GitMode

type GitMode int

GitMode 定义 GitSyncAdapter 的工作模式.

const (
	// GitModeStandalone 独立仓库模式:localDir 本身是一个 git repo.
	// Pull = git pull --rebase --autostash
	// Push = git add -A && git commit && git push
	GitModeStandalone GitMode = iota

	// GitModeEmbedded 嵌入模式:localDir 在项目 git repo 内,使用专属分支.
	// Pull = git fetch origin && git checkout flyto/memory && git rebase origin/flyto/memory
	// Push = git add <localDir> && git commit && git push origin flyto/memory
	GitModeEmbedded
)

type GitSyncAdapter

type GitSyncAdapter struct {
	// contains filtered or unexported fields
}

GitSyncAdapter 是基于 git 命令的 SyncAdapter 实现.

零值不可用,必须通过 NewGitSyncAdapter 构造.

func NewGitSyncAdapter

func NewGitSyncAdapter(opts GitSyncOptions) *GitSyncAdapter

NewGitSyncAdapter 创建 GitSyncAdapter.

localDir 是记忆目录路径(不是 git repo 根目录). 如果 gitBin 为空,自动在 PATH 中查找 git.

opts.Executor 必填, nil 会 panic. 严格 DI 契约 (M1 方案 β).

func (*GitSyncAdapter) InitRepo

func (g *GitSyncAdapter) InitRepo(ctx context.Context, localDir string, remoteURL string) error

InitRepo 在 localDir 初始化 git repo 并设置 remote.

用于首次使用 GitSyncAdapter 时的一次性初始化. 如果已是 git repo,只确保 remote 存在.

func (*GitSyncAdapter) IsAvailable

func (g *GitSyncAdapter) IsAvailable() bool

IsAvailable 检查 git 二进制是否存在,且 localDir 在 git repo 内.

注意:IsAvailable 本身不执行网络操作,只检查本地条件. 远端不可达时 IsAvailable 仍返回 true,Pull/Push 时才发现网络错误.

func (*GitSyncAdapter) Pull

func (g *GitSyncAdapter) Pull(ctx context.Context, localDir string) (int, error)

Pull 从远端拉取最新状态.

根据 ConflictPolicy 已在 Push 时处理,Pull 统一使用 rebase 策略:

  • 保留本地 commit,将远端 commit 应用到本地之前
  • --autostash 自动 stash 未提交的工作区变更

如果 localDir 不在 git repo 内,尝试 git init + git remote add.

func (*GitSyncAdapter) Push

func (g *GitSyncAdapter) Push(ctx context.Context, localDir string, policy ConflictPolicy) (int, error)

Push 将本地变化提交并推送到远端.

流程:git add -A → git commit → git push 如果没有可提交的变化(working tree clean),跳过 commit+push,返回 (0, nil).

ConflictPolicy 语义:

  • ConflictLocalWins:push --force-with-lease(本地 commit 优先,但检查远端是否意外进展)
  • ConflictServerWins:先 Pull(reset --hard),再 push(此时无冲突)
  • ConflictMerge:git pull --no-rebase(三路合并),有冲突时提交冲突标记文件
  • ConflictFail:检测到本地与远端 diverge 时直接返回 ErrSyncConflict

type GitSyncOptions

type GitSyncOptions struct {
	// Mode 工作模式,默认 GitModeStandalone.
	Mode GitMode

	// Remote git remote 名称,默认 "origin".
	Remote string

	// Branch 分支名,默认:
	//   - Standalone: "main"
	//   - Embedded: "flyto/memory"
	Branch string

	// CommitAuthorName git commit 的作者名,默认 "Flyto Agent".
	CommitAuthorName string

	// CommitAuthorEmail git commit 的作者邮箱,默认 "agent@flyto.local".
	CommitAuthorEmail string

	// GitBin git 二进制路径,默认在 PATH 中查找 "git".
	// 用于测试时注入 mock git 路径.
	GitBin string

	// Executor 是子进程启动抽象 (M1 方案 β 严格 DI). 必填, 零值会在
	// NewGitSyncAdapter 里 panic. 本地 CLI 传 execenv.DefaultExecutor{},
	// 云端 SaaS 由 platform 层传 sandbox.Backend (ClassMemoryGit 映射到
	// system pod 或 tenant VM 路由决策发生在 backend 内).
	Executor execenv.Executor
}

GitSyncOptions 是 GitSyncAdapter 的构造选项.

type MemoryExtractor

type MemoryExtractor interface {
	// Name 返回提取器的名称标识.
	Name() string

	// ShouldExtract 判断是否应触发提取.
	// turnCount: 当前对话已完成的轮数
	// lastExtractTurn: 上次提取时的轮数(0 = 从未提取)
	ShouldExtract(turnCount int, lastExtractTurn int) bool

	// BuildPrompt 构建提取提示词.
	// existingMemories: 当前已有的记忆条目(避免重复提取)
	// newMessageCount: 自上次提取以来的新消息数(SubAgent 凭此精准定位分析范围)
	// 返回发送给提取子 agent 的完整 prompt.
	//
	// 升华改进(ELEVATED): 相比早期方案 Go 签名 BuildPrompt(existingMemories []*Entry),
	// 加入 newMessageCount--早期实现 buildExtractAutoOnlyPrompt(newMessageCount, existingMemories)
	// 会在 prompt 中注入 "Analyze the most recent ~N messages",让 SubAgent 只看最近的
	// 消息而非全部历史,避免重复提取旧内容.我们把这个参数提升到接口层,
	// 所有场景(编程/仓储/金融)都能利用精准定位.
	// 替代方案:由 Engine 在调用后包装一句话(职责扩散,场景 prompt 无法定制定位粒度).
	BuildPrompt(existingMemories []*Entry, newMessageCount int) string

	// AllowedTools 返回提取代理允许使用的工具名列表.
	// 提取子 agent 只能使用这些工具,防止越权操作.
	AllowedTools() []string

	// MaxTurns 返回提取代理的最大轮数.
	MaxTurns() int
}

MemoryExtractor 从对话中提取值得记住的信息.

升华改进(ELEVATED): 接口只定义策略,执行由 Engine 的 SubAgent fork 模式负责. 这样 Extractor 完全不知道 API,模型,token 这些执行细节, 只专注于"提取什么"和"什么时候提取". 替代方案:Extractor 自己持有 API client 直接调用模型(职责越界,变成小 Engine).

Shape: synchronous callback. Engine calls ShouldExtract / Extract at turn boundaries; the extractor decides if extraction is warranted and produces candidate memories.

形态: 同步回调. 引擎在 turn 边界同步调 ShouldExtract / Extract; extractor 判断是否该抽取并产出候选记忆.

type MemoryHeader

type MemoryHeader struct {
	Frontmatter Frontmatter // 解析出的 frontmatter 元数据
	Path        string      // 文件绝对路径
	ModTime     time.Time   // 文件最后修改时间
}

MemoryHeader 是记忆文件的头部信息(轻量级,不含正文). 用于列表展示和相关性评估,避免加载完整内容.

func ScanMemoryDir

func ScanMemoryDir(dir string) ([]MemoryHeader, error)

ScanMemoryDir 递归扫描目录下的所有 .md 记忆文件.

设计决策:

  • 排除 MEMORY.md 索引文件(它不是记忆本身)
  • 只读前 30 行提取 frontmatter,不加载完整文件(性能优化)
  • 最多扫描 200 个文件(防止意外扫描到巨大目录)
  • 结果按 mtime 倒序排列(最新的在前面)

如果目录不存在,返回空切片而非错误(目录尚未创建是正常情况).

func SelectRelevant

func SelectRelevant(query string, headers []MemoryHeader, limit int, scorer ...RelevanceScorer) []MemoryHeader

SelectRelevant 从记忆头信息列表中选出与查询最相关的记忆.

升华改进(ELEVATED): 接受可选的 RelevanceScorer 参数, scorer 为 nil 时使用默认 TextScorer,保持向后兼容. 替代方案:强制传入 scorer(破坏现有调用方).

流程:

  1. 对每个记忆使用评分器计算相关性分数
  2. 过滤掉低于阈值的结果
  3. 按分数降序排列
  4. 返回前 limit 个

type MemorySelector

type MemorySelector interface {
	Select(ctx context.Context, query string, headers []MemoryHeader, opts SelectOpts) ([]MemoryHeader, error)
}

MemorySelector 是批量记忆选择器接口.

升华改进(ELEVATED): 与 RelevanceScorer 的区别--

  • RelevanceScorer: 单条打分(0.0~1.0),适合排序 MemorySelector: 批量选择,一次 AI 调用完成,支持额外过滤参数

替代方案:<复用 RelevanceScorer 逐条打分再排序> - 否决: AI 打分成本高,逐条打分 token 消耗是批量选择的数倍.

错误契约 (2026-04-14 反转):

  • ctx 取消: 返回 (nil, nil), 正常中止不算错误.
  • 其他错误 (模型请求失败 / JSON 解析失败 / ...): 返回 (nil, err), 由调用方 (Store) 决定 fallback 策略.

不在 selector 内部 fallback 的原因: selector 无 back-reference 到 调用方 Store 的配置 (scorer / typeRegistry / ...), 内部 fallback 只能用 nil 参数, 会静默忽略用户的 WithScorer 等配置. fallback 必须由持有配置 的层 (Store.FindRelevant) 做, 才能把 s.scorer 正确传给 SelectRelevant.

Shape: synchronous callback. Store.FindRelevant calls Select synchronously during prompt assembly; the selector implementation (AI or heuristic) ranks MemoryHeader candidates for the query.

形态: 同步回调. Store.FindRelevant 在拼 prompt 时同步调 Select; selector 实现 (AI 或启发式) 对 MemoryHeader 候选按 query 排序.

type MemoryTypeInfo

type MemoryTypeInfo struct {
	Name          string   // 类型名称:"user" / "inventory_rule" / ...
	Scope         string   // 作用域:"private" / "team" / "org" / "local"
	Description   string   // 给模型看的描述(会注入到系统提示词)
	WhenToSave    string   // 什么时候应该保存这种记忆
	HowToUse      string   // 怎么使用这种记忆
	BodyStructure string   // 记忆体的推荐格式(如 "规则 → Why → How to apply")
	Examples      []string // 示例
	SortOrder     int      // 在索引中的排列顺序
}

MemoryTypeInfo 描述一种记忆类型的完整元信息. 这些信息会注入到系统提示词中,指导模型何时保存,如何使用该类型的记忆.

type MemoryTypeRegistry

type MemoryTypeRegistry struct {
	// contains filtered or unexported fields
}

MemoryTypeRegistry 是分层的记忆类型注册表.

精妙之处(CLEVER): parent 指针实现只读继承链. 查询时 local 优先(近端覆盖远端),沿 parent 链向上冒泡. 注册只写入本级 local map,不会污染上级.

var DefaultTypeRegistry *MemoryTypeRegistry

DefaultTypeRegistry 全局默认注册表,内置编程场景的 4 种类型. 措辞从早期方案精心移植(经过大量用户反馈打磨).

历史包袱(LEGACY): 保留全局变量以兼容旧代码直接引用. 新代码建议通过 NewTypeRegistry(WithParent(DefaultTypeRegistry)) 继承.

func NewTypeRegistry

func NewTypeRegistry(opts ...RegistryOption) *MemoryTypeRegistry

NewTypeRegistry 创建一个新的类型注册表.

精妙之处(CLEVER): functional options 模式-- 无参调用创建独立注册表,WithParent 创建继承链. 调用方只表达"我要继承谁",不关心内部数据结构.

func (*MemoryTypeRegistry) All

func (r *MemoryTypeRegistry) All() []*MemoryTypeInfo

All 返回合并所有层级后的类型列表,按 SortOrder 排序. local 覆盖 parent 同名类型(近端优先).

精妙之处(CLEVER): 先收集 parent(递归),再用 local 覆盖, 实现"越近越优先"的继承语义.最终按 SortOrder 排序, 保证输出顺序可预测.

func (*MemoryTypeRegistry) FormatForPrompt

func (r *MemoryTypeRegistry) FormatForPrompt(format PromptFormat) string

FormatForPrompt 将注册的类型格式化为系统提示词片段. 根据模型自动选择格式.

精妙之处(CLEVER): 格式跟模型走,不硬编码 XML. Anthropic 模型对 XML 标签训练有素,OpenAI 对 Markdown 更友好. 自动检测避免用户操心格式选择.

func (*MemoryTypeRegistry) Get

Get 获取指定名称的类型信息. 先查本级 local,再沿 parent 链向上冒泡. 未找到返回 nil.

func (*MemoryTypeRegistry) IsRegistered

func (r *MemoryTypeRegistry) IsRegistered(name string) bool

IsRegistered 检查指定名称的类型是否已注册(包含 parent 链).

func (*MemoryTypeRegistry) ParseType

func (r *MemoryTypeRegistry) ParseType(raw string) (string, bool)

ParseType 验证原始字符串是否为已注册的类型名称. 返回规范化的类型名和是否有效.

升华改进(ELEVATED): 替代早期方案 parseMemoryType 的硬编码 switch. 注册新类型后自动可用,无需修改解析代码. 替代方案:switch-case 硬编码 4 种类型(每加一种改一处).

func (*MemoryTypeRegistry) Register

func (r *MemoryTypeRegistry) Register(info *MemoryTypeInfo)

Register 注册一种记忆类型到本级(upsert 语义). 如果同名类型已存在于本级,覆盖之(不影响 parent).

type ModelQueryFunc

type ModelQueryFunc func(ctx context.Context, systemPrompt, userPrompt string) (string, error)

ModelQueryFunc 是模型查询函数类型.

打破 memory→engine 的循环依赖:memory 包只依赖此函数签名, engine 层传入闭包实现,不需要 memory import flyto 包.

systemPrompt: 系统提示词 userPrompt: 用户消息 返回: 模型的纯文本回复,error

type NoopSyncAdapter

type NoopSyncAdapter struct{}

NoopSyncAdapter 是不执行任何同步的空实现.

所有现有 NewFileStore* 调用默认使用此实现(即不同步), 向后兼容--现有代码无需任何修改即可升级到支持同步的版本.

精妙之处(CLEVER): IsAvailable() 始终返回 false, fileStore 的 shouldPull/shouldPush 在 IsAvailable=false 时立即跳过, 不会有任何锁竞争或时间戳检查开销. 如果用"空方法直接返回"代替,shouldPull 逻辑里仍然需要 nil 检查,代码更复杂.

func (*NoopSyncAdapter) IsAvailable

func (n *NoopSyncAdapter) IsAvailable() bool

IsAvailable 始终返回 false,使 fileStore 完全跳过同步逻辑.

func (*NoopSyncAdapter) Pull

func (n *NoopSyncAdapter) Pull(_ context.Context, _ string) (int, error)

Pull 是空操作,始终返回 (0, nil).

func (*NoopSyncAdapter) Push

Push 是空操作,始终返回 (0, nil).

type PromptFormat

type PromptFormat string

PromptFormat 提示词输出格式.

const (
	FormatXML      PromptFormat = "xml"
	FormatMarkdown PromptFormat = "markdown"
	FormatJSON     PromptFormat = "json"
)

func AutoPromptFormat

func AutoPromptFormat(modelID string) PromptFormat

AutoPromptFormat 根据模型 ID 自动选择最佳提示词格式.

精妙之处(CLEVER): 复用 DetectProvider 的模型前缀匹配逻辑, 无需引入 permission 包的依赖--用同样的前缀规则独立实现. anthropic → XML, openai → Markdown, google → Markdown, default → Markdown.

type PullPolicy

type PullPolicy int

PullPolicy 定义何时自动触发 Pull 操作.

Pull 的代价因后端不同而差异巨大:

  • Git 本地 fetch:毫秒级
  • HTTP API(304 Not Modified):网络 RTT,通常 50-200ms
  • HTTP API(需要传输内容):RTT + 传输时间,可达数秒

精妙之处(CLEVER): PullPolicy 是 Pull 时机的纯策略描述,不含时间戳状态-- 状态(lastPullTime,pulled 标志)保存在 fileStore 中. 这样同一个 PullPolicy 值可以安全地在多个 store 实例间共享(无数据竞争).

const (
	// PullOnSessionStart 每个 store 实例生命周期内只 Pull 一次.
	//
	// 适用:CLI 模式,session 生命周期与 store 生命周期一致.
	// "首次读操作"触发 Pull,之后不再 Pull,保持 session 内的一致性.
	PullOnSessionStart PullPolicy = iota

	// PullWithTTL 距上次 Pull 超过 SyncConfig.PullTTL 才重新 Pull.
	//
	// 适用:SDK 嵌入,长生命周期服务,请求频繁但不希望每次都 Pull.
	// TTL 是新鲜度与性能的平衡点--TTL 越短一致性越强,TTL 越长 I/O 越少.
	PullWithTTL

	// PullAlways 每次读操作(List/FindRelevant)前都 Pull.
	//
	// 适用:强一致性要求,允许每次读都承受 Pull 开销(如审计日志场景).
	// 警告:高频读场景下会显著增加延迟,谨慎使用.
	PullAlways

	// PullNever 不自动 Pull.
	//
	// 适用:离线场景,纯写场景,调用方手动控制同步时机.
	// 也是 NoopSyncAdapter 的隐含策略(IsAvailable=false 时自动不 Pull).
	PullNever
)

type RegistryOption

type RegistryOption func(*MemoryTypeRegistry)

RegistryOption 是 NewTypeRegistry 的配置选项.

func WithParent

func WithParent(parent *MemoryTypeRegistry) RegistryOption

WithParent 设置上级注册表,实现分层继承.

type RelevanceScorer

type RelevanceScorer interface {
	Name() string
	Score(query string, header *MemoryHeader) float64
}

RelevanceScorer 记忆相关性评分器接口.

升华改进(ELEVATED): 从包级函数提升为接口,支持场景可插拔. 编程场景用文本相似度,仓储场景可能按 SKU/订单号匹配, 法律场景可能按法条编号匹配.不同场景注册不同评分器. 替代方案:硬编码文本相似度函数(原始设计,锁死评分算法).

参数传 *MemoryHeader 而非 name+description: 宽接口缩窄容易,窄接口拓宽要改所有实现. 评分器可以利用 Type,ModTime 等额外信息做更精准的评分.

Shape: synchronous callback. Store calls Score synchronously during FindRelevant to rank memory candidates for the current prompt; consumer can plug keyword-based / embedding / LLM scorers.

形态: 同步回调. Store 在 FindRelevant 期间同步调 Score, 对当前 prompt 排序记忆候选; 消费者可插入关键词 / embedding / LLM 等 scorer.

type SelectOpts

type SelectOpts struct {
	Limit           int             // 最多返回几条,<=0 则用 defaultRelevanceLimit(5)
	RecentTools     []string        // 最近用到的工具名(避免重推这些工具的 ref docs)
	AlreadySurfaced map[string]bool // 本 session 已展示过的文件路径(去重)
}

SelectOpts 是记忆选择器的选项.

type Store

type Store interface {
	// Save 保存一条记忆.
	Save(ctx context.Context, entry *Entry) error

	// List 列出所有记忆(按修改时间倒序).
	List(ctx context.Context) ([]*Entry, error)

	// FindRelevant 查找与查询相关的记忆.
	FindRelevant(ctx context.Context, query string, limit int) ([]*Entry, error)

	// Delete 删除一条记忆.
	Delete(ctx context.Context, name string) error

	// UpdateIndex 更新 MEMORY.md 索引文件.
	UpdateIndex(ctx context.Context) error

	// Dir 返回记忆文件的存储目录(绝对路径).
	// 记忆提取子 agent 用此路径限制 Edit/Write 只能写入记忆目录,
	// hasMemoryWritesSince 用此路径判断主 agent 是否已写过记忆.
	Dir() string
}

Store 是记忆存储接口.

Shape: synchronous callback for writes, pull for reads. Engine calls Save / Delete synchronously after extraction; Get / List / FindRelevant serve as pull API for UIs and the prompt assembly step.

形态: 写是同步回调, 读是 pull. 引擎在抽取后同步调 Save / Delete; Get / List / FindRelevant 作为 pull API 供 UI 和 prompt 拼装使用.

func NewFileStore

func NewFileStore(cwd string) Store

NewFileStore 创建基于文件系统的记忆存储.

存储路径策略: 原项目用 ~/.flyto/projects/<project-hash>/memory/, 这里复用同样的路径规则,通过 cwd 的 SHA256 哈希生成项目标识.

func NewFileStoreWithBaseDir

func NewFileStoreWithBaseDir(baseDir string) Store

NewFileStoreWithBaseDir 创建以指定 baseDir 为存储目录的文件存储. 主要用于测试--测试可以用 t.TempDir() 作为 baseDir,而不需要依赖 HOME 目录.

func NewFileStoreWithOptions

func NewFileStoreWithOptions(cwd string, opts ...FileStoreOption) Store

NewFileStoreWithOptions 创建带配置选项的文件存储.

升华改进(ELEVATED): functional options 模式统一所有可配置项. 比多个 NewFileStoreWithXxx 构造函数更可扩展-- 新增配置项只需加一个 WithXxx,不用加新构造函数. 替代方案:每个配置维度一个构造函数(组合爆炸).

type SyncAdapter

type SyncAdapter interface {
	// Pull 从远端拉取最新状态到本地目录.
	//
	// 实现约定:
	//   - 必须是幂等的:多次 Pull 结果相同
	//   - 不应删除本地存在但远端不存在的文件(防止意外丢失)
	//     除非 ConflictServerWins 语义要求(由实现者决定)
	//   - context 取消时应尽快返回
	//
	// 返回值:pulled 是实际更新(新增或覆盖)的文件数,0 表示无变化.
	Pull(ctx context.Context, localDir string) (pulled int, err error)

	// Push 将本地目录的变化上传到远端.
	//
	// policy 控制写冲突时的行为:
	//   - ConflictLocalWins:本地覆盖远端
	//   - ConflictServerWins:检测到冲突时先 Pull 再 Push(服务器版本保留)
	//   - ConflictMerge:三路合并(Git 后端支持,HTTP 后端可能不支持)
	//   - ConflictFail:冲突时直接返回 ErrSyncConflict
	//
	// 返回值:pushed 是实际上传的文件数(delta),0 表示无变化.
	Push(ctx context.Context, localDir string, policy ConflictPolicy) (pushed int, err error)

	// IsAvailable 检查后端是否可用(凭证有效,网络可达,工具存在等).
	//
	// 精妙之处(CLEVER): fileStore 在每次 shouldPull/shouldPush 前调用此方法.
	// NoopSyncAdapter 永远返回 false,使 fileStore 在无同步需求时完全跳过同步逻辑,
	// 零 overhead.同时允许后端在运行时动态上线(如网络恢复后返回 true).
	IsAvailable() bool
}

SyncAdapter 是记忆同步的可插拔后端接口.

接口设计原则:

  1. 操作目录而非单文件--后端(git,HTTP)天然是批量操作, 逐文件接口会丢失原子性保证.
  2. 不感知记忆格式--SyncAdapter 只传输 .md 文件, 不解析 frontmatter,不懂 Memory.Entry,保持职责分离.
  3. 凭证/认证由实现者管理--接口不传 token, GitSyncAdapter 用 SSH key,HTTPSyncAdapter 用 Bearer header, 引擎不存任何凭证.

升华改进(ELEVATED): 早期实现没有接口抽象, 直接在函数内调用 Anthropic API(硬编码 URL + OAuth). 我们通过接口解耦,同一 fileStore 可在 CLI/SDK/API 模式下使用完全不同的后端, 无需改引擎代码. 替代方案:<像 TS 那样在 memory 包内内置 HTTP sync> - 否决:耦合特定 API server,无法用于私有部署,测试需要 mock HTTP.

Shape: synchronous callback. Engine (at configured sync triggers) calls Push / Pull synchronously; the adapter talks to the remote backend (git / HTTP / Notion / custom) and returns.

形态: 同步回调. 引擎在配置的同步触发点同步调 Push / Pull; adapter 对接 远端后端 (git / HTTP / Notion / 自定义) 然后返回.

type SyncConfig

type SyncConfig struct {
	// ConflictPolicy 是写冲突时的处理方式.
	ConflictPolicy ConflictPolicy

	// PullPolicy 是自动 Pull 的触发条件.
	PullPolicy PullPolicy

	// PullTTL 是 PullWithTTL 策略的冷却时间.
	// 仅在 PullPolicy == PullWithTTL 时有效,其他策略忽略此字段.
	PullTTL time.Duration
}

SyncConfig 组合了冲突策略和 Pull 策略,描述 fileStore 的完整同步行为.

func APISyncConfig

func APISyncConfig(ttl time.Duration) SyncConfig

APISyncConfig 返回适合 HTTP API 高频无状态模式的配置.

策略:TTL 内缓存(避免每次请求都 Pull),冲突时服务器胜(本地是临时缓存). 对应场景:多个无状态 API server 实例共享同一 memory 后端.

ttl 建议值:

  • 强一致性场景:30s-1min
  • 一般场景:5min
  • 只关心启动时状态:PullOnSessionStart(使用 DefaultSyncConfig)

func DefaultSyncConfig

func DefaultSyncConfig() SyncConfig

DefaultSyncConfig 返回适合 CLI 单用户的默认配置.

策略:session 开始时 Pull 一次,本地修改优先. 对应场景:开发者本地运行 CLI,低频写,期望自己的改动不被覆盖.

type TextScorer

type TextScorer struct{}

TextScorer 基于文本相似度的评分器(默认实现). 算法:Jaccard-like + token 权重 + 子串匹配. 这是从原始包级函数 Score() 重构而来,算法完全不变.

func (*TextScorer) Name

func (s *TextScorer) Name() string

func (*TextScorer) Score

func (s *TextScorer) Score(query string, header *MemoryHeader) float64

Score 计算查询与记忆头信息之间的文本相似度. 复用现有逻辑:description 权重 0.7 + name 权重 0.3.

type Type

type Type string

Type 是记忆类型枚举.

const (
	TypeUser      Type = "user"      // 用户画像
	TypeFeedback  Type = "feedback"  // 行为指导
	TypeProject   Type = "project"   // 项目上下文
	TypeReference Type = "reference" // 外部指针
)

type WeightedScorer

type WeightedScorer struct {
	Scorer RelevanceScorer
	Weight float64
}

WeightedScorer 将评分器和权重绑定在一起.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL