内存会话与可重复练习
用 SessionManager.inMemory() 组织最小验证,让练习不污染用户历史,也不依赖可恢复的会话文件。
内存会话与可重复练习
做练习最怕的事是:跑了一堆实验,回头发现自己的会话历史里全是"测试测试测试",分不清哪条是真工作。
Pi 默认会持久化会话。createAgentSession() 不传 sessionManager 时,工厂建的是文件型 SessionManager,会话按工作目录分组写到 ~/.pi/agent/sessions/ 下。这意味着你每次在某个目录跑一次任务,都会往用户历史里塞一条记录。
这篇解决的就是这件事:用内存会话做练习,跑完什么都不留下。
读它需要会用 TypeScript,并了解 Pi 会话的基本概念(会话、消息序列、会话管理器)。Pi 迭代很快,动手前先记录 pi --version;本文依据 Pi 官方文档 与 官方仓库 编写。
一、先把契约说清楚
后面才不会靠猜。
二、四种会话工厂
SessionManager 有四个静态工厂方法,差别在"存不存盘"和"从哪儿来":
inMemory() 的完整签名是 inMemory(cwd: string = process.cwd(), options?: NewSessionOptions, entries?: FileEntry[])。三个参数都有实际用途:
cwd决定工作目录。它影响项目资源发现、上下文文件(AGENTS.md)、会话分组和内置工具的相对路径基准。entries让你从一份在文件系统之外持有的条目数组开始构建会话。options是NewSessionOptions,用来设置会话元数据。
官方 SDK 文档里的最短用法,加上释放之后是这样:
// 最短的内存会话用法:不传 cwd,用 process.cwd();不传 sessionManager 就会落盘
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
const { session } = await createAgentSession({
// 关键这一行:换成 SessionManager.create() 就会往 ~/.pi/agent/sessions/ 写文件
sessionManager: SessionManager.inMemory(),
});
try {
await session.prompt("Hello");
} finally {
// 必须释放:不调 dispose() 会话里的活动工作不会停
session.dispose();
}三、代价是什么
内存会话不是免费的。它放弃的是持久化的全部好处:
另外两个官方文档明确的点:
SessionManager是最终模型上下文的权威。 给session.agent.state.messages赋值不会替换已持久化的上下文。想恢复外部历史,正确做法是构造一个包含那些条目的 manager。- 分支切换不删除被放弃的分支。 内存会话也一样,
SessionManager拥有条目树并跟踪活动叶子。
内存会话仍然会做的事
需要澄清的是"不持久化"只针对会话历史,不影响其它副作用:
所以内存会话解决的是"历史污染",不是"副作用隔离"。练习里让模型改文件,依旧要在容器或一次性目录里做。
四、什么时候该用内存会话
练习污染历史的真实风险
默认持久化按工作目录分组写在 ~/.pi/agent/sessions/ 下,目录名是工作目录路径编码后的安全形式(源码里是 -- 包裹、把 / \ : 换成 -)。
风险有三层:
- 噪音。 练习会话混在真实工作会话里,
/resume的列表变得难筛。 - 误恢复。 哪天在同一个目录敲
pi --continue,可能接到三天前的一次测试。 - 数据残留。 练习里可能贴过真实代码片段、密钥片段,这些留在磁盘上。
用内存会话做练习,这三层风险同时消失。
五、正常输入与一个反例
正常输入
把练习写成一个自包含脚本:显式 cwd、内存会话、跑一个提示词、打印结果、释放。
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
// 显式指定练习目录,避免依赖调用者的当前目录
const labDir = "/tmp/pi-lab/in-memory-demo";
const { session } = await createAgentSession({
cwd: labDir,
// cwd 要传两次:会话的 cwd 和内存会话的 cwd 写成同一个值,
// 避免"会话 cwd 和工具 cwd 不一致"导致路径基准错位
sessionManager: SessionManager.inMemory(labDir),
});
try {
await session.prompt("List the files in the current directory.");
console.log(session.getLastAssistantText());
// sessionFile 是 undefined:内存会话本来就没有对应的磁盘文件
console.log("session file:", session.sessionFile ?? "(none)");
} finally {
// 释放:中止活动工作、使扩展上下文失效、断开 agent、移除事件监听
// 必须放在 finally 里:prompt 抛错时也要释放
session.dispose();
}你应该看到:模型给出了文件列表,session file: 后面是 (none),并且 ~/.pi/agent/sessions/ 下没有新增目录。
反例
下面这段看起来也像"临时跑一下",但它会留下痕迹:
import { createAgentSession } from "@earendil-works/pi-coding-agent";
// 反例:省略 sessionManager,走默认的文件型会话
// 差别只有这一行,但这一行就是"练习不污染用户历史"的全部内容
// 结果:磁盘上多一个会话文件,--continue 和 /resume 都能看到它
const { session } = await createAgentSession({ cwd: labDir });
try {
await session.prompt("试一下这个提示词");
} finally {
session.dispose();
}createAgentSession() 在没有传 sessionManager 时建的是 SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir))——持久化会话。结果是磁盘上多了一个会话文件,--continue 和 /resume 都能看到它。
差别只有一行,但这一行就是"练习不污染用户历史"这条完成标准的全部内容。
六、怎么验证"没污染"
光看代码不够,练习结束后应该有办法确认磁盘上真的什么都没多。
# 练习前后各跑一次,比较会话目录的内容
ls ~/.pi/agent/sessions/ > /tmp/pi-sessions-before.txt
# ... 跑练习 ...
ls ~/.pi/agent/sessions/ > /tmp/pi-sessions-after.txt
diff /tmp/pi-sessions-before.txt /tmp/pi-sessions-after.txt没有输出就说明没有新增会话目录。注意这个检查只能发现新增的目录——如果练习用的工作目录以前就有会话,差异法看不出目录内部多没多文件。要更严格,就在练习前记录目标目录下的文件列表。
还有一个更省事的做法:给练习单独设一个 PI_CODING_AGENT_DIR。这个环境变量决定 agent 目录的位置,会话目录跟着它走:
# 把 agent 目录整个重定向到临时目录:会话、设置、日志都落在这里
# 代价是 auth.json 也跟着搬走,这个环境变量下没有你的登录凭据
PI_CODING_AGENT_DIR=/tmp/pi-lab/agent-dir pi这样练习的会话、设置、日志全都落在临时目录里,用完直接删。代价是这个环境变量下没有你的登录凭据——auth.json 也跟着搬家。要么用 PI_CODING_AGENT_DIR 配合 provider 环境变量认证,要么接受它只适合 CI 这类不依赖凭据文件的场景。
七、官方已定义、源码事实与教学设计
写清楚来源,避免把教学安排当成官方承诺。
官方已定义的行为:
SessionManager.inMemory()创建不持久化的会话。cwd决定工作目录,用于项目资源发现、上下文文件、会话分组和内置工具路径。session.dispose()中止活动工作、使扩展上下文失效、断开 agent、移除事件监听。SessionManager是最终模型上下文的权威。- 默认会话按工作目录分组存在
~/.pi/agent/sessions/下。
源码事实但官方文档没有单独成段说明的:
inMemory()的cwd默认值是process.cwd(),不是必须的参数。inMemory()支持第三个entries参数,用于从文件系统之外的条目数组起步。- 会话目录名的具体编码规则。
教学设计(不是官方推荐):
- 用
/tmp/pi-lab/...这样的固定练习目录名。 - 把
cwd和sessionManager的cwd写成同一个显式值——官方没要求,但避免了"会话 cwd 和工具 cwd 不一致"的坑。 - 练习脚本统一用
try/finally包住dispose()。
你现在能做什么
- 写一个练习脚本,用
SessionManager.inMemory()跑一个提示词,打印session.sessionFile,确认它是undefined。 - 用
ls ~/.pi/agent/sessions/前后对比,验证练习跑完磁盘上没多出任何会话目录。 - 用
PI_CODING_AGENT_DIR=/tmp/...跑一次完整的pi,确认会话、设置、日志都落在临时目录里。 - 故意去掉
sessionManager参数重跑一次,观察/resume列表里出现了一条记录,然后改回来。 - 把同一个练习脚本放进 CI,断言
session.sessionFile === undefined且session.messages.length > 0——有内容、无文件。 - 用
SessionManager.inMemory(cwd, undefined, entries)从外部持有的条目数组起步,验证能恢复一段外部历史。