Pi Web 的 Chat Only 资源策略把空工具选择固化为持久化会话状态【免费下载链接】pi-webWeb UI for the pi coding agent项目地址: https://gitcode.com/GitHub_Trending/pi/pi-web导读Pi Webpi coding agent 的 Web 界面将显式空的工具选择定义为一种独立的持久化资源策略——Chat Only而不是一个恰好工具数组为空的普通 AgentSession。本文从 docs/adr/0002-chat-only-tool-selection.md 出发结合仓库源码与测试完整讲解Chat Only 与普通会话在系统提示词、资源加载上的本质差异Pi Web 如何通过版本化的pi-web:tool-selection自定义条目把工具选择持久化进原生会话格式以及切换工具选择跨越 Chat Only 边界时包装器wrapper重建的底层机制。读完你既能掌握配置与调用方法也能理解这一策略在 SDK 生命周期中的落点。一、背景Pi 原生会话格式不保存工具选择Pi SDK 的原生会话格式JSONL只记录消息、工具调用等会话条目不包含当前激活了哪些工具这一状态。因此当 Web UI 重新打开一个已持久化的会话时SDK 无法知道该会话之前选择了哪些内置工具只能回落到 Pi 的默认行为加载全部默认工具。Pi Web 的解法是在会话中追加**带版本号的自定义条目custom entry**来记录工具选择使工具选择成为会话文件的一部分随会话持久化、可恢复。这一思路对应仓库中的 lib/session-tool-selection.ts核心定义如下{ type: custom, customType: pi-web:tool-selection, data: { version: 1, tools: [] } }其中customType常量在源码中定义为pi-web:tool-selection见 lib/session-tool-selection.ts数据载荷SessionToolSelectionData的version固定为1tools是内置工具名的字符串数组。二、三态模型未选择 ≠ 空选择 ≠ 有选择解析持久化条目时Pi Web 区分三种截然不同的状态这是理解整个策略的钥匙状态判定条件语义传统会话legacy找不到任何有效的pi-web:tool-selection条目保留 Pi 默认行为加载默认工具集Chat Only最新有效条目的tools为空数组[]不加载任何工具视为独立的资源策略内置工具选择最新有效条目的tools为非空数组恢复所选的 Pi 内置工具读取逻辑实现于readSessionToolSelection()它从会话条目末尾向前扫描取最新的有效条目为权威遇到无效条目版本不符、包含非内置工具名等会跳过继续向前直到找到有效条目一条都没有则返回undefined表示传统会话。对应测试见 lib/session-tool-selection.test.mjs其中明确验证了空选择与缺失选择是两种不同的状态// 缺失 → legacyundefined assert.equal(readSessionToolSelection([]), undefined); // 空数组 → Chat Only[] assert.deepEqual(readSessionToolSelection([entry({ version: 1, tools: [] })]), []);写入侧同样严格validateSessionToolSelection()会拒绝任何非内置工具名如扩展工具并做去重appendSessionToolSelection()则把选择以版本化条目追加到会话管理器见 lib/session-tool-selection.ts 及对应测试 lib/session-tool-selection.test.mjs。这里需要区分两个空的含义工具预设中的none即PRESET_NONE []与 Chat Only 在 UI 层面对应但底层落地时空数组被解释为所有工具关闭toolsOption []空允许列表会禁用一切工具见 lib/rpc-manager.ts 的注释这正是 Chat Only 在会话创建时的实际形态。内置工具全集定义在 lib/tool-presets.tsread、bash、edit、write、grep、find、ls以及 Windows 下的powershell。三、普通会话的 Chat Only只保留上下文文件构成的精确系统提示词3.1 不加载任何可选资源对普通会话而言Chat Only 模式下不加载扩展extensions、技能skills、提示词模板prompt templates、主题themes也不加载 Pi 基础系统提示词。资源加载器选项集中定义在 lib/chat-only.tsexport const CHAT_ONLY_RESOURCE_LOADER_OPTIONS { noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: false, // 唯一保留的发现通道Pi 默认的上下文文件 systemPrompt: , appendSystemPrompt: [ ], systemPromptOverride: () undefined, // 占位符不参与提示词 appendSystemPromptOverride: () [], };noContextFiles: false是其中唯一保留的开关——Chat Only不跳过 Pi 默认加载器对上下文文件的发现只是把其他一切可插拔资源关掉。占位符 的目的是阻止加载器去发现已配置的提示词文件同时通过 override 保证占位符本身绝不会混入最终提示词lib/chat-only.test.mjs 验证了这一组开关与 override 行为。3.2 精确系统提示词 上下文文件的顺序拼接Chat Only 会话的精确系统提示词exact system prompt是 Pi 默认加载器发现的全部上下文文件内容的有序拼接包括全局与项目级全局 / 项目的AGENTS.mdAGENTS.override.mdCLAUDE.md拼接函数见 lib/chat-only.tsexport function contextFilesSystemPrompt(files: readonly ContextFileContent[]): string { return files.map((file) file.content).join(\n\n); }Pi Web不会添加任何自己的前缀、后缀或当前工作目录文本——提示词就是上下文文件的原始内容按加载器发现顺序拼接。测试 lib/chat-only.test.mjs 验证了这一点且对CLAUDE.md不做过滤const prompt contextFilesSystemPrompt([ { path: /global/AGENTS.md, content: global agents }, { path: /repo/CLAUDE.md, content: project claude }, { path: /repo/app/AGENTS.override.md, content: nested override }, ]); assert.equal(prompt, global agents\n\nproject claude\n\nnested override);在会话创建流程中Chat Only 的精确提示词取自inner.resourceLoader.getAgentsFiles().agentsFiles见 lib/rpc-manager.ts通过AgentSessionWrapper的exactSystemPrompt选项注入。四、子代理Subagent的 Chat Only由 profile 决定子代理的情况与普通会话不同它的 Chat Only 判定完全由解析后的 profile驱动判定公式在 lib/subagent-prompt.tsconst chatOnly options.tools.length 0 !options.loadSkills !options.loadExtensions;即profile 没有配置任何工具且加载技能加载扩展两个资源开关同时关闭时该子代理即为 Chat Only。4.1 精确系统提示词 profile 系统提示词Chat Only 子代理同样不加载扩展、技能、提示词模板、主题、上下文文件与 Pi 基础系统提示词其精确系统提示词就是 profile 自身配置的系统提示词profileSystemPrompt见 lib/subagent-prompt.ts。创建子代理会话时资源加载器选项体现为 lib/subagent-runtime.tsresourceLoaderOptions: { noExtensions: !profile.loadExtensions, noSkills: !profile.loadSkills, noPromptTemplates: true, noThemes: true, noContextFiles: true, // 子代理 Chat Only 连上下文文件也关闭 ... appendSystemPrompt, },注意与普通会话的差异普通 Chat Only 保留noContextFiles: false而子代理 Chat Only 关闭noContextFiles因为它的全部上下文来自 profile 系统提示词。4.2 父上下文进任务不进系统提示词如果启用了父上下文继承inheritContext父上下文不会追加到子代理的系统提示词而是作为委托任务delegated task的一部分注入lib/subagent-prompt.tsdelegatedTask: options.inheritedParentContext chatOnly ? ${options.task}\n\n${options.inheritedParentContext} : options.task,这样既让子代理能看到父会话背景又保住了精确系统提示词 profile 系统提示词的不变性。非 Chat Only 子代理若继承父上下文则走appendSystemPrompt追加通道。此外子代理运行前有独立的max_turns轮次限制逻辑见 lib/subagent-runtime.ts。4.3 可选资源与保留工具豁免profile 可以独立选择开启技能或扩展开启技能/扩展后子代理就自动脱离 Chat Only见判定公式其资源加载器会按projectTrustReloadOptions进行受信任重载lib/subagent-runtime.ts。扩展工具会与 profile 的内置工具一起激活但 Pi Web保留的子代理控制工具SUBAGENT_CONTROL_TOOL_NAMES始终被排除excludeTools见 lib/subagent-runtime.ts以防止子代理再嵌套派发 Agent形成无限递归。4.4 input_files输入准备而非工具宿主父会话可以在派发前解析input_files把它们的UTF-8 文本内容包含进委托任务。这是一种输入准备input preparation不是子代理的工具它既不改变激活工具列表也不改变 Chat Only 的精确系统提示词。实现见 lib/subagent-input.ts最多 8 个文件、总计不超过 512 KiB、强制 UTF-8 解码fatal: true、路径必须落在会话 cwd 之内复用isPathWithinRoots做路径安全校验。读取后通过appendSubagentInputFiles()以documents块拼接到任务末尾document pathrelative/path 文件内容 /document调用链为start()→loadSubagentInputFiles(parent.cwd, request.inputFiles)→appendSubagentInputFiles(request.task, inputFiles)见 lib/subagent-runtime.ts。五、持久化时机在会话服务创建之前完成解析持久化的工具选择必须在createAgentSessionServices()之前解析完成这是硬性约束因为 Chat Only 的加载器选项CHAT_ONLY_RESOURCE_LOADER_OPTIONS会直接传入createAgentSessionServices()若解析晚了Chat Only 可能已经加载并执行了会话扩展——违背策略初衷。解析顺序在 lib/rpc-manager.tsconst subagentResources sessionFile ? readSubagentSessionResources(sessionManager.getEntries()) : null; const persistedToolNames subagentResources ? undefined : readSessionToolSelection(sessionManager.getEntries()); const selectedToolNames subagentResources?.tools ?? persistedToolNames ?? requestedToolNames; // 新选择首次出现时立即持久化 if (!subagentResources persistedToolNames undefined requestedToolNames ! undefined) { appendSessionToolSelection(sessionManager, requestedToolNames); } const chatOnly selectedToolNames?.length 0 !subagentLoadsResources;优先级为子代理资源快照 持久化的工具选择 本次请求指定的工具名。chatOnly最终由selectedToolNames.length 0判定同时排除子代理开启了技能/扩展的情况。子代理走 resourceSnapshot不复用该条目子代理不在自己的会话里重复写入pi-web:tool-selection而是把资源策略放进自己的元数据resourceSnapshot见 lib/subagent-runtime.tsresourceSnapshot: { version: 1, appendSystemPrompt: [...appendSystemPrompt], tools: [...activeTools], loadSkills: profile.loadSkills, loadExtensions: profile.loadExtensions, ...(promptPlan.exactSystemPrompt ! undefined ? { exactSystemPrompt: promptPlan.exactSystemPrompt } : {}), },快照记录了子代理的激活工具、profile 的技能/扩展加载开关以及精确系统提示词使得重开会话后能恢复相同的资源策略——这正是Chat Only 是一种持久化的资源策略在子代理侧的体现。读取侧由readSubagentSessionResources()完成并据此跳过pi-web:tool-selection的解析。六、系统提示词必须在模型调用前重新应用即使资源加载选项正确还有一个 SDK 生命周期问题Pi SDK 会在模型调用前before_agent_start阶段立即重建自己的基础提示词若不在这个阶段之后重新应用精确系统提示词Chat Only 的提示词会被 SDK 的默认基础提示词覆盖。Pi Web 的应对是AgentSessionWrapperlib/rpc-manager.ts中的两处机制applyExactSystemPrompt()在包装器构造时、agent_start事件、setActiveToolSelection等多个时机直接把inner.agent.state.systemPrompt覆盖为精确提示词lib/rpc-manager.ts。installExactSystemPromptContinuation()包装inner.agent.prepareNextTurnWithContext在每一轮开始前把上下文里的systemPrompt强制替换为精确提示词从而抵抗 SDK 在before_agent_start后的重建lib/rpc-manager.tsprivate installExactSystemPromptContinuation(): void { if (!this.exactSystemPrompt) return; const previous this.inner.agent.prepareNextTurnWithContext; this.inner.agent.prepareNextTurnWithContext async (turn, signal) { const prepared await previous?.(turn, signal); return { ...prepared, context: { ...(prepared?.context ?? turn.context), systemPrompt: this.exactSystemPrompt!(), }, }; }; }包装器的chatOnly标志通过isChatOnly()对外暴露供工具选择变更逻辑判断是否跨越资源策略边界。七、跨越 Chat Only 边界的工具切换原地更新 vs 重建包装器用户随时可以修改一个会话的工具选择Pi Web 根据是否跨越 Chat Only 边界选择不同的处理路径逻辑集中在setRpcSessionTools()lib/rpc-manager.ts7.1 不跨边界非空预设之间切换在两个非空工具预设之间切换时不涉及资源策略变化可以原地更新现有包装器if (!crossesChatOnlyBoundary) { existing.setActiveToolSelection(toolNames); return { session: existing, sessionId, recreated: false }; }setActiveToolSelection()内部调用setActiveToolsByName()并附加扩展工具withExtensionTools然后重新应用精确提示词lib/rpc-manager.ts。7.2 跨边界进入或离开 Chat Only必须重建一旦切换跨越 Chat Only 边界必须追加新的选择并重建包装器。原因非常直观普通包装器已经加载了扩展资源Chat Only 包装器没有这些资源可供原地启用从 Chat Only 切出时原包装器缺少扩展、技能等资源无法在现有实例上补上从普通切进 Chat Only 时已加载的扩展必须被卸载。重建路径lib/rpc-manager.tsconst crossesChatOnlyBoundary !hasCurrentResourcePolicy || existing.isChatOnly() ! (toolNames.length 0); appendSessionToolSelection(existing.inner.sessionManager, toolNames); ... if (persistedFile) { // 已持久化的会话保留 id 与 JSONL 文件用同一会话文件重启 const started await startRpcSession(sessionId, persistedFile, undefined); return { session: started.session, sessionId: started.realSessionId, recreated: true }; } // 未持久化的空 composer 会话丢弃并新建内部 id const started await startRpcSession(__recreate__${randomUUID()}, , sessionCwd, { toolNames, ... });两种重建方式的取舍已持久化的会话保留原sessionId与 JSONL 文件新选择已先写入会话文件重启后由readSessionToolSelection自然恢复返回recreated: true未持久化的空 composer 会话还没有会话文件则直接丢弃以新的内部 id 重新创建——因为其内容本来就不需要保留。同时注意子代理的工具选择由 profile 固定setRpcSessionTools会对子代理会话抛出Subagent tool selection is fixed by its profilelib/rpc-manager.ts会话运行中isRunning()也不允许变更工具。八、设计要点与实战提示最后把这一策略在实践中的几个关键结论汇总如下空数组 ≠ 没写持久化tools: []才是 Chat Only条目缺失则视为传统会话沿用 Pi 默认行为。UI 上选择无工具预设对应 lib/tool-presets.ts 的PRESET_NONE即写入空数组。精确系统提示词的两种来源普通会话 全局/项目AGENTS.md、AGENTS.override.md、CLAUDE.md的有序拼接无前缀、无后缀、无 cwd 文本Chat Only 子代理 profile 系统提示词本身。父上下文给子代理时走任务而非提示词这是保持精确提示词语义一致性的关键设计。持久化解析必须先于createAgentSessionServices()否则扩展可能在 Chat Only 下被加载执行SDK 在before_agent_start后重建基础提示词因此精确提示词要通过包装器的 continuation 在每一轮模型调用前重新应用。跨边界切换必然重建包装器已持久化会话保留 id 与 JSONL未持久化 composer 会话丢弃重建子代理工具选择由 profile 固定、运行中不可改。input_files是输入准备不是工具至多 8 个文件、512 KiB 总量、UTF-8 校验、路径限制在会话 cwd 内且不影响激活工具列表与精确提示词。这套设计把工具选择从进程内的瞬时状态提升为随会话文件持久化的资源策略并在 SDK 的两个生命周期关键点服务创建前、模型调用前分别落地了解析与重应用是理解 Pi Web 会话体系普通会话 子代理资源边界的基础。相关实现可继续深入 lib/session-tool-selection.ts、lib/chat-only.ts、lib/rpc-manager.ts、lib/subagent-runtime.ts 与 lib/subagent-prompt.ts测试佐证见 lib/session-tool-selection.test.mjs 与 lib/chat-only.test.mjs。【免费下载链接】pi-webWeb UI for the pi coding agent项目地址: https://gitcode.com/GitHub_Trending/pi/pi-web创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考