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

会话工厂与工作目录

拆开 createAgentSession() 的默认装配,说明每个边界能覆盖什么、cwd 决定什么,以及会话该在哪里释放。

会话工厂与工作目录

createAgentSession() 看起来只是"创建会话",实际上它是一次装配:模型运行时、设置、会话存储、资源发现、工具集,五个部件各有默认来源,也各有覆盖方式。不了解默认来源,你会遇到"我明明改了配置却没生效"这类问题——因为改的地方和读的地方不是同一个。

还有一个必须说清的概念:工作目录(cwd)不是一个路径参数,它是会话的归属声明。 上下文文件、工具的相对路径、会话的分组目录,全都从它推导。

这篇讲清一次会话创建时装配了什么、每个边界能覆盖什么、cwd 到底决定哪几件事,以及会话该在哪里释放。读它需要会用 TypeScript,并且写过或读过至少一个用 Pi 的程序;读完你能对着源码说出每一项默认来自哪里,并判断"资源到底归谁管"。

Pi 迭代很快,接口可能变化。动手前先记录 pi --version,并按你安装的版本复核参数。

一次会话创建时装配了什么

不给任何选项时,createAgentSession() 会装配下面这些部件。

部件默认来源对应选项覆盖后由谁负责
模型运行时
ModelRuntime,读 agent 目录下的 auth.json 和 models.json
modelRuntime
宿主导向凭据与目录来源
设置
文件型 SettingsManager,读 settings.json 和项目 .pi/settings.json
settingsManager
宿主决定合并后的设置
会话存储
持久化 SessionManager,按工作目录分组落到 sessions/ 下
sessionManager
宿主决定是否落盘、落在哪
资源发现
DefaultResourceLoader,发现扩展、Skill、提示模板、主题、上下文文件
resourceLoader
宿主决定发现范围还是完全接管
工具集
设置里的 defaultTools,或内置默认工具(read、bash、edit、write)
tools / noTools / excludeTools / customTools
宿主决定模型能调用什么

除了这五个部件,工厂还装配几个"状态":

状态默认来源对应选项
模型
设置里的默认模型,否则第一个可用模型
model
思考级别
设置里的值,否则 medium,再按模型能力收紧
thinkingLevel
可切换模型集
不限候选
scopedModels
代理目录
~/.pi/agent
agentDir

模型和思考级别的解析顺序是固定的:显式选项 → 会话里已保存的记录 → 设置 → 默认值。恢复已有会话时,会话里记录的模型和思考级别会优先于设置,但只有在凭据可用时才恢复成功;恢复不了会给出 modelFallbackMessage 说明。

工作目录的解析顺序是:cwd 选项 → 会话管理器自带的 cwd → process.cwd()。这条顺序有实际后果:如果你给了一个 SessionManager.inMemory("/some/dir") 但没传 cwd,会话的工作目录会跟着 SessionManager 走。

覆盖边界:哪些可以接管,哪些必须留

每个可覆盖边界解决的是不同的问题。选错了不是"写多了",是资源归属混乱。

你要解决的问题该用哪个选项不该用的做法
凭据或目录不在默认位置
modelRuntime
手改 ~/.pi/agent/ 下的文件
不想要项目级设置影响行为
settingsManager
删掉项目里的 .pi/settings.json
会话不落盘,或者落到自定义目录
sessionManager
跑完手动删 sessions/ 下的文件
资源发现范围要收窄或完全自定义
resourceLoader
依赖 cwd 恰好落在某个目录上
模型能调用的工具要限定
tools / excludeTools
在提示词里求模型别用某个工具
要加自己的工具
customTools
把工具注册成扩展再加载

tools 是白名单,excludeTools 是黑名单。两者同时给时,黑名单在白名单之后生效。noTools 有两种取值:"all" 表示一个工具都不启用,"builtin" 表示关掉内置工具但保留扩展和自定义工具。

tools 列表如果只含 +name 和 -name 形式的条目,它不是白名单而是增量修改——在默认选择上加或减,和设置里的 defaultTools 语义相同。这种列表不能和普通条目混用,混用会抛错。

cwd 决定什么

cwd 不是一个"告诉工具去哪里找文件"的参数。它是这个会话属于哪个项目的声明。四件事从它推导:

影响说明
资源发现
项目级 .pi/ 目录的加载位置
上下文文件
该目录及其父目录的 AGENTS.md 等文件是否生效
会话分组
保存的会话按工作目录归类,不同项目的会话互不混淆
内置工具路径
内置工具的相对路径以此为基准

会话文件的路径把工作目录编码进目录名:~/.pi/agent/sessions/--<path>--/<timestamp>_<session-id>.jsonl。路径的前导分隔符被去掉,/、\、: 替换成 -。

上下文文件的发现有一个重要细节:一个上下文文件在 Pi 运行于它所在目录或其下任意子目录时都会生效。父目录的 AGENTS.md 会被子目录的会话继承。这不是 bug,是刻意的分层设计——仓库根目录放一份通用约定,子目录可以再加更具体的。

需要提醒的是这个继承的粒度:向上查找时每个目录最多贡献一个上下文文件,优先级顺序是 AGENTS.override.md、AGENTS.md、AGENTS.MD、CLAUDE.md、CLAUDE.MD——同一个目录里只有第一个存在的生效。所以仓库根有一份 AGENTS.md、子目录有一份 AGENTS.md,两份都会进上下文,而不是后者覆盖前者。

typescript
// cwd.ts
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

const projectRoot = "/srv/my-project";

const { session } = await createAgentSession({
	// 显式传 cwd,不要依赖 process.cwd():cwd 决定会话归属哪个项目
	cwd: projectRoot,
	// inMemory 会话:不落盘,但 cwd 仍要传,否则工作目录会退回进程目录
	sessionManager: SessionManager.inMemory(projectRoot),
	// tools 是白名单:只有列出的工具会被声明给模型
	tools: ["read", "grep", "find", "ls"],
});

try {
	await session.prompt("这个项目的测试怎么跑?");
	console.log(session.getLastAssistantText());
} finally {
	// dispose 是终态操作:不调用的话扩展的清理处理器不会运行
	session.dispose();
}

显式传 cwd 的理由很简单:不要依赖调用进程的当前目录。你的应用可能在任何目录下被启动,进程工作目录是外部条件,不是你的设计。

不落盘:SessionManager.inMemory()

SessionManager 拥有持久化或内存中的条目树,并跟踪当前的活跃叶子。会话默认是持久的——不覆盖它,prompt() 之后就会在磁盘上留下文件。

不需要持久化时用 SessionManager.inMemory():

typescript
// ephemeral.ts
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

const { session } = await createAgentSession({
	// 用内存会话:跑完不落盘,磁盘上不会留下 sessions/ 下的文件
	sessionManager: SessionManager.inMemory(),
});

try {
	await session.prompt("总结一下这段文字");
	console.log(session.getLastAssistantText());
} finally {
	// 释放后这个会话不再使用;想继续对话用 abort() 而不是 dispose()
	session.dispose();
}

SessionManager 的四个主要静态工厂方法:

方法用途
SessionManager.create(cwd, sessionDir?)
新建持久化会话
SessionManager.open(path, sessionDir?, cwdOverride?)
打开指定会话文件
SessionManager.continueRecent(cwd, sessionDir?)
继续最近的会话,没有就新建
SessionManager.inMemory(cwd?, options?, entries?)
内存会话,不写文件

还有两个用于派生和查找的:SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?) 从别的会话派生一个属于当前项目的新会话,SessionManager.findById(cwd, id, sessionDir?) 按 ID 精确查找。

SessionManager 是模型上下文的权威来源。想让外部的历史参与对话,要构造一个包含那些条目的 manager 再传给会话。直接给 session.agent.state.messages 赋值不会替换已持久化的上下文——这一点在做"注入历史消息"时特别容易踩。

释放:dispose() 做的四件事

session.dispose() 具体做这些:

  1. 中止进行中的工作。
  2. 失效扩展上下文——之后捕获的扩展 ctx 不再可用。
  3. 与 agent 断开连接。
  4. 移除事件监听器。

对应的源码层面还包含中止重试、中止压缩、中止分支摘要、中止 bash、清理缓存预热器、清理会话级资源,但对外的可观察效果就是上面四条。

typescript
// run.ts
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";

const { session } = await createAgentSession({
	sessionManager: SessionManager.inMemory(),
});

// subscribe 返回退订函数:只移除你注册的这一个监听器
const unsubscribe = session.subscribe((event) => {
	if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
		process.stdout.write(event.assistantMessageEvent.delta);
	}
});

try {
	await session.prompt("解释这个仓库的结构");
} finally {
	// 先退订再释放:顺序清楚,两者都做是安全的
	unsubscribe();
	// dispose 清空全部监听器并断开与 agent 的连接
	session.dispose();
}

unsubscribe() 和 dispose() 不是一回事。unsubscribe() 只移除你注册的那个监听器;dispose() 清空全部监听器并断开与 agent 的连接。两个都做是安全的,顺序上先退订再释放更清晰。

只调 dispose() 而不退订也能停掉事件——监听器会被移除而不是被调用。但如果你在别处还持有那个退订函数,显式调用它能让"谁负责清理"这件事在代码上更清楚。

注意 dispose() 不是 abort() 的别名。abort() 停掉当前运行并等待会话进入空闲,它保留会话,之后还能继续 prompt。dispose() 是终态操作,之后这个会话不应再使用。

谁负责什么

职责由谁承担说明
决定会话属于哪个项目
宿主
显式传 cwd
决定哪些资源被发现
宿主
resourceLoader 或 cwd + 设置
决定会话是否落盘
宿主
sessionManager
决定模型能调用的工具
宿主
tools / noTools / excludeTools / customTools
决定模型与凭据来源
宿主
modelRuntime / model
组装请求、执行工具、维护会话树
Pi
AgentSession 内部职责
释放资源
宿主
必须调用 dispose(),Pi 不会替你做

你现在能做什么

  • 在你的程序里打印 session.sessionFile、session.sessionId 和 session.getActiveToolNames(),确认这次会话落在了哪个文件、启用了哪些工具。
  • 显式传一个 cwd(比如某个仓库目录)创建会话,再在里面问一句"这个项目的测试怎么跑",观察模型是否读到了那个目录下的上下文文件——验证 cwd 真的在起作用。
  • 建两个工作目录不同的会话,用 SessionManager.list(cwd) 分别列出,确认两个目录的会话互不混淆。
  • 写一个只跑一次的临时会话(SessionManager.inMemory()),跑完 prompt() 后去 ~/.pi/agent/sessions/ 下确认没有新文件——验证不落盘。

排障速查

现象检查点处理
项目里的 .pi/settings.json 没生效
设置来源
SDK 默认的 SettingsManager.create(cwd, agentDir) 会读项目级 settings.json;文件路径是 <cwd>/.pi/settings.json。CLI 场景下项目级配置还需要项目已被信任
会话文件出现在别的项目目录下
cwd
显式传 cwd,不要依赖 process.cwd()
父目录的 AGENTS.md 影响了子目录
上下文文件发现规则
这是设计行为:上下文文件对其所在目录及以下全部生效。不需要就把它移出目录树
工具在提示词里说了但调不出来
工具集
tools 白名单生效了,excludeTools 又在之后过滤。检查两处的交集
会话文件不断增长
持久化
默认会话是持久的。用 SessionManager.inMemory() 或删掉旧的 .jsonl
注入的历史消息没进模型上下文
上下文来源
SessionManager 是权威来源。构造含那些条目的 manager,不要给 session.agent.state.messages 赋值
dispose() 之后代码还在用会话
生命周期
dispose() 是终态。要继续对话用 abort(),它只停当前运行
扩展在会话切换后报错
上下文失效
dispose() 会失效扩展上下文,捕获的 ctx 不再可用;重新从 withSession 拿
找不到 codemode / MCP 工具
SDK 与 CLI 的差异
SDK 会话不加载 CLI 的内置扩展。需要就显式加 createCodemodeExtension() 等,并调用 session.bindExtensions()

参考资料