pi-web 源码架构解读:一次看懂 Agent 网页端会话管理的完整链路
【免费下载链接】pi-webWeb UI for the pi coding agent项目地址: https://gitcode.com/GitHub_Trending/pi/pi-web
我是常年给开源项目写源码导读的工程师,今天带你看 pi-web——pi coding agent 的网页端。它的核心功能是把 Agent 会话搬进浏览器:浏览、恢复历史会话,发提示词跑一轮对话,配置模型、技能和项目文件。一句话点破它的核心机制:会话的最终状态落在磁盘的 JSONL 文件里,内存里只是随时可弃、可随时重建的代理进程。
一轮对话的完整链路:从你敲下回车开始
想象 pi-web 是一家餐厅。你在网页上的输入框就是餐台,API 路由是传菜窗口,每个会话背后挂着一个 AgentSession 进程是后厨,而~/.pi/agent下的 JSONL 文件是账本——每一笔订单都记在上面,打烊了第二天照单能续。
你输入文本按回车,整条链路这样走:
- components/ChatInput.tsx 把文本交给状态机 hooks/useAgentSession.ts,组装成
{ type: "prompt", ... }命令对象; - 经 lib/agent-client.ts 的
sendAgentCommand发出POST /api/agent/[id]; - 路由 app/api/agent/[id]/route.ts 先查内存再查磁盘:
const existing = getRpcSession(id); if (existing?.isAlive()) { const result = await existing.send(body); return NextResponse.json({ success: true, data: result }); } const filePath = await resolveSessionPath(id);内存里有活着的会话就直接投递;没有就用 lib/session-reader.ts 把会话 id 解析成 JSONL 路径,startRpcSession从文件恢复上下文再继续。这就是"关掉服务再打开,会话照样能续"的全部秘密。
- 后厨开火:代理引擎调用模型、执行工具,每条新消息同步追加进 JSONL;同时 app/api/agent/[id]/events/route.ts 的 SSE 长连接把事件流推回浏览器,
ChatWindow增量渲染出流式回复。
说白了,浏览器从头到尾只是账本的展示屏——真正的记账发生在服务端。知道了这条主线,再看静态结构就不会迷路。
架构分层总览:前端、服务端与账本三块
前端层(components/ + hooks/):只管界面与会话状态机,useAgentSession消费 SSE 事件、维护消息列表和流式状态,组件本身不直接发请求,一律走 lib 里的客户端封装。
服务端层:Node 22 上跑 Next.js 16,UI 和 API 同进程。app/api/ 下几十个路由按功能域划分——agent、sessions、files、models、skills、terminal——但路由只做参数校验和分发,一行逻辑不写;全部业务沉在 lib/ 的两百多个文件里,终端功能靠 node-pty 在服务侧开真实伪终端、前端用 xterm 渲染。
存储层:就是 pi 的本地配置目录和~/.pi/agent下的会话 JSONL。前端和服务端共用同一套会话文件,没有独立数据库,也没有同步问题——因为根本只有一份数据。
说白了,分层边界很干净:界面上任何行为,顺着"组件 → agent-client → app/api 路由 → lib"这条线往下翻,一定能找到落点。分层讲清楚了,下面挑三个最值得打开的文件细读。
三个最值得读的源文件:核心文件读法
lib/rpc-manager.ts——后厨的排班表。它在干什么:globalThis.__piSessions里维护"sessionId → AgentSessionWrapper"映射,管会话的启动、空闲超时(默认 10 分钟)、销毁与重建,所有 prompt、工具切换、状态查询都从这过。对外暴露:startRpcSession(id, filePath)拉起或恢复会话、getRpcSession(id)查存活会话、setRpcSessionTools运行时换工具集。谁在用它:app/api/agent下每个路由。注意它挂在globalThis而不是模块级变量,因为 Next.js 热更新会销毁模块状态。
lib/session-reader.ts——账本的读法。它在干什么:定位和读取sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl,全程不拉起代理进程。对外暴露:resolveSessionPath(id)把 id 换成磁盘路径、listAllSessions扫会话列表、各类限字节读取——头部最多读 64KB,靠readBoundedLines这类函数保证列几百个会话也不读整文件。谁在用它:sessions、files、worktrees 路由共享。
lib/agent-client.ts——前端的统一点单口。它在干什么:把散在 13 处的 fetch 块收敛成一个函数,并固化契约——/api/agent/[id]响应只有{ success: true, data }或{ error }两种形状。对外暴露:
export async function sendAgentCommand<T = unknown>( sessionId: string, command: Record<string, unknown>, ): Promise<T>谁在用它:useAgentSession。有了它,判断"提示词是否被拒"只需看一个字段error.code === "prompt_rejected"。
说白了,读 pi-web 就按"进程(rpc-manager)→ 文件(session-reader)→ 契约(agent-client)"这个顺序,三天能变三天变三天能变三分钟。
设计取舍:那些不明显的决定
账本优先,进程可弃。内存里的 AgentSession 随时可杀(空闲 10 分钟即回收),状态永远以 JSONL 为准。代价:恢复会话要重新走一遍文件加载,热路径上多一次磁盘解析(用路径缓存缓解);换来的:进程崩了、服务重启、甚至从命令行pi里继续同一个会话,数据都不丢,fork 分支也只操作磁盘副本、从不碰运行中的进程(见 docs/agents/sessions.md)。
UI 与 API 同进程,不拆后端。要读本地文件、要管进程,反正得有个常驻 Node,不如让 Next.js 的 API Routes 就地干活。代价:没有跨进程隔离,会话注册表必须挂globalThis才能扛热更新;换来的是部署只有一条命令,前后端契约就在两个目录的距离内。
SSE 带背压,敢丢事件。lib/agent-event-stream.ts 给每条连接设了水位:积压超 512KB 时丢弃"可被后续事件修复"的增量(流式 delta、工具进度更新),超 16MB 直接断开让客户端重连重取快照。代价是弱网下丢几帧中间态;换来的是手机熄屏掉线不会在服务端留几十 MB 队列。
说白了,这个项目反复在同一道选择题里选了"持久层优先":宁可每次多读一次盘,不要任何一份只在内存里存在的数据。取舍想明白了,上手改代码就只剩找对文件的问题。
跑起来 + 改起来
本地跑(需 Node 22.19+):
npx @agegr/pi-web@latest默认监听127.0.0.1:30141,浏览器自动打开;跑单测用npm test。
想加一条新的 agent 命令?动两个文件:先在 app/api/agent/[id]/route.ts 里按body.type加分发分支,再到 lib/rpc-manager.ts 补对应的处理逻辑。契约不用动——sendAgentCommand对任何命令对象都是透传的,前端自然兼容。
会话账本放在磁盘、后厨进程随叫随到,这就是 pi-web 全部的架构故事。
git clone https://gitcode.com/GitHub_Trending/pi/pi-web【免费下载链接】pi-webWeb UI for the pi coding agent项目地址: https://gitcode.com/GitHub_Trending/pi/pi-web
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考