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
- Variables
- func AgeDuration(modTime time.Time) time.Duration
- func AgeString(d time.Duration) string
- func FormatFrontmatter(fm Frontmatter, body string) string
- func FreshnessNote(modTime time.Time, threshold time.Duration) string
- func FreshnessText(modTime time.Time, threshold time.Duration) string
- func IndexAnnotation(modTime time.Time, threshold time.Duration) string
- func Score(query, description string) float64
- func ShouldWarn(modTime time.Time, threshold time.Duration) bool
- func TruncateIndex(content string) string
- type AIMemorySelector
- type Backupper
- type CompositeScorer
- type ConflictPolicy
- type DefaultCodeExtractor
- func (e *DefaultCodeExtractor) AllowedTools() []string
- func (e *DefaultCodeExtractor) BuildPrompt(existingMemories []*Entry, newMessageCount int) string
- func (e *DefaultCodeExtractor) MaxTurns() int
- func (e *DefaultCodeExtractor) Name() string
- func (e *DefaultCodeExtractor) ShouldExtract(turnCount, lastExtractTurn int) bool
- type Entry
- type ExternalScorer
- type ExternalScorerOptions
- type FileStoreOption
- func WithFileHistory(bk Backupper) FileStoreOption
- func WithFreshness(cfg FreshnessConfig) FileStoreOption
- func WithMemorySelector(sel MemorySelector) FileStoreOption
- func WithObserver(obs flyto.EventObserver) FileStoreOption
- func WithScorer(scorer RelevanceScorer) FileStoreOption
- func WithSecretGuard(g security.SecretGuard) FileStoreOption
- func WithStrictSymlink() FileStoreOption
- func WithSyncAdapter(adapter SyncAdapter, cfg SyncConfig) FileStoreOption
- func WithTypeRegistry(reg *MemoryTypeRegistry) FileStoreOption
- type FreshnessConfig
- type Frontmatter
- type FrontmatterMigrateFunc
- type GitMode
- type GitSyncAdapter
- func (g *GitSyncAdapter) InitRepo(ctx context.Context, localDir string, remoteURL string) error
- func (g *GitSyncAdapter) IsAvailable() bool
- func (g *GitSyncAdapter) Pull(ctx context.Context, localDir string) (int, error)
- func (g *GitSyncAdapter) Push(ctx context.Context, localDir string, policy ConflictPolicy) (int, error)
- type GitSyncOptions
- type MemoryExtractor
- type MemoryHeader
- type MemorySelector
- type MemoryTypeInfo
- type MemoryTypeRegistry
- func (r *MemoryTypeRegistry) All() []*MemoryTypeInfo
- func (r *MemoryTypeRegistry) FormatForPrompt(format PromptFormat) string
- func (r *MemoryTypeRegistry) Get(name string) *MemoryTypeInfo
- func (r *MemoryTypeRegistry) IsRegistered(name string) bool
- func (r *MemoryTypeRegistry) ParseType(raw string) (string, bool)
- func (r *MemoryTypeRegistry) Register(info *MemoryTypeInfo)
- type ModelQueryFunc
- type NoopSyncAdapter
- type PromptFormat
- type PullPolicy
- type RegistryOption
- type RelevanceScorer
- type SelectOpts
- type Store
- type SyncAdapter
- type SyncConfig
- type TextScorer
- type Type
- type WeightedScorer
Constants ¶
const SELECT_MEMORIES_SYSTEM_PROMPT = `` /* 972-byte string literal not displayed */
SELECT_MEMORIES_SYSTEM_PROMPT 是记忆选择的系统提示词.
不翻译为中文的原因: 模型对英文指令的理解更精准(大多数模型的预训练语料以英文为主), 且 Flyto Agent 本身是多语言工具,保留英文可以服务于多语言用户场景.
Variables ¶
var ErrSyncConflict = &syncConflictError{}
ErrSyncConflict 是 ConflictFail 策略下检测到冲突时返回的错误. 调用方可以用 errors.Is(err, memory.ErrSyncConflict) 判断.
Functions ¶
func AgeDuration ¶
AgeDuration 返回记忆的年龄(当前时刻 - 最后修改时间). modTime 为零值时返回 0.
func AgeString ¶
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 ¶
FreshnessNote 将新鲜度警告包裹为 <system-reminder> 标签格式.
返回空字符串表示不需要警告(ShouldWarn == false). 调用方可直接注入返回值到消息流,空字符串安全可忽略.
精妙之处(CLEVER): 返回值可以直接拼接到消息内容中, 无需调用方做 if note != "" 的判断--空字符串拼接无害. 当然调用方在意性能时仍可先判断再调用.
func FreshnessText ¶
FreshnessText 返回针对该条记忆的完整新鲜度警告文本.
仅在 ShouldWarn(modTime, threshold) == true 时调用才有意义. 调用方应先 ShouldWarn 判断,再决定是否调用本函数.
文本结构参考早期实现的注入风格:
- 说明事实(年龄)
- 解释含义(记忆是点时刻观测值)
- 给出行动建议(主动核实)
升华改进(ELEVATED): 早期实现 无任何新鲜度提示文本,只在 MEMORY.md 里加了 "_(last updated N days ago)_" 注记,完全靠模型自己推断是否过时. 我们加显式的自然语言警告,让模型知道"应该主动核实". 替代方案:<只在 MEMORY.md 里加注记,不加运行时提示> - 否决原因:MEMORY.md 是索引,不保证每轮都被读取;运行时注入才能保证覆盖.
func IndexAnnotation ¶
IndexAnnotation 返回适合嵌入 MEMORY.md 行末的紧凑年龄注记.
格式:" _(last updated N days/hours ago)_"(含前导空格) 返回空字符串表示不需要注记(ShouldWarn == false).
精妙之处(CLEVER): 前导空格是有意为之-- 调用方可以无条件 sb.WriteString(IndexAnnotation(...)) 而不需要 先检查返回值是否为空再决定是否加空格. 格式参考早期实现的 "_(last updated N days ago)_" 斜体 markdown.
func Score ¶
Score 计算查询和描述之间的相关性分数(向后兼容的包装函数). 历史包袱(LEGACY): 保留导出签名,内部转发到 textScore. 新代码应使用 RelevanceScorer 接口.
func ShouldWarn ¶
ShouldWarn 判断给定修改时间的记忆是否需要新鲜度警告.
threshold == 0 时总是返回 true(医疗/金融等强制警告场景). modTime 为零值时返回 false(新建记忆尚未落盘,无需警告).
func TruncateIndex ¶
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 接口.
流程:
- 过滤 opts.AlreadySurfaced(先于 AI 调用,节省 token)
- 过滤后若 headers 为空,直接返回 nil, nil
- 构建 validFilenames set(只接受这些文件名作为 AI 回复)
- 调用 formatMemoryManifest 构建清单
- 若 opts.RecentTools 非空,追加 "\n\nRecently used tools: tool1, tool2"
- 构建 user prompt: "Query: {query}\n\nAvailable memories:\n{manifest}{toolsSection}"
- 调用 queryFn(ctx, SELECT_MEMORIES_SYSTEM_PROMPT, userPrompt)
- 解析 JSON 回复 {"selected_memories": ["file1.md", "file2.md"]}
- 过滤不在 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) 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 )
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 实现,补入了两项早期实现 的关键设计:
- "most recent ~N messages" 精准定位--SubAgent 只看新消息,不重复分析旧内容;
- 并行读写策略--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 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 ¶
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 ¶
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 ¶
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(破坏现有调用方).
流程:
- 对每个记忆使用评分器计算相关性分数
- 过滤掉低于阈值的结果
- 按分数降序排列
- 返回前 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 ¶
func (r *MemoryTypeRegistry) Get(name string) *MemoryTypeInfo
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 ¶
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) Push ¶
func (n *NoopSyncAdapter) Push(_ context.Context, _ string, _ ConflictPolicy) (int, error)
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 ¶
NewFileStore 创建基于文件系统的记忆存储.
存储路径策略: 原项目用 ~/.flyto/projects/<project-hash>/memory/, 这里复用同样的路径规则,通过 cwd 的 SHA256 哈希生成项目标识.
func NewFileStoreWithBaseDir ¶
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 是记忆同步的可插拔后端接口.
接口设计原则:
- 操作目录而非单文件--后端(git,HTTP)天然是批量操作, 逐文件接口会丢失原子性保证.
- 不感知记忆格式--SyncAdapter 只传输 .md 文件, 不解析 frontmatter,不懂 Memory.Entry,保持职责分离.
- 凭证/认证由实现者管理--接口不传 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 WeightedScorer ¶
type WeightedScorer struct {
Scorer RelevanceScorer
Weight float64
}
WeightedScorer 将评分器和权重绑定在一起.