01 业内评价
“Pi is the most interesting agent harness.”
“Pi 是最有意思的 Agent Harness。”
Tobi LütkeShopify 联合创始人、CEO · [3]
“Pi happens to be, at this point, the coding agent that I use almost exclusively.”
“目前,Pi 几乎是我唯一使用的编码 Agent。”
Armin RonacherFlask 作者、Pi 项目参与者 · [4]
02 用 Pi 做二次开发
OpenClaw 是一个让用户通过 WhatsApp、Telegram 等消息渠道使用 Agent 的应用。它把 Pi 当作运行内核,自己负责消息网关、渠道适配和业务工具。
收到消息后,OpenClaw 先找到对应的会话,再调用 Pi 的 createAgentSession() 建立 AgentSession。Pi 负责读取上下文、请求模型、处理模型提出的工具调用、保存会话记录,并把运行事件交给 OpenClaw。
OpenClaw 在会话外面补上自己的功能:按渠道生成系统提示词,加入消息发送、浏览器和定时任务等工具,根据权限筛选工具,再把 Pi 的流式回复转换成聊天消息发回去。
所以,OpenClaw 使用的是 Pi 的 Agent 运行和会话能力,消息接入和具体业务逻辑仍然由 OpenClaw 实现。[5]
03 Pi 的特点
Pi 的特点可以概括为:[1] [2]
核心精简默认能力保持小,先把 Agent 的基本运行做好。
扩展优先功能可以通过扩展、技能、提示模板、主题和包继续增加。
模型可替换可以在多个模型供应商之间切换,也可以加入自定义供应商。
Pi 提供的是一个可以继续改造的 Agent 基础。下面进入源码,看这些部分怎样组合。
01 按能力划分代码仓库
仓库根节点
目录或能力总块
独立 npm 包
具体能力
Runtime 和会话层在图中的位置
Runtime · pi-agent-core
Pi Agent 代码仓库 → Agent 内核与会话基础库 → Agent 执行内核 → 核心循环
Runtime 位于 Agent 内核分支,推进一次模型与工具循环,并根据运行结果决定继续还是结束。
会话层 · pi-coding-agent
Pi Agent 代码仓库 → Coding Agent 宿主 → 会话与运行控制、会话消息与上下文处理
会话层分布在这两个能力组中:AgentSession 组织运行,SessionManager 管理历史记录和上下文恢复。
两者的连接:AgentSession 调用 Agent 内核开始或继续运行;运行事件返回会话层,用于更新界面和保存会话记录。
Q&A
“会话”指 Pi 中一段可以保存并继续的对话记录,包括用户输入、模型回复和工具结果。同一个会话可以有多次运行;会话保留此前的对话内容,Runtime 则推进每一次具体运行。
01 会话记录
Pi 把会话记录放在 ~/.pi/agent/sessions/ 下,并按打开时的工作目录创建文件夹。通常一个项目或工作区对应一个文件夹;其中每个 JSONL 文件代表一次会话。开始新会话时,Pi 新建文件;继续已有会话时,则把新记录追加到原文件。图中列出三个真实目录及其中的会话文件。
JSONL 文件内容
文件夹名由工作目录路径编码而来;JSONL 文件名包含创建时间和会话 ID。JSONL 是按行保存 JSON 对象的文本格式:第一行是会话头部,之后每行是一条记录。Pi 按记录产生的顺序追加内容。下面的命令使用图中的 Session D 路径;输出是结构示例,字段和值均已简化:
$ cat ~/.pi/agent/sessions/--Users-ooooyasumi-develop-Project-04_test-202609-pi--/2026-09-02T02-26-27-887Z_01a05ff0-6faf-755f-8334-50fa8d8bec73.jsonl
{"type":"session","version":3,"id":"01a05ff0-6faf-755f-8334-50fa8d8bec73","timestamp":"2026-09-02T02:26:27.887Z","cwd":"/Users/ooooyasumi/develop/Project-04_test-202609/pi"}
{"type":"message","id":"entry-1","parentId":null,"timestamp":"2026-09-02T02:26:28.001Z","message":{"role":"user","content":"你好"}}
{"type":"message","id":"entry-2","parentId":"entry-1","timestamp":"2026-09-02T02:26:29.120Z","message":{"role":"assistant","content":[{"type":"text","text":"你好。"}]}}
{"type":"model_change","id":"entry-3","parentId":"entry-2","timestamp":"2026-09-02T02:30:00.000Z","provider":"anthropic","modelId":"claude-sonnet-4"}
{"type":"compaction","id":"entry-4","parentId":"entry-3","timestamp":"2026-09-02T02:35:00.000Z","summary":"此前对话的摘要","firstKeptEntryId":"entry-2","tokensBefore":183616}
会话文件中的记录类型
除首行的会话头部外,其他记录都有 id、parentId(可以为空)和 timestamp。下表列出文件中的记录类型和各自保存的信息。
| 记录类型 | 保存的信息 |
session 会话头部 | version 版本、id 会话 ID、timestamp 创建时间、cwd 工作目录,以及可选的 parentSession 父会话 ID。 |
message | message 保存完整的 AgentMessage:用户输入、模型回复、工具结果,以及命令执行、扩展消息、分支摘要和压缩摘要。各类消息的字段见本章「04 消息类型与转换」。 |
thinking_level_change | thinkingLevel:切换后的模型思考等级。 |
model_change | provider 服务商名称和 modelId 模型 ID。 |
compaction | summary 压缩摘要、firstKeptEntryId 保留历史的起点、tokensBefore 压缩前的 token 数;还可包含 details、usage 和 fromHook。 |
branch_summary | fromId 被切换分支的起点、summary 分支摘要;还可包含 details、usage 和 fromHook。 |
custom | customType 扩展标识和可选的 data。扩展可用它在恢复会话时还原自己的状态;它不进入模型上下文。 |
custom_message | customType 扩展标识、content 文本或图片内容、display 是否显示,以及可选的 details。消息内容会进入模型上下文。 |
label | targetId 被标记条目的 ID 和 label 标签文字;标签为空时表示移除标签。 |
session_info | 可选的 name:会话显示名称。 |
02 上下文结构与组装
上下文结构
AgentContext 是 Pi 定义的 TypeScript 接口,不是一个类;它规定传给 Agent Runtime 的上下文对象有哪些字段。这个对象包含系统提示词、当前消息历史和本轮可用工具。发起模型请求时,消息和工具再转换成模型访问层使用的类型。
上下文的组装时机
- 打开或恢复会话时:从 JSONL 读取当前分支的历史消息;Coding Agent 准备系统提示词和本次启用的工具。
- 运行过程中:用户输入、模型回复(可含工具调用)和工具结果进入消息列表;排队的即时或后续消息在下一轮模型回复前加入。Coding Agent 也可能加入扩展消息或 Bash 执行记录。
- 每次请求模型前:Runtime 使用最新的提示词、消息和工具;模型访问层再将消息和工具转换为模型接口所需的结构。
上下文的三个阶段
为了说明上下文在处理过程中怎样变化,我把它按状态分成三个阶段:
- 来源:会话消息、提示词资源和工具配置还分散在各自的位置,尚未组成这次运行的上下文。
- Runtime:Runtime 把本次运行要用的内容放进
agent.state,形成符合 AgentContext 接口的对象。
- 模型调用层:发起请求时,消息和工具转换为该层使用的
Message[] 和 Tool[],再与系统提示词组成 Context。
03 系统提示词组成
Pi 在每次模型请求中附上系统提示词。按内置模板构建时,依次加入基础提示词、追加内容、项目规则、技能清单和当前工作目录。工具名称与一句话说明位于基础提示词的“可用工具”中,工具使用规则位于其中的“行为要求”中。
01 基础提示词
按默认规则加载时,Pi 先检查受信任项目当前工作目录下的 .pi/SYSTEM.md;项目未受信任或文件不存在时,再查全局目录 ~/.pi/agent/SYSTEM.md。两处都没有时,Pi 使用内置模板。找到自定义文件后,文件内容会替代整段内置基础提示词。
下面是内置模板的中文译文,以默认启用的 read、bash、edit、write 四个工具为例。方括号中的行为要求和路径会在运行时填入;工具配置变化时,清单也会更新。
你是在 pi(一个编码 Agent 框架)中工作的专业编程助手。你通过读取文件、执行命令、修改代码和编写新文件来帮助用户。
可用工具:
- read: 读取文件内容。
- bash: 执行 bash 命令,例如 ls、grep、find。
- edit: 用精确文本替换修改文件,一次调用可修改多处互不重叠的位置。
- write: 创建或覆盖文件。
除了上面的工具,项目还可能提供其他自定义工具。
行为要求:
[根据当前工具及其规则生成的要求]
- 回复保持简洁。
- 操作文件时清楚写明路径。
Pi 文档(仅当用户询问 Pi 本身、SDK、扩展、主题、技能或 TUI 时阅读):
- 主文档:[README.md 的安装路径]
- 其他文档:[docs 的安装路径]
- 示例:[examples 的安装路径](扩展、自定义工具、SDK)
- 阅读 Pi 文档或示例时,docs/... 从“其他文档”目录查找,examples/... 从“示例”目录查找,不要从当前工作目录查找。
- 用户询问以下主题时,查阅对应文档或示例:扩展(docs/extensions.md、examples/extensions/)、主题(docs/themes.md)、技能(docs/skills.md)、提示词模板(docs/prompt-templates.md)、TUI 组件(docs/tui.md)、快捷键(docs/keybindings.md)、SDK 集成(docs/sdk.md)、自定义 Provider(docs/custom-provider.md)、添加模型(docs/models.md)、Pi 包(docs/packages.md)、环境变量(docs/environment-variables.md)。
- 处理 Pi 相关工作时,先阅读文档和示例,并在实现前查看 Markdown 文档之间的引用。
- 完整阅读 Pi 的 Markdown 文件,并继续查看其中链接的相关文档(例如涉及 TUI API 时阅读 tui.md)。
02 追加内容
项目受信任时,Pi 先查当前工作目录下的 .pi/APPEND_SYSTEM.md;项目未受信任或文件不存在时,再查全局目录 ~/.pi/agent/APPEND_SYSTEM.md。找到的文字接在基础提示词后面,不会替换基础提示词。命令行参数和扩展也可以提供追加内容;没有来源时,这一部分为空。
03 项目规则
Pi 先查全局目录 ~/.pi/agent/,再从当前工作目录向上逐级查找。每个目录最多加载一份规则文件,优先顺序是 AGENTS.override.md、AGENTS.md、AGENTS.MD、CLAUDE.md、CLAUDE.MD。找到一份后,该目录不再查找其他候选文件。
规则按全局目录、上层目录到当前目录的顺序加入系统提示词。Pi 将每份文件原文放入带有文件路径的 <project_instructions> 区块,再统一放在 <project_context> 中。使用 --no-context-files 可以关闭这部分加载。
04 技能清单
技能可以来自用户目录 ~/.pi/agent/skills/、~/.agents/skills/,也可以来自受信任项目中的 .pi/skills/、.agents/skills/,或由配置和扩展提供的路径。有可供模型调用的技能且启用 read 工具时,系统提示词才会列出技能名称、说明和文件路径;技能正文留在各自的 SKILL.md 中,模型遇到相关任务时再读取。
05 当前工作目录
最后,Pi 把当前工作目录的绝对路径附在系统提示词末尾,供模型判断文件的相对位置。Windows 路径中的反斜杠会转换为正斜杠。
Current working directory: [当前工作目录的绝对路径]
04 消息类型与转换
模型调用层使用三种消息:UserMessage、AssistantMessage 和 ToolResultMessage。它们分别保存用户输入、模型回复和工具结果,共同组成 Message[]。Runtime 的 AgentMessage[] 还允许加入 Coding Agent 自己的消息类型。以下字段名后的 ? 表示可选。
01 用户消息
UserMessage 保存用户交给模型的内容。正文既可以是一段文字,也可以带图片:
| 字段 | 内容 |
|---|
role | 固定为 user。 |
content | 用户正文;可以是字符串,也可以是文字块与图片块组成的数组。 |
timestamp | 消息时间,使用 Unix 毫秒时间戳。 |
02 助手消息
AssistantMessage 保存模型的一次回复,以及模型来源、用量和停止原因。模型提出的工具调用也放在这条消息中:
| 字段 | 内容 |
|---|
role | 固定为 assistant。 |
content | 内容块数组,可以包含文字、思考内容和工具调用;同一条回复可以同时有文字和工具调用。 |
api | 生成这条回复使用的接口协议。 |
provider | 生成这条回复的服务商标识。 |
model | 请求时使用的模型 ID。 |
responseModel? | 服务商实际返回的模型名称,例如自动路由选中的具体模型。 |
responseId? | 服务商返回的响应或消息 ID。 |
usage | 输入、输出和缓存 token 用量,以及费用;服务商提供时还记录思考 token 等细分用量。 |
stopReason | 停止原因:pending、stop、length、toolUse、error、aborted 或 deferred。 |
errorMessage? | 请求失败或取消时的错误说明。 |
rawStopReason? | 服务商原始的停止原因。 |
endTurn? | 服务商是否明确结束这一轮的标记;保留用于诊断,当前不直接控制 Agent 循环。 |
diagnostics? | 失败或恢复过程中的诊断信息。 |
deferred? | 延后处理请求的句柄,供后续取回结果。 |
timestamp | 消息时间,使用 Unix 毫秒时间戳。 |
03 工具结果消息
ToolResultMessage 保存一次工具执行的结果。它通过 toolCallId 对应助手消息中的调用;工具失败也会生成结果消息,供模型读取错误并决定下一步。
| 字段 | 内容 |
|---|
role | 固定为 toolResult。 |
toolCallId | 对应助手消息中工具调用的 id,将调用与结果关联起来。 |
toolName | 执行的工具名称。 |
content | 返回给模型的内容块数组,可以包含文字或图片。 |
isError | 是否为错误结果。 |
timestamp | 消息时间,使用 Unix 毫秒时间戳。 |
details? | 结构化附加信息,供界面渲染、日志或扩展使用。 |
usage? | 工具自身的用量;不计入主模型回复的上下文用量统计。 |
addedToolNames? | 这次执行后新增的可用工具名称,供支持延后工具加载的协议使用。 |
三种消息的 content 使用以下内容块。用户正文也可以直接使用字符串,助手正文始终是内容块数组:
| 内容块类型(type) | 字段 |
|---|
text | text 保存文字,textSignature? 保留部分协议需要的消息签名。 |
image | data 保存 Base64 图片数据,mimeType 表示图片格式;用于用户消息和工具结果。 |
thinking | thinking 保存思考内容,thinkingSignature? 保存协议要求的签名,redacted? 标记内容是否被隐去。 |
toolCall | id、name、arguments 分别表示调用 ID、工具名称和参数;thoughtSignature?、namespace? 保存部分协议要求的签名或命名空间。 |
04 Coding Agent 扩展消息
Coding Agent 还使用 bashExecution 保存用户通过终端命令入口执行的命令及输出,custom 保存扩展消息,branchSummary 保存分支摘要,compactionSummary 保存压缩摘要。这些消息需要在模型请求前转换为公共消息。
05 模型请求前的消息转换
恢复会话时,Pi 按当前分支还原消息。发起请求前,它会重新整理这份列表:
- 保留对话角色。用户消息、助手回复和工具结果仍以各自角色发送。
- 转换终端命令记录。命令和输出整理成用户消息;标记为排除上下文的记录会跳过。
- 转换扩展消息和摘要。扩展消息按文本或图片内容发送;分支摘要和压缩摘要转成带摘要标记的用户消息。仅供界面或扩展内部使用的字段不发送。
转换结果只用于本次请求,不会改写运行中的原消息或 JSONL 会话记录。下一次请求会按当时的消息列表重新转换。
05 工具定义
上下文中的 tools 是本轮允许模型调用的工具定义列表。每项定义告诉模型工具叫什么、能做什么、参数应该怎样填写。模型返回的是调用名称和参数,实际动作由本地执行函数完成。
01 请求中的工具字段
模型调用层的 Tool 类型包含以下字段。协议适配器按这些字段生成服务商要求的工具定义:
| 字段 | 内容 |
|---|
name | 工具名称。模型提出调用时使用这个名称。 |
description | 完整用途说明,告诉模型何时使用、有什么限制。 |
parameters | 参数 Schema,声明各参数的名称、类型、说明和必填要求。 |
constrainedSampling? | 可选的参数生成约束,例如 JSON Schema 严格模式或语法约束;是否支持由协议适配器决定。 |
以 read 为例,下面保留名称、简化说明和实际参数结构;参数说明翻译为中文。required 中的 path 是必填参数,offset 与 limit 可省略:
{
"name": "read",
"description": "读取文本或图片;长文本可以按行继续读取。",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "相对或绝对文件路径" },
"offset": { "type": "number", "description": "起始行,行号从 1 开始" },
"limit": { "type": "number", "description": "最多读取的行数" }
},
"required": ["path"]
}
}
系统提示词中的一句话说明只帮助模型辨认工具;这里的 description 和 parameters 提供完整用途和调用要求。请求发出时,适配器会转换字段名称,例如 Anthropic 使用 input_schema 表示参数结构。
02 本地执行与提示词字段
Runtime 使用的 AgentTool 还包含执行和界面信息。Coding Agent 的工具定义进一步提供提示词贡献及扩展渲染配置。下表区分这些字段的用途;执行函数和渲染函数留在本地,提示文字只作为系统提示词的内容发送。
| 字段 | 内容 |
|---|
label | 在界面中显示的工具名称。 |
execute | 执行函数,接收调用 ID、参数、取消信号和进度回调,返回工具结果。 |
prepareArguments? | 参数校验前的预处理函数。 |
executionMode? | 这项工具的执行方式,可以指定串行或并行。 |
promptSnippet? | 工具的一句话说明,写入内置系统提示词的“可用工具”清单。 |
promptGuidelines? | 工具使用规则,写入内置系统提示词的“行为要求”。 |
renderCall? / renderResult? | 扩展工具在界面中展示调用与结果的渲染函数。 |
renderShell? | 扩展工具使用标准卡片外框,或自行绘制外框。 |
name、description、parameters 是模型调用层的基础字段;label、execute、prepareArguments、executionMode 属于 Runtime 工具接口。提示词与渲染字段由 Coding Agent 提供,不能把所有字段都当成服务商请求中的工具参数。
03 工具调用步骤
- 选定本轮工具。宿主按当前运行设置启用工具,未启用的工具不会提供给模型。
- 发送工具定义。Pi 将工具名称、说明和参数结构交给模型;不同服务商要求的字段格式由模型调用层转换。
- 检查模型的调用。Runtime 确认工具在本轮可用,并检查参数是否符合定义。
- 执行或返回错误。检查通过后,Pi 调用本地工具实现,并把结果加入消息列表。工具不存在或参数不合格时,Pi 返回错误结果,不执行工具。
会话会保存工具调用与执行结果;工具定义及执行函数由宿主提供。恢复会话后,Pi 按宿主当前启用的工具准备可用列表。
06 上下文压缩
上下文压缩将较早的对话改为摘要,让后续模型请求使用“摘要+近期原文”。原始消息仍保存在会话文件中。压缩由会话层安排,Runtime 在需要时等待它完成,再使用更新后的消息继续请求模型。
01 压缩检查的时机
自动压缩不是每收到一个流式片段就检查。Pi 在以下位置检查用量和模型结果:
- 提交新任务之前。会话层完成模型与认证检查后,先检查上一条助手消息,再把这次新输入加入消息列表。这次检查针对已有上下文,尚未计算即将加入的新输入。
- 同一次运行进入下一轮之前。上一轮模型回复和工具结果已处理完,Runtime 确认需要继续后,调用会话层的下一轮准备接口。会话层先估算当前上下文,必要时压缩,再把新消息列表交回 Runtime。这发生在下一轮排队消息注入和模型请求之前。
- 整次运行结束之后。发出
agent_end 后,会话层先检查暂时性错误重试,再检查上下文超限、可恢复截断和用量阈值。正常结束也会经过这次检查。
用户输入 /compact 可以手动压缩,不必先达到自动阈值。自动路径由 compaction.enabled 控制,默认开启;手动压缩仍使用保留量和摘要预算等配置。
02 Token 计算与触发条件
下一轮准备时,Pi 将服务商已经报告的有效用量,与之后新增消息的估算用量相加:
T = U + ΣE(最近有效回复之后的消息)
- T:当前总用量。用于判断是否需要压缩。
- U:服务商报告的有效计数。取最近一次有效助手回复的
usage,排除错误、取消和全零用量。有非零 totalTokens 时直接使用;否则将 input + output + cacheRead + cacheWrite 相加。缓存 token 也占上下文空间;另报的思考 token 已包含在 output 中,不重复相加。
- ΣE:新增消息的估算计数之和。Σ 表示求和,E 表示单条消息的估算量。工具结果或其他消息在有效回复后加入时,服务商尚未统计它们,因此要逐条估算并补上。
没有有效 usage 时,不使用 U,改为估算当前全部消息,即 T = ΣE(当前全部消息)。
整次运行结束后的检查通常直接使用最后回复的 U;回复是错误或用量为零时,再使用上述估算。结束检查跳过用户取消;新任务前的补充检查则可以使用上一条取消回复中已经报告的非零用量。会话层还会检查统计是否早于最近一次压缩,避免旧用量再次触发压缩。
E 是消息大小的粗略估算,不是调用模型的分词器。文字按字符数除以 4 后向上取整;工具调用计入名称和参数 JSON 的字符数;思考正文也计入。用户消息和工具结果中的每张图片按 4,800 个等效字符,也就是约 1,200 token 估算:
E(消息) = ceil(计入的字符数 ÷ 4)
服务商报告的输入用量通常已包含系统提示词和工具定义。没有有效用量、仅按消息估算时,Pi 只计算消息列表,不另加系统提示词与工具定义,因此估算值与真实请求用量可能有差异。
自动阈值压缩的判断是严格大于,公式如下:
自动压缩已开启,且 T > W − R 时触发
W 是当前模型的上下文窗口 contextWindow;R 是预留量 compaction.reserveTokens,默认 16,384。
例如 W 为 200,000 时,触发线是 200,000 − 16,384 = 183,616。T 等于 183,616 不触发,大于它才触发。R 用来提前留出余量,和下面决定保留多少近期原文的配置是两回事。
上下文超限和可恢复截断走另一条恢复路径,不必先满足这条阈值公式。Pi 可以根据错误文字识别超限,也会识别部分服务商“输入已超过窗口”或“输入几乎填满窗口且没有输出”的用量表现。可恢复截断的条件是 stopReason=length 且 usage.output < Model.maxTokens,比较的是模型描述中的输出上限。
03 切点选择与原文保留
第二步是在当前会话分支中选出要保留的原文。之前已经压缩过时,只处理上次保留的消息及之后的新记录,旧摘要另行参与更新;不会重新把全部原始历史交给模型。
切点使用逐条消息估算值 E,与触发检查所用的服务商 usage 分开计算。默认 compaction.keepRecentTokens = 20,000,选择步骤如下:
- 列出合法位置。用户消息、助手消息和可转换为上下文的宿主消息前可以作为切点。工具结果前不能作为切点,避免保留结果却丢掉对应调用。
- 从最新记录向前累加。每条记录按其中消息的估算量计算,累计达到或超过 20,000 时停止;不产生消息的记录不增加 token。
- 调整到合法切点。从达到预算的记录位置开始,向较新的方向找最近的合法位置,并从这里保留到历史末尾。如果达到预算的是工具结果,就不能直接从这个结果开始保留。
- 保留相邻配置记录。切点前紧邻、但不进入模型上下文的元数据记录也一起保留;不会越过之前的压缩记录或另一条可见消息。
20,000 是选边界的目标,不是严格保留量。对齐完整消息和合法切点后,实际量可能多于或少于它。如果消息总量不足预算,或没有可移动的合法位置,算法会保留更早的消息;没有剩余内容需要摘要时,不执行压缩。
下面用历史末尾的一轮举例,数值都是消息估算量。从底部向上计算:2,000+3,000+4,000+12,000=21,000,在第一个工具结果处达到预算。但这里不能切,所以移到下一条助手消息前,最终保留 9,000 token 原文:
参与摘要
更早的其他轮次…
用户要求3,000
助手:第一次工具调用3,000
工具结果:达到预算的位置12,000
切点:从下一条助手消息开始保留
保留原文 · 共 9,000 token
助手:第二次工具调用4,000
对应工具结果3,000
助手:本轮回复2,000
如果切点落在用户消息前,早先的完整轮次生成历史摘要。如果落在某轮的助手消息前,例如上图,Pi 会找到发起这一轮的用户消息,将“更早的完整轮次”和“该轮被切掉的前半段”分别总结,再合并。这样,保留的工具调用和结果仍有原始任务与前期进展作为背景。
04 摘要生成
第三步是准备并发送独立的摘要请求。它使用专门的总结提示词,输入为待压缩的对话文本,不沿原来的工具循环继续做任务。正常历史摘要使用一次请求;如果切点位于一轮内部,同时需要历史摘要和前半轮摘要,就分别请求后合并,失败重试另计。
- 划分内容。取出切点之前需要总结的消息,记录第一条保留记录的
firstKeptEntryId。切点位于一轮内部时,另取这一轮从用户要求到切点之间的内容。
- 整理模型输入。将扩展消息转换为公共消息,再把对话序列化为文字,放入
<conversation>。更新已有历史摘要时,还会将旧摘要放入 <previous-summary>。
- 生成摘要。正常历史摘要要求保留目标、限制与偏好、已完成和进行中的工作、关键决定、下一步及必要背景。切开的前半轮另用提示词保留原始要求、早期进展和理解剩余消息所需的信息。
- 检查并合并。确认摘要请求没有失败、被取消、输出不完整或异常提出工具调用。有前半轮摘要时,将它与历史摘要合并,再附上已读文件和修改文件清单。
摘要请求使用专门的 Context.systemPrompt,消息列表中只有一条用户消息,放入待总结的对话和下列任务说明,不附带可执行工具定义。以下是源码提示词的中文译文;实际发送的是英文,Markdown 标题沿用源码要求的格式。
所有摘要请求共用的系统提示词:
你是上下文摘要助手。你的任务是阅读用户与 AI 助手之间的对话,并严格按照指定格式生成结构化摘要。
不要继续这段对话。不要回答对话中的任何问题。只输出结构化摘要。
首次生成历史摘要时,用户消息使用以下内容。方括号中的对话在请求时替换为实际文本:
<conversation>
[切点之前需要总结的对话文本]
</conversation>
上面的消息是需要总结的对话。请生成结构化的上下文检查点摘要,供另一个大语言模型继续工作。
严格使用以下格式:
## Goal
[用户希望完成什么?如果会话涉及多个任务,可以列出多个目标。]
## Constraints & Preferences
- [用户提到的限制、偏好或要求]
- [没有时写“(none)”]
## Progress
### Done
- [x] [已完成的任务或修改]
### In Progress
- [ ] [当前正在做的工作]
### Blocked
- [阻碍进展的问题,如有]
## Key Decisions
- **[决定]**: [简要理由]
## Next Steps
1. [接下来应做的事情,按顺序列出]
## Critical Context
- [继续工作需要的数据、示例或参考信息]
- [没有时写“(none)”]
每节保持简洁。准确保留文件路径、函数名称和错误消息。
已有摘要时,旧摘要与本次需要总结的新对话一起提交,任务改为更新摘要:
<conversation>
[本次需要并入摘要的新对话文本]
</conversation>
<previous-summary>
[上一次保存的摘要]
</previous-summary>
上面的消息是新增对话,需要合并到 <previous-summary> 中的已有摘要。
用新信息更新现有结构化摘要。规则:
- 保留旧摘要中的全部现有信息。
- 加入新进展、新决定和新的上下文。
- 更新 Progress:把已完成的事项从 In Progress 移到 Done。
- 根据已经完成的工作更新 Next Steps。
- 准确保留文件路径、函数名称和错误消息。
- 已经不相关的信息可以删除。
严格使用以下格式:
## Goal
[保留已有目标;任务范围扩大时加入新目标。]
## Constraints & Preferences
- [保留已有要求,加入新发现的要求。]
## Progress
### Done
- [x] [包含此前完成的事项和新完成的事项。]
### In Progress
- [ ] [根据进展更新当前工作。]
### Blocked
- [当前阻碍;已解决的移除。]
## Key Decisions
- **[决定]**: [简要理由](保留已有决定,加入新决定)
## Next Steps
1. [根据当前状态更新。]
## Critical Context
- [保留重要背景,按需加入新信息。]
每节保持简洁。准确保留文件路径、函数名称和错误消息。
切点位于一轮内部时,被切掉的前半轮使用另一份用户提示词,交代保留原文所需的背景:
<conversation>
[这一轮从用户要求到切点之间的对话文本]
</conversation>
这是因内容过长而无法完整保留的一轮对话的前半部分。后半部分,也就是近期工作,会保留原文。
总结前半部分,为保留的后半部分提供背景:
## Original Request
[用户在这一轮要求做什么?]
## Early Progress
- [前半部分的重要决定和已经完成的工作]
## Context for Suffix
- [理解保留的近期工作所需的信息]
保持简洁。重点保留理解后半部分所需的内容。
手动压缩提供额外要求时,历史摘要的用户提示词末尾还会附加 Additional focus 区段。前半轮摘要使用上面的独立模板。
历史摘要请求的输出上限为 min(floor(0.8 × R), Model.maxTokens);前半轮摘要请求为 min(floor(0.5 × R), Model.maxTokens)。使用默认 R,且模型输出上限足够大时,分别是 13,107 和 8,192 token。这是两次请求各自的生成上限,最终摘要长度并不固定。
会话层在压缩开始时发出 compaction_start,界面可以显示正在压缩。扩展也可以在生成摘要前取消压缩或提供自己的压缩结果。默认实现使用当前模型,摘要生成也会产生模型用量和费用。
05 保存与上下文重建
第四步是在摘要成功生成后追加一条 compaction 记录,保存摘要、第一条保留记录的 ID、压缩前用量,以及文件清单和摘要请求用量。原始历史没有删除。
会话层根据这条记录重新建立消息列表:先放摘要,再放从切点起保留的原文,最后接上压缩记录之后的新消息。更新后的列表写回 agent.state.messages,后续模型请求使用这份上下文。
后续请求的消息列表
历史摘要+必要的前半轮摘要
从切点起保留的消息原文
压缩记录之后加入的新消息
系统提示词和当前工具定义仍由宿主准备,并与这份消息列表一起发送;它们不会被这次历史摘要替代。
- 下一轮前的阈值压缩。完成后直接回到原有 Runtime,使用新消息列表请求下一轮模型。
- 正常结束后的阈值压缩。不重跑已经完成的回复;之后有新任务或排队消息时再使用压缩后的上下文。
- 超限或截断恢复。移除失败回复,压缩后通过
Agent.continue() 再尝试一次。同一次恢复不会无限重复压缩和重试。
06 压缩失败与取消
摘要请求失败、被取消或达到输出上限而未完成时,不保存新的压缩记录,原始历史仍然保留,界面会收到失败或取消信息。暂时性的摘要请求错误可以使用会话配置的重试策略;这与“超限后压缩并重跑一次原请求”是两个不同的次数限制。
压缩可以丢失原文细节,保留 20,000 token 原文也不是压缩后一定适合所有模型窗口的保证。需要核对具体操作时,仍应查看会话中的原始记录。
01 统一调用入口
Pi 为模型调用规定了同一个函数入口。Runtime 传入模型、上下文和请求选项,得到一条可逐项读取的事件流:
streamSimple(
model: Model,
context: Context,
options?: SimpleStreamOptions
): AssistantMessageEventStream
Model 指定要调用哪个模型,其中 provider 标识服务商,api 标识它使用的接口协议。Context 放系统提示词、消息和工具;options 放思考等级、输出上限等本次请求的设置。函数返回事件流,文本、工具调用和最终结果都从这条流中读取。
统一的是调用方式和数据类型。Pi 根据 provider 找到服务商,再由服务商根据 api 选用协议适配器。同一个服务商可以使用一种或多种接口协议;适配器负责生成该协议要求的请求。
02 请求中的数据结构
从 Runtime 到服务商,请求数据依次经过三个阶段:
- Runtime 消息
AgentMessage[]。保存用户输入、助手回复、工具结果,也允许 Coding Agent 加入自己的消息类型。
- 公共模型消息
Message[]。宿主将扩展消息转换或过滤后,得到模型调用层认识的用户消息、助手消息和工具结果。这些消息与系统提示词、工具一起放进 Context。
- 服务商请求体。协议适配器把
Context、Model 和请求选项写成目标接口要求的字段,随后发送请求。
01 Runtime 数据结构
AgentContext 是 TypeScript 接口,规定了交给核心循环的上下文对象有哪些字段。它包含系统提示词、当前消息列表和可执行工具:
interface AgentContext {
systemPrompt: string;
messages: AgentMessage[];
tools?: AgentTool<any>[];
}
systemPrompt 保存系统提示词;messages 保存当前运行使用的历史;tools 保存本轮可用工具。每个 AgentTool 有名称 name、说明 description、参数结构 parameters、界面名称 label 和执行函数 execute,还可以设置参数预处理 prepareArguments 与执行方式 executionMode。代码中的 ? 表示字段可省略。
AgentMessage 是多种消息类型的联合。除下一部分列出的三种公共消息外,Coding Agent 还加入了以下四种消息。它们都有 role 和毫秒时间戳 timestamp,其他字段随类型变化:
| 消息类型(role) | 其他字段 | 保存的内容 |
bashExecution | command、output、exitCode、cancelled、truncated、fullOutputPath?、excludeFromContext? | 命令、输出、退出码及取消或截断状态;可记录完整输出路径,也可排除出模型上下文。 |
custom | customType、content、display、details? | 扩展消息的类型、正文、是否在界面显示,以及扩展附加信息。 |
branchSummary | summary、fromId | 离开分支的摘要,以及摘要对应的分支位置。 |
compactionSummary | summary、tokensBefore | 压缩后的历史摘要,以及压缩前的 token 用量。 |
例如,compactionSummary 在运行时保留摘要和用量信息。转换时,摘要会成为一条带摘要标记的用户消息,tokensBefore 留作程序使用。终端命令和扩展消息也按各自规则转换,标记为排除上下文的命令记录会跳过。
02 公共模型请求结构
模型调用层接收 Context。它与 Runtime 上下文的三个字段对应,但消息和工具使用模型调用层的类型:
interface Context {
systemPrompt?: string;
messages: Message[];
tools?: Tool[];
}
type Message = UserMessage | AssistantMessage | ToolResultMessage;
interface Tool {
name: string;
description: string;
parameters: TSchema;
constrainedSampling?: false | ConstrainedSamplingConfig;
}
这里的工具字段告诉模型工具名称、用途和参数要求。TSchema 表示参数的 Schema 结构;constrainedSampling 可以声明 JSON Schema 或语法约束,设为 false 时显式关闭,由支持它的适配器转换。运行中的工具对象仍可能携带执行函数等本地字段,适配器只选取模型需要的内容写入请求。
Message[] 只包含下面三种消息。它们在 Runtime 中也使用同一套结构;转换时主要处理的是宿主新增的消息类型。
| 消息类型(role) | 主要字段 | 内容 |
user | content、timestamp | 用户输入;正文可以是字符串,也可以是文字和图片组成的数组。 |
assistant | content、api、provider、model、usage、stopReason、timestamp、errorMessage? | 助手回复、来源模型、token 用量与费用、停止原因和错误信息。 |
toolResult | toolCallId、toolName、content、isError、timestamp、details?、usage?、addedToolNames? | 关联的工具调用、工具名称、返回内容和错误标记;可附带界面信息、工具用量和新增工具名称。 |
助手消息还可保存实际响应模型 responseModel、响应 ID responseId、诊断信息 diagnostics、延后请求句柄 deferred、服务商原始停止原因 rawStopReason 和轮次结束标记 endTurn。这些字段用于保留响应信息,是否进入下一次请求由适配器决定。
消息中的 content 还可以拆成内容块。工具调用放在助手消息里,执行结果则单独成为 toolResult 消息,并通过调用 ID 关联:
| 内容块(type) | 字段 | 出现位置 |
text | text、textSignature? | 用户、助手或工具结果的文字;签名用于保留服务商要求的消息信息。 |
image | data、mimeType | 用户或工具结果中的图片;data 为 Base64 数据,mimeType 为图片类型。 |
thinking | thinking、thinkingSignature?、redacted? | 助手思考内容及用于后续请求的签名;redacted 标记内容是否已被隐去。 |
toolCall | id、name、arguments、thoughtSignature?、namespace? | 助手提出的工具调用,包含调用 ID、名称和参数;部分协议还需要签名或命名空间。 |
Context 之外,调用还会传入模型描述 Model 和本次选项 SimpleStreamOptions。它们不属于消息列表。下面列出常用字段:
| 结构 | 字段 | 用途 |
Model | id、name、provider、api、baseUrl | 模型标识与显示名称、所属供应商、接口协议和服务地址。 |
Model | input、reasoning、thinkingLevelMap?、contextWindow、maxTokens、cost | 模型支持的输入、思考能力、上下文窗口、输出上限和价格。 |
Model | headers?、samplingParams?、compat? | 默认请求头、采样参数和协议兼容设置。 |
SimpleStreamOptions | maxTokens?、temperature?、reasoning?、thinkingBudgets?、toolChoice? | 本次输出上限、采样温度、思考等级或预算,以及是否允许模型调用工具。 |
SimpleStreamOptions | apiKey?、headers?、signal?、timeoutMs?、maxRetries?、maxRetryDelayMs? | 本次认证、请求头、取消、超时和服务商请求重试设置。 |
SimpleStreamOptions | samplingParams?、sessionId?、cacheRetention?、transport?、deferred? | 补充采样参数、缓存关联与保留设置、传输方式和延后处理选项。 |
Model.maxTokens 描述模型的输出上限,选项中的 maxTokens 设置本次请求的上限。不同模型与适配器支持的选项不同,不能假定每个字段对所有模型都生效。
03 服务商请求结构
服务商收到的是协议适配器组装的原生请求体,各协议的字段不同。下面以本地 OpenAI Responses 适配器为例,只展示系统提示词、一条用户消息和一个简化的 read 工具定义:
{
"model": "<所选模型 ID>",
"input": [
{ "role": "system", "content": "<系统提示词>" },
{ "role": "user", "content": [
{ "type": "input_text", "text": "读取 README.md" }
] }
],
"tools": [{
"type": "function",
"name": "read",
"description": "读取文件",
"parameters": {
"type": "object",
"properties": { "path": { "type": "string" } },
"required": ["path"]
},
"strict": false
}],
"max_output_tokens": 4096,
"stream": true,
"store": false
}
| 原生请求字段 | 来自哪里 | 转换后的含义 |
model | Model.id | 要调用的模型 ID。 |
input | Context.systemPrompt 与 Context.messages | 系统提示词转换为 system 或 developer 消息;历史消息转换为该协议的消息、工具调用和工具结果项。 |
tools | Context.tools | 工具转换为协议要求的定义;普通函数工具包含 type、name、description 和 parameters 等字段。 |
max_output_tokens | 本次 maxTokens 选项 | 本次回复的输出上限;适配器按兼容设置决定是否发送。 |
reasoning | 思考选项与模型能力 | 转换为协议要求的思考设置;支持时才使用,未在上面的示例中展开。 |
stream、store | 适配器设置 | 本地适配器设置 stream: true 请求流式回复,设置 store: false 关闭响应存储。 |
请求地址来自服务配置,认证通常写入请求头;取消信号、超时和客户端重试选项用于控制请求过程,不会作为这些字段原样写进请求体。消息时间戳、界面详情和本地执行函数也不会整份发送。
换成 Anthropic Messages 时,系统提示词放入 system,历史放入 messages,工具参数结构使用 input_schema,输出上限使用 max_tokens。公共结构保持一致,具体字段由适配器转换。
服务商返回的原生响应则转换为 Pi 的 AssistantMessageEventStream,最终形成一条 AssistantMessage。回复中的内容块、停止原因和用量,随后又可以成为下一轮消息历史的一部分。
03 一次模型请求的顺序
Runtime 准备好本轮上下文后,模型调用按下面的顺序进行:
- Runtime 将自己的消息转换为
Message[],连同系统提示词和工具交给模型调用层。
- 模型调用层查找
Model.provider 对应的服务商,取得本次请求使用的凭据、地址和请求头。
- 服务商按
Model.api 选择协议适配器。适配器先处理跨模型消息的兼容问题,再组装原生请求。
- 适配器发送请求,边读取服务商的响应,边生成统一事件。Runtime 收到完整助手消息后,才判断是否执行工具或进入下一轮。
模型返回文字时,界面可以随着事件逐步显示;工具调用的名称和参数也可以逐步显示,但 Runtime 要等完整回复结束后才执行工具。
04 消息与协议转换
请求发出前有两次数据结构转换,此外还有两处可能调整消息内容,但不改变消息类型。
- Runtime 预处理。如果宿主配置了
transformContext,它可以先裁剪或补充上下文;输入和输出仍是 AgentMessage[]。
- 转换为公共消息。
convertToLlm 在 agent-core 中把 AgentMessage[] 转成 Message[]。这是第一次结构转换。
- 目标模型兼容处理。协议适配器调用
transformMessages,处理目标模型不支持的图片、思考内容和工具调用 ID 等情况;输出仍是 Message[]。
- 组装原生请求。协议适配器把公共消息、系统提示词、工具和选项转换为目标 API 的请求体。这是第二次结构转换。
例如,同样一段系统提示词,Anthropic Messages 将其放在请求的 system 字段;OpenAI Responses 将其作为 system 或 developer 消息放进 input。工具调用与结果在 Anthropic 中使用 tool_use / tool_result,在 OpenAI Responses 中使用 function_call / function_call_output。这些字段由适配器填写,Runtime 不需要按服务商分别组装。
有些模型使用相同的 API,却对字段或工具格式有不同要求。模型的兼容设置用于调整这些请求字段,例如输出上限和思考参数的写法;它不会修改会话里保存的原始消息。
05 流式响应与最终消息
服务商的响应也经过一次转换:协议适配器读取原生响应块,先发出 start 事件,再按内容发出文本、思考内容或工具调用事件。这三类内容各有开始、增量和结束事件。整条响应以 done 或 error 结束。
delta 表示刚收到的新片段,界面可以据此更新显示;中间事件里的 partial 是截至当时的助手消息。结束事件携带完整的 AssistantMessage,Runtime 根据其中的停止原因和工具调用决定下一步。
因此,一轮模型回复可以同时包含展示给用户的文字和工具调用。文字不必等工具执行完才显示;工具则必须等参数收齐后才能执行。
06 切换模型
切换模型会更新当前使用的 Model,已经保存的会话消息不会改写。下一次请求仍从这些消息出发,但会重新选择服务商和接口协议,并按新模型的能力处理历史内容。
这一步可能影响图片、思考内容和工具调用 ID 的处理,也可能改变认证方式、请求地址和参数字段。切换前后的消息因此可以留在同一段会话中;真正发送给新模型的格式由适配器在请求时生成。
07 认证
选定模型后,Pi 需要取得该服务商的认证信息,才能发送请求。认证方式由 Provider 声明;凭据的保存由宿主提供。每次模型调用前都会重新解析认证信息,因此 OAuth 令牌可以在过期前更新。
01 认证方式与凭据保存
常见方式是 API Key 和 OAuth。API Key 由用户提供密钥,OAuth 通过浏览器授权或设备码登录取得令牌。服务商还可以支持 AWS 配置、Google 默认凭据等环境认证方式;具体读取规则由各 Provider 实现。
用户选择服务商和登录方式后,Pi 调用该 Provider 的登录流程。API Key 登录提示用户输入密钥;OAuth 登录引导用户完成授权。登录成功返回凭据,再写入凭据存储。退出登录则删除该服务商已保存的凭据。
pi-ai 通过 CredentialStore 读取和更新凭据,默认实现只保存在内存中。Coding Agent 接入 AuthStorage,通常将凭据保存到 ~/.pi/agent/auth.json,下次启动时仍可使用。环境变量中的密钥则在解析时读取,不因读取而写入这个文件。
02 请求前的认证流程
Runtime 每轮调用模型访问入口时,都会经过认证解析。首次请求、工具结果交回模型后的下一轮,以及会话层重试后重新发起的模型调用,都要重新取得本次使用的认证信息。宿主主动查询认证信息时,也可以调用同一流程。
认证解析包括读取凭据、检查类型和有效期、生成请求头等。只有需要登录或刷新 OAuth 令牌时,才向认证服务申请新的凭据。协议适配器内部重发 HTTP 请求,通常复用这次模型调用已经取得的认证信息。
- 找到服务商。根据
Model.provider 找到 Provider,读取它支持的认证方式。
- 选择凭据来源。优先处理本次调用显式传入的
apiKey;没有时读取该服务商已保存的凭据。没有已保存凭据,才尝试环境变量或该服务商支持的其他环境认证。
- 解析凭据。API Key 按服务商规则转换为认证信息;OAuth 检查令牌有效期,需要时先刷新,再转换为请求使用的认证信息。
- 合并请求配置。将认证提供的请求头、服务地址等信息与模型配置和本次调用选项合并。本次显式传入的请求头可以覆盖默认值。
- 发送模型请求。协议适配器用合并后的地址、认证信息和请求体调用服务。服务商最终确认凭据是否有效、是否有权调用所选模型。
显式传入 API Key 的优先处理适用于支持该方式的 Provider。已保存的凭据会按自身类型解析:保存的是 OAuth 凭据,就走 OAuth 流程;保存的是 API Key,就走密钥解析。若类型与 Provider 不匹配,或 OAuth 刷新失败,会返回认证错误,不会自动换用环境中的另一份凭据。
例如,OpenAI 的标准密钥解析先使用传入或保存的密钥,没有密钥时再读取 OPENAI_API_KEY。本地取得密钥只表示找到了认证信息;服务商仍可能在请求时返回“密钥无效”或“没有权限”。
03 OAuth 令牌刷新
OAuth 凭据包含访问令牌、记录到期时刻的 expires,以及刷新令牌等信息。访问令牌能使用多久由服务商决定,Pi 没有为所有服务商统一设置一个有效期。例如 OpenAI Codex 的登录与刷新实现使用服务商返回的 expires_in 计算到期时刻。
| 时间 | 本地实现 | 用途 |
| 令牌有效期 | 来自服务商授权结果,保存为 expires。 | 判断已有访问令牌何时到期。 |
| 提前刷新窗口 | 默认 300000 毫秒,即 5 分钟。 | 剩余有效期不超过该值时,先刷新,再发送模型请求。 |
| 刷新请求超时 | 固定 15000 毫秒,即 15 秒。 | 限制一次 OAuth 刷新请求的等待时间。 |
通用认证接口的 minOAuthValidityMs 可以要求更长的剩余有效期,实际使用的刷新窗口至少为五分钟。五分钟和十五秒是源码中的默认窗口与超时值,不是每隔五分钟申请一次,也不是令牌只能使用十五秒。它们没有对应的普通会话设置项;SDK 调用认证接口时可以传入 minOAuthValidityMs,模型请求的 retry.provider.timeoutMs 不修改这里的刷新超时。
刷新窗口 = max(300000, minOAuthValidityMs 或 0)
剩余有效期 = expires − 当前时间
剩余有效期 ≤ 刷新窗口:调用令牌刷新接口
剩余有效期 > 刷新窗口:直接使用已有令牌
例如,本地记录令牌在 12:00 到期:11:30 请求模型时仍有 30 分钟,解析认证但不刷新;11:55 请求时只剩五分钟,会先刷新。没有新的请求或主动认证查询时,这段代码不会启动定时器,到 11:55 自动申请一次。
- 读取保存的 OAuth 凭据,检查剩余有效期。
- 需要刷新时,通过凭据存储锁定更新,再次读取最新值。其他请求已经刷新过,就直接使用更新后的令牌。
- 调用 Provider 的刷新接口,取得新的凭据。默认刷新超时为 15 秒,也会响应取消信号。
- 先保存刷新后的凭据,再用它生成本次请求需要的认证信息。
04 有效期内的认证失败
剩余有效期充足时,每次模型调用仍会解析认证,只是通常不向认证服务申请新令牌。本地时间检查也不能证明凭据一定有效:授权可能已被撤销,密钥可能填错,账号也可能没有调用所选模型的权限。服务商在实际请求时仍可能拒绝认证。
当前通用认证流程按本地到期时间决定是否刷新,没有“模型请求返回 401 / 403 后,无视有效期强制刷新一次”的统一机制。未到刷新窗口的 OAuth 令牌不会仅因模型请求被拒绝就自动重新申请。API Key 配置错误需要更新密钥;服务地址、模型名称或权限配置有误,需要修正对应设置;OAuth 授权失效则需要重新登录。
已经进入刷新窗口、但刷新请求因网络问题失败时,会保留原凭据并返回错误。后续再次调用认证解析,仍会检查到期时间并尝试刷新,不必等待五分钟。会话层是否自动重新发起模型调用,按下一节的暂时性错误重试规则判断;刷新令牌本身失效时,重复申请也不能替代重新登录。
08 错误处理
模型调用可能在准备请求、发送请求或读取回复时失败。这几个位置的错误表现不同,恢复方式也不同:配置错误需要修改设置,暂时性故障可以重试,上下文超限需要先缩减输入。
01 错误出现的位置
| 位置 | 常见错误 | 处理方式 |
| 准备请求 | 未找到 Provider 或协议实现、没有凭据、凭据文件读取失败、OAuth 刷新失败。 | 返回错误,不发送模型请求。配置或授权问题需要修正;刷新中的网络故障按暂时性错误判断。 |
| 服务商检查请求 | 密钥无效或权限不足(常见为 401 / 403)、模型不存在、参数不支持。 | 将服务商返回的原因写入错误消息,通常需要更新凭据、模型或参数。 |
| 连接或服务暂时不可用 | 连接失败、超时、临时限流、服务过载或 5xx 错误。 | 请求层按自己的规则重试;仍失败时返回错误,会话层再判断是否重试。 |
| 额度或账单检查 | insufficient_quota、额度耗尽、预算或账单限制;有时也返回 429。 | 会话层识别到额度或账单限制后,不自动重试;需要处理账户额度或更换服务。 |
| 读取响应 | 连接在生成中途断开、没有收到结束事件、内容格式无法解析。 | 返回失败回复;意外断流可由会话层重试,单纯格式错误不会因为发生在响应流中就自动重试。 |
| 上下文或生成长度 | 输入超过模型窗口,或输出提前截断。 | 交给会话层判断压缩恢复。并非所有 length 停止都属于可恢复错误。 |
| 用户取消 | 请求期间收到取消信号。 | 停止请求,适配器将取消标记为 aborted;会话层不自动重试被取消的回复。 |
02 错误回传与运行结束
模型调用层把失败写成一条助手消息:stopReason 为 error,errorMessage 保存具体原因;协议适配器处理用户取消时使用 aborted。这条消息通过事件流的 error 事件返回。下面省略了模型来源、时间戳和用量等字段:
{
"type": "error",
"reason": "error",
"error": {
"role": "assistant",
"content": [],
"stopReason": "error",
"errorMessage": "Provider is not configured: openai"
}
}
如果准备请求时就失败,事件流可以直接发出 error,不必先发 start。如果生成过程中失败,已经收到的文字可能仍在失败消息中,也可能已经显示在界面上;它们不代表回复正常完成。
Runtime 收到完整的错误或取消消息后,发出 turn_end,再发出 agent_end,结束本次运行。这种情况下不执行回复中的工具调用。运行结束后,会话层才检查是否需要重试或压缩恢复。
03 请求层重试
OpenAI Responses 和 Anthropic Messages 适配器在建立请求时使用 retryProviderRequest。它检查 SDK 报告的连接错误,以及 408、409、429 和 5xx 状态;服务商通过 x-should-retry 指示是否重试时,优先采用该指示。
符合条件且仍有次数时,先按 Retry-After 等响应头等待;服务商没有指定时间,就使用带随机浮动的指数退避。等待也能被取消。服务商要求的等待超过 maxRetryDelayMs 时,本次请求直接失败,交给上层处理。
这个重试函数默认重试次数为 0,由本次请求的 maxRetries 指定次数;Coding Agent 对应设置是 retry.provider.maxRetries。这两处适配器关闭 SDK 内部自动重试,统一由该函数控制。其他协议实现的支持方式可能不同。
这里重试的是建立请求的阶段,不会在响应流中途断开后接着读取原来的流。已经开始接收回复后出错,会形成失败助手消息,交给会话层判断。
请求层按状态码和响应头判断,因此启用后也可能重试返回 429 的额度错误;会话层还会检查错误文字,排除已识别的额度与账单限制。两层的规则不能只用“429 会重试”概括。
04 会话层重试与压缩恢复
AgentSession 等本次运行结束后检查最后一条助手消息。自动重试必须已开启、次数未用尽,并且 stopReason 为 error。随后检查 errorMessage:命中网络错误、连接断开、超时、临时限流、服务端故障或缺少流结束事件等特征,才进入重试;已识别的额度、账单错误和上下文超限会先排除。
- 从 Agent 当前消息列表移除失败回复;会话记录中的失败历史仍保留。
- 等待本次退避时间,并向界面报告正在重试。默认最多重试三次,依次等待 2、4、8 秒。
- 调用
Agent.continue(),使用已有上下文启动新的一次运行,重新请求模型。它不追加一条新的用户任务,也不会恢复原响应流的剩余部分。
- 成功后结束重试;再次失败则重新判断条件,达到上限或用户取消时停止。
AgentSession 按错误文字中的特征识别暂时性故障,并非所有响应流错误都会重试。例如,“连接断开”或“未收到流结束事件”可以命中特征;仅有“JSON 无法解析”则不会自动命中。
上下文超限另走压缩恢复。自动压缩已开启、错误来自当前模型且满足恢复条件时,会话层移除失败或可恢复截断的回复,压缩历史,再调用 Agent.continue() 尝试一次。长度截断的具体判定是 stopReason=length,且 usage.output 小于当前 Model.maxTokens;这里比较的是模型描述中的输出上限。达到该上限的普通长度停止,不走这条恢复路径。
请求层重试期间,当前 Agent 运行尚未结束;会话层重试则是在 agent_end 之后启动新的运行。重试次数、等待时间、超时和最长等待限制的配置见第三章的“异常与重试”。
01 工具定义
Pi 用一份工具定义连接模型、本地执行代码和界面。同一个字段不会全部发给模型:
模型返回工具名称和参数后,Runtime 按名称找到实现,检查参数并调用 execute。工具结果再作为消息交给模型。系统提示词中的工具清单按本轮实际启用的工具生成;有工具定义,也不代表该工具一定在本轮开放。
内置工具按用途分为读取、检索、修改和执行命令四组。下面每个工具都先列配置文字,再列调用参数,最后说明本地实现。
02 读取文件
read
read 读取文本或图片。长文本可以从指定行开始续读。
配置文字
调用参数
| 参数 | 要求 | 作用 |
path | 必填,字符串 | 文件路径,可以是相对路径或绝对路径。 |
offset | 可选,数字 | 从第几行开始,行号从 1 算起;超出文件末尾会报错。 |
limit | 可选,数字 | 最多读取多少行;结果仍受 2000 行和 50KB 上限约束。 |
本地实现
文本由 Node 的文件接口读取。达到输出上限时,结果会给出下一次读取所需的 offset。图片先根据文件内容识别格式,再按需要缩放,最后作为图片内容交给模型。集成方可以替换文件读取接口;默认读取当前机器上的文件。
03 文件检索
find 找文件路径,grep 找文件里的文字,ls 列出一个目录。三者返回的内容不同,适合在不同的查找阶段使用。
find:查找文件路径
配置文字
调用参数
| 参数 | 要求 | 作用 |
pattern | 必填,字符串 | 文件名或路径模式,例如 *.ts、src/**/*.ts。 |
path | 可选,字符串 | 搜索起点;默认当前工作目录。 |
limit | 可选,数字 | 返回条数;默认最多 1000 条。 |
本地实现
Pi 调用 fd 查找文件。模式不含斜杠时按文件名匹配,含斜杠时按路径匹配;结果转成相对于搜索目录的路径。需要时由工具管理器取得 fd 可执行文件。
grep:搜索文件内容
配置文字
调用参数
| 参数 | 要求 | 作用 |
pattern | 必填,字符串 | 搜索词或正则表达式。 |
path | 可选,字符串 | 文件或目录;默认当前工作目录。 |
glob | 可选,字符串 | 只搜索匹配的文件,例如 *.ts。 |
ignoreCase | 可选,布尔值 | 是否忽略大小写;默认区分。 |
literal | 可选,布尔值 | 是否按原样匹配;默认按正则表达式处理。 |
context | 可选,数字 | 在匹配行前后附带多少行;默认 0。 |
limit | 可选,数字 | 最多返回多少处匹配;默认 100。 |
本地实现
Pi 用 ripgrep 搜索,再按参数补充匹配行前后的内容。limit 由工具计数控制;单行太长时会截短。需要时由工具管理器取得 ripgrep 可执行文件。
ls:列出目录
配置文字
调用参数
| 参数 | 要求 | 作用 |
path | 可选,字符串 | 目标目录;默认当前工作目录。 |
limit | 可选,数字 | 最多返回多少项;默认 500。 |
本地实现
ls 使用 Node 的目录接口读取条目,再查询文件属性并排序。它不调用操作系统的 ls 命令。
04 文件修改
edit 修改文件中的指定片段,write 写入整份文件。两者的差别在于写入前是否需要匹配原文。
edit:精确替换
配置文字
调用参数
| 参数 | 要求 | 作用 |
path | 必填,字符串 | 要修改的文件。 |
edits | 必填,数组 | 一组替换操作;每项包含 oldText 和 newText。 |
oldText | 每项必填 | 原文件中唯一匹配的文本。 |
newText | 每项必填 | 替换后的文本;空字符串表示删除原文。 |
本地实现
Pi 先读取原文件,检查每段 oldText 是否唯一、修改区域是否重叠。全部通过后才写入,并返回修改前后的差异。参数预处理还能把旧版调用格式整理为 edits 数组。同一进程中针对同一文件的修改会排队执行;这不限制其他进程改动该文件。
write:写入完整文件
配置文字
调用参数
| 参数 | 要求 | 作用 |
path | 必填,字符串 | 目标文件路径。 |
content | 必填,字符串 | 要写入的完整内容;空字符串会清空文件。 |
本地实现
write 用 Node 的文件接口创建父目录,再写入全部内容。它不会像 edit 一样匹配原文,因此会覆盖已有文件。
05 执行命令
bash
bash 在当前工作目录运行一条 shell 命令,测试、构建和脚本执行都可以使用它。PowerShell 使用同一套工具实现,但会换成对应的名称与命令入口。
配置文字
调用参数
| 参数 | 要求 | 作用 |
command | 必填,字符串 | 要执行的 shell 命令。 |
timeout | 可选,正数 | 超时秒数;不填则没有工具层的超时限制。 |
本地实现
Pi 创建 shell 子进程执行命令,并在命令运行时持续回传输出。结束后保留输出末尾;如果内容过长,完整输出写入临时文件,结果中附上路径。结果还包含退出码、是否取消和是否截断。超时、取消或非零退出码都会作为失败结果返回。
取消会停止正在运行的命令,但命令已经造成的文件或网络变化不会自动撤销。
06 输出限制
read、grep、find 和 ls 保留结果开头,方便先看命中内容;bash 保留末尾,方便查看命令结束时的结果或报错。文本结果受行数或字节数上限约束,先达到哪个上限就按哪个截断。
read 和 bash 的文本上限为 2000 行或 50KB。检索工具还有条数上限:grep 100 条、find 1000 条、ls 500 条。被截断的结果会说明原因;read 提供续读行号,bash 提供完整输出的临时文件路径。