M10 · SOURCE-GROUNDED TUTORIAL
Pi从源码学会它怎么工作 优秀的可嵌入 Agent Harness 内核与 SDK;默认安全、MCP 和内建多 Agent 控制面刻意保持轻量。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。
TypeScript · Embeddable Agent Kernel MIT c820aa26fe09 25 个结论 · 76 处引用
这门课怎么读
先建立直觉,再沿一条任务链钻进代码 参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Pi 的 25 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。
你会得到 一张可复述的架构地图 一次完整任务的链路追踪 能迁移到自研 Harness 的设计判断
你不会得到 把 README 功能当成已验证事实 把 prompt 约束说成 OS 沙箱 把一次双模型调用夸成多 Agent 平台
M00 · MAP
先看全景:这个 Agent 的控制面在哪里 下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。
核心机制 低层 steering/tool/follow-up;上层 AgentHarness turn boundary
上下文 输出预留;不切 tool result;overflow 只自动重试一次
适用建设 作为自研 Agent SDK 底座、需要极强可编程性
M00.5 · TRACE
跟踪一个任务:从输入到交付 把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。
01 提交 prompt 与 runtime config 任务进入
↓
02 turn boundary 运行 hooks 把结果交给下一层
↓
03 构造 tool/context/system 把结果交给下一层
↓
04 多协议 provider 流式采样 把结果交给下一层
↓
05 截断响应禁止工具 把结果交给下一层
↓
06 shared/sequential 调度 把结果交给下一层
↓
07 精确 Edit 与双向截断 把结果交给下一层
↓
08 overflow 压缩后重试一次 把结果交给下一层
↓
09 append-only 事件交付 交付/续跑
读图提醒 箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。
M01 · ORIENTATION
先把 Agent 看成一台会交付的机器 如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。
先用一个生活比喻 把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 1 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M02 · LOOP
主循环:模型为什么会继续动 一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?
先用一个生活比喻 像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。
这套实现先回答了什么? 一边是可嵌入任何产品的发动机,一边是已经带 CLI、会话、扩展和交互界面的整车;当前两套代码有重叠,不能把发动机的新接口直接当成整车每条路径都已采用。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 仓库是“通用 Harness 内核 + 完整 Coding Agent 产品层”的双轨架构 packages/agent/src/harness/agent-harness.ts:171一边是可嵌入任何产品的发动机,一边是已经带 CLI、会话、扩展和交互界面的整车;当前两套代码有重叠,不能把发动机的新接口直接当成整车每条路径都已采用。 低层循环把 steering、工具执行和 follow-up 分成内外两层 packages/agent/src/agent-loop.ts:155用户中途插话会先纠偏当前工作,排队的新任务则等当前回合稳定后再接着做。 截断响应禁止执行工具;每回合可热刷新完整运行状态 packages/agent/src/agent-loop.ts:208模型半句话里拼出的命令不会贸然执行;同时下一轮可以换模型、换工具或换说明书。 新 AgentHarness 把请求 hooks、消息持久化和运行时变更统一到 turn boundary packages/agent/src/harness/agent-harness.ts:354每轮开始先拍一张配置快照,模型请求前后都能挂钩;消息落盘和配置变化在明确边界完成,减少并发写乱序。
01 L1 · fact · pi-architecture-001
仓库是“通用 Harness 内核 + 完整 Coding Agent 产品层”的双轨架构
先看源码事实 packages/agent 的 AgentHarness 直接拥有 session、model、tools、resources、queue 和 hooks;packages/coding-agent 的 AgentSession 仍有自己的 runtime build、resource loader、tool registry、retry 与 compaction 适配。
翻译成白话 一边是可嵌入任何产品的发动机,一边是已经带 CLI、会话、扩展和交互界面的整车;当前两套代码有重叠,不能把发动机的新接口直接当成整车每条路径都已采用。
为什么这对自研重要 架构抽象领先,但迁移期会产生两个 session/compaction/tool 生命周期,需要明确长期收敛边界。
固定提交源码摘录 复制
171 export class AgentHarness<
172 TContext extends object | undefined = undefined,
173 TSkill extends Skill = Skill,
174 TPromptTemplate extends PromptTemplate = PromptTemplate,
175 TTool extends AgentHarnessTool<TContext> = AgentHarnessTool<TContext>,
176 > {
177 private session: Session;
178 readonly models: Models;
179 private phase: AgentHarnessPhase = "idle";
180 private runAbortController?: AbortController;
181 private runPromise?: Promise<void>;
182 private pendingSessionWrites: PendingSessionWrite[] = [];
183 private model: Model<any>;
184 private thinkingLevel: ThinkingLevel;
185 private systemPrompt: AgentHarnessSystemPrompt<TContext, TSkill, TPromptTemplate, TTool> | undefined;
186 private toolContext: AgentHarnessToolContextSource<TContext> | undefined;
187 private streamOptions: AgentHarnessStreamOptions;
188 private retry: RetryPolicy | undefined;
189 private resources: AgentHarnessResources<TSkill, TPromptTemplate>;
190 private tools = new Map<string, TTool>();
191 private activeToolNames: string[];
192 private steerQueue: UserMessage[] = [];
193 private steeringQueueMode: QueueMode;
194 private followUpQueue: UserMessage[] = [];
195 private followUpQueueMode: QueueMode;
… 17 lines omitted; exact range 171–223 …
213 }
214 this.model = options.model;
215 this.thinkingLevel = options.thinkingLevel ?? "off";
216 this.activeToolNames = options.activeToolNames
217 ? [...options.activeToolNames]
218 : (options.tools ?? []).map((tool) => tool.name);
219 this.validateUniqueNames(this.activeToolNames, "Duplicate active tool name(s)");
220 this.validateToolNames(this.activeToolNames);
221 this.steeringQueueMode = options.steeringMode ?? "one-at-a-time";
222 this.followUpQueueMode = options.followUpMode ?? "one-at-a-time";
223 }
为什么相信这条结论?查看 3 处证据
02 L1 · fact · pi-loop-001
低层循环把 steering、工具执行和 follow-up 分成内外两层
先看源码事实 agentLoop 外层消费 follow-up,内层在 assistant tool call 或 steering 消息存在时继续;每轮发出 agent/turn/message 事件并把工具结果加入上下文。
翻译成白话 用户中途插话会先纠偏当前工作,排队的新任务则等当前回合稳定后再接着做。
为什么这对自研重要 交互打断与自治续跑共享同一 loop,而不是 UI 另开旁路。
固定提交源码摘录 复制
155 async function runLoop(
156 initialContext: AgentContext,
157 newMessages: AgentMessage[],
158 initialConfig: AgentLoopConfig,
159 signal: AbortSignal | undefined,
160 emit: AgentEventSink,
161 streamFunction: StreamFn,
162 ): Promise<void> {
163 let currentContext = initialContext;
164 let config = initialConfig;
165 let firstTurn = true;
166 // Check for steering messages at start (user may have typed while waiting)
167 let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];
168
169 // Outer loop: continues when queued follow-up messages arrive after agent would stop
170 while (true) {
171 let hasMoreToolCalls = true;
172
173 // Inner loop: process tool calls and steering messages
174 while (hasMoreToolCalls || pendingMessages.length > 0) {
175 if (!firstTurn) {
176 await emit({ type: "turn_start" });
177 } else {
178 firstTurn = false;
179 }
… 85 lines omitted; exact range 155–275 …
265 // Set as pending so inner loop processes them
266 pendingMessages = followUpMessages;
267 continue;
268 }
269
270 // No more messages, exit
271 break;
272 }
273
274 await emit({ type: "agent_end", messages: newMessages });
275 }
为什么相信这条结论?查看 2 处证据
03 L1 · fact · pi-loop-002
截断响应禁止执行工具;每回合可热刷新完整运行状态
先看源码事实 assistant stopReason 为 length 时,即使带 tool call 也不执行,因为参数可能被截断;prepareNextTurn 可替换 context、model、thinking、system prompt 和 tools。
翻译成白话 模型半句话里拼出的命令不会贸然执行;同时下一轮可以换模型、换工具或换说明书。
为什么这对自研重要 工具安全包含协议完整性检查,动态配置不必重启会话。
固定提交源码摘录 复制
208 // A "length" stop means the output was cut off by the token limit, so
209 // every tool call in the message may carry truncated arguments. Fail
210 // them all instead of executing potentially borked calls.
211 const executedToolBatch =
212 message.stopReason === "length"
213 ? await failToolCallsFromTruncatedMessage(toolCalls, emit)
214 : await executeToolCalls(currentContext, message, config, signal, emit);
215 toolResults.push(...executedToolBatch.messages);
216 hasMoreToolCalls = !executedToolBatch.terminate;
217
218 for (const result of toolResults) {
219 currentContext.messages.push(result);
220 newMessages.push(result);
221 }
222 }
223
224 await emit({ type: "turn_end", message, toolResults });
225
226 const nextTurnContext = {
227 message,
228 toolResults,
229 context: currentContext,
230 newMessages,
231 };
232 const nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);
… 2 lines omitted; exact range 208–245 …
235 config = {
236 ...config,
237 model: nextTurnSnapshot.model ?? config.model,
238 reasoning:
239 nextTurnSnapshot.thinkingLevel === undefined
240 ? config.reasoning
241 : nextTurnSnapshot.thinkingLevel === "off"
242 ? undefined
243 : nextTurnSnapshot.thinkingLevel,
244 };
245 }
为什么相信这条结论?查看 2 处证据
04 L1 · fact · pi-harness-001
新 AgentHarness 把请求 hooks、消息持久化和运行时变更统一到 turn boundary
先看源码事实 Harness 在每轮创建 state snapshot,通过 beforeProviderRequest/beforeProviderPayload/afterProviderResponse 包装模型请求;message_end 串行写 session,turn 结束后刷新状态并消费 steer/follow-up;model/tool/resource 变更也持久化并发事件。
翻译成白话 每轮开始先拍一张配置快照,模型请求前后都能挂钩;消息落盘和配置变化在明确边界完成,减少并发写乱序。
为什么这对自研重要 适合作为自研 Harness 的可复用内核参考,尤其是 transport、storage 和 execution capability 的解耦。
固定提交源码摘录 复制
354 private async createTurnState(): Promise<AgentHarnessTurnState<TContext, TSkill, TPromptTemplate, TTool>> {
355 const context = await this.session.buildContext();
356 const resources = this.getResources();
357 const sessionMetadata = await this.session.getMetadata();
358 const toolContext = await this.resolveToolContext();
359 const tools = [...this.tools.values()];
360 const activeTools = this.activeToolNames
361 .map((name) => this.tools.get(name))
362 .filter((tool): tool is TTool => tool !== undefined);
363 let systemPrompt = "You are a helpful assistant.";
364 if (typeof this.systemPrompt === "string") {
365 systemPrompt = this.systemPrompt;
366 } else if (this.systemPrompt) {
367 systemPrompt = await this.systemPrompt({
368 session: this.session,
369 model: this.model,
370 thinkingLevel: this.thinkingLevel,
371 activeTools,
372 resources,
373 });
374 }
375 return {
376 messages: context.messages,
377 resources,
378 toolContext,
… 108 lines omitted; exact range 354–497 …
487 const nextTurnState = await this.createTurnState();
488 setTurnState(nextTurnState);
489 return {
490 context: this.createContext(nextTurnState),
491 model: nextTurnState.model,
492 thinkingLevel: nextTurnState.thinkingLevel,
493 };
494 },
495 getSteeringMessages: async () => this.drainQueuedMessages(this.steerQueue, this.steeringQueueMode),
496 getFollowUpMessages: async () => this.drainQueuedMessages(this.followUpQueue, this.followUpQueueMode),
497 };
为什么相信这条结论?查看 3 处证据
小练习 2 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M03 · MODEL
模型调用:流式输出如何变成可执行步骤 模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?
先用一个生活比喻 模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。
这套实现先回答了什么? 每家模型的方言由独立翻译器处理,而不是假设所有服务都说 OpenAI 方言。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 Provider 不是单一 OpenAI 兼容层,而是多协议适配矩阵 packages/ai/src/providers/all.ts:5每家模型的方言由独立翻译器处理,而不是假设所有服务都说 OpenAI 方言。 Provider 可热注册和覆盖,失败时退回内建组合 packages/coding-agent/src/core/model-runtime.ts:193扩展可以接入私有模型甚至替换流协议;某个扩展写坏时,内置模型仍尽量可用。 传输重试与会话重试分层,上下文溢出单独处理 packages/ai/src/utils/provider-retry.ts:22HTTP 临时故障在网络层重试;整轮失败在会话层重试;没钱和记忆塞满都不会被误当成网络抖动。
05 L1 · fact · pi-provider-001
Provider 不是单一 OpenAI 兼容层,而是多协议适配矩阵
先看源码事实 内建 provider registry 装配 Anthropic、OpenAI/Codex/Azure、Google/Vertex、Bedrock、Mistral、Kimi/Moonshot、OpenRouter、GitHub Copilot、xAI 等数十个 provider;API 目录分别实现 messages、responses、chat completions、Gemini、Bedrock 和 pi-messages 协议。
翻译成白话 每家模型的方言由独立翻译器处理,而不是假设所有服务都说 OpenAI 方言。
为什么这对自研重要 覆盖面很广,也意味着协议行为和 usage/tool schema 回归成本高。
固定提交源码摘录 复制
5 import { amazonBedrockProvider } from "./amazon-bedrock.ts";
6 import { antLingProvider } from "./ant-ling.ts";
7 import { anthropicProvider } from "./anthropic.ts";
8 import { azureOpenAIResponsesProvider } from "./azure-openai-responses.ts";
9 import { cerebrasProvider } from "./cerebras.ts";
10 import { cloudflareAIGatewayProvider } from "./cloudflare-ai-gateway.ts";
11 import { cloudflareWorkersAIProvider } from "./cloudflare-workers-ai.ts";
12 import modelDataManifest from "./data/.manifest.json" with { type: "json" };
13 import { deepseekProvider } from "./deepseek.ts";
14 import { fireworksProvider } from "./fireworks.ts";
15 import { githubCopilotProvider } from "./github-copilot.ts";
16 import { googleProvider } from "./google.ts";
17 import { googleVertexProvider } from "./google-vertex.ts";
18 import { groqProvider } from "./groq.ts";
19 import { huggingfaceProvider } from "./huggingface.ts";
20 import { kimiCodingProvider } from "./kimi-coding.ts";
21 import { minimaxProvider } from "./minimax.ts";
22 import { minimaxCnProvider } from "./minimax-cn.ts";
23 import { mistralProvider } from "./mistral.ts";
24 import { moonshotaiProvider } from "./moonshotai.ts";
25 import { moonshotaiCnProvider } from "./moonshotai-cn.ts";
26 import { nvidiaProvider } from "./nvidia.ts";
27 import { openaiProvider } from "./openai.ts";
28 import { openaiCodexProvider } from "./openai-codex.ts";
29 import { opencodeProvider } from "./opencode.ts";
… 4 lines omitted; exact range 5–44 …
34 import { qwenTokenPlanCnProvider } from "./qwen-token-plan-cn.ts";
35 import { radiusProvider } from "./radius.ts";
36 import { togetherProvider } from "./together.ts";
37 import { vercelAIGatewayProvider } from "./vercel-ai-gateway.ts";
38 import { xaiProvider } from "./xai.ts";
39 import { xiaomiProvider } from "./xiaomi.ts";
40 import { xiaomiTokenPlanAmsProvider } from "./xiaomi-token-plan-ams.ts";
41 import { xiaomiTokenPlanCnProvider } from "./xiaomi-token-plan-cn.ts";
42 import { xiaomiTokenPlanSgpProvider } from "./xiaomi-token-plan-sgp.ts";
43 import { zaiProvider } from "./zai.ts";
44 import { zaiCodingCnProvider } from "./zai-coding-cn.ts";
为什么相信这条结论?查看 3 处证据
06 L1 · fact · pi-provider-002
Provider 可热注册和覆盖,失败时退回内建组合
先看源码事实 ModelRuntime 合并内建、文件配置、原生扩展配置和 extension provider;扩展可注册模型、OAuth 与自定义 stream。组合错误会保留诊断并回退 builtin,而不是让整个模型目录不可用。
翻译成白话 扩展可以接入私有模型甚至替换流协议;某个扩展写坏时,内置模型仍尽量可用。
为什么这对自研重要 插件化 provider 适合企业网关,但必须把最终解析来源和错误显式展示。
固定提交源码摘录 复制
193 private providerIds(): Set<string> {
194 return new Set([
195 ...this.builtins.keys(),
196 ...this.nativeExtensionProviders.keys(),
197 ...this.config.getProviderIds(),
198 ...this.extensionProviders.keys(),
199 ]);
200 }
201
202 private recomposeProvider(providerId: string): void {
203 const base = this.nativeExtensionProviders.get(providerId) ?? this.builtins.get(providerId);
204 const extension = this.extensionProviders.get(providerId);
205 if (!base && !this.config.getProvider(providerId) && !extension) {
206 this.models.deleteProvider(providerId);
207 this.compositionErrors.delete(providerId);
208 return;
209 }
210 if (base && !this.config.getProvider(providerId) && !extension) {
211 // No overlays: use the builtin untouched so its auth/login/stream behavior is exact.
212 this.models.setProvider(base);
213 this.compositionErrors.delete(providerId);
214 return;
215 }
216 try {
217 this.models.setProvider(composeModelProvider(providerId, base, this.config, extension));
… 2 lines omitted; exact range 193–230 …
220 this.compositionErrors.set(providerId, error instanceof Error ? error.message : String(error));
221 if (base) this.models.setProvider(base);
222 else this.models.deleteProvider(providerId);
223 }
224 }
225
226 private rebuildProviders(): void {
227 this.models.clearProviders();
228 this.compositionErrors.clear();
229 for (const providerId of this.providerIds()) this.recomposeProvider(providerId);
230 this.updateModelSnapshot();
为什么相信这条结论?查看 2 处证据
07 L1 · fact · pi-retry-001
传输重试与会话重试分层,上下文溢出单独处理
先看源码事实 AI provider retry 解析 status、retry-after 和 headers,使用带 jitter 的可中止等待且单次上限 60 秒;AgentSession 把 transient error 与 quota/billing/overflow 分开,overflow 不进入普通指数退避。
翻译成白话 HTTP 临时故障在网络层重试;整轮失败在会话层重试;没钱和记忆塞满都不会被误当成网络抖动。
为什么这对自研重要 避免重复重试不可恢复错误,也让 compaction 接管真正的上下文问题。
固定提交源码摘录 复制
22 /** Mirrors the pinned OpenAI/Anthropic SDK retry policy; review when either SDK is upgraded. */
23 function isRetryableProviderError(error: ProviderError): boolean {
24 const shouldRetry = error.headers?.get("x-should-retry");
25 if (shouldRetry === "true") return true;
26 if (shouldRetry === "false") return false;
27
28 if (error.status === undefined) return true;
29 return (
30 error.status === 408 ||
31 error.status === 409 ||
32 error.status === 429 ||
33 (typeof error.status === "number" && error.status >= 500)
34 );
35 }
36
37 function validateServerRetryDelayMs(
38 delayMs: number,
39 maxRetryDelayMs: number | undefined,
40 providerErrorMessage: string,
41 ): number {
42 const maxDelayMs = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
43 if (maxDelayMs > 0 && delayMs > maxDelayMs) {
44 throw new Error(
45 `Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxDelayMs / 1000)}s). ${providerErrorMessage}`,
46 );
… 9 lines omitted; exact range 22–66 …
56 }
57
58 const retryAfter = error.headers?.get("retry-after");
59 if (retryAfter) {
60 const seconds = Number.parseFloat(retryAfter);
61 const delayMs = Number.isNaN(seconds) ? Date.parse(retryAfter) - Date.now() : seconds * 1000;
62 return validateServerRetryDelayMs(delayMs, maxRetryDelayMs, error.message);
63 }
64
65 const exponentialDelay = Math.min(0.5 * 2 ** retryIndex, 8) * 1000;
66 return exponentialDelay * (1 - Math.random() * 0.25);
为什么相信这条结论?查看 3 处证据
小练习 3 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M05 · CONTEXT
上下文:有限窗口怎样装下长任务 当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?
先用一个生活比喻 上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。
这套实现先回答了什么? 不会等输入把窗口完全塞满;先给回答留座位。能拿到模型账单就用真实 token,拿不到才用字符粗算。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 压缩阈值给输出预留固定预算,并尽量使用真实 usage packages/coding-agent/src/core/compaction/compaction.ts:190不会等输入把窗口完全塞满;先给回答留座位。能拿到模型账单就用真实 token,拿不到才用字符粗算。 cut point 不切 tool result,并支持拆分超长 turn packages/coding-agent/src/core/compaction/compaction.ts:345工具调用和结果不会被拦腰拆散;最近一个超长回合也不必全丢或全留,前半段概括、后半段保真。 overflow 最多压缩后自动重试一次,扩展可取消或替换摘要 packages/coding-agent/src/core/agent-session.ts:1783记忆爆仓会整理再试一次,不会反复总结到死;插件也能接管公司自己的摘要策略。
11 L1 · fact · pi-context-001
压缩阈值给输出预留固定预算,并尽量使用真实 usage
先看源码事实 默认 reserveTokens 为 16384,contextTokens 超过 contextWindow-reserve 才压缩。估算优先取最后 assistant usage 加尾随消息,缺失时用 chars/4,图片按近似字符成本处理。
翻译成白话 不会等输入把窗口完全塞满;先给回答留座位。能拿到模型账单就用真实 token,拿不到才用字符粗算。
为什么这对自研重要 比单纯按消息数量可靠,但固定 16k 对不同模型/任务可调性很重要。
固定提交源码摘录 复制
190 function getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {
191 for (let i = messages.length - 1; i >= 0; i--) {
192 const usage = getAssistantUsage(messages[i]);
193 if (usage) return { usage, index: i };
194 }
195 return undefined;
196 }
197
198 /**
199 * Estimate context tokens from messages, using the last assistant usage when available.
200 * If there are messages after the last usage, estimate their tokens with estimateTokens.
201 */
202 export function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {
203 const usageInfo = getLastAssistantUsageInfo(messages);
204
205 if (!usageInfo) {
206 let estimated = 0;
207 for (const message of messages) {
208 estimated += estimateTokens(message);
209 }
210 return {
211 tokens: estimated,
212 usageTokens: 0,
213 trailingTokens: estimated,
214 lastUsageIndex: null,
… 12 lines omitted; exact range 190–237 …
227 trailingTokens,
228 lastUsageIndex: usageInfo.index,
229 };
230 }
231
232 /**
233 * Check if compaction should trigger based on context usage.
234 */
235 export function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {
236 if (!settings.enabled) return false;
237 return contextTokens > contextWindow - settings.reserveTokens;
为什么相信这条结论?查看 2 处证据
12 L1 · fact · pi-context-002
cut point 不切 tool result,并支持拆分超长 turn
先看源码事实 算法从后往前保留 recentTokens,切点不会落在 tool result;若单回合太长可把 turn prefix 单独摘要、保留后段。summary 使用 Goal/Constraints/Progress/Decisions/Next Steps/Critical Context 结构并可迭代更新旧摘要。
翻译成白话 工具调用和结果不会被拦腰拆散;最近一个超长回合也不必全丢或全留,前半段概括、后半段保真。
为什么这对自研重要 上下文压缩保留执行连续性,结构化摘要便于后续 Agent 接棒。
固定提交源码摘录 复制
345 /**
346 * Find valid cut points: indices of context-visible user-like or assistant messages.
347 * Never cut at tool results (they must follow their tool call).
348 * When we cut at an assistant message with tool calls, its tool results follow it
349 * and will be kept.
350 */
351 function findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {
352 const cutPoints: number[] = [];
353 for (let i = startIndex; i < endIndex; i++) {
354 const entry = entries[i];
355 if (entry.type === "compaction") {
356 continue;
357 }
358 if (sessionEntryToContextMessages(entry).some(isCutPointMessage)) {
359 cutPoints.push(i);
360 }
361 }
362 return cutPoints;
363 }
364
365 /**
366 * Find the context-visible user-role message that starts the turn containing the given entry index.
367 * Returns -1 if no turn start found before the index.
368 */
369 export function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {
… 80 lines omitted; exact range 345–460 …
450
451 // Determine if this is a split turn
452 const cutEntry = entries[cutIndex];
453 const startsTurn = isTurnStartEntry(cutEntry);
454 const turnStartIndex = startsTurn ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
455
456 return {
457 firstKeptEntryIndex: cutIndex,
458 turnStartIndex,
459 isSplitTurn: !startsTurn && turnStartIndex !== -1,
460 };
为什么相信这条结论?查看 3 处证据
13 L1 · fact · pi-context-003
overflow 最多压缩后自动重试一次,扩展可取消或替换摘要
先看源码事实 manual/auto compaction 都先触发 extension hook;扩展可取消或提供完整 compaction。overflow 路径移除错误消息后只重试一次,阈值型压缩不会无条件续跑,只有队列还有消息才继续。
翻译成白话 记忆爆仓会整理再试一次,不会反复总结到死;插件也能接管公司自己的摘要策略。
为什么这对自研重要 有明确的恢复上限和可插拔压缩策略。
固定提交源码摘录 复制
1783 async compact(customInstructions?: string): Promise<CompactionResult> {
1784 this._disconnectFromAgent();
1785 await this.abort();
1786 this._compactionAbortController = new AbortController();
1787 this._emit({ type: "compaction_start", reason: "manual" });
1788
1789 try {
1790 if (!this.model) {
1791 throw new Error(formatNoModelSelectedMessage());
1792 }
1793
1794 const { apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
1795
1796 const pathEntries = this.sessionManager.getBranch();
1797 const settings = this.settingsManager.getCompactionSettings();
1798
1799 const preparation = prepareCompaction(pathEntries, settings);
1800 if (!preparation) {
1801 // Check why we can't compact
1802 const lastEntry = pathEntries[pathEntries.length - 1];
1803 if (lastEntry?.type === "compaction") {
1804 throw new Error("Already compacted");
1805 }
1806 throw new Error("Nothing to compact (session too small)");
1807 }
… 106 lines omitted; exact range 1783–1924 …
1914 reason: "manual",
1915 result: undefined,
1916 aborted,
1917 willRetry: false,
1918 errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
1919 });
1920 throw error;
1921 } finally {
1922 this._compactionAbortController = undefined;
1923 this._reconnectToAgent();
1924 }
为什么相信这条结论?查看 3 处证据
小练习 5 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M06 · SECURITY
权限与沙箱:能做什么,在哪里做 审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?
先用一个生活比喻 审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。
这套实现先回答了什么? 接口上可以换成远端 VM 或容器,但开箱即用的那套仍是在你的真实电脑里读写和跑命令。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 默认 CLI 在宿主 shell/filesystem 执行,不是沙箱 packages/coding-agent/src/core/tools/bash.ts:82接口上可以换成远端 VM 或容器,但开箱即用的那套仍是在你的真实电脑里读写和跑命令。 Project Trust 保护仓库可执行资源,但不审批每条命令 packages/coding-agent/src/core/project-trust.ts:24它问的是“要不要加载这个仓库自带的插件和说明书”,不是“接下来这条 rm 命令能不能执行”。 本地凭据文件使用权限收紧和跨进程锁 packages/coding-agent/src/core/auth-storage.ts:21密钥本不仅不让其他本机用户随便看,两个 Pi 进程同时刷新 token 时也不会互相踩掉。 Sandbox 与命令审批是可选示例,不是默认安全基线 packages/coding-agent/examples/extensions/sandbox/index.ts:1仓库教你如何装保险箱和门卫,但新车出厂默认没有把它们启用。
14 L1 · limitation · pi-execution-001
默认 CLI 在宿主 shell/filesystem 执行,不是沙箱
先看源码事实 bash 默认用 cwd、继承宿主 env、spawn 用户 shell、无默认 timeout;read/edit/write 支持绝对路径和 ~,没有 cwd containment。通用 Harness 虽抽象 FileSystem/Shell/ExecutionEnv,默认 NodeExecutionEnv 仍直连宿主。
翻译成白话 接口上可以换成远端 VM 或容器,但开箱即用的那套仍是在你的真实电脑里读写和跑命令。
为什么这对自研重要 企业无人值守场景必须提供另一套 ExecutionEnv,不能把接口抽象等同于安全隔离。
固定提交源码摘录 复制
82 export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {
83 return {
84 exec: async (command, cwd, { onData, signal, timeout, env }) => {
85 const timeoutMs = resolveTimeoutMs(timeout);
86 if (signal?.aborted) {
87 throw new Error("aborted");
88 }
89 const shellConfig = getShellConfig(options?.shellPath);
90 try {
91 await fsAccess(cwd, constants.F_OK);
92 } catch {
93 throw new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`);
94 }
95
96 const commandFromStdin = shellConfig.commandTransport === "stdin";
97 const child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], {
98 cwd,
99 detached: process.platform !== "win32",
100 env: env ?? getShellEnv(),
101 stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"],
102 windowsHide: true,
103 });
104 if (commandFromStdin) {
105 child.stdin?.on("error", () => {});
106 child.stdin?.end(command);
… 31 lines omitted; exact range 82–148 …
138 throw new Error(`timeout:${timeout}`);
139 }
140 return { exitCode };
141 } finally {
142 if (child.pid) untrackDetachedChildPid(child.pid);
143 if (timeoutHandle) clearTimeout(timeoutHandle);
144 if (signal) signal.removeEventListener("abort", onAbort);
145 }
146 },
147 };
148 }
为什么相信这条结论?查看 4 处证据
15 L1 · fact · pi-trust-001
Project Trust 保护仓库可执行资源,但不审批每条命令
先看源码事实 检测到项目 settings、extensions、skills、prompts、themes、SYSTEM/APPEND 等资源才询问 trust;headless 默认不信任。未信任时清空项目 settings 并禁止项目资源装载/写入,但 built-in bash/edit 仍没有逐命令 approval。
翻译成白话 它问的是“要不要加载这个仓库自带的插件和说明书”,不是“接下来这条 rm 命令能不能执行”。
为什么这对自研重要 resource trust 和 action authorization 是两道不同的门,报告与产品 UI 都必须分开。
固定提交源码摘录 复制
24 function formatProjectTrustPrompt(cwd: string): string {
25 return `Trust project folder?\n${cwd}\n\nThis allows pi to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
26 }
27
28 async function selectProjectTrustOption(
29 cwd: string,
30 ctx: ProjectTrustContext,
31 ): Promise<ProjectTrustOption | undefined> {
32 const options = getProjectTrustOptions(cwd, { includeSessionOnly: true });
33 const selected = await ctx.ui.select(
34 formatProjectTrustPrompt(cwd),
35 options.map((option) => option.label),
36 );
37 return options.find((option) => option.label === selected);
38 }
39
40 function saveProjectTrustPromptResult(trustStore: ProjectTrustStore, result: ProjectTrustOption): void {
41 if (result.updates.length > 0) {
42 trustStore.setMany(result.updates);
43 }
44 }
45
46 export async function resolveProjectTrusted(options: ResolveProjectTrustedOptions): Promise<boolean> {
47 if (options.trustOverride !== undefined) {
48 return options.trustOverride;
… 36 lines omitted; exact range 24–95 …
85
86 if (!options.projectTrustContext.hasUI) {
87 return false;
88 }
89
90 const selected = await selectProjectTrustOption(options.cwd, options.projectTrustContext);
91 if (selected !== undefined) {
92 saveProjectTrustPromptResult(options.trustStore, selected);
93 return selected.trusted;
94 }
95 return false;
为什么相信这条结论?查看 4 处证据
16 L1 · fact · pi-auth-001
本地凭据文件使用权限收紧和跨进程锁
先看源码事实 auth storage 创建父目录为 0700、文件为 0600,使用 proper-lockfile 串行更新并在锁内重新读取,避免多个进程覆盖外部修改;API key 也可从环境或命令动态解析。
翻译成白话 密钥本不仅不让其他本机用户随便看,两个 Pi 进程同时刷新 token 时也不会互相踩掉。
为什么这对自研重要 本地 secret hygiene 扎实,但动态命令取 key 本身仍属于受信任配置。
固定提交源码摘录 复制
21 const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf-8", mode: 0o600 } as const;
22
23 export interface AuthStorageBackend {
24 withLock<T>(fn: (current: string | undefined) => LockResult<T>): T;
25 withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;
26 }
27
28 export class FileAuthStorageBackend implements AuthStorageBackend {
29 private authPath: string;
30
31 constructor(authPath: string = join(getAgentDir(), "auth.json")) {
32 this.authPath = normalizePath(authPath);
33 }
34
35 private ensureParentDir(): void {
36 const dir = dirname(this.authPath);
37 if (!existsSync(dir)) {
38 mkdirSync(dir, { recursive: true, mode: 0o700 });
39 }
40 }
41
42 private ensureFileExists(): void {
43 if (!existsSync(this.authPath)) {
44 writeFileSync(this.authPath, "{}", AUTH_FILE_WRITE_OPTIONS);
45 chmodSync(this.authPath, 0o600);
… 89 lines omitted; exact range 21–145 …
135 return result;
136 } finally {
137 if (release) {
138 try {
139 await release();
140 } catch {
141 // Ignore unlock errors when lock is compromised.
142 }
143 }
144 }
145 }
为什么相信这条结论?查看 3 处证据
17 L1 · limitation · pi-sandbox-example-001
Sandbox 与命令审批是可选示例,不是默认安全基线
先看源码事实 sandbox example 依赖 Anthropic sandbox-runtime,在 macOS sandbox-exec/Linux bubblewrap 中覆盖 bash/user_bash;permission-gate example 只用正则拦 rm -rf、sudo、chmod/chown 777 并弹 UI。两者位于 examples/extensions,必须安装/加载。
翻译成白话 仓库教你如何装保险箱和门卫,但新车出厂默认没有把它们启用。
为什么这对自研重要 能力表必须写“可扩展实现”,不能打成“默认支持沙箱/审批”。
固定提交源码摘录 复制
1 /**
2 * Sandbox Extension - OS-level sandboxing for bash commands
3 *
4 * Uses @anthropic-ai/sandbox-runtime to enforce filesystem and network
5 * restrictions on bash commands at the OS level (sandbox-exec on macOS,
6 * bubblewrap on Linux).
7 *
8 * Note: this example intentionally overrides the built-in `bash` tool to show
9 * how built-in tools can be replaced. Alternatively, you could sandbox `bash`
10 * via `tool_call` input mutation without replacing the tool.
11 *
12 * Config files (merged, project takes precedence):
13 * - ~/.pi/agent/extensions/sandbox.json (global)
14 * - <cwd>/.pi/sandbox.json (project-local)
15 *
16 * Example .pi/sandbox.json:
17 * ```json
18 * {
19 * "enabled": true,
20 * "network": {
21 * "allowedDomains": ["github.com", "*.github.com"],
22 * "deniedDomains": []
23 * },
24 * "filesystem": {
25 * "denyRead": ["~/.ssh", "~/.aws"],
… 11 lines omitted; exact range 1–47 …
37 * Setup:
38 * 1. Copy sandbox/ directory to ~/.pi/agent/extensions/
39 * 2. Run `npm install` in ~/.pi/agent/extensions/sandbox/
40 *
41 * Linux also requires: bubblewrap, socat, ripgrep
42 */
43
44 import { spawn } from "node:child_process";
45 import { existsSync, readFileSync } from "node:fs";
46 import { join } from "node:path";
47 import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
为什么相信这条结论?查看 3 处证据
小练习 6 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M07 · ECOSYSTEM
指令、MCP、Skills 与插件:能力如何接进来 一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?
先用一个生活比喻 这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。
这套实现先回答了什么? 模型看到的不是一段写死提示词,而是当前工具说明、用户规则、项目规则、技能目录和工作目录拼成的最终说明书。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 系统指令由 tool、context files、skills、custom/append prompt 多层拼装 packages/coding-agent/src/core/system-prompt.ts:28模型看到的不是一段写死提示词,而是当前工具说明、用户规则、项目规则、技能目录和工作目录拼成的最终说明书。 Skill 采用懒加载目录,项目范围受 trust 控制 packages/coding-agent/src/core/skills.ts:118先给模型一本技能目录,不把每本说明书都塞进上下文;它决定要用时再打开。仓库自带技能则要先信任仓库。 扩展在同一进程执行,覆盖面接近完整产品内核 packages/coding-agent/src/core/extensions/loader.ts:66插件不是只能加一个小工具,它几乎能摸到 Agent 每个关节;代价是插件代码和主进程同等权限。 固定提交未实现内建 MCP client/server packages/coding-agent/src/core/agent-session.ts:2454Pi 可以通过扩展接任意协议,但开箱没有像部分竞品那样配置 MCP server 后自动发现 tools/resources/prompts。
18 L1 · fact · pi-instructions-001
系统指令由 tool、context files、skills、custom/append prompt 多层拼装
先看源码事实 system prompt 动态包含活跃工具及其 guidelines、产品核心指令、全局和从 cwd 向上的 AGENTS.md/CLAUDE.md、可用 skills XML、cwd;custom system 可替换核心正文但仍追加项目 context/skills,append prompt 再叠加。
翻译成白话 模型看到的不是一段写死提示词,而是当前工具说明、用户规则、项目规则、技能目录和工作目录拼成的最终说明书。
为什么这对自研重要 必须提供最终展开 prompt 的诊断视图,否则层级覆盖很难排查。
固定提交源码摘录 复制
28 export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
29 const {
30 customPrompt,
31 selectedTools,
32 toolSnippets,
33 promptGuidelines,
34 appendSystemPrompt,
35 cwd,
36 contextFiles: providedContextFiles,
37 skills: providedSkills,
38 } = options;
39 const promptCwd = cwd.replace(/\\/g, "/");
40
41 const appendSection = appendSystemPrompt ? `\n\n${appendSystemPrompt}` : "";
42
43 const contextFiles = providedContextFiles ?? [];
44 const skills = providedSkills ?? [];
45
46 if (customPrompt) {
47 let prompt = customPrompt;
48
49 if (appendSection) {
50 prompt += appendSection;
51 }
52
… 8 lines omitted; exact range 28–71 …
61 }
62
63 // Append skills section (only if read tool is available)
64 const customPromptHasRead = !selectedTools || selectedTools.includes("read");
65 if (customPromptHasRead && skills.length > 0) {
66 prompt += formatSkillsForPrompt(skills);
67 }
68
69 prompt += `\nCurrent working directory: ${promptCwd}`;
70
71 return prompt;
为什么相信这条结论?查看 3 处证据
19 L1 · fact · pi-skills-001
Skill 采用懒加载目录,项目范围受 trust 控制
先看源码事实 Skill discovery 递归查找 SKILL.md,校验 lower-kebab name 和 description,处理 ignore/symlink/冲突;系统 prompt 只放 name/description/location XML,要求模型需要时再读全文。用户 skills 总是可用,项目 .pi/skills 与祖先 .agents/skills 仅在信任后加载。
翻译成白话 先给模型一本技能目录,不把每本说明书都塞进上下文;它决定要用时再打开。仓库自带技能则要先信任仓库。
为什么这对自研重要 节省 token,且把技能供应链纳入项目 trust。
固定提交源码摘录 复制
118 const errors: string[] = [];
119
120 if (!description || description.trim() === "") {
121 errors.push("description is required");
122 } else if (description.length > MAX_DESCRIPTION_LENGTH) {
123 errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
124 }
125
126 return errors;
127 }
128
129 export interface LoadSkillsFromDirOptions {
130 /** Directory to scan for skills */
131 dir: string;
132 /** Source identifier for these skills */
133 source: string;
134 }
135
136 function createSkillSourceInfo(filePath: string, baseDir: string, source: string): SourceInfo {
137 switch (source) {
138 case "user":
139 return createSyntheticSourceInfo(filePath, {
140 source: "local",
141 scope: "user",
142 baseDir,
… 35 lines omitted; exact range 118–188 …
178 rootDir?: string,
179 ): LoadSkillsResult {
180 const skills: Skill[] = [];
181 const diagnostics: ResourceDiagnostic[] = [];
182
183 if (!existsSync(dir)) {
184 return { skills, diagnostics };
185 }
186
187 const root = rootDir ?? dir;
188 const ig = ignoreMatcher ?? ignore();
为什么相信这条结论?查看 3 处证据
20 L1 · fact · pi-extension-001
扩展在同一进程执行,覆盖面接近完整产品内核
先看源码事实 loader 用 jiti 直接加载 TS/JS;extension API 可监听 trust/resource/session/compaction/context/provider/agent/turn/message/tool/input/user_bash 事件,并注册工具、命令、快捷键、flags、renderer 和 provider。多数 handler 错误被捕获并报告,before tool 失败会阻断执行。
翻译成白话 插件不是只能加一个小工具,它几乎能摸到 Agent 每个关节;代价是插件代码和主进程同等权限。
为什么这对自研重要 扩展生态极强,但必须把安装源和信任当作代码执行供应链治理。
固定提交源码摘录 复制
66 "@mariozechner/pi-tui": _bundledPiTui,
67 "@mariozechner/pi-ai": _bundledPiAiCompat,
68 "@mariozechner/pi-ai/compat": _bundledPiAiCompat,
69 "@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
70 "@mariozechner/pi-ai/providers/all": _bundledPiAiProviders,
71 "@mariozechner/pi-coding-agent": _bundledPiCodingAgent,
72 };
73
74 const require = createRequire(import.meta.url);
75
76 /**
77 * Get aliases for jiti (used in Node.js/development mode).
78 * In Bun binary mode, virtualModules is used instead.
79 */
80 let _aliases: Record<string, string> | null = null;
81
82 function getAliases(): Record<string, string> {
83 if (_aliases) return _aliases;
84
85 const __dirname = path.dirname(fileURLToPath(import.meta.url));
86 const packageIndex = path.resolve(__dirname, "../..", "index.js");
87
88 const typeboxEntry = require.resolve("typebox");
89 const typeboxCompileEntry = require.resolve("typebox/compile");
90 const typeboxValueEntry = require.resolve("typebox/value");
… 23 lines omitted; exact range 66–124 …
114 _aliases = {
115 "@earendil-works/pi-coding-agent": piCodingAgentEntry,
116 "@earendil-works/pi-agent-core": piAgentCoreEntry,
117 "@earendil-works/pi-tui": piTuiEntry,
118 "@earendil-works/pi-ai/providers/all": piAiProvidersEntry,
119 "@earendil-works/pi-ai/compat": piAiCompatEntry,
120 "@earendil-works/pi-ai/oauth": piAiOauthEntry,
121 "@earendil-works/pi-ai": piAiCompatEntry,
122 "@mariozechner/pi-coding-agent": piCodingAgentEntry,
123 "@mariozechner/pi-agent-core": piAgentCoreEntry,
124 "@mariozechner/pi-tui": piTuiEntry,
为什么相信这条结论?查看 4 处证据
21 L2 · limitation · pi-connectors-001
固定提交未实现内建 MCP client/server
先看源码事实 产品 runtime 枚举的 built-in tools 为 read/bash/edit/write,随后合并 SDK 和 extension tools;resource 与 extension contracts 中没有 MCP transport/session/tool discovery 实现。仓库 packages 源码检索未发现 MCP runtime contract。
翻译成白话 Pi 可以通过扩展接任意协议,但开箱没有像部分竞品那样配置 MCP server 后自动发现 tools/resources/prompts。
为什么这对自研重要 自研平台若依赖 MCP,需要额外的官方扩展或内核 connector 层。
固定提交源码摘录 复制
2454 private _refreshToolRegistry(options?: { activeToolNames?: string[]; includeAllExtensionTools?: boolean }): void {
2455 const previousRegistryNames = new Set(this._toolRegistry.keys());
2456 const previousActiveToolNames = this.getActiveToolNames();
2457 const allowedToolNames = this._allowedToolNames;
2458 const excludedToolNames = this._excludedToolNames;
2459 const isAllowedTool = (name: string): boolean =>
2460 (!allowedToolNames || allowedToolNames.has(name)) && !excludedToolNames?.has(name);
2461
2462 const registeredTools = this._extensionRunner.getAllRegisteredTools();
2463 const allCustomTools = [
2464 ...registeredTools,
2465 ...this._customTools.map((definition) => ({
2466 definition,
2467 sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
2468 })),
2469 ].filter((tool) => isAllowedTool(tool.definition.name));
2470 const definitionRegistry = new Map<string, ToolDefinitionEntry>(
2471 Array.from(this._baseToolDefinitions.entries())
2472 .filter(([name]) => isAllowedTool(name))
2473 .map(([name, definition]) => [
2474 name,
2475 {
2476 definition,
2477 sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
2478 },
… 55 lines omitted; exact range 2454–2544 …
2534 nextActiveToolNames.push(tool.name);
2535 }
2536 } else if (!options?.activeToolNames) {
2537 for (const toolName of this._toolRegistry.keys()) {
2538 if (!previousRegistryNames.has(toolName)) {
2539 nextActiveToolNames.push(toolName);
2540 }
2541 }
2542 }
2543
2544 this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
为什么相信这条结论?查看 3 处证据
小练习 7 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M08 · COLLABORATION
子 Agent:把一个大任务拆成可治理的协作 什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?
先用一个生活比喻 不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。
这套实现先回答了什么? 它展示了“主 Agent 叫几个临时 Pi 工人”的方法,但没有内核级 durable queue、共享记忆、远程 worker 或统一权限继承。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 子 Agent 是独立 pi 进程示例,不是内建调度控制平面 packages/coding-agent/examples/extensions/subagent/index.ts:1它展示了“主 Agent 叫几个临时 Pi 工人”的方法,但没有内核级 durable queue、共享记忆、远程 worker 或统一权限继承。
22 L1 · limitation · pi-subagent-001
子 Agent 是独立 pi 进程示例,不是内建调度控制平面
先看源码事实 subagent extension 支持 single/parallel/chain,最多 8 tasks、并发 4、每任务输出上限 50KB;每个任务 spawn `pi --mode json -p --no-session`,用 0600 临时 prompt,聚合 usage,取消时 TERM 后 KILL。Agent 定义来自 user/project Markdown。
翻译成白话 它展示了“主 Agent 叫几个临时 Pi 工人”的方法,但没有内核级 durable queue、共享记忆、远程 worker 或统一权限继承。
为什么这对自研重要 适合参考 subprocess orchestration,不应与成熟多 Agent 控制平面等同。
固定提交源码摘录 复制
1 /**
2 * Subagent Tool - Delegate tasks to specialized agents
3 *
4 * Spawns a separate `pi` process for each subagent invocation,
5 * giving it an isolated context window.
6 *
7 * Supports three modes:
8 * - Single: { agent: "name", task: "..." }
9 * - Parallel: { tasks: [{ agent: "name", task: "..." }, ...] }
10 * - Chain: { chain: [{ agent: "name", task: "... {previous} ..." }, ...] }
11 *
12 * Uses JSON mode to capture structured output from subagents.
13 */
14
15 import { spawn } from "node:child_process";
16 import * as fs from "node:fs";
17 import * as os from "node:os";
18 import * as path from "node:path";
19 import type { AgentToolResult } from "@earendil-works/pi-agent-core";
20 import type { Message } from "@earendil-works/pi-ai";
21 import { StringEnum } from "@earendil-works/pi-ai";
22 import {
23 CONFIG_DIR_NAME,
24 type ExtensionAPI,
25 getAgentDir,
26 getMarkdownTheme,
27 withFileMutationQueue,
28 } from "@earendil-works/pi-coding-agent";
29 import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
30 import { Type } from "typebox";
31 import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts";
32
33 const MAX_PARALLEL_TASKS = 8;
34 const MAX_CONCURRENCY = 4;
35 const COLLAPSED_ITEM_COUNT = 10;
36 const PER_TASK_OUTPUT_CAP = 50 * 1024;
为什么相信这条结论?查看 3 处证据
小练习 8 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M09 · STATE
会话、持久化与观测:让一次运行变成可追溯事实 如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?
先用一个生活比喻 内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。
这套实现先回答了什么? 聊天不是一条会被覆盖的直线,而是一棵只追加的版本树;回到旧节点不会删除未来分支。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 会话是 append-only JSONL 树,可移动叶子、fork 和保存扩展状态 packages/coding-agent/src/core/session-manager.ts:30聊天不是一条会被覆盖的直线,而是一棵只追加的版本树;回到旧节点不会删除未来分支。 事件和会话账本细,输出模式丰富,但内核不是完整 OTEL 平台 packages/agent/src/types.ts:138本地回放和多种客户端接入很强,能知道花了多少 token、每个工具发生了什么;但不像企业观测平台那样开箱把每步发到 OTEL 后端。
23 L1 · fact · pi-session-001
会话是 append-only JSONL 树,可移动叶子、fork 和保存扩展状态
先看源码事实 每个 entry 有 id/parentId,类型覆盖 message、compaction、branch summary、model、thinking、custom;append 不改旧记录,buildContext 投影当前分支。navigation 只移动 leaf,可总结被放弃分支;fork 把路径复制到新 session 并记录 parentSession。
翻译成白话 聊天不是一条会被覆盖的直线,而是一棵只追加的版本树;回到旧节点不会删除未来分支。
为什么这对自研重要 天然支持回溯、分叉和扩展持久状态,适合审计和复杂交互。
固定提交源码摘录 复制
30 export const CURRENT_SESSION_VERSION = 3;
31
32 export interface SessionHeader {
33 type: "session";
34 version?: number; // v1 sessions don't have this
35 id: string;
36 timestamp: string;
37 cwd: string;
38 parentSession?: string;
39 }
40
41 export interface NewSessionOptions {
42 id?: string;
43 parentSession?: string;
44 }
45
46 export interface SessionEntryBase {
47 type: string;
48 id: string;
49 parentId: string | null;
50 timestamp: string;
51 }
52
53 export interface SessionMessageEntry extends SessionEntryBase {
54 type: "message";
… 88 lines omitted; exact range 30–153 …
143 /** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */
144 export type SessionEntry =
145 | SessionMessageEntry
146 | ThinkingLevelChangeEntry
147 | ModelChangeEntry
148 | CompactionEntry
149 | BranchSummaryEntry
150 | CustomEntry
151 | CustomMessageEntry
152 | LabelEntry
153 | SessionInfoEntry;
为什么相信这条结论?查看 3 处证据
24 L1 · fact · pi-observability-001
事件和会话账本细,输出模式丰富,但内核不是完整 OTEL 平台
先看源码事实 Agent 发出 agent/turn/message/tool start/update/end 事件,assistant message 记录 provider/model、token、cost;CLI 支持 interactive、text、json、rpc 和 HTML export。telemetry core 主要是安装 telemetry 开关,固定提交未见完整 trace/span exporter 管线。
翻译成白话 本地回放和多种客户端接入很强,能知道花了多少 token、每个工具发生了什么;但不像企业观测平台那样开箱把每步发到 OTEL 后端。
为什么这对自研重要 自研平台可复用事件合同,但需补 trace id、span、指标/日志导出和集中查询。
固定提交源码摘录 复制
138 /** Thinking level for the next provider request. */
139 thinkingLevel?: ThinkingLevel;
140 }
141
142 export interface PrepareNextTurnContext extends ShouldStopAfterTurnContext {}
143
144 export interface AgentLoopConfig extends SimpleStreamOptions {
145 model: Model<any>;
146
147 /**
148 * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.
149 *
150 * Each AgentMessage must be converted to a UserMessage, AssistantMessage, or ToolResultMessage
151 * that the LLM can understand. AgentMessages that cannot be converted (e.g., UI-only notifications,
152 * status messages) should be filtered out.
153 *
154 * Contract: must not throw or reject. Return a safe fallback value instead.
155 * Throwing interrupts the low-level agent loop without producing a normal event sequence.
156 *
157 * @example
158 * ```typescript
159 * convertToLlm: (messages) => messages.flatMap(m => {
160 * if (m.role === "custom") {
161 * // Convert custom message to user message
162 * return [{ role: "user", content: m.content, timestamp: m.timestamp }];
… 47 lines omitted; exact range 138–220 …
210 * If it returns true, the loop emits `agent_end` and exits before polling steering or follow-up queues,
211 * without starting another LLM call. The current assistant response and any tool executions finish normally.
212 *
213 * Use this to request a graceful stop after the current turn, e.g. before context gets too full.
214 *
215 * Contract: must not throw or reject. Throwing interrupts the low-level agent loop without producing a normal event sequence.
216 */
217 shouldStopAfterTurn?: (context: ShouldStopAfterTurnContext) => boolean | Promise<boolean>;
218
219 /**
220 * Called after `turn_end` and before the loop decides whether another provider request should start.
为什么相信这条结论?查看 4 处证据
小练习 9 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M10 · ENGINEERING
测试、恢复与工程取舍:把漂亮机制变成可靠产品 哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?
先用一个生活比喻 这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。
这套实现先回答了什么? 发动机零件测试很多,真正让整车跑复杂编程赛道的公开考试却还很少。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 机制测试密集,但仓库行为 eval 目前很窄 packages/evals/src/pi-harness.ts:40发动机零件测试很多,真正让整车跑复杂编程赛道的公开考试却还很少。
25 L3 · fact · pi-evals-001
机制测试密集,但仓库行为 eval 目前很窄
先看源码事实 仓库包含 317 个 *.test.ts,覆盖 loop、compaction、trust、extensions、provider 和文件并发;packages/evals 的真实 AgentSession harness 会隔离临时目录、捕获 transcript/usage/tool calls,但 suite 只有“巴黎” smoke 与创建/重载/调用 hello extension 两类。
翻译成白话 发动机零件测试很多,真正让整车跑复杂编程赛道的公开考试却还很少。
为什么这对自研重要 工程成熟度不能只看单测数量;自研应补 repo-level edit/test/recovery、安全红队和长期任务 benchmark。
固定提交源码摘录 复制
40 return { provider, model };
41 }
42
43 function toTranscriptEvents(messages: AgentSession["messages"]): TranscriptEvent[] {
44 const events: TranscriptEvent[] = [];
45 for (const message of messages) {
46 if (message.role === "user") {
47 events.push({ type: "message", role: "user", content: contentText(message.content) });
48 } else if (message.role === "assistant") {
49 const text = contentText(message.content);
50 if (text) events.push({ type: "message", role: "assistant", content: text });
51 for (const part of message.content) {
52 if (part.type === "toolCall") {
53 events.push({
54 type: "tool_call",
55 id: part.id,
56 name: part.name,
57 arguments: normalizeRecord(part.arguments),
58 });
59 }
60 }
61 } else if (message.role === "toolResult") {
62 const text = contentText(message.content);
63 events.push({
64 type: "tool_result",
… 95 lines omitted; exact range 40–170 …
160 outputTokens: stats.tokens.output,
161 totalTokens: stats.tokens.total,
162 toolCalls: stats.toolCalls,
163 },
164 },
165 };
166 } finally {
167 signal?.removeEventListener("abort", abort);
168 }
169 } catch (error) {
170 outcome = { success: false, error };
为什么相信这条结论?查看 3 处证据
小练习 10 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M11 · PRACTICE
把读懂变成会判断 下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。
Q1 仓库是“通用 Harness 内核 + 完整 Coding Agent 产品层”的双轨架构 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 架构抽象领先,但迁移期会产生两个 session/compaction/tool 生命周期,需要明确长期收敛边界。
证据:packages/agent/src/harness/agent-harness.ts:171 Q2 低层循环把 steering、工具执行和 follow-up 分成内外两层 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 交互打断与自治续跑共享同一 loop,而不是 UI 另开旁路。
证据:packages/agent/src/agent-loop.ts:155 Q3 截断响应禁止执行工具;每回合可热刷新完整运行状态 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 工具安全包含协议完整性检查,动态配置不必重启会话。
证据:packages/agent/src/agent-loop.ts:208 Q4 新 AgentHarness 把请求 hooks、消息持久化和运行时变更统一到 turn boundary 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 适合作为自研 Harness 的可复用内核参考,尤其是 transport、storage 和 execution capability 的解耦。
证据:packages/agent/src/harness/agent-harness.ts:354 Q5 工具默认可并行,声明 sequential 或全局策略才串行 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 吞吐高,但自定义工具作者必须正确声明副作用和并发语义。
证据:packages/agent/src/agent-loop.ts:411
APPENDIX · SOURCE INDEX
本课读过的实现文件 文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。
01 packages/agent/src/harness/agent-harness.tsL171–223, L354–497, L512–656, L884–1023 02 packages/coding-agent/src/core/agent-session.tsL375–400, L2560–2598, L520–540, L2630–2705, L1943–2041, L1783–1924, L2047–2195, L2454–2544 03 packages/agent/src/agent-loop.tsL155–275, L208–245, L411–553, L600–650 04 packages/agent/test/agent-loop.test.tsL1–80 05 packages/coding-agent/test/agent-session-concurrent.test.tsL1–90 06 packages/ai/src/providers/all.tsL5–44, L86–127 07 packages/coding-agent/src/core/model-runtime.tsL95–173, L193–230 08 packages/coding-agent/src/core/extensions/types.tsL1340–1414, L1179–1340, L1280–1340 09 packages/ai/src/utils/provider-retry.tsL22–66, L105–124 10 packages/coding-agent/src/core/compaction/compaction.tsL190–237, L345–460, L467–537, L710–788 11 packages/coding-agent/test/agent-session-auto-compaction-queue.test.tsL1–100 12 packages/coding-agent/src/core/tools/edit.tsL33–53, L287–361 13 packages/coding-agent/src/core/tools/file-mutation-queue.tsL28–60 14 packages/coding-agent/test/file-mutation-queue.test.tsL1–100 15 packages/coding-agent/src/core/tools/truncate.tsL1–12 16 packages/coding-agent/src/core/tools/read.tsL203–300 17 packages/coding-agent/src/core/bash-executor.tsL46–129 18 packages/coding-agent/src/core/tools/bash.tsL82–148 19 packages/coding-agent/src/core/tools/path-utils.tsL44–50 20 packages/agent/src/harness/types.tsL146–373 21 packages/agent/src/harness/env/nodejs.tsL344–497 22 packages/coding-agent/src/core/project-trust.tsL24–95 23 packages/coding-agent/src/core/trust-manager.tsL29–37 24 packages/coding-agent/src/core/settings-manager.tsL450–476 25 packages/coding-agent/test/trust-manager.test.tsL1–68 26 packages/coding-agent/src/core/auth-storage.tsL21–145, L217–239 27 packages/coding-agent/src/core/resolve-config-value.tsL1–80 28 packages/coding-agent/src/core/system-prompt.tsL28–71, L79–159 29 packages/coding-agent/src/core/resource-loader.tsL67–123, L38–47 30 packages/coding-agent/src/core/skills.tsL118–188, L327–360 31 packages/coding-agent/src/core/package-manager.tsL2303–2466 32 packages/coding-agent/src/core/extensions/loader.tsL66–124, L230–300 33 packages/coding-agent/test/extensions-runner.test.tsL1–100 34 packages/coding-agent/examples/extensions/sandbox/index.tsL1–47, L201–295 35 packages/coding-agent/examples/extensions/permission-gate.tsL1–35 36 packages/coding-agent/examples/extensions/subagent/index.tsL1–36, L267–428 37 packages/coding-agent/examples/extensions/subagent/agents.tsL26–115 38 packages/coding-agent/src/core/session-manager.tsL30–153, L1015–1188, L1354–1490 39 packages/agent/src/types.tsL138–220 40 packages/coding-agent/src/cli/args.tsL63–170 41 packages/coding-agent/src/main.tsL109–124 42 packages/coding-agent/src/core/telemetry.tsL1–14 43 packages/evals/src/pi-harness.tsL40–170 44 packages/evals/src/smoke.eval.tsL5–16 45 packages/evals/src/extensions.eval.tsL16–41
下一步 从课程回到报告,做一次反向核验 教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。
查看 Pi 报告 ↗