Pi

可分支、可扩展的终端编程智能体
反馈(在 GitHub 新标签页打开)

主要技术:Node.js、Pi、JSONL

项目目录

项目简介

Pi 是一套基于 TypeScript、运行于 Node.js 的可扩展编程智能体。它以读取、编辑文件和执行命令为基础,接收仓库中的开发任务,调用模型选择工具,再把执行结果交给模型继续处理。

开发者可以让它阅读项目、修改组件、根据编译错误调整代码,也可以返回旧会话节点尝试另一种方案。项目提供终端、脚本输出、RPC 和 SDK 入口;团队的工具、命令和请求前处理可通过 Extension 接入,核心没有预设完整的 Plan Mode 或子智能体工作流。

项目亮点

  • 模型、循环与会话分层: pi-ai 适配不同模型的消息和流,pi-agent-core 执行工具闭环,pi-coding-agent 负责仓库上下文、会话与交互,宿主可以分别复用这些能力。
  • 追加式会话树: JSONL 条目通过 id、parentId 记录分支,模型只读取当前路径;切换方案时可选择携带分支摘要,原讨论仍保留。会话分支不会回滚磁盘代码。
  • 历史与模型输入分开: 压缩条目提供摘要及近期保留边界,context_edit 可替换或省略旧条目的模型输入贡献,两者都不改写原始历史。
  • 运行时扩展与消息钩子: Extension 可注册工具、命令并处理调用前后事件。普通 context 与完整 context_with_system 分别处理对话和含系统消息的请求;活动工具集合只控制模型声明,不构成沙箱。

技术架构

任务执行: 编程会话加载项目资源和当前消息,Agent 循环经模型适配层发起请求,按工具调用执行文件或命令操作,将结果及事件返回会话与界面。

会话与定制: Session 管理器追加历史,并按当前分支、压缩和编辑记录投影上下文;扩展加载器接入工具、事件和界面定制。终端、RPC 与 SDK 共用这些运行能力。

代码位置 主要职责
packages/ai/ 模型定义、供应商协议与流式消息
packages/agent/src/agent-loop.ts 模型请求、工具执行与结果回填
packages/coding-agent/src/core/session-manager.ts 会话树、上下文投影和追加记录
packages/coding-agent/src/core/compaction/、packages/coding-agent/src/core/extensions/ 压缩及运行时扩展
packages/coding-agent/src/modes/、packages/tui/ 交互、RPC 等运行入口与终端组件

如何写进简历

适配通 · 工业设备 SDK 升级助手 | 工业软件场景示例

项目描述:面向设备接入软件研发的 SDK 升级助手,结合版本说明、旧接口调用和编译日志推进适配改码,保留不同迁移方案的讨论与工具记录,交付补丁和检查结果。

技术栈:TypeScript、Node.js、Pi、JSONL

主要工作:

  • SDK 嵌入与适配工具接入:通过 createAgentSession() 指定接入代码目录,以 Pi Extension 接入 SDK 资料查询和项目编译入口;将版本差异、旧调用位置与诊断输出送入工具循环,衔接分析和适配改码。
  • 迁移方案的分支会话:利用带 id / parentId 的 JSONL 会话树记录保留兼容层、直接替换接口等不同尝试;按活动分支重建模型上下文,保留各方案的讨论、工具结果与分支摘要。
  • 编译日志投影与原始历史保留:结合压缩摘要和近期消息构建迁移上下文,通过追加 context_edit 省略或替换已处理的长编译输出;模型输入按当前分支投影,原始条目继续保存在 JSONL 中供回查。
  • 检查阶段的工具暴露控制:编写检查阶段扩展,通过 setActiveTools() 切换声明给模型的工具集合,在 context 钩子补充阶段要求;退出检查时恢复原集合,使接口阅读与实际改码按阶段推进。

前端搭档 · 在线课程界面维护助手 | 在线教育场景示例

项目描述:面向在线教育研发团队的课程界面维护助手,接收播放器和课程导航需求,调用代码工具与项目检查入口推进修改,保留界面方案和执行过程以便审查补丁。

技术栈:TypeScript、Node.js、Pi、JSONL

主要工作:

  • SDK 接入与工具结果回填:将 Pi SDK 嵌入课程维护入口,按工程目录接入代码工具和项目检查命令;以 toolCallId 关联启动、增量输出与终态事件,把参数校验或执行异常转为 isError 结果交给模型继续修正。
  • 界面方案与分支上下文:以 JSONL 会话树分别记录播放器交互和章节导航方案,选定分支后只沿活动路径组织模型输入;结合分支摘要承接必要背景,减少其他尝试对当前修改的干扰。
  • 长任务压缩与输入投影:利用压缩摘要整理近期交互要求和待完成项,再以追加式 context_edit 省略过期代码及检查输出;后续请求使用编辑后的投影,原始工具记录仍可回查。
  • 运行中更正与后续任务调度:区分 steer 和 followUp 输入,把当前修改的更正放到助手回合及工具完成后处理,将后续需求排到当前运行结束;协调播放器修改中的临时反馈与下一轮任务。

面试题与回答

3 题
  1. Pi Coding Agent 如何设计?

    展开收起

    核心回答

    Pi Coding Agent 是基于 TypeScript、运行于 Node.js 的可组合编程助手。它把模型适配、Agent 运行循环和编程会话分开:pi-ai 统一模型消息与流式协议,pi-agent-core 组织模型请求、工具执行和结果回填,pi-coding-agent 在上面提供仓库工具、会话持久化、分支、压缩及扩展能力。

    一次任务中,模型根据当前上下文提出工具调用,运行时执行工具并回填结果,再请求模型决定下一步。长任务的历史保存为会话树,模型只读取活动分支经过投影和压缩后的内容;团队需要的工具、提示词和运行规则可通过 Extension 接入。这使核心循环可以复用,终端、RPC 和 SDK 则承担不同的交互与集成方式。

    各层怎样分工?

    • 模型适配层 pi-ai: 提供模型描述、结构化消息、工具声明、流式事件和最终响应,再由供应商适配器处理实际请求与响应协议。上层不用逐家解析流,但不同模型的能力、错误和调用质量仍可能不同。
    • 运行循环 pi-agent-core: 管理当前消息、可用工具与运行状态;调用模型、执行工具、追加结果,并处理用户插入的新指令、后续消息和中断。它不等于会话文件管理器。
    • 编程会话层 pi-coding-agent: 组合项目上下文、读写与命令工具、模型设置和资源加载,把消息写入 Session,提供树导航、压缩和 Extension。
    • 交互层: pi-tui 提供终端组件,交互模式展示运行事件;RPC 通过标准输入输出交换 JSONL,SDK 则让 Node.js 程序直接创建和控制会话。

    模型接口、循环和会话可分别复用,产品入口也共用会话与运行机制。

    一次请求怎样完成工具闭环?

    以“读取 README 并概括项目”为例,第一次模型请求携带用户消息和 read 工具定义。模型返回带调用 ID 的工具调用,循环按工具名找到实现、处理参数并执行,生成带相同 ID 的 toolResult。下一次请求加入这个结果,模型才能根据真正读到的文件回答。

    这段闭环有两个关键边界:工具调用表示模型想做什么,工具结果才表示执行观察到了什么;模型停止生成也不等于开发任务已经验收。 文件修改是否正确、测试是否覆盖需求,仍要根据真实文件和命令结果判断。

    运行循环把工具结果追加回上下文,再判断是否需要下一次请求。模型错误或中断会结束当前循环;输出长度达到限制时,可能不完整的工具调用参数会被转成失败结果,避免直接执行半截调用。

    流式文本、工具开始和结束等事件用于驱动界面和宿主程序;Session 中的消息、调用和结果用于后续重建。不能只保存屏幕上的文本增量,却丢掉工具调用 ID、错误和完整结果。

    为什么会话历史和模型输入要分开?

    Pi 的持久 Session 是追加式 JSONL 文件。消息、模型切换、摘要等条目都有 id 和 parentId,形成会话树;当前 leaf 表示选中的位置。返回旧节点后继续追加,就能保留方案 A,再尝试方案 B,而不用覆盖原来的记录。

    模型输入从当前叶子沿父指针回到根,只采用这条活动路径。切换分支时,可以选择将离开路径到共同祖先之间的内容生成分支摘要,并挂到目标位置;没有选择摘要时,其他分支的发现不会自动全部带过来。会话导航改变的是对话路径,不会把工作目录中的文件一并回滚。

    上下文投影进一步处理压缩和上下文编辑:原始记录保留,模型看到的是当前路径中实际有效的消息。context_edit 可以追加一条编辑记录,替换或省略旧消息对模型输入的贡献,而不改写原始消息。这便于保留历史依据,同时减少过期或无用内容对后续判断的影响。

    长任务怎样压缩?

    Pi 结合最近一次模型用量和后续新增消息估算上下文,当接近模型窗口预留边界时触发压缩,也支持手动压缩。它将摘要及保留起点写成新的 compaction 条目,后续请求采用摘要、近期保留消息和新增消息,旧记录仍留在会话文件中。

    切分时要维护工具调用与结果的配对关系,不能为了达到一个 Token 数就从工具结果中间截断。较大的交互可能需要单独总结前半段,同时保留后半段继续执行。压缩准备基于模型可见的投影选择这些范围;上下文编辑或压缩使旧用量失效后,也需要重新估算。

    压缩摘要和分支摘要解决的问题不同:前者减少当前路径的输入,后者把离开分支的有用发现带到新位置。两者都有信息损失风险;精确错误、关键约束和文件状态应能回到原记录或实际环境核对。

    Extension 能怎样接入运行过程?

    Extension 是运行在 Pi 进程内的 TypeScript 模块,可以注册工具、命令及事件处理器。例如,在运行开始前调整提示词,在工具调用前返回阻止结果,在工具返回后整理内容,或定制分支摘要和压缩行为。

    请求前的上下文钩子有两层:普通 context处理对话消息,Pi 会恢复系统提示与工具声明;context_with_system 处理包含系统消息的完整输入,扩展要自己维护提示词和工具声明。这些请求时的变换不会自动改写持久历史。

    扩展状态同样要区分用途:custom 条目用于保存程序状态,默认不进入模型上下文;custom_message 才用于注入模型需要知道的内容。只需要提供操作知识时可以使用 Skill,不必写成扩展代码;需要运行时执行和拦截时再使用 Extension。

    这种设计把工作流定制留给扩展,但扩展与工具仍受所在进程的权限影响。钩子拦截、项目信任和操作系统隔离各有职责,不能把“支持扩展”当成默认沙箱或完整权限保证。

  2. 压缩多轮对话后,如何拼接用户消息、模型消息与摘要?

    展开收起

    Pi 1.4.0 将会话保存为带 id、parentId 的条目树。模型输入由当前分支投影生成:当前路径上的最新压缩摘要+保留的近期条目+压缩后的新增条目,并应用该路径上的上下文编辑。

    先选路径,再应用压缩边界

    buildSessionPath() 从当前叶子沿 parentId 回到根,再反转为执行顺序;它不会遍历所有兄弟分支。buildContextEntries() 找到这条路径上最新的 compaction,把该条目放在最前,然后接上:

    • 从 firstKeptEntryId 开始、位于压缩条目之前的保留内容;这段中的旧 system 消息会被排除。
    • 压缩条目之后、本分支新增的内容。

    压缩条目保存了 systemMessage 时,投影先恢复这份提示与工具状态快照,再放摘要。即使较旧的压缩条目仍落在保留范围里,也不会再次注入旧摘要,因此不会出现多份历史摘要层层堆叠。

    保留原角色,摘要明确标记为历史

    普通消息保留 user、assistant、toolResult 等角色。内部的 compactionSummary 经过 convertToLlm() 转成 user 角色的文本消息,并带有明确的“此前历史已压缩为以下摘要”前缀与摘要标签。这里使用 user 作为兼容接口的载体,不表示摘要是用户刚发出的新要求。

    默认压缩准备会避开工具结果作为切分起点,保留它前面的模型工具调用;若一个用户请求本身过长而需要从中间切开,则另做该请求前半段的摘要,接上后半段消息,而不是硬保留整个超长请求。

    上下文编辑不改写原始会话树

    context_edit 通过目标条目 ID 替换其上下文 content,或用 replacement: null 使它不进入模型输入;同一目标采用当前有效范围中最后一次编辑。原始条目、角色与其他元数据继续留在记录里,编辑只影响投影。

    因此,界面和历史工具能看到的完整过程,不等于模型这一轮收到的内容。扩展省略工具调用或结果时仍需维护对应关系;投影机制本身不意味着任何任意删改都符合提供商消息要求。切换会话分支也只改变对话路径,磁盘文件或外部动作需要另行核对,不能视为已经回滚。

  3. 展示 Pi 的自定义 Extension 配置。

    展开收起

    核心回答

    可以用一个 /inspect 扩展展示完整配置:默认导出函数接收 ExtensionAPI,注册命令切换检查工具集,再注册 context 事件处理器,为当轮模型输入补一条阶段说明。下面示例采用 @earendil-works/pi-coding-agent 1.4.0 的接口,/inspect off 会恢复进入前的工具集合。扩展入口与加载方式。

    最小扩展文件

    将文件保存为项目中的 .pi/extensions/inspect.ts:

    import type { UserMessage } from "@earendil-works/pi-ai";
    import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
    
    export default function inspectExtension(pi: ExtensionAPI) {
      let previousTools: string[] | undefined;
      const upgradeTask = {
        targetVersion: "3.0(示例)",
        compatibility: "保留旧接口入口;列出必须修改的调用点"
      };
    
      pi.registerCommand("inspect", {
        description: "切换检查阶段;/inspect off 恢复原工具集",
        handler: async (args, ctx) => {
          if (args.trim() === "off") {
            if (previousTools) pi.setActiveTools(previousTools);
            previousTools = undefined;
            ctx.ui.notify("已退出检查阶段", "info");
            return;
          }
          previousTools ??= pi.getActiveTools();
          const available = new Set(pi.getAllTools().map((tool) => tool.name));
          pi.setActiveTools(["read", "grep", "find", "ls"].filter((name) => available.has(name)));
          ctx.ui.notify(`检查工具:${pi.getActiveTools().join(", ")}`, "info");
        }
      });
    
      pi.on("context", (event) => {
        if (!previousTools) return;
        const notice: UserMessage = {
          role: "user",
          content: `[inspect 阶段说明] 目标 SDK 版本:${upgradeTask.targetVersion};兼容约束:${upgradeTask.compatibility}。当前只检查代码并提出建议,不修改文件或执行命令。`,
          timestamp: Date.now()
        };
        const messages = [...event.messages];
        const at = messages.at(-1)?.role === "user" ? messages.length - 1 : messages.length;
        messages.splice(at, 0, notice);
        return { messages };
      });
    }
    

    这里用了三个具体入口:

    • registerCommand() 注册 /inspect,命令回调保存原工具集合,再选取已注册的 read、grep、find、ls;重复开启不会覆盖最初保存的集合。
    • setActiveTools() 改变向模型声明的活动工具;getAllTools() 用于排除当前环境中不存在的工具名。它切换的是工具集合,不是重新注册工具。
    • on("context", ...) 在模型请求前返回新的消息列表,把明确标识的阶段说明放在最后一条用户问题之前。原历史数组没有被改写,当前问题也仍在末尾。API 与事件类型、context 事件执行。

    context 处理的是不含系统消息的对话列表,运行器会恢复系统提示和工具状态;示例没有调用持久化消息 API,只为当轮输入补充说明。previousTools 是扩展实例内状态,若要支持重新加载和会话分支切换,需要另行保存并恢复这份状态。

    upgradeTask 用示例值展示目标版本和兼容约束怎样进入当轮消息;业务接入时应从当前升级任务装载这些字段,切换任务或迁移分支后重新绑定,避免沿用上个任务的约束。阶段消息只补充模型输入,代码是否兼容仍由实际调用点和测试结果判断。

    怎样加载和触发

    项目扩展在授予项目信任后加载;开发时也可以显式指定文件:

    pi --extension ./.pi/extensions/inspect.ts
    

    不需要为这个本地 TypeScript 文件单独打包。修改文件后,可在交互会话中执行 /reload 重新加载。项目扩展目录。

    验证时可按三个观察点检查:

    1. 输入 /inspect,界面应显示本环境可用的检查工具名;命令本身不需要发起模型请求。
    2. 触发一次 context 事件时,返回列表增加一条阶段说明,原问题保持末尾,传入的历史列表保持原样。可以先用模拟事件检查,不必调用模型。
    3. 再次输入 /inspect 后执行 /inspect off,应恢复第一次进入前的工具集合;退出后 context 钩子不再增加说明。

    单文件转译只能检查可转译性,模拟 API 能检查命令与事件逻辑;完整依赖类型检查、真实加载和模型行为需要分别验证。import type 在转译后会被移除,不会成为运行时模块导入。

    工具集合与执行权限的区别

    这个示例定制了模型看到的工具和当前任务阶段,并不提供操作系统沙箱。该版本的 codemode、deferred 工具即使不在活动集合中,仍可能由 codemode 脚本调用;需要约束真实动作时,还应按工具调用入口和执行环境设置相应策略。工具暴露与活动集合。

    参考资料