Pi 插件解剖|todo.ts:297 行,拼出工具+命令+状态的完整插件

一句话定位

给 pi 加一个待办列表:LLM 通过 todo 工具增删改查,用户通过 /todos 命令查看------297 行,完整插件的四件套范本。

作用(为什么存在)

前两期拆的都是「单点能力」插件(拦截、快照),这期是完整插件:工具 + 命令 + 状态持久化 + 自定义渲染,四件套全齐。

它最值钱的地方是状态怎么存 :把状态写进工具结果的 details 字段,持久化到会话历史。这带来一个白送的能力------fork 后状态自动对齐分支,重启也不丢。你不需要自己维护任何文件或存储。

关键信息

项 内容
源码位置 examples/extensions/todo.ts(297 行)
核心 API registerTool + registerCommand + ctx.ui.custom + ctx.sessionManager.getBranch() + renderCall/renderResult
插件类型 工具 + 命令 + 状态

触发流程 / 数据流

css 复制代码
LLM 调 todo 工具(add/toggle/clear/list)
  → execute 返回 content(给 LLM)+ details(完整状态快照:todos + nextId)
  → toolResult 消息 append 进 session.jsonl
  → session_start / session_tree 触发 reconstructState
      → getBranch() 遍历当前分支 → 筛 toolResult && toolName==="todo"
      → 最后一条 details 胜出 → 内存重建
用户 /todos → ctx.ui.custom 渲染 TodoListComponent

架构 / 流程

关键代码解读

typescript 复制代码
// 状态重建:从当前分支历史重放(session-manager 的 getBranch 返回分支 entries)
const reconstructState = (ctx) => {
  todos = [];          // ① 清空内存(可能已过期,不能信任)
  nextId = 1;
  for (const entry of ctx.sessionManager.getBranch()) {  // ② 遍历当前分支
    if (entry.type !== "message") continue;
    const msg = entry.message;
    if (msg.role !== "toolResult" || msg.toolName !== "todo") continue;  // ③ 只筛本工具
    const details = msg.details;
    if (details) { todos = details.todos; nextId = details.nextId; }     // ④ 最后一条胜出
  }
};

// 工具返回:状态在 details 里(完整快照)
async execute(_toolCallId, params, ...) {
  // add 分支
  const newTodo = { id: nextId++, text: params.text, done: false };
  todos.push(newTodo);
  return {
    content: [{ type: "text", text: `Added todo #${newTodo.id}: ${newTodo.text}` }],
    details: { action: "add", todos: [...todos], nextId },  // ← 完整状态快照
  };
}

亮点 / 踩坑

亮点 1:状态持久化到会话历史,分支对齐是白送的。 状态跟着工具调用走(每次返回完整快照),getBranch() 只返回当前分支的 entries,所以 fork 后重建出的就是那个分支的状态------不用自己处理"分支里状态该是什么"。

亮点 2:渲染分层。 content 给 LLM(纯文本语义)、details 存结构化数据、renderCall/renderResult 管 TUI 展示。展示层读 result.details 而不是解析 content 文本。

踩坑提示:跨机器会丢。 session 文件在 ~/.pi/agent/sessions/(全局目录),不在项目仓库------git 不跟踪、常规迁移不带。换机器后扫不到任何 todo 结果,状态变空。

边界(Limitations)

维度 表现
fork/分支 ✅ 状态在会话历史,分支自动对齐
进程重启 ✅ 落盘 session,启动重建
跨机器 ❌ session 文件在全局目录,git 不跟踪、迁移不带
状态粒度 每次完整快照、最后一条胜出;清空重扫防状态穿越

场景(Scenarios)

  • 能恢复:同机重启 / fork 到历史点 → 重放当前分支 → 重建对应状态
  • 静默丢失:换机器(session 目录空)/ 删 session 文件 → 重扫不到 → 空列表

可借鉴的模式

  1. 状态持久化到会话历史 :借用工具结果 details 把状态写进会话历史,状态跟会话走、分支对齐白送,免去单独维护存储。
  2. 完整快照而非增量 :每次返回整份 todos + nextId,让历史可重放------最后一份就是状态。增量记录做不到从历史重建。
  3. 清空重扫而非增量累加:内存可能过期(fork 切换后残留旧分支状态),直接丢弃内存、以历史重放为准。
  4. 双入口分工:工具管增删改查(唯一写入方),命令只读查看,避免多写入方竞争。
  5. 渲染分层 :content 给 LLM、details 结构化、renderCall/renderResult 管 TUI,展示层直接拿结构化数据。

一句话总结

297 行告诉你完整插件长什么样:工具给 LLM、命令给用户、状态持久化进会话历史------分支对齐和重启恢复都是白送的。

相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
1点东西5 天前
做了近两年的Agent开发,其实真正要学的就是这五件事
llm·agent·ai编程
晨米酱5 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
invicinble5 天前
记录一个学习技术栈的想法和思路
agent
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
染指11105 天前
122.Agent-LangChain核心组件-中间件-动态提示词(dynamic_promapt)
人工智能·langchain·agent·agents