跳到主内容
创建时间:最后更新:作者:白川

内存会话与可重复练习

用 SessionManager.inMemory() 组织最小验证,让练习不污染用户历史,也不依赖可恢复的会话文件。

内存会话与可重复练习

做练习最怕的事是:跑了一堆实验,回头发现自己的会话历史里全是"测试测试测试",分不清哪条是真工作。

Pi 默认会持久化会话。createAgentSession() 不传 sessionManager 时,工厂建的是文件型 SessionManager,会话按工作目录分组写到 ~/.pi/agent/sessions/ 下。这意味着你每次在某个目录跑一次任务,都会往用户历史里塞一条记录。

这篇解决的就是这件事:用内存会话做练习,跑完什么都不留下。

读它需要会用 TypeScript,并了解 Pi 会话的基本概念(会话、消息序列、会话管理器)。Pi 迭代很快,动手前先记录 pi --version;本文依据 Pi 官方文档 与 官方仓库 编写。

一、先把契约说清楚

后面才不会靠猜。

维度定义
输入
一个工作目录 cwd、一段要验证的提示词、一套模型与凭据配置
输出
session.messages、session.getLastAssistantText()、工具执行记录;不产生会话文件
状态
会话条目树和活动叶子只活在内存里;进程退出即消失
责任边界(Pi 负责)
组装上下文、调用模型、执行工具、维护消息序列
责任边界(宿主负责)
提供 cwd、凭据、模型选择;决定练习是否写文件;session.dispose()

二、四种会话工厂

SessionManager 有四个静态工厂方法,差别在"存不存盘"和"从哪儿来":

工厂方法是否持久化用途
SessionManager.create(cwd, sessionDir?)
是
新建一个会话文件
SessionManager.open(path, sessionDir?, cwdOverride?)
是
打开已有会话文件
SessionManager.continueRecent(cwd, sessionDir?)
是
打开该工作目录最近的一次会话,没有就新建
SessionManager.inMemory(cwd?, options?, entries?)
否
只在内存里

inMemory() 的完整签名是 inMemory(cwd: string = process.cwd(), options?: NewSessionOptions, entries?: FileEntry[])。三个参数都有实际用途:

  • cwd 决定工作目录。它影响项目资源发现、上下文文件(AGENTS.md)、会话分组和内置工具的相对路径基准。
  • entries 让你从一份在文件系统之外持有的条目数组开始构建会话。
  • options 是 NewSessionOptions,用来设置会话元数据。

官方 SDK 文档里的最短用法,加上释放之后是这样:

typescript
// 最短的内存会话用法:不传 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();
}

三、代价是什么

内存会话不是免费的。它放弃的是持久化的全部好处:

你放弃的后果
会话文件
session.sessionFile 是 undefined,磁盘上没有对应文件
恢复能力
进程退出后无法恢复这次对话
/resume 可见性
这个会话不会出现在会话选择器里
--continue 命中
下次 --continue 不会回到这次对话
跨进程延续
换一次运行就从头开始

另外两个官方文档明确的点:

  • SessionManager 是最终模型上下文的权威。 给 session.agent.state.messages 赋值不会替换已持久化的上下文。想恢复外部历史,正确做法是构造一个包含那些条目的 manager。
  • 分支切换不删除被放弃的分支。 内存会话也一样,SessionManager 拥有条目树并跟踪活动叶子。

内存会话仍然会做的事

需要澄清的是"不持久化"只针对会话历史,不影响其它副作用:

行为内存会话里仍然发生吗
调用模型、消耗 token
会。这是真实开销,与会话是否落盘无关
工具改动仓库里的文件
会。工具该改还是改
模型凭据被读取
会。凭据来自 auth.json 或环境变量,与会话无关
上下文文件(AGENTS.md)被加载
会。cwd 决定加载哪些
会话条目写入磁盘
不会

所以内存会话解决的是"历史污染",不是"副作用隔离"。练习里让模型改文件,依旧要在容器或一次性目录里做。

四、什么时候该用内存会话

场景适合原因
练习与实验
是
跑一百遍不留痕迹
单元测试 / CI
是
不依赖本机历史,断言可重复
临时宿主
是
进程生命周期与会话生命周期对齐
需要恢复的长任务
否
内存会话救不了你
需要 /resume 找回的工作
否
用文件型会话

练习污染历史的真实风险

默认持久化按工作目录分组写在 ~/.pi/agent/sessions/ 下,目录名是工作目录路径编码后的安全形式(源码里是 -- 包裹、把 / \ : 换成 -)。

风险有三层:

  1. 噪音。 练习会话混在真实工作会话里,/resume 的列表变得难筛。
  2. 误恢复。 哪天在同一个目录敲 pi --continue,可能接到三天前的一次测试。
  3. 数据残留。 练习里可能贴过真实代码片段、密钥片段,这些留在磁盘上。

用内存会话做练习,这三层风险同时消失。

五、正常输入与一个反例

正常输入

把练习写成一个自包含脚本:显式 cwd、内存会话、跑一个提示词、打印结果、释放。

typescript
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/ 下没有新增目录。

反例

下面这段看起来也像"临时跑一下",但它会留下痕迹:

typescript
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 都能看到它。

差别只有一行,但这一行就是"练习不污染用户历史"这条完成标准的全部内容。

六、怎么验证"没污染"

光看代码不够,练习结束后应该有办法确认磁盘上真的什么都没多。

bash
# 练习前后各跑一次,比较会话目录的内容
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 目录的位置,会话目录跟着它走:

bash
# 把 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) 从外部持有的条目数组起步,验证能恢复一段外部历史。

排障速查

现象检查点处理
练习完 /resume 里出现了记录
会话管理
没传 sessionManager,走的是默认文件型会话;改用 SessionManager.inMemory()
session.sessionFile 是 undefined
预期行为
内存会话本来就没有文件;需要文件就换 SessionManager.create()
工具找不到文件
cwd
内存会话的 cwd 决定工具路径基准;显式传 cwd 而不是依赖 process.cwd()
改了 agent.state.messages 但上下文没变
上下文权威
SessionManager 才是权威;要恢复外部历史,用带 entries 的 manager 构造
换一次运行对话就空了
内存会话特性
需要跨进程延续就用文件型会话
进程退出了但连接还在
释放
session.dispose() 必须在 finally 里
CI 里跑挂了找不到原因
凭据
内存会话不改变凭据来源;确认环境变量在启动进程里
练习留下了文件改动
副作用范围
内存会话只隔离会话历史;工具改的文件照旧在磁盘上

参考资料