Beads KV 存储设计解读:用bd kv为编码 Agent 构建跨会话的轻量级记忆
【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads
Beads 项目为编码 Agent 提供了记忆增强能力,而bd kv子命令体系就是其中一块关键拼图:它以键值对的形式持久化那些"放不进 issue 模型"的轻量级元数据——特性开关、项目配置、工作流状态乃至 Agent 跨上下文轮换存续的短记忆。本文以仓库中的设计文档 engdocs/design/kv-store.md 为骨架,结合当前仓库中已落地的实现代码(cmd/bd/kv.go、internal/storage/kvkeys/kvkeys.go)与测试用例,完整讲解bd kv的命令用法、底层存储模型、同步机制、保留命名空间约束以及面向未来的扩展设计,读完即可上手使用并理解其内部原理。
一、为什么需要 KV 存储:issue 模型之外的轻量元数据
Beads 的核心数据模型是 issue(bead),它承载 id、标题、描述、状态、优先级、类型等完整字段,生命周期遵循"open → work → close"。但对很多场景来说,这套模型过于笨重:
- 特性开关:
bd kv set debug_mode true,一条布尔标记即可; - 项目配置:
bd kv set entry_point src/main.ts,供后续 Agent 会话读取; - 跨会话工作流状态:
bd kv set current_sprint 42,记录进行中的上下文; - Agent 短记忆:在上下文轮换后依然存活的轻量记忆。
设计文档明确给出了两条"为什么不用现有机制"的决策理由:
- 为什么不用 config 表:config 表承载的是 Beads 内部设置(同步模式、集成配置等),把用户数据与内部配置混在一起会产生命名空间冲突;独立表更干净,也能避免未来冲突。
- 为什么不把 KV 对变成 issue/bead:KV 讲究轻量,issue 有显著的开销(id、标题、描述、状态等);两者的生命周期完全不同——KV 是"set and forget"(设置后不再维护),issue 是"open → work → close";而且把 KV 做成 issue 会污染
bd list的查询结果。
从当前仓库的实现看,这一设计意图得到了保留:cmd/bd/kv.go中kvCmd的 Long 描述明确写道,KV 存储用于存储"在会话之间持久化的 flags、环境变量或其他用户自定义数据"。
二、命令体系:set / get / clear / list
设计文档规划的命令签名如下:
bd kv set <key> <value> # Set a key-value pair bd kv get <key> # Get a value (exit 1 if not found) bd kv delete <key> # Delete a key bd kv list [prefix] # List all pairs (optionally filtered by prefix)所有命令都支持--json输出。需要注意的一点是:文档草案规划的是delete,而仓库中实际落地实现为clear。在 cmd/bd/kv.go 中,删除子命令的Use是clear <key>,输出为Cleared <key>。这是"设计与实现存在差异"的典型例证,使用时应以仓库当前行为为准。
2.1 完整用法示例
# 存储项目元数据 bd kv set primary_language go bd kv set entry_point cmd/bd/main.go # 读取值(未找到时输出 "(not set)" 并静默以非零码退出) bd kv get primary_language # Output: go # 列出全部键值对(按键名排序) bd kv list # Output: # Key-Value Store: # entry_point = cmd/bd/main.go # primary_language = go # 前缀过滤(文档规划的能力,注意当前实现按 --json 及全量返回为主) bd kv list entry # Output: # entry_point = cmd/bd/main.go # JSON 输出 bd kv list --json # Output: {"entry_point":"cmd/bd/main.go","primary_language":"go"} # 删除键 bd kv clear primary_language # Output: Cleared primary_language执行细节(与文档差异的说明):
- 文档示例的 JSON 输出形如
[{"key":"primary_language","value":"go","set_at":"2026-01-21T10:30:00Z","set_by":"beads/crew/collins"},...],即包含set_at、set_by元数据的数组形态;而当前实现中bd kv list --json输出的是{"key":"value"}形式的扁平 map(见 cmd/bd/kv.go 的printKVListResult)。这对应了文档中"是否在 config 之上单独建表记录set_at/set_by"这一尚待 Dolt 团队评审的设计分歧——当前实现优先保证了机器可读性,set_at/set_by元数据留待后续演进。 bd kv get对不存在的键遵循"SilentExit"契约:非 JSON 模式下向 stderr 输出key (not set)并以非零码退出,便于脚本判错;JSON 模式下返回{"key":...,"value":"","found":false}(见printKVGetResult,cmd/bd/kv.go)。
2.2 参数校验与保留命名空间
validateKVKey(cmd/bd/kv.go)在写入前对 key 施加严格约束,这是一般 KV 工具不具备的安全层:
| 校验规则 | 理由 |
|---|---|
| key 不能为空或纯空白 | 基础合法性 |
key 不能以kv.开头 | 防止产生嵌套的kv.kv.*前缀,破坏前缀隔离 |
key 不能以memory.开头 | 保留给bd remember/bd forget的持久记忆命名空间,避免与 merge resolver 的自动冲突解决机制互相干扰(GH#2474) |
key 不能以sync.、conflict.、federation.、jira.、linear.、export.、import.开头 | 这些是内部配置前缀,用户写入会造成命名空间污染 |
其中的kv.与memory.前缀由独立包 internal/storage/kvkeys/kvkeys.go 集中定义:Prefix = "kv."、MemoryPrefix = "memory."、MemoryConfigKeyPrefix = "kv.memory."。该包的文档注释揭示了设计动机:此前前缀散落在cmd/bd/kv.go、cmd/bd/memory.go和 storage 层三处结构上被迫复制的副本里,任何一处的重命名都会静默漂移,导致 merge resolver 匹配不到真实记忆键、pull/sync 配置楔子(config wedge)在改名后悄悄复发。集中定义后,一次修改即可被契约测试 internal/storage/kvkeys/kvkeys_test.go 捕获——该测试把kv.memory.前缀钉死为不可变契约。
三、存储模型:config 表上的前缀层
设计文档规划的是独立 Dolt 表:
CREATE TABLE kv ( `key` VARCHAR(255) PRIMARY KEY, value TEXT NOT NULL, set_at DATETIME NOT NULL, set_by VARCHAR(255) NOT NULL );| Column | Type | Description |
|---|---|---|
key | VARCHAR(255) | 主键,查找键 |
value | TEXT | 存储的值(恒为字符串) |
set_at | DATETIME | 设置时间(UTC) |
set_by | VARCHAR(255) | 设置者(如 "beads/crew/collins"、"human") |
而当前仓库的实际实现走的是另一条路:KV 存储是"config 表之上的薄前缀层"(thin prefix layer)。kvPairsFromConfig(cmd/bd/kv.go)把全部 config 键值过滤出kv.前缀并剥离前缀,得到用户视角的 KV 对;写入时则反向拼接kvPrefix + key作为实际存储键,通过store.SetConfig/store.GetConfig/store.DeleteConfig/store.GetAllConfig完成读写(见kvSetCmd/kvGetCmd/kvClearCmd/kvListCmd的 RunE)。这套"前缀层"方案天然继承了 config 表已有的同步、合并、并发控制能力,与文档中"独立 Dolt 表 + 独立 RPC"的规划形成对照,属于设计演进中的合理简化。
这一点在 proxied-server 模式的实现注释中讲得最直白:cmd/bd/kv_proxied_server.go 写道"kv store 是 config 表之上一个薄前缀层(kv.*),因此这些处理器镜像 config_proxied_server.go:每次写调用一次 RunTx 并携带真实提交消息,读用 RunTxRead"。其中:
- 写操作(set/clear)走
uow.RunTx,提交消息形如bd: kv set <key>,成功后置位commandDidWrite; - 读操作(get/list)走
uow.RunTxRead; - 底层存储错误原样透传(如
Merge conflict detected、constraint violation, transaction rolled back),调用方据此重试——这保证了错误语义在经典模式与代理服务器模式之间不漂移。
四、同步行为:随 config 一起走的共享记忆
设计文档规划的同步链路是:
- 导出:push 时 KV 表导出到
.beads/kv.jsonl; - 导入:pull 时
.beads/kv.jsonl导回 KV 表; - 合并:基于
set_at时间戳的 last-write-wins。
JSONL 格式为每行一个 JSON 对象:
{"key":"primary_language","value":"go","set_at":"2026-01-21T10:30:00Z","set_by":"beads/crew/collins"} {"key":"entry_point","value":"cmd/bd/main.go","set_at":"2026-01-21T10:31:00Z","set_by":"human"}文档论证了该格式的三个优势:git 中人类可读且可 diff、流式友好(追加无需重写)、与issues.jsonl模式一致。
在当前实现中,由于 KV 落位在 config 表内,同步行为与配置同步天然统一:kv.*行跟随 config 数据一起参与同步与合并。doctor命令提供了可视化的健康检查——cmd/bd/doctor/kv.go中的CheckKVSyncStatus打开数据库、统计kv.前缀条目数,输出类似12 KV pairs stored (syncs via Dolt)的检查结果,表明 KV 数据通过 Dolt 同步是当前实现的事实路径。set_at/set_by归属信息在文档中被论证为支持"多方写入场景下的冲突解决",当前实现把这一职责留给了 Dolt 的合并能力与 config 层的冲突处理。
五、服务器模式下的 RPC 操作(设计规划)
对于 server 模式,设计文档规划在 RPC 协议中新增以下操作:
| Operation | Args | Response |
|---|---|---|
kv_set | {key, value} | {success: bool} |
kv_get | {key} | {value: string, found: bool} |
kv_delete | {key} | {success: bool} |
kv_list | {prefix?: string} | {items: [{key, value, set_at, set_by}]} |
当前仓库以另一种等价机制实现了 server 模式支持:bd kv在检测到usesProxiedServer()时自动路由到 cmd/bd/kv_proxied_server.go 的四个runKV*ProxiedServer处理器,通过 UnitOfWork 的RunTx/RunTxRead在代理服务器上执行,而输出复用printKV*共享助手——注释特别强调"下游邮件传输解析kv list --json,输出必须跨模式字节级一致"。这保证了经典直连模式与代理服务器模式的输出形状永不漂移。
六、并发与行为验证:测试怎么说
仓库用三层测试锁定了 KV 的行为契约:
- 单元测试cmd/bd/kv_test.go:直接以
kv.为前缀的键调用SetConfig/GetConfig/DeleteConfig,覆盖 set/get、不存在的键返回空串、覆盖更新、删除后不可见等基础语义; - 嵌入式集成测试cmd/bd/kv_embedded_test.go:以真实
bd二进制运行bd kv set/get/list/clear,覆盖写入覆盖(overwrite)、含空格的值、--json解析、缺参报错;并发测试TestEmbeddedKVConcurrent用 8 个 worker 各写 5 个键再读回,验证了并发写受"one writer at a time"排他锁约束,未持锁的写方会收到明确错误; - 代理服务器集成测试cmd/bd/kv_proxied_integration_test.go:验证 proxied-server 路径的输出与经典模式一致。
运行嵌入式集成测试需要设置BEADS_TEST_EMBEDDED_DOLT=1环境变量(见测试文件头部的 skip 逻辑),这也是本仓库 Dolt 相关集成测试的统一约定。
七、未来扩展方向(v1 之外的预留设计)
设计文档明确列出了不在 v1 范围内、但设计已为之留出空间的四个方向:
- 本地专用键(Local-only keys):可用
_local.前缀约定表示不同步的键; - TTL/过期:后续可增加
expires_at列; - 命名空间(Namespaces):可增加
namespace列做作用域隔离; - 值类型:当前仅支持字符串,未来可加
--type=json标志。
结合当前实现,这些扩展方向与kv.前缀层模型的兼容性值得关注:memory.保留命名空间已经是"命名空间化"的先行案例;set_at/set_by元数据(文档中为冲突解决预留)当前未在 CLI 表面暴露,若未来落地 JSON 数组形态的输出或独立kv表,需要同步更新printKVListResult与测试中的 JSON 解析逻辑(cmd/bd/kv_embedded_test.go 的bdKVListJSON以{起始的扁平 map 解析,是一个需要随演进维护的解析契约)。
八、与其他存储机制的边界
bd kv并非 Beads 中唯一的持久化手段,理解边界有助于选择正确的工具:
- config 表(
bd config):内部设置(同步模式、集成配置),用户键受前缀校验保护不得侵入; - issue/bead(
bd create等):完整工作项,有生命周期与状态流转,出现在bd list中; bd remember持久记忆:位于kv.memory.命名空间,merge resolver 对这类键自动以--theirs解决冲突;bd kv set的校验规则阻止普通用户键写入该命名空间,避免用户数据被远端静默覆盖(cmd/bd/kv.go 注释详述了该设计动机);bd kv:轻量、set-and-forget、跨会话的用户自定义数据,随 config 同步。
结语
bd kv是 Beads 中"小而美"的设计样本:一份处于 Draft 状态的设计文档规划了独立 Dolt 表、RPC 操作与set_at/set_by元数据,而落地实现则务实地选择了 config 表前缀层方案,在继承同步、合并、排他锁能力的同时,用kvkeys单点前缀定义与三层测试守住了命名空间契约。对使用者而言,bd kv set/get/clear/list足以覆盖特性开关、项目配置与跨会话 Agent 记忆的绝大多数场景;对想要扩展它的开发者而言,engdocs/design/kv-store.md 中"未来考虑"一节与文档末尾留给 Dolt 团队的四个评审问题(schema 设计、DATETIME 存储方式、同步合并的 Dolt 细节、冲突解决归属),正是下一步演进的路线图。
【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考