← 返回列表
未验证
为编码代理建立规范项目记忆,自动浮现相关文章
尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/3 · 已提供中文文档
Pi 编码代理的规范项目记忆:每个资产一篇治理文章,一个仅追加的日志,胶囊式呈现。一个带有主干的 项目 wiki。
综合分
29.3
GitHub 分
29.3
用户评分
—
★ Stars
1
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add shaneconner/canon该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
pi-canon
Pi 编码代理的规范项目记忆。每个资产至多有一篇统辖它的文章,其地址由资产自身的路径计算得出:src/core/config.ts 由 articles/src/core/config.md 统辖。文章之下是一个仅追加的日志,每个事件一个文件。当工具调用触及某个受统辖的资产时,该文章那一行凝练的内容会未经请求地进入会话,因此代理无需知道有什么东西需要查阅。在工具调用中检测路径是尽力而为;将其解析为一篇文章则不是。
仅测量了一种配置,欢迎其他配置。 pi-canon 是在一种配置下开发和测试的:Codex,以 GPT 5.6 作为工作模型,使用 OpenAI 订阅。本 README 中的每个数字都是在该配置下测得的。其他模型、其他提供商以及按 API 计费的访问均未经测试。如果你在其他配置下运行它,欢迎反馈,也欢迎拉取请求。
以图的形式绘制的存储库,文章系于它们所统辖的资产之下
一个示例存储库:33 篇文章,20 条日志条目,40 个文件。圆盘是文章,圆环是日志条目,悬挂在各自被提炼成的那篇文章之下,而系在圆盘下方的一个方块就是该文章所命名的资产。有六篇文章不匹配任何资产,无所系挂,因为自由知识在这里并非特例。选中一个节点会打开它所包含的内容、它所指向的内容,以及指向它的内容。
安装
Pi
pi install npm:pi-canon
或者将此仓库克隆到 ~/.pi/agent/extensions/。需要 Node 22.18 或更高版本,Pi 0.x 系列上的 0.83 或更高版本。无需配置:存储库在首次写入时创建于 /.canon。该包仅导入 node:fs 和 node:path,别无其他,不进行网络调用,不运行 git,并且可在纯 node 下加载,无需构建步骤。
Codex
该仓库是一个 Codex 市场。添加一次,然后在用户作用域安装该插件:
codex plugin marketplace add shaneconner/canon
codex plugin add canon@canon
对于开发中的本地检出,请将 shaneconner/canon 替换为其绝对路径。安装或更新后,启动一个新的 Codex 线程。
Claude Code
同一仓库也是一个 Claude Code 市场:
claude plugin marketplace add shaneconner/canon --scope user
claude plugin install canon@canon --scope user
同样,绝对检出路径可用于本地开发。安装或更新后,启动一个新的 Claude Code 会话。
DeepSeek Harness
dsh plugin --profile add dsh-canon
在 npm 上单独发布为 dsh-canon,由同一镜像核心构建而成。该构建不包含检索器,也不包含设置界面。
Codex 和 Claude Code 插件启动同一个无依赖的 MCP 服务器,并暴露与 Pi 相同的 canon 操作。Codex 在每个工具结果之后浮现。Claude Code 在每个并行工具批次中,紧接下一个模型请求之前,对一个胶囊数据包去重,从而避免重复的消息框架,同时不延迟智能体的下一次决策。两者都会在智能体停止前给出一次写后提醒。一篇文章在每个压缩周期内至多浮现一次:一次压缩会开启一个新周期,而恢复同一个未压缩的会话则不会。一个会话可能包含多个压缩周期。压缩会丢弃先前的触碰状态,并且不重放任何内容。压缩之后,只有新的工具输入路径才能浮现该资产的精确文章或最近祖先文章;子文章和无关文章不会随之出现。在没有 .canon/articles 的项目中,这些钩子不起作用,而且它们绝不会仅仅因为会话打开就创建存储。当客户端要求时,请审查并批准插件钩子。通过 MCP 服务器写入的日志条目带有明确的 harness 来源,并在客户端提供时带有会话标识符。
默认值
安装后、未配置任何内容时你所拥有的:
- 寻址和触碰时浮现是开启的。 用一次工具调用触碰一个受治理的资产,其文章的胶囊就会未经请求地到达。这正是测量所保留的配对:寻址是随存储增长而存续的东西,而浮现是让寻址可被发现的东西。在规模研究中,每个请求决定性地址的会话都得分,而 30 个未请求的会话中有 0 个得分,因此等待被请求的召回恰好在存储增长到超出会话已知范围时停止工作。
- 日志是仅追加且安静的。 它从不被整体读取,也从不未经请求地浮现;搜索是到达它的唯一通道。将历史当作记忆的代价是每个会话中位数 340,119 个 token,而一份提炼文档为 21,309 个 token,且没有带来更多正确性。
- 推荐是关闭的。 retrieval: "none" 是默认值,因为该通道只在存储持有地址主干无法触及的知识、声明规则不治理任何资产,而该包无法知道它面对的是哪种存储时才值得付出代价。它还需要一个本包刻意不替你选择的检索器。用 retrieval: "lexical" 将其开启,突出闸门会以其测得的默认值 1.4 到达,并带有排空存储防护。
- 没有任何东西向会话打招呼。 定向行和会话结束复查被它们自己的测量移除:仅呈现记忆表面而背后空无一物,将首轮正确性从 32 个中的 25 个降至 32 个中的 8 个,而复查的提醒被送达却从未被付诸行动。
整个选项面是六个键:root、surface、resurface、retrieval、standout、mounts。每个都在 Options 下记录,并附有设定其默认值的测量。其他一切都是刻意设定的常量。
第一篇文章
会话不会受到问候:直到 0.2.0,每个会话都以一行开场白开始。一项使用惰性实现的 2x2 实验发现,那一行和工具 schema 都有显著的负向主效应,但它们 19/32 对 21/32 的比较并未确定哪个组件的代价更大。研究 3 发现移除那一行后没有收益损失,因此 0.2.1 删除了它。工具描述改为承载这一信条。第一篇文章只进行一次工具调用:
{ "action": "write",
"path": "src/core/config",
"capsule": "Loads layered config; env beats file; secrets never land here.",
"body": "Resolution order is defaults, then config.toml, then environment. ..." }
Wrote src/core/config.
存储区,在那次写入和稍后的一些工作之后:
.canon/
articles/
src/core/config.md governs the src/core/config address
lake/prices.md articles are not limited to code
journal/
2026-08-11-vendor-cap.md one file per entry, never rewritten by the tool
文章本身:
capsule: Loads layered config; env beats file; secrets never land here.
updated: 2026-08-11
Resolution order is defaults, then config.toml, then environment. ...
这就是整个存储格式。capsule 是 surfacing 发送的那一行密集文本,无论 agent 发送什么,写入时都会折叠为单行。updated 是最后一次改变了某些内容的写入日期:与已存储文章完全相同的写入是一次 no-op,不会触碰任何东西,因此该时间戳不会因重述而刷新,也不会拿它与资产进行比较。这两个键是 pi-canon 唯一拥有的键。块中的其他所有键,包括 Obsidian 属性,都会在写入时原样保留,而自有值只在纯 YAML 会误读它们的地方才加引号,因此这棵树仍然可以手工编辑。
结果是纯 Markdown 和一个有效的 Obsidian vault。把它与你的仓库一起提交:git 就是历史、diff、blame 和时间机器,而 pi-canon 自己从不运行 git。日志条目也是普通文件。工具只会追加它们;用普通文件工具读取它们。
这是为哪种失败而设计的
一个 agent 为人类读者格式化一列原始整数美分,2,255.65,而文件里原本是 225565。测试通过了。在另一个仓库中,一个在那次会话期间没人打开过的仓库,一个财务解析器读取该文件,并把任何含有逗号的行视为损坏,于是它丢弃该行并继续读取。没有异常,没有构建失败,一个数字从下游总计中缺失,直到有人手工对账。
检索无法防止这种情况,因为搜索只有在某个东西想到要运行它时才会运行。为了可读性而格式化一个数字并不是一个会引发疑问的时刻。没有什么可怀疑的,所以也没有什么可搜索的。
项目工作中代价高昂的失败,并不是智能体去查了某个东西然后得到了糟糕答案的那种。而是没有人知道存在一个问题需要去问。
这个场景不是战争故事。它是下面基准测试中五条链之一,之所以写下来,是因为它正是这个包旨在防止的失败形态。该基准测试没有任何一条链是由搜索驱动的,所以这是设计动机,而不是与检索进行的实测对比。
这从何而来
pi-canon 底层的形态是 LLM wiki:一个由 Markdown 文章组成的文件夹,智能体在其中撰写和重写,文章之间相互链接,且不预先声明任何模式。Andrej Karpathy 在 llm-wiki.md 中引入并推广了这一模式,其定位是反对在查询时重新检索原始片段。它做对的大部分内容在这里都被原样保留。纯文件,因此任何东西都能读取它们,人也能在编辑器中修改某一篇。没有数据库,也没有需要重建的索引。随仓库一起提交,因此 git 提供了历史、diff 和 blame。而且没有预先定义的模式,因此知识会呈现项目实际具有的形态。
这种自由也正是这类存储失败的地方,而且是两个具体方向上的失败,而不是含糊的失败。
散乱。 没有任何东西把某篇文章标记为关于某个主题的那篇文章,所以找不到现有文章的智能体就会再写一篇。现在有了一篇关于供应商 feed 的笔记,第二篇关于 feed 分页,第三篇关于同步任务,全都从三个角度描述同一个约束,没有一篇是错的。检索会找到全部三篇,智能体读取排名最高的那篇,而当它们彼此不一致时,没有任何东西在它们之间作出裁决。
日志漂移。 智能体在工作期间写入的存储会充满事件,因为工作就是由事件构成的:尝试了什么、什么失败了、什么被修复了。当前真相最终被埋在一段关于它如何成为真相的运行日志之下,而本应说明规则是什么的页面,说的却是十四号那天发生了什么。
两者都不是存储失败。在这两种情况下,知识都存在,被写了下来,就放在那个文件夹里。散乱是寻址失败:优先级在多个副本之间未定义,因为没有任何东西指定其中一个是权威版本。日志漂移是可变性失败:事件历史和当前参考知识共用同一个页面,而后来的重写可能把其中一方从另一方底下编辑掉。
pi-canon 是对这一模式的增量改进,而不是替代,它增加了三样东西。
一份日志,仅追加,每个事件一个文件。智能体无论你是否希望,都会记录,而这种冲动必须落到某个不是参考页面的地方。进入时的指令是按来源到达时的样子记录它,包括名称和确切数字,因为文章会提炼,而只有日志保留原始内容。
一条脊柱,即寻址约定。一篇文章的地址是由资产计算得出的,而不是搜索得来的,并且无需为此映射成立配置任何东西,这使得脊柱成为一种约定而非一种模式。这也正是 RECALL 路径中没有任何搜索的原因:当一次触碰已经决定了地址,就没有什么可寻找的了。search 动作是为相反的方向而存在的,即想要发问的智能体,并且它从不未经请求就运行。
浮现,是推送而非拉取。当检测到一次工具调用触及受治理的资产时,该文章的胶囊会为该会话暂存,只要它留在上下文中,每篇文章至多一次(见“浮现”),因此没有人需要想到去发问。对工具调用内部路径的检测是尽力而为的。而一旦路径在手,解析则不是。
下面的评估并不检验那条谱系论证:没有任何被评估的分支是搜索驱动的 LLM wiki,因此这里没有任何内容表明 pi-canon 胜过一个训练有素的 wiki。
寻址
地址是资产路径去掉文件扩展名后的结果,而去掉只发生一次,在边界处。名称必须位于点之前,因此 .env 保持为 .env。只有最后一个斜杠之后的点才算数,因此 docs/v1.2/notes.md 规范化为 docs/v1.2/notes。而 src/core/config.test.ts 落在 src/core/config.test,与 config 并列,而不是覆盖它。点段在根处被钳制,并且包含性在 write 内部被再次检查,因此没有任何地址能逃出 articles/。
解析先尝试精确地址,然后一次向上走一个路径段,直到最近的现有文章,如果到达顶部仍未命中则返回空。没有排名,没有评分,也没有相似度:给定一个路径,治理文章是树中现有内容的函数。因此并非每个文件都需要一篇文章:src/feed 处的一篇文章为其下所有没有更近文章的内容作答。创建一篇文章是不常见的行为;常见的行为是更新那篇已经治理它的文章。
重命名是你自己进行的文件移动。pi-canon 不监视文件系统,也没有重命名动作。将文章移动到新路径所派生出的地址。Lint 会检查接下来所写入的任何文章内部的 wikilinks,因此一个仍指向旧地址的链接会在该文章下次被写入时被指出,而不是在移动的那一刻。
不匹配任何资产的文章是普通的自由知识。脊柱为项目已有的资产保证一个地址;它并不把存储限制于这些资产。这个权衡值得在同一口气中说明:浮现是以资产为范围的,因此一篇离脊柱的文章是通过链接或显式读取来抵达的,而不是在触碰时被推送,或者在配置了检索器时通过相关性来抵达。
这样一篇文章可能会这么说,并在写入时带上 scope: rule。忘记声明永远不会把一篇脱离主干的文章排除在检索之外。声明它则把有意归档的规则与资产已消失的文章区分开来,并且如果某个资产之后出现在同一地址,该规则仍保留在检索语料库中。scope: asset 则撤回该声明。
工具
一个工具,canon,五个动作:read、write、journal、map 和 search。
| 动作 | 参数 | 作用 |
|---|---|---|
| read | path | 返回起支配作用的文章:标题、capsule、updated、正文,以及一行日志索引。未命中时返回一句话,指明该地址,并邀请在任务完成后写入。当由某个祖先回答时,标题读作 governs ,因此层级可见。 |
| write | path、capsule、body、scope | 创建或更新文章,然后返回 Wrote . 以及任何建议性 lint。从不拒绝。空字符串表示未改动,而非擦除。与已存储文章完全相同的写入会被报告为已是最新,且不触碰任何内容,因此 updated 始终表示内容最后变更的日期。 |
| journal | body、subject、slug | 将一条带日期的条目作为独立文件追加,-[-n].md。canon 永远无法重写它。空正文会返回一句话,询问发生了什么。 |
| map | path(可选前缀) | 每篇文章一行,格式为 address: capsule;当存储或过滤器为空时返回一句话。输出无上限。 |
| search | query、journal | 根据词语对文章进行排名,十条结果,每条都带有界定其范围的内容:文章则带有其地址和 capsule。日志通过 journal: true 选择加入,因为事件是历史而非当前真相,并且在真实存储上实测,关于某个事件的日志条目会挤掉承载它的文章;默认搜索从不读取条目正文,并说明日志存在。选择加入后,日志条目是一等结果,按时刻和主题界定范围,文章保留一半窗口,较短的一方让出其槽位。说明上限丢弃了多少匹配项。这是唯一会触及日志内容的动作。 |
subject 是一个地址数组。作为裸字符串传入的 subject 会被忽略,条目最终不带任何 subject。
当读取那些文章时,用 subject 地址记录的条目会以文件名的一行索引形式返回,最新三条:历史可供查看,但默认从不加载。该索引只携带文件名,从不包含条目内容,且匹配是精确的,因此在 src/core/config 归档的条目不会在读取 src/core 时出现。日志始终位于项目存储中。
写入时的 lint 是附加在响应中的提示性字符串,绝不是拒绝,因为被阻止的写入会教会 agent 停止写入,而警告则教会它下一步该做什么。唯一的例外是存储本身在其 schema 中声明为 required 的规则,记录在下面的文章 schema中。它在正文超过 8,000 个字符时发出警告,并在超过 20,000 个字符时建议改为分层结构。它会指出缺失的 capsule、超过 1,000 个字符的 capsule,或写成变更日志的 capsule。带有 log、journal、session、standup 或 meeting 段,或 ISO 日期的地址,会被重定向到 journal。失效的 wikilink 每行指出一个。资产丢失的文章会在读取和写入时被指出:一个嵌套地址,其父目录在磁盘上存在,但没有任何内容与资产匹配,就会引出孤儿问题,并列出解决途径(移动文章、将其并入父级,或声明 scope: rule)。根级地址、已声明的规则,以及地址从未映射到文件的存储保持沉默。
正文增长的改写会在自己的结果中说明:Body grew 812 -> 1304 bytes.,随后提醒文章承载当前状态,而叙述性历史(旧值、转换)属于 journal。任何增长都会触发;创建不是增长,仅写入 capsule 也绝不会使已存储的正文增长。这一行是被测量出来的,而测量的是方向而非大小。在三次双臂捕获中,针对字节完全相同的八会话谱系,其工具说出这一行的臂每次结束时留下的被取代值都更少:在一个模型上是 51 对 96 中的 88,在第二个模型上是 45 对 87,而当第一个模型再次运行、且在一半谱系中反转臂顺序时,是 71 对 85。取方向,不要取幅度。第三次捕获保持了方向,却失去了大部分差距,而两天后重新运行一个匹配的未处理单元,使其存储中位数移动了 39%,因此该工具无法可靠地测量自身的幅度。存储大小也从未承载这一点:它先下降五分之一,然后几乎不动,接着又下降五分之二,而在配对谱系之间,这一变化的方向在前两次捕获中与随机无法区分。96 名读者中有两名因陈旧值而受损,每个臂各一名,因此尚不知道这一行能保护读者。系统提示中的同一信条清除了三十二个被取代值中的一个,不过该臂运行在一个单独的单会话研究中,二者从未被并排比较。
有一行 lint 在性质上有所不同。当一次写入提供了正文,而此前已存在一篇文章时,新正文会与之前的正文进行比较,如果某一行曾带有约束性语言却消失了,它会被引用回删除它的那次写入。词汇表是固定的:must、never、always、require 及其 requires 和 required 形式、do not 和 don't。每次写入最多点名两行,每行截取到前 160 个字符,并附注:如果该约束仍然成立,它就应该保留;如果它确实发生了变化,变化应记录在日志中。引用是前缀而非摘要。它是建议性的:写入已经落盘,没有什么能让 agent 把那行放回去。
文章 schema
每个存储都将其契约作为一个文件携带:存储根目录下的 schema.json,在存储首次持久化任何内容时,随附的默认值会被显式写出。它是数据而非配置,因此它随存储一起移动,因项目而异,任何其他读取该存储的工具都能从同一个文件执行相同的契约。随附的默认值不要求任何内容,并镜像建议性上限,因此未改动的文件不会改变任何行为;它只是让契约可见且可编辑。
三个字段可以携带规则:capsule(front matter 行,surface 注入)、title(正文开头的 # 标题;有意不设单独的标题输入,因此规则检查标题唯一可能存在的地方)和 body。四个规则键:required、min_chars、max_chars、hint。
执行是有意不对称的,而且这种不对称是经过测量的:在本项目实验室的五次写入质量模型捕获中,由工具边界执行的规则出错率为零,而交由模型判断的规则出错率约为百分之一。因此,真正重要的那类遗漏是被强制执行而非建议的:
- 标记为 required 的规则会拒绝违反它的写入,包括 hint,且不会触碰磁盘。一次写入根据它所更改的内容来评判,再加上文章首次创建时的全部内容,因此仅更新 capsule 永远不会被遗留正文所挟持。
- 其他所有违规都会警告:写入落盘,消息指出需要修复的内容。
- 读取从不拒绝,但会报告现存问题,因为持有不合规文章的 agent 才是最有能力修复它的一方。
- 格式错误的 schema.json 会以开放且响亮的方式失败:规则停止执行,并且每次写入都会说明这一点,因为所有者以为契约正在执行、而一个拼写错误却将其禁用,是最糟糕的状态。
- schema 声明的边界拥有自己的消息:针对同一方面的内置建议行保持沉默,而不是重复说两遍。
一个要求每篇文章都以标题开头的存储:
{
"schema_version": 1,
"article": {
"capsule": { "required": true, "max_chars": 1000, "hint": "One dense line of current truth." },
"title": { "required": true, "hint": "Start the body with a # heading naming the asset." }
}
}
删除一条规则即可将其丢弃;删除该文件即可完全禁用 schema 检查。已编辑的文件绝不会被覆盖。
关系规则位于同一文件中的 relations 下,因为引用图也是契约的一部分;每个工具都会强制执行它所能看到的规则,因此一个文件管辖所有读取该存储的工具。本包在写入边界处查看文章自身的引用,并强制执行 refs:required 会拒绝正文未引用任何内容的写入,其判定方式与字段规则相同(即改变引用集的那次写入,或一次创建),而 min_count 会在低于下限时发出警告,代码围栏示例和大小写折叠后的重复项永远不会被计入。两条图级规则在此处解析,并由读取图的工具(例如 canon-atlas)强制执行:orphan.warn 在没有其他文章引用此文章时发出警告,children.listed 在文章未引用其地址下的每个直接子项时发出警告。
{
"schema_version": 1,
"relations": {
"refs": { "required": true, "hint": "Name what this concerns." },
"orphan": { "warn": true },
"children": { "listed": true }
}
}
呈现
一次工具调用会为它所触及的任何内容暂存管辖文章,并且不发送任何内容。每一轮结束时,会将所有暂存内容作为单条消息刷新,因为 pi 的引导队列每次提供商往返只排空一条消息,而每次工具调用一条消息会让每条提醒都拥有自己的模型调用。带有存在标记的文章在该标记仍保留在提供商所接收的上下文中时最多呈现一次;一旦被折叠或压缩掉,它就会重新回到呈现流程,并在其资产下一次被触及时再次出现(见下方 resurface 选项)。交付文本若短于 24 个规范化字符,则无法被安全测试,并保守地在整个会话中保持为已见。任何内容都不会跨会话持久化:新会话会重新呈现所有内容。
这一切都不由字符数决定。一个 capsule 的编写目标是适配 1,000 个字符,而这是写入时交给 agent 的目标,不是读取时的门槛:如果某一轮触及了某篇文章的管辖资产,那么该文章要么整体呈现,要么不呈现。早期版本会从会话额度中扣除 capsule 文本,并将溢出内容降级为裸指针。该额度已在 2.0 中移除。它是一个常量,在猜测一项无人测量过的策略,而它所决定的是 agent 能看到多少内容。取而代之的是测量:每一行呈现内容都会记录它消耗了多少窗口,因此被占用的上下文可以在事后对照相关性来解读,而不是事先由一个常量来裁定。某一行不是 capsule 文本的唯一剩余原因是该文章没有 capsule,此时它会作为指针呈现,指出地址并告诉 agent 去读取它。
通过 canon 阅读一篇文章,会在消息发出前撤回为其暂存的那一行,因此拉取优先于推送。读取资产文件本身则不会,因为读取文件并不等于读取关于它的已知信息,而胶囊可能恰好持有文件所不包含的约束。只读会话会安静退出。在一次成功的写入、编辑、补丁或被识别的变更型 shell 调用命名了受治理资产之后,结算会为其文章抽取一条提醒(如果该文章未被更新),每批一次,并由下一次修改型调用重新武装。未知工具在命名路径时仍会浮现知识,但不会在没有确凿变更证据的情况下凭空捏造更新义务。
在工具调用中查找路径是尽力而为的。只有工具调用的输入会被扫描。结果永远不会被扫描,模型的散文也不会。输入会被扫描以查找完整的短字符串以及磁盘上存在的路径形状 token,或其父目录存在的路径形状 token,因此一个即将被创建的文件仍会浮现其治理祖先,而一个位于更长字符串中且内部含有空格的路径则会被遗漏。它所供给的——从路径到治理文章的解析——是确定性的。这两项主张有意保持分离。
/pi-canon 打印一行状态:存储根目录、挂载数量(如果有的话)、文章数量、日志条目、本次会话中浮现的文章,以及其中有多少仍在上下文中及其占用情况。它输出到 UI,不向模型发送任何内容,因此询问不消耗上下文。PI_CANON_TRACE= 为每个浮现决策追加一行 JSON,当该变量未设置时不产生任何效果。
选项
作为包安装时,pi 加载默认导出并采用默认值。要传递选项,请编写你自己的扩展文件,并让它调用具名导出:
// ~/.pi/agent/extensions/my-canon.js
import { registerPiCanon } from "pi-canon"
export default function (pi) {
registerPiCanon(pi, { mounts: ["/data/lake"] })
}
六个键,任何其他键都会在注册时按名称抛出错误,因为其他一切都是有意设为常量的。
四个行为键(surface、resurface、retrieval、standout)也可以来自 ~/.config/pi-canon/settings.json,/canon-settings 命令会在 TUI 内部编辑该文件:布尔值和检索周期,standout 截止值沿其格点以左/右步进,并在按 Enter 时取精确值,且每一项应用的更改都会通过注册所使用的同一验证立即保存。显式选项优先于文件。root 和 mounts 是逐项目拓扑,仅保留在代码中;它们在编辑器中没有任何行,在文件中也没有任何位置。
- root 放置存储。绝对路径按给定值使用,相对路径会加入项目 cwd。默认 /.canon。
- surface: false 会静默每回合刷新和结算提醒。canon 工具和 /pi-canon 仍保持注册并正常工作。
- resurface: false 使一篇文章在每次会话中最多重新浮现一次,无论它多久之前离开了窗口。默认值为 true:带有存在标记的文章仅在该标记仍存在于提供者接收到的上下文中时才算作已读,因此一旦被折叠或压缩掉,下次其资产被触及时就会再次浮现。短于 24 个规范化字符的文本没有安全标记,因此保守地保留每会话一次的行为。正是新的触及才使带标记的文章回归,所以没有任何内容会自行重新浮现。
- retrieval 根据智能体正在做的事情对检索语料库进行排序:每一篇偏离主干的文章,加上任何声明为 scope: rule 的文章,这样如果某个资产稍后出现在其地址处,规则仍然可达。普通的资产作用域文章不参与,因为地址主干已经能够到达它们。默认值为 "none",即不按相关性排序也不浮现任何内容:仅靠主干,与 1.0 完全一致。"lexical" 是标准库上的 BM25,无依赖也无模型。任何需要模型的内容都在此处以 { name, score, index? } 的形式提供,因此本包从不携带模型,也从不决定你运行哪一个。配置了检索器后,工具的归档规则也随之改变,因为该建议在两个方向上都要付出知识代价。在默认情况下,它说归档在资产路径之外的知识永远不会浮现,这是事实,也是你不应将其归档在那里的原因。有了检索器后,它说的则相反:一条约束管理许多资产且不拥有任何资产,应放在以其自身命名该规则的地址处,因为不相关包唯一共享的父级是根,而根文章会在任何事物的每次触及时浮现。
- standout 指的是排名最佳的文章必须比那篇无论如何都不会被采用的最佳文章高出多少,也就是第四篇——三篇每条消息的上限本来就会将其抛下的那一篇。它是一个倍数,而不是一个分数:standout: 1.5 要求最佳文章得分达到第一个被拦下的竞争者的 1.5 倍。默认值 1.4,这是一个由 120 格基准测试定价而非随意选取的操作点:其与未截断通道的 n=15 对比在 p=1.0 时规则事实相差 -0.07,同时将建议从每会话 26 条削减到 3 条,将智能体据此行动的比例从 0.17 提高到 0.82,并且在没有任何相关内容可说的存储上从未触发,139 次中 0 次排名。这一微小的观测差异并非通用的检测界限。精确率是需要保护的一侧,不过实测论据是 token 而非注意力:一项配套的 124 格研究发现,好的建议被埋在二十七条之中与作为四条之一时,被打开的比例相近,因此截断所节省的是它拒绝花在那些永远不会被阅读的行上的上下文。另一侧也有代价,同一基准测试也付出了这个代价:截断值设得超过决定性排名所能达到的水平会使通道沉默,而在 2.0 时它什么也没交付,并让通道赢得的一切都付诸东流。standout: 1 不是截断,而是用于阅读你自己存储轨迹的测量设置。低于 1 会在注册时抛出异常,因为它要求最佳文章比其竞争者更差,这是仍在用分数思考的调用者会写出的东西。
它是一个比率,因为分数不是同一个量出现两次。 lexical 会针对由查询计算出的饱和上限进行归一化,因此分数是该查询可能达到的最佳匹配的占比,而不是你存储中可用最佳匹配的占比,并且它会随着智能体说得更多而下降。在一个 380 篇文章的存储上,同一篇文章在相同相关性下,针对一个简短问题得分为 0.68,而在周围有一百个词的工具有输出时得分为 0.16。在两次基准测试运行中,它不只是不稳定,而是发生了反转:在一个语料库上保留答案需要低于 0.11 的截断值,而在另一个语料库上使其沉默则需要高于 0.73 的截断值。除以来自同一查询的另一个分数则会将两者抵消。
比较是相对于排名的顶部,而不是其分位数,因为一个智能体的回合很长,几乎触及所有内容:在一次基准会话中,378 篇文章里有 377 篇被触及,所以沿列表向下十分之一的位置已深入共享同一个常见词的大量内容之中,与它的比值描述的是你的语料库,而不是这个查询。在那里测量时,普通查询达到 2.64 到 3.28,而那个确实有东西可找的查询达到 3.10,处于该范围内而不是高于它。在最前面的几个排名中,同样的会话清晰地区分开来:每个携带决定性文章的排名都以 1.68 到 1.81 击败其第一个被保留的竞争对手,而每个没有携带的排名都保持在 1.00 到 1.28 之间。仍然要在你自己的存储上测量:设置 PI_CANON_TRACE 并读取 ranked 行,它们记录每个查询达到了什么以及是否通过。
被抽干的存储不会让该比值变成一张免费通行证。 在长会话的后期,一旦一个小存储要说的大部分内容都已交付,仍然符合条件的文章就是分数接近零的尾部,而仅基于剩余内容的比值会让垃圾凭借极小的数字通行。因此,当截止值生效时,最佳文章必须击败的竞争对手被设下限为同一个查询已经引出的最强已交付文章:剩下的内容必须击败该查询在可能的情况下会重新引出的内容。一个真正的新主题能越过这个下限,因为已交付文章在其查询上得分很弱;剩余内容则不能。在一个真实的 33 篇文章存储上重放时,未设下限的比值曾把该存储完全清空进窗口,33 篇文章一路降到最低搭载分数 0.002,而该下限将会话削减到 15 次搭载,下限为 0.075,并且最强搭载有所改善,因为在该得到它的查询到来之前,存储并没有被花在垃圾上。
查询是意图,绝不是证据:用户自己的话来自实时上下文,最新优先且有界,对于长到无法整体携带的消息保留两端,因为两端都不是可靠的请求,再加上本回合的工具调用,按其名称和第一个参数。工具结果永远不会到达它,模型的散文也不会,pi-canon 自己的提示也被排除,这样一篇文章就不会因为已经被呈现过而获得高分。相关性和传输分别有界:standout 决定查询是否得到答案,并且在此之上,最多三篇排名文章随一条消息搭载,最佳分数优先,而通过地址到达的文章从不计入其中,因为地址是确定性,而分数是猜测。一篇排名文章是由新的意图来支付,而不是由另一个回合通过来支付,所以一个未改变的问题不会一直再释放三篇,直到残余耗尽。跟踪记录被保留的最佳分数与已发送的最差分数。一个抛出异常的检索器只会让该回合失去其排名,别无其他。
- mounts 列出项目之外、在其资产旁带有自己的 .canon 的目录。mounts: ["/data/lake"] 将文章作为 lake:prices 提供,可通过该名称或挂载点内任意绝对路径寻址。两个挂载同一目录的工作区读写同一个存储,因为存储与其所管辖的资产放在一起,共享无需任何协议。挂载没有自己的日志:事件属于项目历史,每一条记录都落入项目存储。
代码持有什么,又要求什么
一份不可变的日志、一条寻址主干,以及不请自来的召回,听起来像是一种移除了对模型行为依赖的设计。它并没有。它把这种依赖移到了界线的一侧,并约束了另一侧,而这条界线短到可以完整陈述。
由运行时持有:
- 日志条目以独占创建标志创建,因此 canon 从不重写或删除条目,名称冲突会递增后缀而不是丢失条目。文件保持为普通 Markdown,因此任何其他工具仍可重写或删除它:仅追加是该工具的属性,而非文件系统的属性。
- 一旦路径在手,它便解析为恰好一篇文章,向上走到最近拥有文章的祖先,或者什么都没有。
- 文章完整呈现,没有任何字符数能将其截断或阻止其出现。
- 带有存在标记的文章在提供方收到的上下文中该标记仍存在时,至多呈现一次。存在性是从该投影中读取的,而非记住的,因此折叠或压缩会让被标记的文章重新呈现;无法测试的过短投递,或报告无投影的测试框架,会降级为每个会话至多一次。
- Codex 和 Claude Code 钩子无法检查该投影。它们改用显式的压缩周期:会话开始、上下文清除和压缩会丢弃先前的触碰状态;恢复则不会。这些事件都不会呈现文章。之后的资产触碰才会。Codex 在每次工具结果后投递;Claude Code 在下一次模型请求前将当前并行批次合并为一个数据包。
- 通过工具阅读文章会在消息发出前撤回其暂存的胶囊。
- 只读工具可以呈现文章,但永远不会为其写后提醒武装。成功的修改工具可以。
要求 agent 做到,且不被任何东西检查:
- 在处理资产前阅读管辖文章,并在真正更改后更新它。没有任何写入以先前的读取为门槛,而结算提醒是消息而非门槛。
- 按源到达时的样子记录,包括名称和确切数字,因为文章会提炼,只有日志保留原始内容。
- 将条目归入正确的主题下,并将约束归档在它所管辖的资产处,而不是你恰好编辑的那个资产处。在默认的 retrieval: "none" 下,归档在资产路径之外的知识永远不会浮现;配置了检索器后,一条横切规则反而可以存在于它自己声明的地址上,并按相关性浮现。
- 当某个胶囊或指针说有一篇文章时,就打开它。上下文中的一行不算读过。
- 判断一条被丢弃的约束是否仍然成立。然后遵循规则,去对抗一个要求别的东西的实时提示。
包中没有任何东西能强迫智能体保留它已决定删掉的一行。
这个包不做什么,说明如下,以免上面的内容被读得超出其本意:
- 没有未经请求就运行的搜索。search 是智能体调用的一个动作;接触通过精确地址或祖先遍历解析到文章,从不通过排名,并且没有任何查询会代表智能体触发。
- 没有嵌入,也没有模型。retrieval: "lexical" 在脊柱外文章加上已声明的规则上构建一个 BM25 索引,而普通的资产范围文章从不参与排名;任何其他排名器都是调用方提供的函数。
- 没有文件系统监视,也没有陈旧检测:updated 是最后一次写入的日期,从不与资产进行比较。
- 没有删除,也没有重命名。移除或移动一篇文章是你执行的文件操作。
- 文章是最后写入者胜,没有锁,没有合并,也没有别人改了文件的警告。只有日志条目会得到冲突重试。
- 没有重复检测。每个资产一个规范地址是结构性的,而非经过检查的。
- 没有任何东西会自行写入、总结或压缩,也没有任何东西会过滤进入的内容:没有机密扫描,也没有脱敏。pi-canon 写下的每一行都来自一次显式的工具调用。
- 关于浮现的任何东西都不会在会话之间持久化。新会话会重新浮现一切。
- 存在性是针对文章地址以及它实际放入提供方投影的尾部进行测试的。任何短于 24 个规范化字符的投递都没有安全标记,并被保守地视为存在;那可能是一次很小的读、写,或一条异常短的浮现行。对于有标记的投递,没有已投递尾部的摘要算作不存在。
证据
这个包是被测量出来的,而不是被断言的,测量结果存在于
它们自己的仓库中:canon-bench,
这是这条工作线的基准和证据仓库。
头条研究在一个共享工作树中运行五条多会话链,每条都在一个会话中种下一个约束,并在之后的会话中探测它,然后给智能体是否仍然遵守它打分。相对于一个获得先前记录并确有阅读记录的未扩展基线,这个包在 20 个陷阱单元中避开了 19 个,而基线避开了 8 个,并且以大约基线中位 token 成本的三分之一回答了回忆审计。回忆准确率本身在各组之间不相上下,而一个
静态教义文件在两项计量指标上都更便宜,同时少通过三个陷阱单元。完整表格、各实验组以及局限性见
RESULTS.md。
正是那项研究的取证审查确立了当前的研究方向:在十四次召回失误中,有十三次最初出错于写入台(从未捕获,或捕获后又被覆盖),没有一次出错于检索。随后的写入侧研究计划见
write-desk/,
上文记录的增长线正来自那里:两个实验组跑在字节完全相同的八会话历史上,其中工具名称明确表述增长的实验组在全部三次捕获中留下的被取代值都更少。其中第三次是对衡的,也是最该先读的一次,因为它保持了方向,又把大部分幅度收了回去。
论文
每篇论文都在 Zenodo 上带有其逐单元的人工产物轨迹。以下每个 DOI 都是概念 DOI,因此它解析到该论文的最新版本,而不是某一次冻结的存档。
- Mutable Canonical Memory over an Immutable Journal, with Recall by Surfacing,doi:10.5281/zenodo.21890647。第一轮研究,也是追问该设计究竟是否成立的那一轮:每项资产一篇治理性文章,其下是一份仅追加的日志,召回在触碰时到达,并以一个未扩展的底线为对照进行测量——该底线接收了先前的转录文本,且有记录显示它确实读取了这些文本。
- Pricing Recall in Long-Term Memory for AI Agents,doi:10.5281/zenodo.21960350。六项研究,考察召回的成本以及其中哪些部分值得保留。它为定位行和工具模式定了价(两者均为负值,均被删除),把 standout 截断值设在了一个实测工作点上,并发现了存储规模超过某个阈值后,等待被询问的召回就不再起作用。
- The Write Desk,doi:10.5281/zenodo.22057257。前两篇论文测量了召回,并把存储中所持内容为真当作理所当然。这一篇对此进行检验,发现它并不成立:写入者反复把被取代的值留在记录中,而这些记录的契约是陈述当前为真之事。在一种条件下,工具在写入边界发声,该终点在 24 次捕获谱系比较中有 20 次更低,2 次持平,2 次更高,但这一幅度未能经受住对衡重复的检验,因此被撤回而非加以限定。它还冻结了此前一直实时读取其语料的检索基准,并报告了在冻结过程中由审查发现的两个缺陷所带来的成本。
- 持久的契合,doi:10.5281/zenodo.22087390。这条线尚未提出的生命周期问题:一旦某个结果进入智能体的实时上下文,它必须在那里停留多久,又该由谁来决定?六项预注册研究将已注册的上下文撤回设计阶梯运行到了终点。一个瞬态引导线索可以在其唯一一次消费性回复后离开;在精确输出契约下的任务证据,在测试的固定窗口下,无法被逐出到任务工作集以下;而当由模型决定时,它在披露的临时默认设置下保留了全部 139 个被查阅的结果。已发布的默认持久设计在此用例中得到了验证,这条线就此收束,没有计划中的下一步。
更多
- 基准测试、驱动程序、冻结协议,以及从产物重新计算每篇论文定量声明的验证器:canon-bench。
- canon-atlas,一个用于此类存储的查看器:文章图、每个节点指向什么,以及什么指回它。它是在考虑 pi-canon 的情况下构建的,并保持包无关,因此它可以读取任何作为记忆的结构化 Markdown 目录。它还强制执行本包解析但自身无法看到的两个全图 relations 规则:orphan.warn 和 children.listed。
- 图表的交互版本以及完整的测量故事,第 1 部分和第 2 部分:shaneconner.com/projects/pi-canon。
- 第一轮活动的叙事版本:我的智能体的 wiki 写得比读得快。
- 第二轮活动关于召回定价:AI 智能体长期记忆中的召回定价。
- pi-fold,一个服务于工作层的独立可选包。pi-canon 提供同一四层栈中的两个持久层:日志是情景层,正典是语义层。两者可组合,彼此都不需要对方,也都不知道对方花费了什么。
MIT。在此仓库的克隆中,node tests/verify.mjs 运行门禁套件:每个不变量都按名称打印,运行必须以 all N gates green 结束,在此版本中为 174 个。同作者(shaneconner)的其他插件
扫码进群