memory

package
v0.0.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 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 NewFileStoreWithBaseDirAndOptions

func NewFileStoreWithBaseDirAndOptions(baseDir string, opts ...FileStoreOption) Store

NewFileStoreWithBaseDirAndOptions builds a file store rooted at an explicit baseDir while still applying the full FileStoreOption set (secret guard / observer / file history / freshness / scorer / sync, etc.).

Motivation: NewFileStoreWithBaseDir takes a baseDir but no options, and NewFileStoreWithOptions takes options but derives baseDir from cwd via memoryDirForProject (which routes to the single global ~/.flyto root). The per-scope server seam needs both at once: an arbitrary scope-rooted baseDir AND the production options. This additive constructor closes that gap without touching the two existing constructors (zero break for current callers).

NewFileStoreWithBaseDirAndOptions 以显式 baseDir 为根创建文件存储, 同时应用 完整的 FileStoreOption (secret guard / observer / file history / freshness / scorer / sync 等).

动机: NewFileStoreWithBaseDir 接 baseDir 但不接 options, 而 NewFileStoreWithOptions 接 options 但 baseDir 由 cwd 经 memoryDirForProject 派生 (落到单一全局 ~/.flyto 根). per-scope 服务端 seam 两者都要: 任意 scope 根的 baseDir 加生产 options. 这个 additive 构造函数补上空缺, 不动现有 两个构造函数 (现有调用方零破坏).

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