Harness · Coding Agent Book05 / Little Coder
研究总览
M05 · SOURCE-GROUNDED TUTORIAL

Little Coder
从源码学会它怎么工作

用小而硬的约束增强 Pi,特别适合本地小模型;安全仍主要是规则而非隔离。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

TypeScript · Local-model Pi ExtensionApache-2.0e758baa38f6f20 个结论 · 63 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Little Coder 的 20 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

Pi 内核 + 动态知识尾部注入 + 80% watchdog

上下文

大 Read 压到 30 行;知识按失败/近期工具/意图排序

适用建设

llama.cpp/Ollama、小模型、本地受控编码

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。

M01 · ORIENTATION

先把 Agent 看成一台会交付的机器

如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。

先用一个生活比喻

把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 1

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M02 · LOOP

主循环:模型为什么会继续动

一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?

先用一个生活比喻

像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。

这套实现先回答了什么?

可以把它理解成给 pi 装了一套“小模型护栏与外挂”,对话循环、会话和基础工具仍由 pi 驱动。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
它是 pi 的 Harness 增强层,而不是另一套 Agent 内核package.json:33可以把它理解成给 pi 装了一套“小模型护栏与外挂”,对话循环、会话和基础工具仍由 pi 驱动。
01
L1 · fact · little-architecture-001

它是 pi 的 Harness 增强层,而不是另一套 Agent 内核

先看源码事实

launcher 解析依赖中的 pi CLI,显式加载 bundled/env/user 扩展,以固定 AGENTS.md 启动 pi;package.json 直接依赖 @earendil-works/pi-coding-agent。

翻译成白话

可以把它理解成给 pi 装了一套“小模型护栏与外挂”,对话循环、会话和基础工具仍由 pi 驱动。

为什么这对自研重要

分析和选型时必须把 little-coder 的差异能力与 pi 基座分开计分。

固定提交源码摘录
   33    "scripts": {
   34      "pi": "pi",
   35      "test": "vitest run",
   36      "test:py": "python3 -m pytest benchmarks/test_rpc_client.py -q",
   37      "typecheck": "tsc --noEmit"
   38    },
   39    "dependencies": {
   40      "@earendil-works/pi-coding-agent": "^0.79.4",
   41      "@sinclair/typebox": "^0.34.49",
   42      "playwright": "^1.59.1"
   43    },
为什么相信这条结论?查看 4 处证据
小练习 2

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M03 · MODEL

模型调用:流式输出如何变成可执行步骤

模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?

先用一个生活比喻

模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。

这套实现先回答了什么?

服务端若实际开了 128K,它不会死信配置里的 32K;换模型后也会重新量尺。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
面向 llama.cpp/Ollama 的 provider 注册会探测真实上下文窗.pi/extensions/llama-cpp-provider/index.ts:13服务端若实际开了 128K,它不会死信配置里的 32K;换模型后也会重新量尺。
02
L1 · fact · little-provider-001

面向 llama.cpp/Ollama 的 provider 注册会探测真实上下文窗

先看源码事实

provider 由 shipped models、用户 override 和 URL env 合并;llama.cpp 启动时调用 /props 探测 live n_ctx,模型切换后重新探测并重新注册,使 read-guard 和预算使用真实窗口;失败静默回退声明值。

翻译成白话

服务端若实际开了 128K,它不会死信配置里的 32K;换模型后也会重新量尺。

为什么这对自研重要

上下文治理依赖 provider 元数据准确,动态模型路由后应重新校准。

固定提交源码摘录
   13  // Data-driven provider registration. Reads:
   14  //   1. <pkgRoot>/models.json                       (shipped default)
   15  //   2. $LITTLE_CODER_MODELS_FILE (if set), else
   16  //      $XDG_CONFIG_HOME/little-coder/models.json, else
   17  //      $HOME/.config/little-coder/models.json     (user override; per-provider replace)
   18  //   3. LLAMACPP_BASE_URL / OLLAMA_BASE_URL env    (per-provider baseUrl override)
   19  //
   20  // Issue #13: previously the model list was hardcoded here and models.json was
   21  // only documentation, which made any user edit a no-op until they forked.
为什么相信这条结论?查看 3 处证据
小练习 3

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

默认追求可预测:装哪些插件是确定的;想接入 pi 大生态可以开开关,但会牺牲冷启动上下文和固定能力面。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
扩展来源分层,默认固定集合,pi 生态桥显式 opt-inbin/little-coder.mjs:157默认追求可预测:装哪些插件是确定的;想接入 pi 大生态可以开开关,但会牺牲冷启动上下文和固定能力面。
禁止整文件覆写是跨 Write 与 shell 的不变量.pi/extensions/write-guard/index.ts:35小模型想偷懒把整个文件重写,换成 `cat > file` 也绕不过去;它被迫做小块精确修改。
Edit 强制先 Read,成功 Write/Edit 也更新已知文件集.pi/extensions/read-guard-edit/index.ts:4模型不能凭印象猜 oldText;先亲眼看过文件,才有资格改。
文本化 tool call 只能纠偏,不能由扩展代执行.pi/extensions/output-parser/index.ts:5它能看出模型把工具调用写成了普通文字,但没法替模型按下执行键,只能让模型重发,或要求修服务器模板。
03
L1 · fact · little-extensions-001

扩展来源分层,默认固定集合,pi 生态桥显式 opt-in

先看源码事实

launcher 按 bundled、LITTLE_CODER_EXTRA_EXTENSIONS、用户扩展目录的顺序加载,后者可覆盖前者;默认传 --no-extensions 禁止 pi 自动发现,只有 --with-pi-extensions 才开启且由 pi 对项目扩展做 trust prompt。

翻译成白话

默认追求可预测:装哪些插件是确定的;想接入 pi 大生态可以开开关,但会牺牲冷启动上下文和固定能力面。

为什么这对自研重要

这是“小模型少即是多”的插件治理取舍。

固定提交源码摘录
  157  // ---- 4. Auto-discover bundled extensions ----
  158  // Load order matters: bundled first, then the env var, then the user
  159  // directory. pi applies later `--extension` flags after earlier ones, so a
  160  // user extension can override bundled behavior rather than being shadowed by
  161  // it. The three sources are recorded in LITTLE_CODER_EXTENSION_MANIFEST below
  162  // so the `/extensions` command can tell the user where each one came from.
  163  const extDir = join(pkgRoot, ".pi", "extensions");
  164  const extArgs = [];
  165  const loadedBundled = [];
  166  if (existsSync(extDir)) {
  167    for (const name of readdirSync(extDir).sort()) {
  168      const subdir = join(extDir, name);
  169      const idx = join(subdir, "index.ts");
  170      try {
  171        if (statSync(subdir).isDirectory() && existsSync(idx)) {
  172          extArgs.push("--extension", idx);
  173          loadedBundled.push(idx);
  174        }
  175      } catch {
  176        // skip unreadable entries
  177      }
  178    }
  179  }
  180  
  181  // ---- 4b. Third-party extensions via LITTLE_CODER_EXTRA_EXTENSIONS ----
      … 25 lines omitted; exact range 157–217 …
  207  let userExtensionWarnings = [];
  208  {
  209    const discovered = discoverUserExtensions(process.env);
  210    userExtensionsDir = discovered.dir;
  211    userExtensionWarnings = discovered.warnings;
  212    for (const w of discovered.warnings) console.error(w);
  213    for (const entry of discovered.entries) {
  214      extArgs.push("--extension", entry);
  215      loadedFromUserDir.push(entry);
  216    }
  217  }
为什么相信这条结论?查看 3 处证据
04
L1 · fact · little-edit-001

禁止整文件覆写是跨 Write 与 shell 的不变量

先看源码事实

write tool_call 被原地规范路径;已有文件或 Windows 保留设备名会被拒并给出 Edit recipe。shell 的 >、heredoc、tee 等写目标也用同一 verdict 检查,append 例外。

翻译成白话

小模型想偷懒把整个文件重写,换成 `cat > file` 也绕不过去;它被迫做小块精确修改。

为什么这对自研重要

安全/质量不变量应按“副作用”覆盖所有等价工具,而不是只拦一个工具名。

固定提交源码摘录
   35   * Resolve a write `path` argument to a concrete on-disk path.
   36   *
   37   * Two deterministic rewrites:
   38   *
   39   * 1. `"/<single-segment>"` (e.g. `/foo.md`) → `<cwd>/<single-segment>`.
   40   *    Background: the model has been seen to anchor at filesystem root when
   41   *    given an "Absolute file path" schema and no obvious directory context.
   42   *    Genuine system-path writes always include at least one intermediate
   43   *    directory (`/etc/X`, `/tmp/Y/Z`), so a root + bare filename is almost
   44   *    always a mistake. Rewriting to cwd matches user intent and avoids
   45   *    accidentally writing to `/`.
   46   *
   47   * 2. Bare filename / relative path (no leading slash) → resolved against cwd.
   48   *
   49   * Anything else (absolute path with at least one intermediate directory) is
   50   * left untouched.
   51   */
   52  export function normalizeWritePath(
   53    filePath: string,
   54    cwd: string = process.cwd(),
   55  ): { path: string; rewrittenFrom?: string } {
   56    if (/^\/[^/]+$/.test(filePath)) {
   57      return { path: join(cwd, filePath.slice(1)), rewrittenFrom: filePath };
   58    }
   59    if (!isAbsolute(filePath)) {
      … 5 lines omitted; exact range 35–75 …
   65  // Read whichever key carries the destination path. pi's built-in `write` uses
   66  // `path`; older little-coder builds and some prompts use `file_path`. We accept
   67  // both so the guard is independent of which write implementation is in play.
   68  function pathKey(input: Record<string, unknown>): "path" | "file_path" | undefined {
   69    if (typeof input.path === "string") return "path";
   70    if (typeof input.file_path === "string") return "file_path";
   71    return undefined;
   72  }
   73  
   74  // Tools that hand a string to a shell, and so can reach the filesystem without
   75  // going anywhere near the `write` tool (issue #70).
为什么相信这条结论?查看 4 处证据
05
L1 · fact · little-edit-002

Edit 强制先 Read,成功 Write/Edit 也更新已知文件集

先看源码事实

每会话维护 canonicalized readFiles;只有成功的 read/edit/write tool_result 才加入,未读文件的 edit 在执行前阻断,新会话清空。

翻译成白话

模型不能凭印象猜 oldText;先亲眼看过文件,才有资格改。

为什么这对自研重要

把编辑前置条件做成运行时门禁,比仅写进提示词可靠。

固定提交源码摘录
    4  
    5  // Read-before-edit guard.
    6  //
    7  // Small models routinely fire `edit` with an `oldText` they never actually saw
    8  // — guessing at the current file contents — which either fails the exact-match
    9  // requirement (wasting a turn) or, worse, matches the wrong span. Editors the
   10  // user is used to (Claude Code et al.) enforce a simple invariant: a file must
   11  // be Read before it can be Edited. We reproduce that here.
   12  //
   13  // Mechanism mirrors write-guard: we don't own pi's built-in `read`/`edit`
   14  // tools, so we enforce at the event layer. We remember every file that was
   15  // successfully `read` this session (`tool_result`, !isError), and block any
   16  // `edit` whose target hasn't been read, redirecting the model to Read first.
   17  //
   18  // Why a separate extension from `read-guard`: read-guard trims an oversized
   19  // read so it can't overflow a small context window — a different concern from
   20  // the read-before-edit invariant. Keeping them apart keeps each single-purpose.
   21  //
   22  // A successful `edit` or `write` also marks the path as known: an edit only
   23  // succeeds when the file was already read (we'd have blocked it otherwise), and
   24  // a write means the model authored the file's contents, so a follow-up edit to
   25  // either is legitimate without a re-read.
   26  
   27  // Files read (or authored) in the current session. Module-scoped: one pi
   28  // process drives one session at a time, and we clear on session_start.
   29  export const readFiles = new Set<string>();
为什么相信这条结论?查看 3 处证据
06
L1 · limitation · little-output-001

文本化 tool call 只能纠偏,不能由扩展代执行

先看源码事实

turn_end 检测 fenced/XML/bare JSON 调用后发送 follow-up,Liquid Pythonic 格式只提示服务端启用匹配 --jinja;源码明确指出 pi ExtensionAPI 无法执行解析出的调用并合成 tool_result。

翻译成白话

它能看出模型把工具调用写成了普通文字,但没法替模型按下执行键,只能让模型重发,或要求修服务器模板。

为什么这对自研重要

对弱模型的协议容错受上游 ExtensionAPI 能力上限约束。

固定提交源码摘录
    5  // Detects malformed/fenced tool calls in assistant text and nudges the model
    6  // back onto native tool-calling. Active-repair (executing extracted calls
    7  // and synthesizing tool_result messages) is intentionally not attempted on
    8  // the headline Qwen3.6-35B-A3B path, which uses native tool calling. When
    9  // extracted calls ARE detected, we log them via ctx.ui.notify and queue a
   10  // follow-up nudge for the next turn.
   11  //
   12  // One format is handled differently: LFM2/Liquid "Pythonic" tool calls
   13  // (`<|tool_call_start|>[Read(path='…')]<|tool_call_end|>`, issue #42). Pythonic
   14  // IS that model's native channel, so a "use native tool calls" nudge can't move
   15  // it to another format — it would just re-emit the same text every turn and
   16  // loop. little-coder also can't execute the calls itself (pi exposes no
   17  // extension API to run a tool + synthesize its result). So for that format we
   18  // surface a single, accurate diagnostic pointing at the real fix — serving
   19  // llama.cpp with `--jinja` and the model's chat template, which parses the
   20  // calls into native tool_calls upstream — instead of looping a futile nudge.
为什么相信这条结论?查看 2 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M05 · CONTEXT

上下文:有限窗口怎样装下长任务

当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?

先用一个生活比喻

上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。

这套实现先回答了什么?

模型若连续几十轮调用工具、不把控制权还给用户,原生 pi 可能迟迟不压缩;这个扩展在每一小轮都看油表,快满了就主动整理上下文再接着做。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
80% 中途压缩 watchdog 补上 pi 的长自主运行缺口.pi/extensions/context-watchdog/index.ts:3模型若连续几十轮调用工具、不把控制权还给用户,原生 pi 可能迟迟不压缩;这个扩展在每一小轮都看油表,快满了就主动整理上下文再接着做。
超大 Read 结果在进入 LLM 前缩成 30 行.pi/extensions/read-guard/index.ts:4文件虽然已经从磁盘读了,但在送给模型前会截流,避免一份两千行源码把小模型的记忆一次塞爆。
07
L1 · fact · little-context-001

80% 中途压缩 watchdog 补上 pi 的长自主运行缺口

先看源码事实

每个 turn_start 读取 live context usage,默认 80% 触发 ctx.compact;完成后自动发送继续消息。它用 in-flight guard、5% 滞回与效果测量防止重复压缩陷入 Nothing to compact。

翻译成白话

模型若连续几十轮调用工具、不把控制权还给用户,原生 pi 可能迟迟不压缩;这个扩展在每一小轮都看油表,快满了就主动整理上下文再接着做。

为什么这对自研重要

长自治任务的上下文治理不能只挂在用户轮边界。

固定提交源码摘录
    3  // Mid-run context watchdog (issue #59).
    4  //
    5  // pi only evaluates auto-compaction at a *user-turn boundary* — its
    6  // `_checkCompaction` runs inside `_handlePostAgentRun`, which fires only after
    7  // `agent.prompt()` has fully returned (i.e. once the model stops requesting
    8  // tools and goes idle). During one long autonomous run this boundary is never
    9  // reached: little-coder's small models routinely chain dozens of tool-call
   10  // turns before yielding, so context grows unchecked and can blow straight past
   11  // the window — pi then only reacts to the *overflow error* after the fact.
   12  // charly1r reproduced exactly this: context climbing 34k → 40k → … → 64k across
   13  // many `slot release` turns with no compaction until the request overflowed.
   14  //
   15  // pi does expose the levers to fix this from an extension: `ctx.getContextUsage()`
   16  // reports live token usage against the active model's window, and `ctx.compact()`
   17  // triggers pi's own compaction without awaiting it. This extension watches usage
   18  // at every turn boundary and, once it crosses a threshold, proactively kicks off
   19  // compaction — so a long single run compacts *before* it overflows, at roughly
   20  // the same point pi would have if the model had yielded.
   21  //
   22  // Tuning / opt-out:
   23  //   LITTLE_CODER_COMPACT_AT_PERCENT   trigger threshold, percent of the context
   24  //                                     window (default 80). <=0 or >=100 disables.
   25  //   LITTLE_CODER_NO_COMPACT_WATCHDOG=1  hard off.
   26  //
   27  // This is complementary to pi's end-of-run compaction, not a replacement — the
   28  // `compacting` guard below keeps us from re-firing while a compaction is already
   29  // in flight, and pi's own threshold/overflow paths still run at run boundaries.
为什么相信这条结论?查看 3 处证据
08
L1 · fact · little-context-002

超大 Read 结果在进入 LLM 前缩成 30 行

先看源码事实

read 的成功 tool_result 会用 3.5 chars/token 估算;已知 usage 时按剩余窗口判断,未知时单文件不得超过窗口一半。超限则替换为头 30 行和 grep/定向 read 指令,图片保持原样。

翻译成白话

文件虽然已经从磁盘读了,但在送给模型前会截流,避免一份两千行源码把小模型的记忆一次塞爆。

为什么这对自研重要

工具输出也是上下文预算的一等公民,不能只压聊天历史。

固定提交源码摘录
    4  // Harness intervention: trim a `read` result that would overflow the context window.
    5  //
    6  // little-coder drives SMALL local models with small context windows (the
    7  // model's registered contextWindow, read live below via getContextUsage()).
    8  // pi's built-in `read` returns up to ~2000 lines in a single tool result
    9  // — for a small model that one result can blow past the remaining budget, evict
   10  // earlier conversation, and wreck the run. That's exactly the class of failure
   11  // the harness-intervention layer exists to catch (cf. thinking-budget cap,
   12  // write-guard redirect, turn-cap).
   13  //
   14  // When a read result would push context usage past the window, we replace it
   15  // with only the file's first HEAD_LINES lines plus a message telling the model
   16  // why it was trimmed and to use those lines to understand the structure, then
   17  // locate what it needs with grep/find or a targeted read (offset/limit) — rather
   18  // than re-reading the whole file. The user sees one uniform "harness
   19  // intervention: …" line, like every other intervention.
   20  //
   21  // Why `tool_result`, not `tool_call`: a `tool_call` handler can only `block`
   22  // with a `reason` string (no file content) or mutate `input.limit` (lines but no
   23  // message). Delivering BOTH the first 30 lines AND an explanation in one result
   24  // requires `tool_result`, whose return value replaces the content the model sees
   25  // (ToolResultEventResult.content). The full file is still read from disk (pi
   26  // already caps that at ~2000 lines) but the oversized text never reaches the LLM
   27  // context because we swap it out before it lands.
为什么相信这条结论?查看 3 处证据
小练习 5

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M06 · SECURITY

权限与沙箱:能做什么,在哪里做

审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?

先用一个生活比喻

审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。

这套实现先回答了什么?

不是看到开头是 `ls` 就放行 `ls && rm -rf /`,整条链每一段都要安全。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
shell 权限是分段白名单,并显式检测写重定向.pi/extensions/permission-gate/index.ts:11不是看到开头是 `ls` 就放行 `ls && rm -rf /`,整条链每一段都要安全。
默认执行是宿主 bash,不是容器或内核沙箱.pi/extensions/shell-session/index.ts:6一旦命令通过权限门,它就在你的真实机器上跑;白名单是保安问话,不是墙。
09
L1 · fact · little-permission-001

shell 权限是分段白名单,并显式检测写重定向

先看源码事实

auto 模式把命令按 &&、||、;、| 拆段,所有段都必须命中 safe prefix;在此前先检测写目标,阻止 cat/tee/redirection 绕过。manual 阻止未预批准命令,accept-all 放行供 benchmark 使用。

翻译成白话

不是看到开头是 `ls` 就放行 `ls && rm -rf /`,整条链每一段都要安全。

为什么这对自研重要

字符串白名单比简单前缀更稳,但仍不是操作系统沙箱。

固定提交源码摘录
   11  //   LITTLE_CODER_PERMISSION_MODE=auto|accept-all|manual
   12  //   LITTLE_CODER_BASH_ALLOW="cmd1,cmd2 sub,..."  extra allow-prefixes,
   13  //                                                merged with the built-in list.
   14  //
   15  // Issue #70: the gate used to match only `bash`/`Bash`, so a model that hit a
   16  // refusal could re-run the same thing through the `ShellSession` tool and land
   17  // in an execSync with no gate at all. Every shell-executing tool is listed in
   18  // SHELL_TOOLS now, and they all go through the same whitelist.
   19  
   20  const BUILTIN_SAFE_PREFIXES: readonly string[] = [
   21    "ls", "cat", "head", "tail", "wc", "pwd", "echo", "printf", "date",
   22    "which", "type", "env", "printenv", "uname", "whoami", "id",
   23    "git log", "git status", "git diff", "git show", "git branch",
   24    "git remote", "git stash list", "git tag",
   25    "find ", "grep ", "rg ", "ag ", "fd ", "sed ",
   26    "python ", "python3 ", "node ", "ruby ", "perl ",
   27    "pip show", "pip list", "npm list", "cargo metadata",
   28    "df ", "du ", "free ", "top -bn", "ps ",
   29    "curl -I", "curl --head",
   30    // Routine filesystem scaffolding. Trailing space = word boundary, so
   31    // "cp " matches "cp a b" but not "cpufetch". rm stays off the list by
   32    // design; use LITTLE_CODER_BASH_ALLOW=rm if a deployment needs it.
   33    "cp ", "mv ", "mkdir ", "touch ",
   34  ];
   35  
      … 12 lines omitted; exact range 11–58 …
   48  export function getSafePrefixes(): string[] {
   49    return [...BUILTIN_SAFE_PREFIXES, ...parseExtraPrefixes(process.env.LITTLE_CODER_BASH_ALLOW)];
   50  }
   51  
   52  /**
   53   * True when EVERY command in `command` is whitelisted and none of them writes.
   54   *
   55   * Two hardenings over the original `startsWith` check, both from issue #70:
   56   *
   57   * 1. **Judge every segment.** The check ran on the raw string, so only the
   58   *    first command was ever inspected — `ls && rm -rf /` was "safe" because it
为什么相信这条结论?查看 2 处证据
10
L1 · limitation · little-sandbox-001

默认执行是宿主 bash,不是容器或内核沙箱

先看源码事实

ShellSession 的本地后端直接 execSync(command, shell=/bin/bash),共享 process.cwd,最大 buffer 10MB;TB 模式仅代理给外部 tmux adapter。源码未施加 namespace、seccomp、Seatbelt 或容器隔离。

翻译成白话

一旦命令通过权限门,它就在你的真实机器上跑;白名单是保安问话,不是墙。

为什么这对自研重要

高风险仓库或全自动模式应由外层容器/VM 提供强隔离。

固定提交源码摘录
    6  // Port of local/tools/shell_session.py. Two backends implemented:
    7  //   1. tmux-proxy — when LITTLE_CODER_TB_MODE=1, route every command to the
    8  //      parent TB adapter over the extension_ui_request channel. The parent
    9  //      drives the actual TmuxSession so commands appear in TB's trajectory.
   10  //   2. subprocess — child_process.execSync for local use (GAIA doesn't use
   11  //      ShellSession; this is for local REPL + debugging of TB adapter).
   12  //
   13  // The sentinel-prompt pexpect backend from the Python version (persistent
   14  // bash process with state between calls) is deliberately skipped because
   15  // neither Terminal-Bench nor GAIA requires it; TB uses tmux, GAIA uses Bash.
   16  
为什么相信这条结论?查看 3 处证据
小练习 6

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M07 · ECOSYSTEM

指令、MCP、Skills 与插件:能力如何接进来

一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?

先用一个生活比喻

这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。

这套实现先回答了什么?

固定的历史前缀不动,只在最末尾塞这轮真正需要的小纸条,本地模型就不用每轮重算十几万 token 的缓存。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
动态知识放在对话尾部,保护 KV cache.pi/extensions/_shared/inject.ts:1固定的历史前缀不动,只在最末尾塞这轮真正需要的小纸条,本地模型就不用每轮重算十几万 token 的缓存。
技能选择按失败恢复、近期工具、当前意图排序.pi/extensions/skill-inject/index.ts:8它不会把整本工具手册都塞给小模型,只给最可能马上用到的几张卡;刚失败过的工具优先补课。
算法知识以关键词打分并反向声明所需工具.pi/extensions/knowledge-inject/index.ts:8题目像动态规划才临时塞动态规划小抄;而且小抄若要求某工具,会顺带保证那张工具说明也进来。
11
L1 · fact · little-prompt-001

动态知识放在对话尾部,保护 KV cache

先看源码事实

技能、知识、计划与研究 brief 默认作为隐藏 custom message 放在用户消息后,而不是改 system prompt;字节相同的块不重复注入。可用环境变量切回旧 system 模式。

翻译成白话

固定的历史前缀不动,只在最末尾塞这轮真正需要的小纸条,本地模型就不用每轮重算十几万 token 的缓存。

为什么这对自研重要

对本地推理,prompt 稳定性本身就是性能架构。

固定提交源码摘录
    1  // Where little-coder's per-turn context augmentation lands (issue #73).
    2  //
    3  // Four extensions add a block of guidance to a turn: skill-inject (tool skill
    4  // cards + the research directive), knowledge-inject (algorithm reference
    5  // entries), plan-mode (planning instructions + research), and deep-research
    6  // (the report brief). All four used to append to the SYSTEM PROMPT.
    7  //
    8  // That destroyed the KV cache. The system prompt is the first thing in the
    9  // request, so changing it invalidates the entire cached prefix — and these
   10  // blocks are recomputed per turn from the user's prompt, so they changed
   11  // almost every turn. manueloverride caught it with `cache-hunter`: llama.cpp
   12  // re-churning 120k of message history "for no reason" mid-conversation.
   13  //
   14  // pi already has the right hook. `before_agent_start` may return a `message`
   15  // instead of a `systemPrompt` (core/extensions/types.d.ts::
   16  // BeforeAgentStartEventResult); pi appends it AFTER the user's message
   17  // (core/agent-session.js) and converts `role: "custom"` to a `user` message on
   18  // the way to the provider (core/messages.js::convertToLlm). So the block lands
   19  // at the TAIL of the conversation with every preceding byte untouched — the
   20  // prefix stays cached and only the new tokens are processed.
   21  //
   22  // The recency argument that put these blocks last in the system prompt gets
   23  // stronger, not weaker: small models weight the end of the context most, and
   24  // the conversation tail is as late as it gets.
   25  //
   26  // `LITTLE_CODER_INJECT_MODE=system` restores the old system-prompt behavior,
   27  // which is what the whitepaper scaffold reproduction was measured against.
为什么相信这条结论?查看 3 处证据
12
L1 · fact · little-skills-001

技能选择按失败恢复、近期工具、当前意图排序

先看源码事实

skills/tools/*.md 带 target_tool 和 token_cost;每轮在默认 300 token 内先放上次失败工具,再放近期工具,最后按用户关键词预测,并过滤当前 allowed-tools。研究任务额外注入 browse→EvidenceAdd→再写的指令。

翻译成白话

它不会把整本工具手册都塞给小模型,只给最可能马上用到的几张卡;刚失败过的工具优先补课。

为什么这对自研重要

技能是按预算路由的运行时知识,而非全量静态 prompt。

固定提交源码摘录
    8  // ── Tool-skill registry ─────────────────────────────────────────────────
    9  // Port of local/skill_augment.py. Loads skills/tools/*.md once, hooks
   10  // `before_agent_start` to add a `## Tool Usage Guidance` block to the turn.
   11  // Per-user-prompt selection using the whitepaper's 3-priority algorithm
   12  // (error recovery > recency > intent). Budget-guarded, cached.
   13  //
   14  // The block is delivered as a tail message rather than appended to the system
   15  // prompt — see _shared/inject.ts for why (issue #73: it was invalidating the
   16  // KV cache on every turn).
   17  
   18  interface ToolSkill {
   19    targetTool: string;
   20    body: string;
   21    tokenCost: number;
   22  }
   23  
   24  const skills = new Map<string, ToolSkill>();
   25  const selectionCache = new Map<string, string>();
   26  let loaded = false;
   27  
   28  // State tracked across the session so we have error-recovery + recency
   29  // signals by the time the next `before_agent_start` fires.
   30  const recentToolCalls: string[] = []; // most-recent-first, capped at 8
   31  let lastFailedTool: string | null = null;
为什么相信这条结论?查看 4 处证据
13
L1 · fact · little-knowledge-001

算法知识以关键词打分并反向声明所需工具

先看源码事实

skills/knowledge 与 skills/protocols 条目按单词 1 分、短语 2 分打分,低于 2 不选,单条最多 150 token、总预算默认 200;当系统 prompt 已超过窗口 40% 或为 subtask 时不注入,并将 requires_tools 发布给 skill injector。

翻译成白话

题目像动态规划才临时塞动态规划小抄;而且小抄若要求某工具,会顺带保证那张工具说明也进来。

为什么这对自研重要

知识选择与工具可用性形成联动,但关键词法的召回精度有限。

固定提交源码摘录
    8  // ── Knowledge-entry registry ────────────────────────────────────────────
    9  // Port of local/knowledge_augment.py. Loads skills/knowledge/*.md plus the
   10  // three root-level protocol skills (skills/protocols/*.md). Scores entries
   11  // against the user's prompt, selects top within budget, publishes
   12  // `requires_tools` on systemPromptOptions so skill-inject can include them.
   13  //
   14  // Like skill-inject, the selected entries ride in as a tail message rather
   15  // than a system-prompt append (issue #73 — see _shared/inject.ts).
   16  
   17  interface KnowledgeEntry {
   18    topic: string;
   19    body: string;
   20    tokenCost: number;
   21    keywords: string[];
   22    requiresTools: string[];
   23  }
   24  
   25  const entries = new Map<string, KnowledgeEntry>();
   26  const cache = new Map<string, string>();
   27  let loaded = false;
   28  
   29  const MIN_SCORE_THRESHOLD = 2.0;
   30  const PER_ENTRY_CAP = 150;
   31  
   32  function dirs(): string[] {
   33    const here = dirname(fileURLToPath(import.meta.url));
   34    const repo = join(here, "..", "..", "..");
   35    return [join(repo, "skills", "knowledge"), join(repo, "skills", "protocols")];
为什么相信这条结论?查看 3 处证据
小练习 7

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M08 · COLLABORATION

子 Agent:把一个大任务拆成可治理的协作

什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?

先用一个生活比喻

不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。

这套实现先回答了什么?

子任务的搜索过程不会把父模型记忆塞满,父亲只看一页简报。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
子 Agent 是独立 little-coder 进程,父上下文只收短报告.pi/extensions/subagent/index.ts:12子任务的搜索过程不会把父模型记忆塞满,父亲只看一页简报。
子 Agent 工具能力收窄且禁止递归 dispatch.pi/extensions/subagent/spawn.ts:25孩子能查资料但不能改仓库,也不能再生孙子;本地单 GPU 默认串行,避免所谓并行反而拖慢。
Plan Mode 本身就是一条多 Agent 工作流.pi/extensions/plan-mode/index.ts:15它不是让一个模型说一句“我先计划”,而是先派侦察、再问人、最后写方案。
Deep Research 用生产同一 pipeline 做多波次研究.pi/extensions/deep-research/pipeline.ts:1它像一个小型研究团队:先定题、分工、第一轮搜集,再专门找遗漏补第二轮;评测调用的也是同一套生产函数。
14
L1 · fact · little-subagent-001

子 Agent 是独立 little-coder 进程,父上下文只收短报告

先看源码事实

dispatch 最多接收 4 个任务;每个 child 以 launcher 的 headless JSON 模式、独立上下文、同模型启动。全 transcript 只放 UI details,父模型只收到 ≤2000 chars 报告。

翻译成白话

子任务的搜索过程不会把父模型记忆塞满,父亲只看一页简报。

为什么这对自研重要

这是上下文隔离型协作,不是共享黑板式多 Agent。

固定提交源码摘录
   12  // The `dispatch` tool: the main little-coder spawns isolated child little-coder
   13  // sessions ("sub-coders") to research a focused question — they read the repo
   14  // and browse online, then return a CONCISE report. The full child transcript
   15  // lives in the tool's `details` (UI-only); only the short report enters the
   16  // parent model's context. A live panel above the input tracks them while they
   17  // run. See spawn.ts for the engine and the read-only constraints.
   18  
   19  const MAX_PARALLEL = 4;
   20  
为什么相信这条结论?查看 4 处证据
15
L1 · fact · little-subagent-002

子 Agent 工具能力收窄且禁止递归 dispatch

先看源码事实

child 环境设置 allowed-tools 为 read/search/browser/bash,不含 edit/write/dispatch,并把 permission mode 设为 auto;tool-gating 对任何不在集合的 tool_call 返回 block。默认并发 1,可配置,支持 watchdog、SIGTERM→SIGKILL 和超时重试一次。

翻译成白话

孩子能查资料但不能改仓库,也不能再生孙子;本地单 GPU 默认串行,避免所谓并行反而拖慢。

为什么这对自研重要

能力交集和递归上限应在执行面强制,而不只写在子 Agent prompt 里。

固定提交源码摘录
   25  // Tools a sub-coder may use: read + search + browse online + read-only bash.
   26  // Enforced by the tool-gating extension in the child. Deliberately omits
   27  // edit/write (children never mutate the tree) and `dispatch` (no fan-out bombs).
   28  export const SUBCODER_ALLOWED_TOOLS = [
   29    "read",
   30    "grep",
   31    "glob",
   32    "find",
   33    "ls",
   34    "bash",
   35    "webfetch",
   36    "websearch",
   37    "BrowserNavigate",
   38    "BrowserClick",
   39    "BrowserType",
   40    "BrowserScroll",
   41    "BrowserExtract",
   42    "BrowserBack",
   43    "BrowserHistory",
   44  ].join(",");
为什么相信这条结论?查看 4 处证据
16
L1 · fact · little-plan-001

Plan Mode 本身就是一条多 Agent 工作流

先看源码事实

计划模式先用 reasoning child 拆出 1–4 个探索任务,再跑只读 explorer,生成 1–3 个澄清问题让用户选择,最后把 digest 和答案隐藏注入主 Agent 写计划;合成阶段启用 edit/write guard。

翻译成白话

它不是让一个模型说一句“我先计划”,而是先派侦察、再问人、最后写方案。

为什么这对自研重要

阶段式 Harness 能把计划质量从 prompt 风格提升为可执行工作流。

固定提交源码摘录
   15  // Plan Mode — a Claude-Code-style "research, ask, then plan" flow.
   16  //
   17  // ctrl+q toggles plan mode (an indicator appears below the input). While it is
   18  // on, submitting a prompt does NOT run a normal coding turn; instead the
   19  // extension orchestrates:
   20  //   1. decompose the request into 1-4 exploration tasks (a reasoning sub-coder),
   21  //   2. dispatch those as read-only explorer sub-coders (isolated context; only
   22  //      their concise reports survive — their transcripts never enter this window),
   23  //   3. generate 1-3 clarifying questions with suggested answers (a sub-coder),
   24  //   4. ask them via the UI (with a free-text "Other" option),
   25  //   5. synthesize the reports + answers into a written plan in the main window,
   26  //   6. exit plan mode.
   27  //
   28  // An extension can't call inference directly, so every reasoning step is a
   29  // child little-coder (spawned via ../subagent/spawn.ts), and the final plan is
   30  // injected as a normal turn via pi.sendUserMessage so it lands in the chat.
   31  //
   32  // ctrl+q is unbound by pi AND by the emacs-style editor (which claims nearly
   33  // every other ctrl+<letter> — ctrl+y is its yank/paste, ctrl+a/e line motion,
   34  // etc.), so the extension can claim it cleanly without a conflict warning or
   35  // shadowing a built-in (shift+tab stays pi's thinking-level cycle — issue #47).
为什么相信这条结论?查看 3 处证据
17
L1 · fact · little-research-001

Deep Research 用生产同一 pipeline 做多波次研究

先看源码事实

pipeline 顺序是澄清→brief→lead 分解→wave 1→gap 分析→可选 wave 2;研究 child 禁 bash、要求实际 URL 与不猜测,推理 child 只拿本地只读工具;每 child 有超时,失败不阻断兄弟与后续阶段,meta 记录耗时、失败和 JSON fallback。

翻译成白话

它像一个小型研究团队:先定题、分工、第一轮搜集,再专门找遗漏补第二轮;评测调用的也是同一套生产函数。

为什么这对自研重要

工作流可测性较好,但多子进程对本地推理延迟很敏感。

固定提交源码摘录
    1  // UI-agnostic research phase engine — the single source of truth for the
    2  // Scope → Research pipeline (phases 1-4). Both the interactive flow
    3  // (index.ts orchestrate()) and the headless batch eval drive THIS function, so
    4  // the eval measures the same code production runs. The WRITE phase (step 5) is
    5  // intentionally NOT here: the interactive flow hands it to the main agent, and
    6  // the eval writes via a dedicated sub-coder — both take {brief, digest} from the
    7  // result below.
    8  //
    9  // Every reasoning step is a child little-coder (an extension can't call
   10  // inference directly); research waves fan out read-only research sub-coders.
   11  // UI is injected via hooks so this module has no ctx/widget dependency.
为什么相信这条结论?查看 4 处证据
小练习 8

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M09 · STATE

会话、持久化与观测:让一次运行变成可追溯事实

如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?

先用一个生活比喻

内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。

这套实现先回答了什么?

引用依据不只躺在长聊天里,而是放到一个小抽屉;聊天被总结后,抽屉还在。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
证据是 session-scoped 结构化对象,并显式跨压缩.pi/extensions/evidence/index.ts:5引用依据不只躺在长聊天里,而是放到一个小抽屉;聊天被总结后,抽屉还在。
质量监控会 steer 自纠,但最多连续两次.pi/extensions/quality-monitor/index.ts:5模型答歪时 Harness 会马上插一句纠偏,但不会无限唠叨把自己困进循环。
checkpoint 是 best-effort 文件快照,且存在 path 键兼容缺口.pi/extensions/checkpoint/index.ts:6它有安全网,但不是保证能回滚的事务;而且某些正常工具参数形态可能连网都没张开。
18
L1 · fact · little-evidence-001

证据是 session-scoped 结构化对象,并显式跨压缩

先看源码事实

EvidenceAdd/Get/List 保存 source、note、≤1KB snippet 到进程内 session map;消息 compaction 不会删除该 extension state,session_compact 后发送 bridge 提醒模型仍可查询。

翻译成白话

引用依据不只躺在长聊天里,而是放到一个小抽屉;聊天被总结后,抽屉还在。

为什么这对自研重要

可寻址的结构化证据比让模型从摘要里回忆来源可靠,但进程退出后不持久。

固定提交源码摘录
    5  // Port of local/tools/evidence.py. Per-session in-memory store of evidence
    6  // entries. GAIA requires cite-before-answer, and these entries survive
    7  // compaction (Phase 10's evidence-compact extension preserves them).
    8  
    9  const SNIPPET_CAP = 1024;
   10  
   11  interface EvidenceEntry {
   12    id: string;
   13    source: string;
   14    note: string;
   15    snippet: string;
   16  }
   17  
   18  // Map<sessionId, entries[]>
   19  const stores = new Map<string, EvidenceEntry[]>();
   20  
   21  function sessionKey(): string {
   22    return process.env.LITTLE_CODER_SESSION_ID || "default";
   23  }
   24  
   25  function bucket(): EvidenceEntry[] {
   26    const key = sessionKey();
   27    let b = stores.get(key);
   28    if (!b) {
   29      b = [];
      … 2 lines omitted; exact range 5–42 …
   32    return b;
   33  }
   34  
   35  // Exported so tests and the evidence-compact extension can reach in.
   36  export function resetSessionStore(sessionId?: string): void {
   37    stores.delete(sessionId ?? sessionKey());
   38  }
   39  
   40  export function getSessionStore(sessionId?: string): EvidenceEntry[] {
   41    return stores.get(sessionId ?? sessionKey()) ?? [];
   42  }
为什么相信这条结论?查看 3 处证据
19
L1 · fact · little-quality-001

质量监控会 steer 自纠,但最多连续两次

先看源码事实

turn_end 基于回答文本、当前与上一轮工具调用及观察到的 known-tools 评估失败模式;非 aborted 失败会立即 steer correction,连续超过 2 次则停止纠正并发出 intervention。

翻译成白话

模型答歪时 Harness 会马上插一句纠偏,但不会无限唠叨把自己困进循环。

为什么这对自研重要

自动修复必须带 backoff 和中断语义。

固定提交源码摘录
    5  // Port of local/quality.py. Hooks turn_end, inspects the assistant message
    6  // + previous turn's tool calls, and — if we detect a failure mode — sends
    7  // a correction user message with deliverAs:"steer" so the model gets it
    8  // immediately on its next turn rather than waiting for the next user input.
    9  
   10  // Session-scoped state. Pi reuses extensions across turns within a session;
   11  // a fresh extension instance is loaded per session via the session lifecycle.
   12  let previousToolCalls: ToolCall[] = [];
   13  let consecutiveFailures = 0;
   14  const MAX_CONSECUTIVE_CORRECTIONS = 2; // stop nudging after 2 failed corrections
   15  
   16  export default function (pi: ExtensionAPI) {
   17    // Populate the known-tools set lazily by observing tool_execution events.
   18    // This avoids needing to read pi's tool registry directly.
为什么相信这条结论?查看 2 处证据
20
L1 · limitation · little-checkpoint-001

checkpoint 是 best-effort 文件快照,且存在 path 键兼容缺口

先看源码事实

首次 Write/Edit 前把原文件或 absent sentinel 写到 ~/.little-coder/checkpoints/<session>;异常全部吞掉。当前 tool_call handler 只读取 input.file_path,而同仓库的现代 pi 守卫已同时兼容 path/file_path,因此 path 形态的内置工具调用可能不备份。

翻译成白话

它有安全网,但不是保证能回滚的事务;而且某些正常工具参数形态可能连网都没张开。

为什么这对自研重要

不能把该 checkpoint 当成 Git 事务或灾难恢复承诺,建设自有 Agent 时应统一 canonical ToolIntent 后再做副作用审计。

固定提交源码摘录
    6  // Port of checkpoint/hooks.py. Snapshots a file's contents before a Write
    7  // or Edit tool modifies it. First-write-wins per session (don't re-backup
    8  // a file already tracked this session). Backups land in
    9  // ~/.little-coder/checkpoints/<session>/.
   10  
   11  const tracked = new Map<string, Set<string>>(); // sessionId -> absolute paths
   12  
   13  function checkpointDir(sessionId: string): string {
   14    const dir = join(homedir(), ".little-coder", "checkpoints", sessionId);
   15    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
   16    return dir;
   17  }
   18  
   19  function safeName(filePath: string): string {
   20    return filePath.replace(/[^A-Za-z0-9._-]/g, "_").slice(-200);
   21  }
   22  
   23  function backupIfNeeded(sessionId: string, filePath: string): void {
   24    if (!sessionId || !filePath) return;
   25    let session = tracked.get(sessionId);
   26    if (!session) {
   27      session = new Set();
   28      tracked.set(sessionId, session);
   29    }
   30    if (session.has(filePath)) return;
      … 4 lines omitted; exact range 6–45 …
   35        writeFileSync(join(checkpointDir(sessionId), safeName(filePath)), content);
   36      } else {
   37        // Sentinel: file didn't exist before modification
   38        writeFileSync(
   39          join(checkpointDir(sessionId), safeName(filePath) + ".absent"),
   40          "",
   41        );
   42      }
   43    } catch {
   44      // Silent — checkpointing is best-effort
   45    }
为什么相信这条结论?查看 3 处证据
小练习 9

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M10 · ENGINEERING

测试、恢复与工程取舍:把漂亮机制变成可靠产品

哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?

先用一个生活比喻

这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 10

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M11 · PRACTICE

把读懂变成会判断

下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。

Q1

它是 pi 的 Harness 增强层,而不是另一套 Agent 内核

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

分析和选型时必须把 little-coder 的差异能力与 pi 基座分开计分。

证据:package.json:33
Q2

扩展来源分层,默认固定集合,pi 生态桥显式 opt-in

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

这是“小模型少即是多”的插件治理取舍。

证据:bin/little-coder.mjs:157
Q3

80% 中途压缩 watchdog 补上 pi 的长自主运行缺口

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

长自治任务的上下文治理不能只挂在用户轮边界。

证据:.pi/extensions/context-watchdog/index.ts:3
Q4

超大 Read 结果在进入 LLM 前缩成 30 行

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

工具输出也是上下文预算的一等公民,不能只压聊天历史。

证据:.pi/extensions/read-guard/index.ts:4
Q5

动态知识放在对话尾部,保护 KV cache

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

对本地推理,prompt 稳定性本身就是性能架构。

证据:.pi/extensions/_shared/inject.ts:1
APPENDIX · SOURCE INDEX

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01package.jsonL33–43
  2. 02bin/little-coder.mjsL84–139, L285–354, L481–513, L157–217, L219–227, L338–367
  3. 03.pi/extensions/context-watchdog/index.tsL3–29, L59–107, L153–223
  4. 04.pi/extensions/read-guard/index.tsL4–27, L29–82, L108–153
  5. 05.pi/extensions/_shared/inject.tsL1–27, L29–65, L67–86
  6. 06.pi/extensions/skill-inject/index.tsL8–31, L75–132, L169–178, L198–270
  7. 07.pi/extensions/knowledge-inject/index.tsL8–35, L38–77, L93–156
  8. 08.pi/extensions/write-guard/index.tsL35–75, L119–168, L158–205
  9. 09.pi/extensions/write-guard/write-guard.test.tsL197–279
  10. 10.pi/extensions/read-guard-edit/index.tsL4–29, L59–89, L32–53
  11. 11.pi/extensions/read-guard-edit/read-guard-edit.test.tsL34–99
  12. 12.pi/extensions/permission-gate/index.tsL11–58, L60–121
  13. 13.pi/extensions/shell-session/index.tsL6–16, L24–38, L65–96
  14. 14.pi/extensions/subagent/index.tsL12–20, L49–73
  15. 15.pi/extensions/subagent/spawn.tsL46–77, L251–320, L25–44, L91–121, L333–375
  16. 16.pi/extensions/tool-gating/index.tsL8–37
  17. 17.pi/extensions/plan-mode/index.tsL15–35, L95–147, L186–278
  18. 18.pi/extensions/deep-research/pipeline.tsL1–11, L30–61, L160–220, L223–290
  19. 19.pi/extensions/evidence/index.tsL5–42, L49–118
  20. 20.pi/extensions/evidence-compact/index.tsL4–31
  21. 21.pi/extensions/quality-monitor/index.tsL5–18, L30–79
  22. 22.pi/extensions/llama-cpp-provider/index.tsL13–21, L43–86, L89–121
  23. 23.pi/extensions/checkpoint/index.tsL6–45, L48–65
  24. 24.pi/extensions/output-parser/index.tsL5–20, L38–85
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 Little Coder 报告 ↗