全部课程
亲手改一个 Agent/ 改造实录 · 亲手改一个 Todo 扩展(含硬核逐行精讲)/ 改造实录①:官方 Todo 崩了,我改一行修好 实录
改造实录 · 亲手改一个 Todo 扩展(含硬核逐行精讲)

改造实录①:官方 Todo 崩了,我改一行修好

前面讲够了机制,这一集真刀真枪改一个——而且全程写实:一个不懂代码的人,怎么把一个崩溃的扩展修好。我们加载 pi 官方的 Todo 扩展,让它列一份公众号待办,结果它当场崩溃退出。不慌:看懂报错、找到根因、改一行、验证。最后还白捡一个漂亮的演示。

前面几课把”扩展怎么改”的机制讲透了。这一集换挡——真刀真枪改一个,全程写实。我不是程序员,你会看到一个不懂代码的人,遇到扩展崩溃时,是怎么一步步把它修好的。

选题挑了最简单的:Todo 待办清单扩展。它自包含、没有复杂判断,而且——你等会儿会看到——它还能反过来把 Session 章那套”账本”演活。


一、玩坏它:让官方 Todo 列一份公众号待办

pi 仓库自带一个官方示例 todo.ts。我加载它,让模型帮我列一份”写公众号”的待办清单。它欢快地加了七八条——然后当场崩溃退出:

pi 因未捕获异常退出,报错 TypeError: Cannot read properties of undefined (reading 'render'),栈里指向 Box.render 和 ToolExecutionComponent.render

关键那行:

TypeError: Cannot read properties of undefined (reading 'render')
  at Box.render (packages/tui/src/components/box.ts)
  at ToolExecutionComponent.render (…/tool-execution.ts:250)

先别慌。 报错不是骂你,是给你的线索。第一件事:顺着那几行 at 某某 (文件:行号) 往下读——它明明白白告诉你崩在哪个文件、哪一行。


二、看懂报错:双重根因

顺着栈读到源码,发现这不是”我们用错了”,是两个坑凑在一起。

根因 ①(pi 自己的):渲染工具调用时,pi 把渲染结果塞进一个容器,却没检查它是不是空:

// tool-execution.ts —— 少了一道判空
const component = resultRenderer(...);
renderContainer.addChild(component);   // component 是 undefined 也照塞

根因 ②(官方 todo.ts 的):它那个决定”怎么显示结果”的函数,是个 switch,却没写兜底的 default:

switch (details.action) {
  case "list":   return ...
  case "add":    return ...
  case "toggle": return ...
  case "clear":  return ...
  // 没有 default —— 万一 action 不是这四个,函数就返回了 undefined
}

两个单独都不致命,凑一起就崩:todo 的显示函数在某种情况下返回了 undefined(②),pi 又没拦住(①),这个 undefined 一路飘到渲染那一刻 → 整个界面崩掉。


三、改一行:给它补个兜底

我不动 pi 的原文件,复制一份官方 todo.ts 成我自己的 my-todo.ts,只在那个 switch 末尾补一个 default:

    case "clear":
      return new Text(... "Cleared all todos" ...);

    // —— 我们的改造:补一个 default 兜底 ——
    default: {
      const text = result.content[0];
      return new Text(text?.type === "text" ? text.text : "", 0, 0);
    }

一行核心思想:让这个显示函数永远返回点东西,绝不返回 undefined——pi 那个没判空的坑,就再也踩不到了。

📦 想自己跑一遍? 这一集的修复版 my-todo.ts(含上手说明、延伸练习)都在配套练习仓库: github.com/pkm365/lucid-learn-agent 先按仓库根 SETUP.md 把 pi 跑起来,再把 my-todo.ts 挂上,你就能复现这整段——包括故意把它玩崩。

这就是”非开发者也能改”的真相:你不用读懂整个代码库。你只要能顺着报错找到那一处,理解它为什么会空,补上兜底——就够了。


四、验证:它活了

加载修复版 my-todo.ts,还是那份公众号待办。这回模型连着加了七条,一次没崩:

修复版运行:模型连续调用 todo add 七次,每条都显示"✓ Added #N",界面正常无崩溃

敲 /todos,那个自绘的清单面板漂亮地弹出来——七条待办、0/7 completed:

/todos 命令弹出的清单面板,标题 Todos,显示 0/7 completed,下面列出 7 条公众号写作待办

你的第一个 extension 改造,成了。 官方示例崩给你看,你诊断出双重根因,改一行,修好,亲手验证——这一整套,你走完了。


五、白捡的演示:清单会跟着”分支”走

修好之后有个意外之喜,正好把 Session 章那套账本演活。

我在同一个会话里,先让它列了七条公众号待办;后来又让它换成”精益生产培训”的三条。于是这条历史线上,前半段是七条、后半段是三条。

现在用 /tree(会话树)跳一下位置——跳到”七条”那个点:

会话树视图,显示完整对话历史:7 条公众号 todo,之后分出一条 /settings 支线,再往下是精益培训的 clear + 3 条 todo

跳过去、敲 /todos → 七条公众号:

跳到"7 项"那个历史点后,/todos 面板显示 0/7 completed,7 条公众号待办

再跳到尾巴那个点、敲 /todos → 变成三条精益(注意上面那句 Navigated to selected point):

跳到尾部历史点后,/todos 面板显示 0/3 completed,3 条精益生产培训待办

同一个会话、同一份代码,你只是在树里跳了一下位置,清单就当场从七条变成三条——你什么都没存。

它怎么做到的?回想 Session 章:todo 每次操作,都把当时的整份清单快照塞进那条工具结果(账本里的一条 message)。你在树里跳到哪个点,它就扫这条历史、拿到那个点最后一份快照,还原出来。

诚实一句:这不是严格意义的”重放增量”,是快照 + 最后写入者赢——但骨架就是 Session 章讲的账本 + 投影(event sourcing)。你亲眼看到了它活着的样子。


带走这一句

改一个 extension,不需要你懂整个代码库。这一集你走完的四步,就是非开发者改扩展的通用打法——① 玩坏它(让它真跑、真崩);② 看懂报错(顺着栈的”文件:行号”找到根因);③ 改一行(补上那处缺失的兜底);④ 验证(再跑一遍,崩没崩一目了然)。官方的代码也会有坑,而你,能修好它。

下一集

这一集是”捡现成的、修好它”。下一集我们从需求出发,亲手设计并做一个 pi 里没有的扩展——从”为什么要它”到”怎么算做好”,一步步走完一个非开发者的完整开发流程。