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

配置系统

settings.json 的分层、合并规则、资源目录与环境变量

Pi 的配置面看着大,实际结构简单得出奇:只有两个文件、一种格式、一套合并规则。绝大多数「我改了配置但没生效」的问题,都出在这三个词上——文件、格式、合并。

先把结论摆出来,因为它决定了本文的读法:

Pi 的配置是 JSON,不是 YAML。配置分两层:用户层的 ~/.pi/agent/settings.json 和项目层的 <工作目录>/.pi/settings.json。项目层覆盖用户层,但有例外:资源列表(扩展、Skill、Prompt Template、Theme、Package)是合并而不是替换,defaultTools 另有一套自己的合并规则。

这一节先把「两层 + 合并规则」讲透,后面几节再逐类拆设置项。

5.1 两层配置与合并规则

两个文件,位置是固定的

层路径什么时候读
用户层(agent 目录)
~/.pi/agent/settings.json
总是读
项目层
<工作目录>/.pi/settings.json
项目信任通过后才读

用户层的根目录叫 agent 目录,默认是 ~/.pi/agent。它可以改:

bash
# 改 agent 目录的位置
PI_CODING_AGENT_DIR=/path/to/agent pi

也可以通过 SDK 的 agentDir 选项指定。整篇专栏里出现的所有 ~/.pi/agent/... 路径,都应该理解成「agent 目录下的 ...」,而不是固定的 home 路径。

项目层受 项目信任 门禁保护。只有一个例外:sessionDir。 官方文档写得很直接:

Project configuration lives in .pi under the working directory and loads after project trust is granted. The only exception is sessionDir, which Pi reads before resolving trust so it can locate sessions.

这个例外的后果在 工具与信任边界 里讲过:拒绝信任也回退不了这一次读取。

合并规则:深合并,但数组整体替换

两层配置不是简单的「项目覆盖用户」。实际规则由一个函数决定:

typescript
// packages/coding-agent/src/core/settings-manager.ts
function deepMergeObjects(base: Record<string, unknown>, overrides: Record<string, unknown>) {
  const result = { ...base };
  for (const key of Object.keys(overrides)) {
    const overrideValue = overrides[key];
    if (overrideValue === undefined) continue;          // undefined 跳过
    const baseValue = base[key];
    result[key] =
      isMergeableObject(baseValue) && isMergeableObject(overrideValue)
        ? deepMergeObjects(baseValue, overrideValue)    // 嵌套对象递归合并
        : overrideValue;                                 // 数组与标量整体替换
  }
  return result;
}

拆成四条可操作的规则:

键的形态合并方式例子
标量(string / number / boolean)
项目层直接覆盖
theme
嵌套普通对象
递归合并,逐键覆盖
compaction、retry、terminal
数组
整体替换,不逐项合并
enabledModels、npmCommand
defaultTools
特例,见 5.4
defaultTools

两条容易被忽略的细节:

第一,嵌套对象是合并的。 这意味着项目层写 "compaction": { "enabled": false } 不会把用户在 compaction 下的其它键清掉——它只覆盖 enabled 这一个键,reserveTokens 和 keepRecentTokens 仍然继承用户层。

第二,undefined 被跳过,但 null 不是。 源码里 if (overrideValue === undefined) continue; 明确跳过 undefined。所以「在项目层把某个设置清掉」这件事做不到——你无法用配置把继承来的值重置为「未设置」。想覆盖成一个值,只能写那个值本身。

三类只在用户层生效的设置

有几个键只能写在 agent 目录的 settings.json 里,写在项目层会被忽略:

设置为什么必须全局
defaultProjectTrust
决定「要不要问」,它自己不能由一个待信任的项目决定
httpProxy
代理指向的是主机的网络出口,不该由仓库内容改变
cacheWarming
官方注释写明「Global only because each refresh costs money」——预热要花钱,不能由项目开关
deviceId
安装级标识,schema 描述就是 "Global only"

这不是文档约定,源码里读的就是用户层:

typescript
// settings-manager.ts
getDefaultProjectTrust(): DefaultProjectTrust {
  const value = this.globalSettings.defaultProjectTrust;   // 不是 this.settings
  ...
}

getCacheWarmingMode(): CacheWarmingMode {
  const mode = this.globalSettings.cacheWarming;            // 不是 this.settings
  ...
}

settings.md 的表格里对这几项都标注了 "Can only be set in agent-directory settings" 或 "Global setting only"。

三个改配置的入口

入口用途说明
/settings
改常用偏好
交互式选择器,主题、思考等级、信任默认值等
直接编辑 JSON 文件
改任意设置项
改完跑 /reload
pi config
配置 Package 加载哪些资源
也是一个交互式 TUI,不是子命令清单

第三种容易误解。pi config 不是一个「列出所有配置项」的命令,而是一个资源选择器:它解析全局和项目两套 Package 声明,让你决定每个包里哪些扩展、Skill、Prompt、Theme 要加载。pi config --help 会打印用法;pi install / pi remove / pi list 才是包管理命令。

给编辑器补全

settings.json 支持一个 $schema 键:

json
{
  "$schema": "https://pi.dev/schemas/settings.schema.json"
}

加上它,VS Code 之类的编辑器就能给出补全和校验。这张 schema 是从 TypeScript 源码生成的(schema 文件头部写着 "This file is generated from TypeScript source. Do not edit it manually"),所以它和源码永远一致。

一个完整的例子

json
{
  "$schema": "https://pi.dev/schemas/settings.schema.json",
  "defaultThinkingLevel": "high",
  "defaultTools": ["read", "bash", "edit", "write", "+grep"],
  "compaction": {
    "reserveTokens": 24576
  },
  "skills": ["./skills", "!./skills/experimental"]
}

这段配置做了三件事:提高默认思考等级、在默认四个工具之上加上 grep(5.4 会讲清楚同一个列表里纯名和增量项是怎么配合的)、把压缩预留从 16384 提到 24576,同时在 Skill 列表里排除了一个目录。

5.2 agent 目录里有什么

agent 目录不只是「放配置文件的地方」。它是 Pi 的用户级状态根目录:凭据、会话、下载的二进制、信任记录、缓存都在这儿。

文件

路径负责什么
settings.json
用户级设置:偏好、默认值、资源路径、Package 声明
keybindings.json
自定义终端 UI 与应用级键位。键是具名动作(如 app.session.new)到按键的映射;一个动作可以绑多个键,写空数组 [] 表示禁用
mcp.json
在所有项目里都可用的 MCP 服务器
models.json
自定义兼容端点、模型与模型覆盖
auth.json
保存的 API key 与 OAuth 凭据,写入时权限是 0600
models-store.json
模型目录缓存,字段是 {models, lastModified, checkedAt, etag}
mcp-auth.json
MCP 服务器的 OAuth 状态,按服务器名与 URL 索引
trust.json
项目信任决定,按规范化目录路径索引
sessions/
会话文件,默认存放位置
bin/
Pi 按需下载的外部二进制(rg、fd)

trust.json 值得单独说一句:它是用户级单文件,项目侧没有对应的标记文件。查找规则是从当前目录逐级向上找最近的一条决定。细节在 工具与信任边界 里。

目录

路径负责什么
extensions/
用户扩展
skills/
用户 Skill 及其配套文件
prompts/
用户 Prompt Template,暴露为 / 命令
themes/
用户 Theme 文件

项目侧的对应物

工作目录下的 .pi/ 是项目层的镜像,结构一一对应:

路径负责什么
.pi/settings.json
项目级设置、资源路径、Package 声明
.pi/mcp.json
项目 MCP 服务器
.pi/extensions/
项目扩展
.pi/skills/
项目 Skill 及配套文件
.pi/prompts/
项目 Prompt Template
.pi/themes/
项目 Theme
.pi/SYSTEM.md
替换项目的系统提示
.pi/APPEND_SYSTEM.md
追加到系统提示

两个 .md 文件的覆盖规则是官方文档里明确写的一条:

For SYSTEM.md and APPEND_SYSTEM.md, the trusted project file takes precedence over the corresponding agent-directory file. Files with the same name are not combined.

也就是说 agent 目录有 APPEND_SYSTEM.md、项目也有 APPEND_SYSTEM.md 时,项目那份完全取代 agent 那份,两份不会拼在一起。想要叠加,得自己在文件里写全。

上下文文件不在 .pi/ 里

AGENTS.md、AGENTS.override.md、CLAUDE.md 这几个不是 .pi/ 的成员。它们是上下文文件,从 agent 目录、工作目录以及工作目录的所有祖先目录加载,一个文件在它所在的目录及以下都生效。

两条规则:

  • AGENTS.override.md 只在同一个目录里替换 AGENTS.md 或 CLAUDE.md,不影响 agent 目录或其它目录的上下文文件。
  • 上下文文件的发现不需要项目信任。 拒绝信任一个项目,它目录里的 AGENTS.md 照样会被读进系统提示。

第二条是安全上最容易被忽略的一条:project trust 防的是代码执行,不是提示注入。细节在 工具与信任边界 里展开。

5.3 模型与思考等级

模型相关的设置分三类:选哪个模型、思考多深、缓存与显示。

设置类型默认值说明
defaultProvider
string
自动
启动时的供应商
defaultModel
string
自动
启动时的模型 ID
defaultThinkingLevel
"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
"medium"
启动时的思考等级
modelThinkingLevels
object
无
按 provider/modelId 精确键控的每模型思考等级
thinkingBudgets
object
内置预算
覆盖 minimal low medium high 四档的 token 预算
enabledModels
string[]
全部可用模型
启动选择与模型循环用的模式列表,格式同 --models
hideThinkingBlock
boolean
false
在转录里隐藏思考块
showCacheMissNotices
boolean
false
显示显著缓存未命中、缓存预热成功、压缩用量、供应商恢复等通知
cacheWarming
"off" | "streaming" | "idle"
"streaming"
保持供应商 prompt 缓存温热。仅全局设置

三个需要展开的点。

思考等级有七档,但预算只覆盖四档

defaultThinkingLevel 的枚举是七档:off、minimal、low、medium、high、xhigh、max。

但 thinkingBudgets 的 schema 只有四个键:

typescript
// packages/coding-agent/src/core/settings-schema.ts
const ThinkingBudgetsSettingsSchema = thinkingBudgetsSettings();
// → { minimal?, low?, medium?, high? }

也就是说 off 和 xhigh(以及 max)没有可配置的 token 预算。你只能改这四档中每一档花多少 token,off 是关闭,xhigh/max 用的是供应商自己那一档的默认预算。写 thinkingBudgets: { "xhigh": 5000 } 不会报错——JSON Schema 没给这个对象设 additionalProperties: false——但这个键从来不会被读,运行时只认那四档。

这个缺口的实际影响很小,但值得知道:如果你觉得 xhigh 太贵,你唯一能做的是降档到 high,而不是给 xhigh 配一个小预算。

每模型覆盖是精确键控的

modelThinkingLevels 和 compaction.modelOverrides 都用 provider/modelId 这种精确字符串做键,没有通配:

json
{
  "modelThinkingLevels": {
    "anthropic/claude-sonnet-4": "high",
    "openai/gpt-5": "low"
  }
}

键必须完全等于 provider/modelId。写 anthropic/claude-* 不会生效——没有模式匹配。

cacheWarming 的两个约束

cacheWarming 默认 "streaming":在活跃的运行期间保持符合条件的供应商 prompt 缓存温热。"idle" 额外覆盖两次运行之间的空档期。

但它有两条硬约束,源码和文档都写了:

  1. 只在模型声明了缓存生命周期时运行,而且 Pi 估算的「省下的缓存未命中成本」至少要值 0.05 美元。
  2. 只在全局设置里生效。 源码注释写得很直白:/** Read from global settings only because warming costs money. */

预热的用量会计入会话统计,但不进模型上下文。/session 会显示下一次决策;扩展可以用 cache_warming_decision 事件覆盖它。

5.4 工具选择:defaultTools 的两种写法

这是整个配置系统里最容易踩坑的一处,所以单独一节。

defaultTools 是一个字符串数组,数组里的条目分两类:

条目例子单独使用时的语义
纯工具名
"read"、"grep"
形成选择本身(整体替换继承的选择)
增量项
"+grep"、"-bash"
修改继承的选择(加一个 / 减一个)

先分清两件事:能不能混,和混了会怎样

这是本文最需要说清楚的一点,因为「纯名列表 vs 增量列表,不能混用,混用报错」这句话只对一半的场景成立:

场景能不能混出处
settings.json 的 defaultTools
能混,而且官方文档明确写了混用规则
docs/settings.md Tools 段
--tools 命令行开关
不能混,混用直接报错
src/cli/args.ts:156
SDK 的 tools 选项
不能混,混用直接报错
src/core/sdk.ts:280

settings.json 这一侧的官方原文是:

In one list, plain names form the selection, and +name and -name then apply in order.

翻译过来就是:同一个列表里,纯名先形成选择,然后增量项按顺序在这个选择上生效。 所以下面这个写法在 settings.json 里是合法的:

json
{
  "defaultTools": ["read", "bash", "edit", "write", "+grep"]
}

它等价于「四个默认工具 + 一个 grep」。

而 --tools 那一侧的校验函数长这样:

typescript
// packages/coding-agent/src/core/settings-manager.ts:97-104
export function getToolListError(entries: readonly string[]): string | undefined {
  const modifiers = entries.filter(isToolModifier);
  if (modifiers.length === 0) return undefined;
  if (modifiers.length < entries.length) return "tool names cannot be mixed with +name or -name entries";
  const pattern = modifiers.find((entry) => entry.includes("*"));
  if (pattern) return `+name and -name entries take exact tool names, not patterns: ${pattern}`;
  return undefined;
}

三行代码三条规则:

  1. 混用会报错。 报错文案是 tool names cannot be mixed with +name or -name entries。pi --tools read,+grep 是不合法的。
  2. 增量形式只接受精确名字。 +grep* 不行,报错文案是 +name and -name entries take exact tool names, not patterns: ...。想按模式禁用要用 --exclude-tools。
  3. 全是纯名字时,第二行不触发,所以纯名单永远合法。

关键在于这个函数的调用点只有两处——src/cli/args.ts:156(--tools)和 src/core/sdk.ts:280(SDK 的 tools)。settings.json 走的是另一条路径,压根不经过它。所以「混用报错」这条规则不会在配置文件里触发。

实践上的推论:

在 settings.json 里放心混用;在命令行上老老实实二选一。

写命令行时想同时表达「保留默认」和「加一个工具」,用纯增量形式 pi --tools +codemode,-write;想指定完整名单,就写纯名字并把想要的都列上。

两层之间怎么合并

这是最容易搞混的地方:同一个数组,在用户层和项目层里的语义不一样。

合并函数长这样:

typescript
// settings-manager.ts:126-131
function mergeDefaultTools(base: string[] | undefined, overrides: string[] | undefined): string[] | undefined {
  if (overrides === undefined) return base;
  // Settings files are not validated; a malformed value replaces instead of throwing here.
  if (!Array.isArray(base) || !Array.isArray(overrides) || !overrides.every(isToolModifier)) return overrides;
  return [...base, ...overrides];
}

读一下这个函数体:

  • overrides 全是 +name/-name → 走最后一行,拼接到 base 后面。
  • overrides 里含任何纯名字 → 走 return overrides,整体替换 base。

所以:

用户层项目层合并结果
["read","bash"]
["-bash","+grep"]
["read","grep"](增量修改用户的选择)
["read","bash"]
["read","grep"]
["read","grep"](整体替换,用户的选择被清空)

这就是官方文档那句话的精确含义:

Project settings apply on top of user settings: a project list with only +name and -name entries changes the user's selection, and a project list with a plain name replaces it.

实践含义:如果你的项目配置里要调整工具集合,用纯增量写法。这样它在用户层之上做加法,不会把用户在全局里精心加的工具清掉。反过来,如果你确实想让项目完全接管工具集合,就写纯名单,并且清楚这会覆盖全局。

完整的演算

假设用户层是:

json
{
  "defaultTools": ["read", "bash", "edit", "write", "+codemode"]
}

解析过程分两步。第一步,合并两层:

plaintext
base    = ["read", "bash", "edit", "write", "+codemode"]
项目层   = ["-bash", "+grep"]
overrides 全是 modifier → 拼接
merged  = ["read", "bash", "edit", "write", "+codemode", "-bash", "+grep"]

第二步,把合并结果解析成最终名单:

typescript
// settings-manager.ts:137-140
function resolveDefaultTools(entries: string[]): string[] {
  const plain = entries.filter((entry) => !isToolModifier(entry));
  return applyToolModifiers(plain.length > 0 || entries.length === 0 ? plain : DEFAULT_TOOL_NAMES, entries);
}
plaintext
plain = ["read", "bash", "edit", "write"]      非 modifier 项
base  = plain(plain 非空)
按顺序应用 modifier:
  "+codemode" → 加入 codemode
  "-bash"     → 移除 bash
  "+grep"     → 加入 grep
结果 = ["read", "edit", "write", "codemode", "grep"]

注意 applyToolModifiers 是按列表顺序执行的。所以 ["-bash", "+bash"] 会得到 bash(先删后加),而 ["+bash", "-bash"] 会得到没有 bash(先加后删)。顺序有意义。

可用的名字

defaultTools 里能写的名字:

来源名字
内置工具(8 个)
read bash powershell edit write grep find ls
内置扩展注册的工具
codemode、tool_search
扩展注册的工具
扩展作者自己起的名字

codemode 和 tool_search 默认是关闭的,因为内置扩展注册它们时带了 defaultActive: false——「注册即激活」对 direct 和 model-only 这两种曝光成立,但这两个工具显式关掉了它。你可以在 defaultTools 里写 "+codemode" 打开它们。

空数组的含义:"defaultTools": [] 会关掉所有内置工具,但不影响扩展和 SDK 注册的工具。这一点和 --no-tools 不一样——--no-tools 关的是内置、扩展、自定义、MCP 工具全部。

/reload 与命令行的优先级

三条规则,官方文档写得很清楚:

  1. /reload 会启用 defaultTools 里新增的工具,但不会禁掉用 -name 关掉的工具,也不会把你手动关掉的工具重新打开。
  2. --tools(纯名单)、--no-tools、--no-builtin-tools 会覆盖 defaultTools,而且在 reload 之后依然覆盖。
  3. --tools 只带 +name/-name 时不是白名单,而是修改解析后的 defaultTools 选择;reload 时这些增量项照样生效,所以用 -name 关掉的工具保持关闭。

这三条合起来给出一个可操作的结论:defaultTools 是「基线」,不是「锁定」。 它决定启动时的选择,/reload 之后仍然由命令行开关和 reload 的语义决定最终状态。

想完全掌控某一次运行,用命令行:

bash
pi --tools read,grep,find,ls --print "Review this project"

命令行的四个开关在 工具与信任边界 里逐个讲过,本文不重复。那里还讲了 --tools 与 MCP 工具的交互——--tools 选的是声明给模型的工具,不负责移除 MCP 工具。

一个反直觉的细节

源码注释里有一句:

Settings files are not validated; a malformed value replaces instead of throwing here.

也就是说,写进 settings.json 的值不做校验。defaultTools 写成一个字符串而不是数组,不会抛错,它会「替换」而不是「报错」。

这一条在 工具与信任边界 里有更完整的讨论,包括它和 --tools 白名单的区别。

5.5 界面与显示

这一节的设置项最多,但它们有个共同点:改错了顶多难看,不会坏。 所以本文只给完整默认值表,不逐个展开。

完整默认值表

设置类型默认值说明
theme
string
"system"
内置或自定义主题名。system 从终端主题派生颜色
quietStartup
boolean | "header"
false
true 隐藏启动头和已加载资源列表;"header" 保留头(版本与快捷键提示),隐藏模型范围行和资源列表
tuiMode
"regular" | "fullscreen"
"fullscreen"
交互式终端 UI 模式
fullscreenExitOutput
"transcript" | "resume-hint"
"transcript"
全屏模式退出时打印什么
fullscreenScrollbar
"auto" | "always" | "hidden"
"auto"
全屏转录的滚动条行为
fullscreenCopyOnSelect
boolean
true
全屏模式下选中即复制
fullscreenWheelScrollLines
"auto" | number
"auto"
每次滚轮事件的行数,1 到 100。"auto" 在本地 macOS 终端每次一行,其它地方(含 SSH)快速滚动时最多 6 行;Alt+滚轮走五倍距离
editorPaddingX
number
0
输入编辑器的水平内边距,0 到 3 格
outputPad
0 | 1
1
消息、工具输出、! 命令输出、摘要块的水平内边距
autocompleteMaxVisible
number
5
自动补全下拉的可见条数,3 到 20
showHardwareCursor
boolean
false
用终端自带光标而不是 Pi 画的;输入法仍由 Pi 定位光标
terminal.showImages
boolean
true
支持时显示内联图片
terminal.imageWidthCells
number
60
内联图片的首选宽度(终端格数)
terminal.clearOnShrink
boolean
false
内容变短时清空空行
terminal.showTerminalProgress
boolean
false
在终端标签页显示 OSC 9;4 进度
terminal.hyperlinks
boolean | "auto"
"auto"
覆盖 OSC 8 超链接探测
terminal.images
"kitty" | "iterm2" | "auto" | false
"auto"
覆盖内联图片协议探测
terminal.trueColor
boolean | "auto"
"auto"
覆盖真彩色探测
images.autoResize
boolean
true
发给模型前把图片缩到最大 2000×2000 像素
images.blockImages
boolean
false
阻止图片发给模型
markdown.codeBlockIndent
string
" "
渲染代码块时的缩进前缀
markdown.mermaid
"off" | "final" | "streaming"
"streaming"
Mermaid 渲染模式

几个值得注意的点

quietStartup 是三态的。 不只是开和关,中间那档 "header" 常被忽略:它保留版本号和快捷键提示(新手需要),但隐藏「当前模型范围」那一行和已加载资源清单(老手不需要,而且刷屏)。在 CI 或录屏场景里 "header" 往往比 true 更合适。

fullscreen* 四个设置在 regular 模式下全部无效。 schema 描述里直接写着 "No effect in regular TUI mode."。tuiMode 默认是 "fullscreen",所以它们默认是生效的;一旦你切到 "regular",这四个键就是死设置。

showHardwareCursor 有环境变量兜底。 源码里是:

typescript
getShowHardwareCursor(): boolean {
  return this.settings.showHardwareCursor ?? process.env.PI_HARDWARE_CURSOR === "1";
}

设置项优先于环境变量。环境变量那一层在 5.8 讲。

terminal.* 这一组是「覆盖探测结果」的。 Pi 会自己探测终端能力(支不支持真彩色、用的是 kitty 还是 iTerm2 的图片协议、有没有超链接),这几个键让你在探测不准则时手动纠正。同名的环境变量(PI_TRUE_COLOR、PI_IMAGE_PROTOCOL、PI_HYPERLINKS)也在 5.8 讲。

theme 的值里不能有 /。 因为 / 被保留给「浅色/深色自动配对」的写法("light/dark")。主题机制本身在 Skills、Prompt Templates 与 Themes 里展开。

5.6 会话与压缩

会话

设置类型默认值说明
sessionDir
string
agent 会话目录(~/.pi/agent/sessions)
会话存储目录。相对路径从工作目录解析
sessionShareDestination
"auto" | "gist" | "radius"
"auto"
/share 的目标。auto 在已认证 Radius 时用 Radius,否则建 GitHub gist;gist 总是 gist;radius 要求 Radius 认证且不回退到 gist

sessionDir 的优先级链有三层,从高到低:

text
--session-dir 命令行开关
  > PI_CODING_AGENT_SESSION_DIR 环境变量
    > settings.json 里的 sessionDir 设置

官方文档一句话写完:PI_CODING_AGENT_SESSION_DIR and --session-dir override this setting。

sessionDir 也是那个「在信任判定之前就被读取」的例外。实践含义是:如果你在不可信的目录里跑 Pi,不要指望拒绝信任能保证会话文件写到你想让它去的地方。

压缩

设置类型默认值说明
compaction.enabled
boolean
true
开启自动压缩
compaction.reserveTokens
number
16384
预留的空闲上下文 token,低于这个数开始自动压缩
compaction.keepRecentTokens
number
20000
不做摘要、原文保留的近期 token 数
compaction.modelOverrides
object
无
按 provider/modelId 精确键控的每模型 token 设置
branchSummary.reserveTokens
number
16384
摘要分支历史时预留的 token
branchSummary.skipPrompt
boolean
false
跳过「要摘要吗?」的提问,默认不摘要

三个数字的关系需要说清楚,因为它们不是同一层的量:

  • reserveTokens = 16384 是触发的余量,不是百分比。触发条件是 contextTokens > contextWindow - reserveTokens。
  • keepRecentTokens = 20000 是压缩时保留多少近期内容。它和 reserveTokens 是两个独立的旋钮:前者决定摘要后留什么,后者决定什么时候动手。
  • branchSummary.reserveTokens = 16384 是另一条路径——分支摘要,不是上下文压缩。

一个实践上的推论:keepRecentTokens 设得比 reserveTokens 大是允许的,源码不会拦(两者只校验「非负安全整数」)。但如果 keepRecentTokens 大到超过触发时刻的可用上下文,压缩会没有空间可省。默认值 20000 > 16384 就是这个关系,官方显然认为这样是合理的——留 20000 token 近期内容、预留 16384 余量,两者相加约 3.6 万 token,对绝大多数模型来说不宽裕但够用。

每个值的解析顺序

压缩的 token 值有三级回退:

text
模型覆盖(compaction.modelOverrides["provider/modelId"])
  → 普通设置(compaction.reserveTokens / keepRecentTokens)
    → 内置默认值(16384 / 20000)

官方文档说得更精确:「Each value resolves independently from the matching model override, then the ordinary compaction setting, then the built-in default. Project and user objects merge before model lookup.」

最后半句很重要:compaction 这个对象在项目层和用户层先合并,然后才按模型查覆盖。所以项目层写 "compaction": {"modelOverrides": {...}} 不会把用户层的 modelOverrides 整块顶掉——两个对象的键会先合并。

三级回退的完整机制在 上下文与压缩 里展开。

5.7 资源加载

五个资源数组

设置类型默认值说明
packages
array
[]
npm、git 或本地的 Pi Package 来源
extensions
string[]
[]
扩展文件或目录
skills
string[]
[]
Skill 文件或目录
prompts
string[]
[]
Prompt Template 文件或目录
themes
string[]
[]
Theme 文件或目录
enableSkillCommands
boolean
true
把 Skill 注册成 /skill:name 命令

资源列表是合并的,不是替换的

这是配置系统里第二条「例外规则」。前面说过标量和嵌套对象怎么合并,数组一般整体替换——但这五个资源数组是合并的。

源码在包管理器里。解析时它同时读两层:

typescript
// packages/coding-agent/src/core/package-manager.ts:947-973
for (const resourceType of RESOURCE_TYPES) {
  const target = this.getTargetMap(accumulator, resourceType);
  const globalEntries   = (globalSettings[resourceType]   ?? []) as string[];
  const projectEntries  = (projectSettings[resourceType]  ?? []) as string[];
  this.resolveLocalEntries(projectEntries, resourceType, target, { scope: "project", ... }, projectBaseDir);
  this.resolveLocalEntries(globalEntries,  resourceType, target, { scope: "user",   ... }, globalBaseDir);
}

两层都写进同一个累加器。所以:

  • 用户层写 "skills": ["./my-skills"],项目层写 "skills": ["./team-skills"] → 两个都加载。
  • 项目层先解析,所以同名冲突时项目优先。

packages 走的是同一条路的上一段:包来源先被解析进同一个累加器(项目包在前、全局包在后,同一个包以项目作用域为准去重),然后才是设置里显式声明的路径,最后是自动发现的目录。整个解析过程里,资源的先后顺序就是「先到的赢」。

路径解析的基准也不同:

层相对路径的基准
用户层
agent 目录
项目层
项目的 .pi 目录

绝对路径和 ~ 都支持。

三种精确操作语法

资源数组的每一项可以是四种形态之一:

形态例子语义
普通路径 / 模式
"skills", "*.md"
包含匹配项
!pattern
"!experimental/*"
排除匹配的项(支持 glob)
+path
"+my-skill/SKILL.md"
精确包含,可以覆盖前面的排除
-path
"-legacy-skill"
精确排除,可以覆盖前面的强制包含

生效顺序是固定的四步:

typescript
// packages/coding-agent/src/core/package-manager.ts:743-797
/**
 * Apply patterns to paths and return a Set of enabled paths.
 * Pattern types:
 * - Plain patterns: include matching paths
 * - `!pattern`: exclude matching paths
 * - `+path`: force-include exact path (overrides exclusions)
 * - `-path`: force-exclude exact path (overrides force-includes)
 */
text
第 1 步:普通模式包含(没有普通模式时 = 全部)
第 2 步:`!pattern` 排除
第 3 步:`+path` 强制包含(从全集里加回来,压过排除)
第 4 步:`-path` 强制排除(压过一切)

关键差别在匹配方式:!pattern 走的是 glob 匹配,+path 和 -path 走的是精确路径匹配。精确匹配对 Skill 文件还有一条特殊规则——匹配到 SKILL.md 时,父目录也算命中,因为一个 Skill 的单位是目录。

这套语义的实际用途是做例外。比如你在全局配置里引入了整个团队技能库,但某个项目只想排除其中一个:

json
{
  "skills": ["~/team-skills", "!~/team-skills/experimental"]
}

反过来,某个被全局排除的 Skill,在某个项目里要单独启用:

json
{
  "skills": ["+~/team-skills/experimental/ocr/SKILL.md"]
}

内置扩展也在这个列表里

四个内置扩展在 extensions 设置里有名:

名字对应扩展
builtin:mcp
内置 MCP 集成
builtin:llama.cpp
本地 llama.cpp
builtin:codemode
codemode 工具
builtin:tool-search
tool_search 工具

它们默认加载。要关掉一个,写 -builtin:mcp。项目层写 +builtin:<name> 或 -builtin:<name> 会覆盖用户层的决定。

Skill 命令开关

enableSkillCommands 默认 true,作用范围很窄:

Register skills as /skill:name commands.

关掉它之后,Skill 不会出现在交互式命令发现(自动补全)里,但手打 /skill:name 仍然可用。这是源码里两处独立逻辑决定的——一个控制自动补全列表,一个控制实际执行。

自动发现的目录

除了设置里显式写的路径,Pi 还会自动发现这些目录:

位置何时发现
<agent-dir>/extensions/ skills/ prompts/ themes/
总是
<工作目录>/.pi/extensions/ skills/ prompts/ themes/
项目受信任后
~/.agents/skills/
总是(用户级)
<工作目录>/.agents/skills/ 及各级祖先
项目受信任后,遇到仓库根目录就停

最后一条是 Agent Skills 规范的兼容位:.agents/skills/ 从当前目录一路往上找,任意一级存在就会被发现,但存在 git 仓库根目录时就在那里停下,不会一路爬到文件系统根。

~/.agents/skills/ 有一个例外规则:即使你的工作目录就是 $HOME 本身,它也永远被当作可信的用户资源。

这些自动发现的目录同样受设置里的 ! / + / - 语法控制。

5.8 环境变量

环境变量在 Pi 里有三种完全不同的用途,混起来是常见困惑的来源:

类别谁读它例子
进程配置
Pi 自己
PI_OFFLINE、PI_CODING_AGENT_DIR
进程标记
子进程
AI_AGENT=pi、PI_CODING_AGENT=true
shell 工具会话环境
LLM 通过 bash 跑的命令
PI_SESSION_ID、PI_PROVIDER、PI_MODEL

第三类最容易被误解,下面单独说。

Pi 自己读的配置变量

变量说明
PI_CODING_AGENT_DIR
覆盖配置目录,默认 ~/.pi/agent
PI_CODING_AGENT_SESSION_DIR
覆盖会话存储位置,被 --session-dir 覆盖
PI_PACKAGE_DIR
覆盖 Package 目录,对 Nix/Guix store 路径有用
PI_OFFLINE
关闭自动网络活动,包括模型目录刷新
PI_SKIP_VERSION_CHECK
关闭 pi.dev 的最新版本请求
PI_TELEMETRY
覆盖安装/更新遥测与供应商归因头:1/true/yes 或 0/false/no
PI_CACHE_RETENTION
设为 long 以在支持的地方启用扩展的供应商 prompt 缓存
PI_SHARE_VIEWER_URL
覆盖 /share 使用的基础 URL
PI_RADIUS_GATEWAY
覆盖 /bug 上传用的 Radius 网关源
PI_HARDWARE_CURSOR
设为 1 显示硬件光标
PI_CLIPBOARD_OSC52
设为 1 时本地剪贴板写入后额外发 OSC 52(SSH 下的做法)
PI_HYPERLINKS
覆盖 OSC 8 超链接探测:1、0 或 auto
PI_PROGRAM_STATUS
覆盖 OSC 7501 程序状态探测
PI_IMAGE_PROTOCOL
覆盖内联图片探测:kitty、iterm2、none 或 auto
PI_TRUE_COLOR
覆盖真彩色探测:1、0 或 auto
PI_TUI_ESC_TIMEOUT
单独 ESC 后等待多久才当作 Escape(毫秒)。SSH 下默认 100,其它默认 10
PI_CLEAR_ON_SHRINK
未在设置里指定时,设为 1 启用内容变短时清空空行
VISUAL、EDITOR
externalEditor 未设置时的外部编辑器回退
HTTP_PROXY、HTTPS_PROXY
代理出站 HTTP 请求

供应商的凭据变量(ANTHROPIC_API_KEY、OPENAI_API_KEY 等)不在这里,单独列在 模型与供应商 里。

两个进程标记

CLI 和 RPC 入口会设置两个标记:

变量值用途
AI_AGENT
pi
通用标记,让工具链识别「这个进程是 Agent 启动的」
PI_CODING_AGENT
true
Pi 专用,让子进程检测自己跑在 Pi 里面

子进程会继承这两个标记。它们不是会话相关的,而且通过 SDK 嵌入 Pi 时不会自动设置。

shell 工具的会话环境:五个变量

这是第三类,也是最容易被写成「Pi 的配置环境变量」的一类。真相是:

这五个变量只注入给 LLM 通过 bash / powershell 工具跑的命令。它们不是 Pi 的配置开关,Pi 自己不读它们。

变量内容
PI_SESSION_ID
当前会话 ID
PI_SESSION_FILE
当前会话 JSONL 文件的绝对路径;临时会话未设置
PI_PROVIDER
当前选中的模型供应商
PI_MODEL
当前选中的模型 ID
PI_REASONING_LEVEL
当前生效的推理等级:off、minimal、low、medium、high、xhigh、max

三条使用约束:

  1. 值在每条命令开始时解析。 所以换模型或改思考等级会影响下一条 shell 命令,不用重启 Pi。
  2. PI_PROVIDER 和 PI_MODEL 指的是 Pi 选中的模型,不是路由器内部可能选的上游模型。
  3. 它们只注入给 LLM 调用的 bash/powershell。用户手打的 ! 或 !! 命令不会拿到。

第 3 条的源码依据在 bash.ts 里:工具启动子进程前会先把继承来的这五个变量全部删掉,然后再按需逐个赋新值。所以父进程里残留的旧值不会漏进子命令。

所以官方文档给的用法是「让模型自己检查这些变量,而不是从系统提示里推断」:

bash
printf '%s/%s\n' "$PI_PROVIDER" "$PI_MODEL"
printf 'reasoning=%s session=%s\n' "$PI_REASONING_LEVEL" "$PI_SESSION_ID"

换模型要写进 settings 的是 defaultProvider / defaultModel(或 /model 命令),不是 PI_PROVIDER / PI_MODEL。 后两个只是给 shell 工具的只读信息。

还有一个不存在的环境变量值得点名:PI_THEME。主题是 settings.json 里的 theme 键,或者一次性的 --use-theme 开关。

自定义 shell 工具的环境

用 createBashTool() / createPowerShellTool() 造的工具,默认也会暴露会话环境。注入发生在 spawnHook 之前,所以钩子能在 ctx.env 里看到这些变量:

typescript
const bashTool = createBashTool(cwd, {
  spawnHook: (ctx) => ({ ...ctx, env: { ...ctx.env, CI: "1" } }),
});

要独立关掉会话元数据(避免嵌套 Pi 进程暴露父会话的陈旧信息),用 exposeSessionEnvironment: false:

typescript
const powershellTool = createPowerShellTool(cwd, {
  exposeSessionEnvironment: false,
  spawnHook: (ctx) => ctx,
});

5.9 加载期的自动迁移

Pi 有两套完全不同的迁移机制,混淆它们是旧资料里最常见的错误来源之一:

migrateSettingsrunMigrations
位置
src/core/settings-manager.ts
src/migrations.ts
触发时机
每次读取 settings 文件时
启动时跑一次
作用对象
settings 的内容
磁盘上的文件与目录
落盘吗
不落盘,只在内存里改
落盘,真的动文件
数量
4 处重写
5 个动作

migrateSettings:4 处内存重写

读 settings.json 时,Pi 会对解析出的对象做四种重写:

#旧形态新形态条件
1
queueMode
steeringMode
有 queueMode 且没有 steeringMode
2
websockets: boolean
transport: "websocket" 或 "sse"
没有 transport 且 websockets 是布尔值
3
skills: { enableSkillCommands, customDirectories }
skills: string[] + 顶层 enableSkillCommands
skills 是对象且不是数组
4
retry.maxDelayMs
retry.provider.maxRetryDelayMs
retry 是对象且 retry.provider.maxRetryDelayMs 未设

源码里 migrateSettings 就这四个 if 块,最后 return settings as Settings。

它不写盘。 迁移后的对象只存在于内存里。只有当你在会话里改了这个设置、触发 save() 时,整个文件才会被重新序列化写回——那时写出去的是已经迁移过的形态。

它不备份。 没有 .bak 文件,没有时间戳副本。

它不打日志。 没有「迁移了 X → Y」这类输出。

这三点合起来推翻了旧资料里「Pi 启动时会备份配置并打印迁移日志」的说法。三条都不成立。

第 4 条重写还有个小细节:它会删掉 retry.maxDelayMs 这个键(delete retrySettings.maxDelayMs;),即使目标值没写进去。因为迁移只发生在内存里,这个删除对磁盘上的文件不生效——只有当 settings.json 因为别的原因被整体重写时,磁盘上的旧键才真正消失。

runMigrations:5 个动磁盘的动作

启动时跑的迁移在另一个文件里,它们真的会改文件系统:

#动作具体做什么
1
migrateAuthToAuthJson
oauth.json 的内容并入 auth.json(模式 0600),原文件重命名成 oauth.json.migrated;settings.json 里的 apiKeys 也搬进 auth.json,并从 settings 里删掉 apiKeys 键后重写 settings 文件
2
migrateSessionsFromAgentRoot
把 agent 目录根下的 *.jsonl(v0.30.0 的旧位置)按会话头里的 cwd 移到 sessions/<编码后的 cwd>/
3
migrateToolsToBin
把 tools/ 里的 fd、rg(含 .exe 版本)移到 bin/
4
migrateKeybindingsConfigFile
迁移 keybindings.json 的旧格式,原地重写该文件
5
migrateExtensionSystem
commands/ 目录重命名为 prompts/(全局和项目各一次);并对已弃用的 hooks/、tools/ 目录发出警告

第 1 条是唯一一处留下痕迹的迁移——oauth.json 会被改名成 oauth.json.migrated 而不是删除。但注意这只是「改名」,不是备份机制:其它所有迁移都是移动或原地重写,没有一个会留副本。

第 5 条是唯一一处真的会打印的地方,但它打印的是弃用警告,不是迁移日志:

typescript
warnings.push(`${label} hooks/ directory found. Hooks have been renamed to extensions.`);
warnings.push(`${label} tools/ directory contains custom tools. Custom tools have been merged into extensions.`);

这些警告会显示在终端上,并等待一次按键才继续。

两条迁移的实际影响

第一,/reload 不会重跑迁移。 runMigrations 在 main.ts 的启动路径上跑一次;/reload 只重新读 settings、keybindings、指令和资源。如果你改了一个旧格式的设置键并 /reload,迁移会在读取时发生(migrateSettings),所以这一半是生效的;但磁盘级别的迁移(会话挪窝、二进制搬家)不会因为 /reload 而重跑。

第二,第 2 条迁移和 5.6 的 sessionDir 例外有同一个根源。 Pi 需要在信任判定之前就知道会话在哪、会话文件是什么。sessionDir 设置、项目 sessionDir 的提前读取、旧版 ~/.pi/agent/*.jsonl 的迁移,都服务于同一件事:尽快定位到会话存储。这也是为什么「拒绝信任」不等于「Pi 没碰过你的项目配置」。

5.10 事实来源

本文所有数字、开关名、默认值都可在仓库里复算:

断言来源
配置文件是 JSON,两层路径
packages/coding-agent/docs/settings.md 首段;src/core/settings-manager.ts:193-194
项目配置需要信任,sessionDir 是例外
docs/configuration.md 第 3 行
agent 目录默认 ~/.pi/agent,可被 PI_CODING_AGENT_DIR 覆盖
src/config.ts:605-611;docs/environment-variables.md
深合并规则、跳过 undefined、嵌套递归
src/core/settings-manager.ts:61-82 deepMergeObjects
defaultProjectTrust / cacheWarming 只读用户层
src/core/settings-manager.ts:1007-1016、:928-932
deviceId、httpProxy 为全局专属
src/core/settings-schema.ts("Global only" 描述);docs/settings.md
settings JSON Schema 由源码生成
packages/coding-agent/schemas/settings.schema.json 的 $comment
agent 目录文件清单
docs/configuration.md 的 Agent directory 表;src/config.ts:618-650
auth.json 写入权限 0600
src/migrations.ts:69
models-store.json 字段
src/core/model-runtime.ts:225;src/core/models-store.ts:52
mcp-auth.json 位置与用途
src/extensions/mcp/oauth.ts:10、:147
bin/ 是外部二进制目录
src/config.ts:639-641
sessions/ 默认会话目录
src/config.ts:648-650
SYSTEM.md / APPEND_SYSTEM.md 不合并
docs/configuration.md 第 39 行
上下文文件名与优先级
src/core/resource-loader.ts:185 loadContextFileFromDir
上下文文件发现不需要信任
docs/configuration.md 第 47 行
defaultThinkingLevel 七档、默认 medium
docs/settings.md 模型段;schemas/settings.schema.json 的 ModelThinkingLevel
thinkingBudgets 只覆盖四档
src/core/settings-schema.ts:121-132
思考等级预算缺 off/xhigh/max
同上
cacheWarming 默认 streaming、仅全局、0.05 美元阈值
docs/settings.md 模型段;src/core/settings-manager.ts:928-932
defaultTools 同一列表内纯名与增量项的配合规则
docs/settings.md Tools 段("In one list, plain names form the selection...")
--tools 与 SDK tools 不能混用纯名与增量项
src/core/settings-manager.ts:97-104 getToolListError
getToolListError 只被 CLI 与 SDK 调用,不作用于 settings 文件
src/cli/args.ts:156、src/core/sdk.ts:280
两层 defaultTools 的合并语义
src/core/settings-manager.ts:126-131 mergeDefaultTools、:137-140 resolveDefaultTools、:110-120 applyToolModifiers
DEFAULT_TOOL_NAMES 四个
src/core/settings-manager.ts:85
defaultTools 空数组只关内置工具
docs/settings.md Tools 段
codemode / tool_search 注册时带 defaultActive: false
src/extensions/codemode/index.ts:44、src/extensions/tool-search/index.ts:14
defaultActive 的语义与曝光的关系
src/core/extensions/types.ts:626-631
ThinkingBudgets 只有四档
packages/ai/src/types.ts:136-141
/reload 对工具的语义
docs/settings.md Tools 段;docs/cli.md Tools 段
命令行工具开关覆盖 defaultTools
docs/settings.md Tools 段;docs/cli.md Tools 段
settings 不做校验,非法值替换而非抛错
src/core/settings-manager.ts:128 注释
全部显示类设置默认值
docs/settings.md Terminal and display 段;src/core/settings-defaults.ts:35-66
showHardwareCursor 的环境变量回退
src/core/settings-manager.ts:1378-1380
terminal.clearOnShrink 的环境变量回退
src/core/settings-manager.ts:1225-1232
externalEditor 回退顺序与平台默认
src/core/settings-manager.ts:961-971
sessionDir 三级优先级
docs/settings.md Sessions 段;docs/cli.md Sessions 段
sessionShareDestination 三个取值
docs/settings.md Sessions 段;src/core/settings-manager.ts:807-812
压缩三个默认值
docs/settings.md Compaction 段;src/core/settings-defaults.ts:9-16
压缩触发条件
src/core/compaction/compaction.ts:269
压缩三级回退与两层对象先合并
docs/settings.md Compaction 段;src/core/settings-manager.ts:833-880
branchSummary 两个默认值
docs/settings.md Branch summaries 段;src/core/settings-defaults.ts:14-17
资源列表两层合并进同一累加器
src/core/package-manager.ts:947-973
项目层先解析,路径基准不同
同上;docs/settings.md Resources 段
!pattern / +path / -path 的四步顺序
src/core/package-manager.ts:743-797 applyPatterns
精确匹配对 Skill 父目录生效
src/core/package-manager.ts:700-718 matchesAnyExactPattern
内置扩展的 builtin: 名字与默认加载
docs/settings.md Resources 段;src/core/package-manager.ts:977-992
enableSkillCommands 只影响命令发现
docs/skills.md;src/modes/interactive/interactive-mode.ts:807-816
.agents/skills 向上查找与仓库根停止
src/core/package-manager.ts:474-493 collectAncestorAgentsSkillDirs
~/.agents/skills 永远视为可信
src/core/trust-manager.ts:183-208
环境变量三类用途
docs/environment-variables.md 第 3-8 行
两个进程标记
docs/environment-variables.md Process Marker 段
shell 工具五个会话变量
docs/environment-variables.md Shell Tool Session Environment 段
会话变量只在 LLM 调用的工具里注入
同上(末句);src/core/tools/bash.ts:196-206
exposeSessionEnvironment 关闭注入
docs/environment-variables.md Custom Shell Tools 段
Pi 读取的配置环境变量清单
docs/environment-variables.md Pi Process Configuration 段
PI_CLEAR_ON_SHRINK
src/core/settings-manager.ts:1230
migrateSettings 的 4 处重写
src/core/settings-manager.ts:396-456
迁移不写盘、不备份、不打日志
同上(函数只 return settings,无 I/O)
runMigrations 的 5 个动作
src/migrations.ts:305-315 runMigrations
oauth.json 改名为 .migrated
src/migrations.ts:41
auth.json 权限 0600
src/migrations.ts:69
会话文件迁移的来源位置
src/migrations.ts:84-131
tools/ → bin/ 迁移
src/migrations.ts:177-216
commands/ → prompts/ 重命名与弃用警告
src/migrations.ts:137-155、:222-252
弃用警告会打印并等按键
src/migrations.ts:277-298
迁移在启动时跑一次
src/main.ts:669

完整事实表(含每条断言的推导命令)见专栏仓库的 research/pi-agent/FACTS.md。