Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

概览 (Overview)

这章的任务不是列目录,而是先让你在脑子里跑起一次真正的 NanoCodeAgent 任务。

1. 为什么这一章重要? (Why)

如果你第一次进入这个仓库,最容易迷路的地方不是“文件太多”,而是系统看起来像同时在做很多事:CLI、配置、workspace、HTTP streaming、tool calls、tool registry、bash、安全边界、测试、文档自动化。把这些名词逐个看一遍,并不会自然拼成一个系统。

NanoCodeAgent 真正要解决的问题其实更具体:给它一个任务后,runtime 如何在本地把这件事安全地跑完,而且在需要调用工具时仍然保持可控、可停、可验证。理解了这条主线,后面的章节才不会变成零散模块说明。

2. 整体图景 (Big Picture)

先把 NanoCodeAgent 想成一个受约束的本地执行宿主,而不是一个“什么都能做”的自由代理。模型负责提出下一步要说什么、要调用什么工具;宿主负责决定这些请求在当前策略下能不能执行、能执行到什么程度,以及什么时候必须停下来。

从代码结构上看,这条主线大致分成三层:

  • 入口层:src/config.cppsrc/cli.cppsrc/main.cpp 把这次运行的条件固定下来。
  • 执行层:src/llm.cppsrc/tool_call_assembler.cppsrc/agent_loop.cpp 把模型响应变成一轮轮可消费的 assistant turn。
  • 约束层:src/tool_registry.cppsrc/workspace.cpp、各类工具 executor 和 tests/ 把“能不能执行、最多执行到哪里、失败后怎么办”钉死成行为合同。

系统总览图如下。这张图现在只负责回答“运行时由哪些层组成、它们大致如何衔接”,不再同时承担完整任务流说明;真正的主任务推进顺序留给下一张图。

flowchart LR
    UserTask[User Task]

    subgraph Entry[Entry Layer]
        EntryHost[CLI + Config + Main Entry]
    end

    subgraph Runtime[Runtime Loop]
        RuntimeHost[Agent Loop + LLM Turn Handling]
    end

    subgraph Guardrails[Execution Guardrails]
        GuardrailHost[Registry + Executors + Workspace Boundary]
    end

    subgraph Assurance[Assurance]
        Tests[Tests]
    end

    UserTask --> EntryHost
    EntryHost --> RuntimeHost
    RuntimeHost --> GuardrailHost
    GuardrailHost --> RuntimeHost
    Tests -. prove contracts for .-> RuntimeHost
    Tests -. prove contracts for .-> GuardrailHost

这张图现在只展示系统地图:任务先进入入口层,再进入 runtime,再受执行边界约束,最后由测试层证明这些行为合同成立。它不展开 LLM bridgeTool Call Assembler、工具分类或 approval 细节;这些细节分别在 HTTP 与 LLM 流式解析工具与安全边界测试策略 里展开。这样第一张图先给读者地图,而不是把实现细节提前塞进总览。

3. 主流程 (Main Flow)

一个任务进入系统后,不是直接“交给模型然后等结果”,而是会先经过一连串前置固定动作。

src/main.cpp 先调用 config_init()cli_parse(),把默认值、配置文件、环境变量和命令行覆盖关系整理成一份最终配置。接着它创建并规范化 workspace,生成 system prompt 和工具 schema,然后才选择 real 或 mock 模式进入 agent_run()

进入 agent_run() 后,系统会维护一份 messages 历史。每一轮开始前,它先检查最大轮数和上下文大小;随后把消息历史交给 LLM 层。真实网络模式下,src/llm.cpp 会发起 streaming 请求,把 SSE 事件里的内容增量拼成 assistant 文本,把碎片化的 tool_calls 交给 src/tool_call_assembler.cpp 拼完整,最后返回一条已经物化好的 assistant message。

如果这一轮 assistant 没有请求工具,agent loop 就把最终回答输出并结束。如果 assistant 返回了 tool_callssrc/agent_loop.cpp 会顺序解析和执行这些调用。真正的执行入口是 ToolRegistry: 模型可以请求工具,但宿主是否允许执行,要看工具类别和当前配置。只读工具默认可用;变更工具和执行工具默认会被挡住,除非明确打开相应策略。

主流程图如下:

sequenceDiagram
    participant User
    participant Entry as MainEntry
    participant Runtime as AgentLoop
    participant LLM as LlmBridge
    participant Reg as ToolRegistry
    participant Tool as ToolExecutor

    User->>Entry: prompt + flags + env + config
    Entry->>Entry: resolve config and workspace
    Entry->>Runtime: agent_run(config, prompt, tools)
    loop eachTurn
        Runtime->>Runtime: enforce context and turn limits
        Runtime->>LLM: messages + tool schema
        LLM-->>Runtime: assistant content or tool_calls
        alt no tool_calls
            Runtime-->>User: final answer
        else has tool_calls
            Runtime->>Reg: execute tool call
            Reg->>Reg: apply approval policy
            Reg->>Tool: run selected executor
            Tool-->>Runtime: structured tool result
            Runtime->>Runtime: append tool message
            Runtime->>Runtime: fail-fast on blocked/failed/timed_out
        end
    end

4. 一个任务是怎么跑完的? (Worked Example)

假设你给 NanoCodeAgent 一个很典型的任务:summarize src/main.cpp

在默认配置下,runtime 首先会把这个任务文本当作 user 消息放进历史里。模型收到上下文和工具 schema 之后,很可能先流出一小段自然语言,比如“我先看看入口文件”,同时请求一个只读工具,例如 read_file_safe {"path":"src/main.cpp"}

这时真正决定结果的不是模型“有没有礼貌”,而是宿主的策略与边界:

  • ToolRegistry 会发现这是只读工具,因此不需要额外批准。
  • read_file_safe() 会把路径限制在 workspace 内,并返回结构化 JSON 结果。
  • agent_loop 把这条工具结果追加回消息历史,再进入下一轮。

如果下一轮模型只给出总结文本,运行结束;如果它进一步请求写文件或执行 shell,而对应 approval 开关没有打开,registry 会返回 blocked,而 loop 会把这视为污染状态并停止。这个 worked example 的重点,不是“模型多聪明”,而是“宿主如何把模型请求变成可控的本地动作”。

5. 模块职责要放在流程里看 (Module Roles)

  • src/config.cppsrc/cli.cpp:决定这次运行的真实条件,而不是执行业务逻辑。它们回答的是“这次运行允许什么、上限是多少、模式是什么”。
  • src/main.cpp:像装配层,把配置、workspace、system prompt、tool schema 和 LLM 回调装成一套可运行的 runtime。
  • src/llm.cpp:把模型 streaming 响应翻译成 runtime 能消费的 assistant message;它不是单纯的网络薄封装。
  • src/tool_call_assembler.cpp:负责把碎片化 tool-call 参数拼回完整 JSON,这是流式模式能稳定工作的关键桥梁。
  • src/tool_registry.cpp:是策略门。它不负责生成工具请求,只负责判断“当前这个工具在这次运行里能不能被执行”。
  • src/agent_loop.cpp:是调度中心。它维护消息历史、控制多轮推进、限制工具数量与上下文膨胀,并在失败时立即停机。
  • tests/:不是附属品,而是这些边界的行为合同来源。

6. 作为贡献者,你通常怎么读这本书? (What You Usually Do)

如果你最关心 runtime 本身,推荐按这条路径阅读:

  1. 先读本章,建立“任务如何进入系统并被宿主约束”的整体地图。
  2. 再读 CLI、配置与工作区,理解运行条件和路径边界是怎么被固定的。
  3. 接着读 HTTP 与 LLM 流式解析,看模型响应如何被变成可消费的 assistant turn。
  4. 然后读 工具与安全边界,理解 approval、registry、executor 和 fail-fast 是怎么配合的。
  5. 最后读 测试策略,看这些边界是如何被证明为真的。

只有当你已经理解了 runtime 主线,再去读 文档自动化 才最有效。那一章不是在解释 agent 如何执行任务,而是在解释仓库如何维护自己的正式文档质量。

7. 边界、误解与项目位置 (Boundaries / Pitfalls)

最常见的误解,是把 NanoCodeAgent 想成“模型直接控制机器”。当前代码更接近另一种结构:模型负责提议,宿主负责决定、执行和刹车。approval policy、workspace boundary、tool output limit、context limit 和 fail-fast 共同构成了这条主线的真实边界。

第二个误解,是把 doc automation 和 runtime 混成同一件事。它们确实都体现了“边界先于自动化”的思想,但 runtime 解决的是任务如何被安全执行,doc automation 解决的是文档如何跟代码事实保持一致。对新读者来说,先把 runtime 跑通,才有资格理解后者为什么重要。

最后要保持克制:当前系统已经具备清晰的本地代理宿主结构,但它并不是一个无限制、无限恢复能力的自治体。很多地方刻意选择了 fail-fast 和明确上限,而不是“尽量继续跑下去”。

8. 继续深入 (Dive Deeper)

CLI、配置与工作区 (CLI, Config & Workspace)

这章讲的是代理真正开始运行之前,系统如何先把三件事固定下来:入口参数、配置来源,以及“哪些路径算工作区内”。

1. 为什么需要这一层? (Why)

NanoCodeAgent 不是单纯把一段 prompt 交给模型就结束的程序。它要决定用哪个模型、从哪里读取密钥、这次运行采用哪组配置,以及哪些文件路径允许被访问。

如果这些事情没有固定顺序,运行结果就会变得不可预测。更严重的是,模型一旦拿到模糊的路径边界,后面的读写工具就很容易把“当前项目里的文件”和“主机上的任意路径”混在一起。CLI、配置和工作区这一层的职责,就是先把运行条件说清楚,再把后面的能力放进受控范围里。

2. 整体图景 (Big Picture)

这一层的主线可以概括成三个连续动作。程序先建立一份默认配置,再从配置文件和环境变量里补齐它,最后让 CLI 参数做最后覆盖。等这些值固定后,src/main.cpp 才会创建或规范化工作区路径,并把这份配置交给后面的 LLM、工具层和 agent loop。

这里最重要的不是“参数很多”,而是“覆盖关系明确”。同一个字段如果同时出现在默认值、配置文件、环境变量和命令行里,最终以命令行为准;而“该读哪个配置文件”这件事本身,也先看 --config,找不到才回退到 NCA_CONFIG

3. 主流程 (Main Flow)

实际启动顺序在 src/main.cpp 很清楚。程序先调用 config_init(argc, argv) 建立配置初值,再调用 cli_parse(argc, argv, config) 让命令行参数覆盖已有配置。只有 CLI 通过后,程序才初始化日志、创建或规范化工作区目录,并检查真实网络模式下是否已经提供 API key。

配置加载本身也分层进行。src/config.cpp 先设置默认值,然后扫描 --config--config=...,如果命令行没有指定,再看 NCA_CONFIG;找到配置文件后,只按简单的 key=value 形式读取内容,忽略空行以及 #; 注释。之后环境变量用 NCA_ 前缀再次覆盖,最后 CLI 在 src/cli.cpp 里做最后一层覆盖。

工作区边界是在配置稳定之后才建立的。src/main.cpp 会把 config.workspace 规范化成绝对路径,后续文件类工具再通过 workspace_resolve() 检查相对路径是否仍位于这个基准目录之内。也就是说,工作区不是“随时变化的当前目录”,而是后续工具共享的一条固定边界。

4. 模块职责 (Module Roles)

  • src/main.cpp:负责编排启动顺序,决定什么时候配置完成、什么时候工作区被固定、什么时候正式进入 agent loop。
  • src/config.cpp:负责默认值、配置文件与环境变量这三层输入;它解决的是“同一个字段从哪里来”。
  • src/cli.cpp:负责命令行解析和最终覆盖;它解决的是“这次运行明确要什么”,并要求必须提供 --execute 这一必填任务参数。
  • src/workspace.cpp:负责把相对路径解释到工作区内,并拒绝绝对路径、空路径和越界路径。
  • src/read_file.cppsrc/write_file.cppsrc/repo_tools.cpp:在工作区解析之上继续做更强的文件系统保护,例如拒绝符号链接穿越、设备文件和超出边界的访问。

5. 你通常会怎么用? (What You Usually Do)

最常见的入口,是先指定工作区和任务,再按需要补充配置来源:

NCA_API_KEY="sk-..." ./build/agent \
  --workspace ./sandbox \
  --config ./agent.conf \
  -e "summarize the repository"

如果你只是想临时覆盖某个值,CLI 是最后一层,所以直接在命令行上传参最明确。比如 --workspace--model--mode--allow-mutating-tools--allow-execution-tools 都会覆盖前面的配置来源。

如果你希望把一组默认运行参数留在仓库外部文件里,可以使用配置文件,但要把它理解成简单的 key=value 清单,而不是完整的通用 INI 系统。当前实现支持的键以 src/config.cpp 中显式处理的字段为准。

6. 边界与易错点 (Boundaries / Pitfalls)

最容易误解的一点是:“工作区边界”并不等于“整个进程被操作系统级沙箱包住”。当前仓库里,强边界主要体现在文件和仓库工具上: 它们会把路径解析到工作区内,再拒绝符号链接穿越、特殊文件和越界访问。

这条边界不能被过度外推到所有执行面。比如 bash_execute_safe() 会在工作区下启动命令、清空子进程环境并施加超时与输出上限,但它不是容器,也不是 chroot 或 seccomp 级别的隔离。因此,这一章更准确的理解方式是:CLI 和配置负责固定运行条件,workspace 负责给文件类工具定义路径边界,而不是“整个系统已经被完全物理隔离”。

另一个常见误区是把旧示例命令直接照抄。当前 CLI 明确要求提供 --execute 对应的任务内容;如果缺少这一项,程序会直接报错退出,而不是进入默认交互。

7. 接下来往哪里看? (Dive Deeper)

大模型网络与流式解析 (HTTP, LLM & Streaming)

这章的核心问题不是“有哪些模块参与了网络层”,而是同一条模型流里,文本增量和 tool-call 参数碎片是如何一路被整理成 agent loop 真正能消费的 assistant turn。

1. 为什么这一层重要? (Why)

如果 NanoCodeAgent 只需要拿到一段完整文本再输出,网络层会简单得多。但代码代理的现实情况不是这样:模型一边生成自然语言,一边还可能提出工具调用;而这些工具调用的参数经常不是一次性给全,而是被拆成很多碎片,甚至和别的输出交错到达。

这就带来一个 runtime 必须解决的问题:系统不能把“正在形成中的意图”误当成“已经完整的调用”。所以这一层真正的价值,不只是流式显示更顺滑,而是把“正在路上的模型输出”安全地翻译成“可以交给下一层决策和执行的结构化消息”。

2. 整体图景 (Big Picture)

把这条链路看成一个流水线最容易理解:

  • src/http.cpp 负责把远端 streaming 响应持续推上来。
  • src/sse_parser.cpp 负责把原始字节切成完整 SSE event。
  • src/llm.cpp 负责解释每个 event 的 JSON 含义。
  • src/tool_call_assembler.cpp 负责把碎片化的 tool-call 参数拼回完整 JSON。
  • src/agent_loop.cpp 只接收最终整理好的 assistant message,并在那之后再施加每轮/全局工具调用上限。

这条流水线最后产出的不是“还带着网络状态的半成品”,而是一条已经物化好的 assistant message:固定带 role=assistant,并按实际情况附带 contenttool_calls。换句话说,这一章讲的不是“HTTP 很重要”,而是“runtime 如何阻止半成品消息越过边界,直接污染后续执行”。

时序图如下。这一处我没有把图拆成“传输解析链”和“tool-call 组装链”两张,因为当前图仍然只回答一个问题:一条 streaming 响应如何在进入 agent_loop 前被整理成完整 assistant message。它的阅读方向稳定、几乎没有交叉线,而且 tool-call 组装只是这条时间线中的一个分支,而不是另一套并列主问题。

sequenceDiagram
    participant Model as LLM Service
    participant HTTP as HTTP Transport
    participant Parser as SSE Parser
    participant Llm as LLM Bridge
    participant Asm as Tool Call Assembler
    participant Runtime as Agent Loop

    Model->>HTTP: streaming chunks
    HTTP->>Parser: raw bytes
    Parser->>Parser: buffer until SSE event boundary
    loop each complete data event
        Parser->>Llm: complete data event
        Llm->>Llm: parse event JSON
        opt content delta present
            Llm->>Llm: append assistant text
        end
        opt tool_call fragments present
            Llm->>Asm: append fragments by index
        end
    end
    Model-->>HTTP: [DONE]
    Asm-->>Llm: finalize tool calls
    Llm-->>Runtime: assistant message {content, tool_calls}

这张图展示的是“流式响应经过传输、事件切分、事件解释和参数组装后,才变成完整 assistant message”的时间顺序。图里把 content 增量和 tool_call 碎片都画成每个 event 中可独立出现的可选步骤,强调它们可以在同一条流里交错到达,而不是互斥分支。它不展开 HTTP 限额细节,也不展开 agent_loop 后续如何审批和执行工具;读法是从上到下沿时间线看每一层接住上一层的产物。

3. 主流程 (Main Flow)

真实网络模式下,src/main.cpp 会把 llm_chat_completion_stream() 作为 agent loop 的 LLM 回调。调用开始时,src/llm.cpp 会根据当前配置组装请求 JSON,包括 modelmessagesstream=true,以及可选的 tools schema;随后它会规范化 base_url,在需要时补上 chat completions 接口。

真正发出请求的是 src/http.cpp。这一层使用的是 libcurl,不是自建 socket 栈。它的职责很明确:持续接收流式响应,并在传输层施加总超时、连接超时、低速超时、TLS 校验和流大小限制。当前默认的总超时是 60s、连接超时是 10s、流大小上限是 100 MB、低速超时窗口是 30s。这说明 transport 的目标不是“尽量把流读完”,而是先保证流不会无限挂住或无限膨胀。

收到 chunk 之后,运行时不会立刻尝试“猜”里面是什么,而是先交给 SseParserSseParser 只负责事件边界:把一串断断续续到来的字节拼成完整的 data: 事件,识别 [DONE] 结束标记,忽略注释行,支持 \n\n\r\n\r\n 两种边界,并把多行 data: 内容用换行拼回一条 payload。它还会限制内部 remainder 缓冲到 256 KB,避免在一直等不到事件结束符时无限累积字节。只有事件完整后,src/llm.cpp 才会开始解析 JSON。

到这一步,流会分叉成两条路:

  • 如果 event 里是 delta.contentllm.cpp 会通过回调把增量文本往外发出,并在流式请求包装层把它累积进最终的 assistant 文本。
  • 如果 event 里是 delta.tool_callsllm.cpp 不会直接执行任何工具,而是把碎片交给 ToolCallAssembler,等整条流结束后再统一 finalize()。组装器会按 index 归并碎片,允许不同调用交错到达;对没有合法整数 index 的碎片会直接忽略,对“最多 32 个 distinct tool call”和“单次调用最多 1 MB 参数缓冲”都设硬上限。

所以 agent_loop 实际上永远看不到半截网络状态。它拿到的是一条已经物化完成的 assistant message:可能有 content,也可能同时包含完整的 tool_calls。这条消息里的 tool_calls[*].function.arguments 仍会保持 OpenAI 风格的 JSON 字符串形态,后面的 agent_loop 会在真正 dispatch 前再解析一次。如果流里出现 API 错误、坏 JSON、用户主动中止、stream 大小超限、SSE remainder 超限,或者 tool-call 参数最终无法拼成合法 JSON,这一层会直接失败,而不是把模糊状态继续往后传。

4. 一个真实例子:半截参数为什么不能提前执行? (Worked Example)

设想模型在分析仓库入口时,想调用 read_file_safe 读取 src/main.cpp。在流式模式下,这个调用很可能不是一次性到达的,而更接近下面这样:

  1. 第一段 event 告诉系统:有一个 tool_call index=0,函数名是 read_file_safe
  2. 第二段 event 才开始给参数前半截,比如 {"path":.
  3. 第三段 event 再补后半截,比如 "src/main.cpp"}
  4. 最后才收到完成标记。

如果系统在第二段就执行,就等于拿着半截 JSON 去赌“模型大概想干什么”。当前实现明确不这么做。tests/test_toolcall_assembler.cpptests/test_stream_robustness.cpp 验证的正是这类场景:参数可以碎片化、可以交错,但只有在 finalize() 真正成功之后,它才会变成可执行的 ToolCall。如果参数始终没有闭合,错误会在组装阶段被明确抛出,并带上出错 index 与尾部片段,方便定位到底是哪一路 tool call 坏掉了。

这次实现里,组装器的 contract 也更明确了:它不是简单地“把字符串拼起来”。对于 tool_calls 内部的局部噪音,它只做不会改变调用语义的最小容错,例如忽略没有合法整数 index 的碎片,只接受每个 index 的第一份非空 id 和函数名,并在必要时给缺失 id 的调用补一个 call_<index> 回退值。若某条调用始终没有参数片段,最终会落成空对象 {};但真正会影响执行语义的问题,例如参数 JSON 最终不能闭合,仍然必须在 finalize() 前被挡住。

这也是为什么 src/llm.cppsrc/tool_call_assembler.cpp 的边界必须分清:前者负责理解 event,后者负责等待“足够完整”。继续往后,src/agent_loop.cpp 才会对已经成形的 tool_calls 施加 max_tool_calls_per_turnmax_total_tool_calls 之类的运行时闸门;默认值分别是 850,也可以通过配置文件、环境变量或命令行覆盖,防止“虽然每个调用都完整,但整体数量已经失控”的情况。

5. 模块职责要放在上下文里看 (Module Roles)

  • src/http.cpp 负责传输和传输层限制。它知道什么时候流超时、什么时候流太大、什么时候调用方主动要求中止,但它并不理解 SSE 语义,更不会理解 tool call 参数。
  • src/sse_parser.cpp 负责把 chunk 切成完整 event。它知道“哪里是一条事件”,负责处理注释、多行 data:,同时兼容 \n\n\r\n\r\n 两种边界,并限制未完成 event 的残余缓冲,但不知道事件里的 JSON 是否合理。
  • src/llm.cpp 是真正的桥接层。它把 event 解析成内容增量或 tool-call 碎片,遇到 API 错误、坏 JSON 或用户回调中止时立即失败,并在结束时把通过校验的结果整理成 assistant message。
  • src/tool_call_assembler.cpp 负责把同一个 index 下的参数字符串累积起来,并在最终阶段统一解析成 JSON。它不只是“拼字符串”,还会支持交错到达的多路调用、限制最多 32 个 distinct tool call、限制单个调用最多 1 MB 参数缓冲,并在失败时给出带 index 和尾部片段的报错。
  • src/agent_loop.cpp 不直接处理原始流。它只消费最终成形的 assistant message,在真正 dispatch 前重新解析 tool_calls[*].function.arguments,然后再决定下一轮是否要执行工具;如果同一轮工具过多、累计工具过多,或某个工具执行失败/超时,它会在这一层停下,而不是继续把污染状态带入下一轮。

6. 作为贡献者,你通常怎么读这条链路? (What You Usually Do)

如果你想排查 streaming 行为,最有效的顺序不是从网络开始一路盲读,而是按“哪一层负责什么”来读:

  1. 先看 src/llm.cpp,确认 assistant message 的最终形状是什么。
  2. 再看 src/sse_parser.cpp,理解 event 是怎么从碎片流里被切出来的。
  3. 然后看 src/tool_call_assembler.cpp,确认参数在哪一步才算“完整”。
  4. 最后回到 src/http.cpp 和相关测试,理解传输层限制和失败模式。

如果你正在定位 streaming bug,对应的测试入口通常是:

  • tests/test_sse_parser.cpp
  • tests/test_llm_stream.cpp
  • tests/test_toolcall_assembler.cpp
  • tests/test_stream_robustness.cpp
  • tests/test_stream_limits.cpp
  • tests/test_agent_loop_limits.cpp

这几组测试并不是重复劳动,它们分别在验证不同的职责边界。

7. 常见误解、边界与失败模式 (Boundaries / Pitfalls)

误解一:http.cpp 负责“理解模型响应”

不是。src/http.cpp 负责的是流式传输与限额控制。它不理解 SSE,也不理解 JSON,更不理解工具调用。它只会在“流太大”“网络报错”或“上层明确要求中止”这些 transport 条件下停下来。

误解二:SseParser 会保证 JSON 合法

也不是。SseParser 只负责事件切分和残余缓冲约束。真正的 JSON 解析失败是在 src/llm.cpp 里被判定为流处理错误,并按 fail-fast 处理。

误解三:llm.cpp 已经在执行工具

没有。src/llm.cpp 的工作是把模型输出翻译成 assistant message。工具执行发生在后面的 agent_loopToolRegistry

误解四:流结束前已经足够接近完整,就可以先跑工具

这正是测试想阻止的事。参数碎片在没有完成前,最多只能算“正在形成中的调用意图”,不能越过边界变成执行动作。

误解五:所有坏片段都会在进入 assembler 之前被拒绝

并不会。当前 contract 更细:事件级坏 JSON 会在 src/llm.cpp 里立刻失败;但对 tool_calls 内部的局部噪音,assembler 只在“不改变调用语义”的范围内做最小容错,例如忽略没有合法 index 的碎片。真正决定一条调用能不能进入执行的,仍然是最后的 finalize() JSON 解析结果和后续 agent_loop 闸门。

误解六:只要单个 tool call 能拼出来,runtime 就不会再拦

也不是。组装成功只说明“这条调用终于完整了”;真正进入执行前,src/agent_loop.cpp 还会检查每轮工具数量、全局累计工具数量,以及工具执行后的失败/超时污染。

最常见的失败模式也集中在这些边界上:坏 JSON、残余缓冲无限增长、被截断的参数、交错 tool-call、流中断、用户回调主动中止、组装后的工具数量洪泛。当前实现宁愿在这些地方早停,也不愿意把模糊状态继续传给下一层。一个值得刻意记住的分界是:事件级坏数据要尽快失败,参数级未完成状态可以暂存,但必须在 finalize() 前完成闭合

8. 继续深入 (Dive Deeper)

工具与安全边界 (Tools & Safety)

这章讲的是 NanoCodeAgent 最关键的一段现实问题:模型提出工具调用之后,宿主机到底凭什么相信它、拦住它,或者在执行后及时停下来。

1. 为什么这一层重要? (Why)

如果没有工具,NanoCodeAgent 只是一个会输出文本的模型客户端。有了工具之后,它才真正变成一个本地代理。但“有了工具”也意味着风险突然变得具体:读文件、写文件、打补丁、跑 shell、构建和测试,都可能把一次错误判断变成真实副作用。

因此这章的核心不是“有哪些工具”,而是“系统如何限制自己”。真正的安全感也不来自一句抽象的“安全沙箱”,而来自一条完整控制链:先注册工具,再按类别决定哪些默认可执行,然后在 executor 里继续施加边界,最后由 agent loop 在失败、超时或失控时收束整轮运行。

2. 整体图景 (Big Picture)

这一层可以拆成三道连续闸门:

  1. 注册闸门:系统到底向模型公开了哪些工具、每个工具属于什么类别、接受什么参数。
  2. 审批闸门:就算模型请求了某个工具,当前运行策略是否允许它执行。
  3. 执行与停机闸门:工具真的开始执行后,它是否仍受路径边界、输出上限、超时和 fail-fast 约束。

这三层关系图如下,阅读时只要顺着“请求是否被接住、是否被允许、执行后是否继续”这条控制链往右看:

flowchart LR
    Request[Assistant Tool Call]
    Registered{Registered?}
    Approved{Allowed By Policy?}
    Executor[Bounded Executor]
    Result[Tool Result]
    Healthy{Result Clean?}
    Stop[Fail-Fast Stop]
    Next[Next Turn]

    Request --> Registered
    Registered -- no --> Stop
    Registered -- yes --> Approved
    Approved -- no --> Stop
    Approved -- yes --> Executor
    Executor --> Result
    Result --> Healthy
    Healthy -- no --> Stop
    Healthy -- yes --> Next

这张图只展示控制链:请求先确认“系统认不认识这个工具”,再确认“当前策略允不允许它执行”,随后才进入受限 executor,最后由 loop 按结果决定继续还是停机。它没有展开 read-only / mutating / execution 的具体分类细目,也没有展开 executor 内部实现;目的只是帮助读者先抓住边界是如何一层层收紧的。

3. 主流程 (Main Flow)

src/agent_loop.cpp 收到一条带有 tool_calls 的 assistant message 后,它会顺序取出每个调用,解析 function.arguments,然后交给 execute_tool()。真正的统一入口在 ToolRegistry

ToolRegistry 会先做两件事:

  • 确认工具是否真的被注册过。
  • 根据工具类别和当前配置判断它能不能执行。

当前实现把工具分成三类:

  • read_only
  • mutating
  • execution

只读工具注册后会被归一化成无需审批;变更类和执行类默认阻止,只有 allow_mutating_toolsallow_execution_tools 被明确打开时才放行。注意,这里的 approval 不是“人工逐条弹窗确认”,而是运行开始前就确定好的策略门。

一旦通过审批闸门,调用才会进入具体 executor。到这里,不同工具的边界开始分化:

  • 文件工具依赖 workspace 解析和 no-follow 打开策略;
  • repo 只读工具依赖受限目录、rg/git 参数硬化和输出限制;
  • bash/build/test 工具依赖工作目录锁定、环境清理、超时和输出截断。

最后,agent_loop 会把工具结果作为 role=tool 消息写回上下文。如果结果里出现 blockedfailedok:falsetimed_out:true 这类污染信号,loop 会直接停止,而不是继续让模型带着坏状态往下跑。

4. 一个真实任务下,工具是如何被允许、被拒绝、然后被停下的? (Worked Example)

设想用户给系统一个任务:请你读取 src/main.cpp,如果需要就修改一点内容并跑一次命令验证。

对 runtime 来说,这不是一个“自由发挥”的请求,而是一连串不同风险级别的动作:

  1. 模型先请求 read_file_safe {"path":"src/main.cpp"}
    这是只读工具,registry 直接放行。

  2. 接着模型想请求 write_file_safeapply_patch
    如果本轮没有打开 allow_mutating_tools,registry 会直接返回 blocked,executor 根本不会被调用。

  3. 如果模型进一步请求 bash_execute_safetest_project_safe,情况更严格。
    这些都属于执行类工具;如果 allow_execution_tools 没打开,它们同样会在 registry 层被挡下。

  4. 如果执行类工具被允许,真正的 executor 仍然要再过一层边界。
    例如 bash_execute_safe() 会固定 cwd 到 workspace、清理环境变量、用双管道读输出、在超时或输出失控时杀掉进程组。

  5. 只要其中任何一步返回失败、超时或阻止状态,agent_loop 就会 fail-fast 停止。
    这也是为什么“工具被看到”和“工具真的被执行”是两回事。

这个例子想说明的不是“系统特别保守”,而是“工具链路的每一层都在明确回答自己的那一个问题”:可见吗、允许吗、怎么执行、失败后怎么办。

5. 模块职责要和控制链一起看 (Module Roles)

  • src/agent_tools.cpp 定义默认工具集合,把名称、描述、参数 schema、类别和执行函数绑在一起。这里决定的是“模型能请求什么”。
  • src/tool_registry.cpp 是策略门。它决定的是“当前这个请求在本轮运行里能不能执行”。
  • src/read_file.cppsrc/write_file.cppsrc/apply_patch.cpp 是文件边界的第一线。它们不是泛泛检查字符串,而是结合 workspace 解析和安全打开策略来防越界与 symlink 穿透。
  • src/repo_tools.cpp 是只读观察面的 executor。它们也需要硬化,因为底层仍然会调用 rg 或 git。
  • src/bash_tool.cppsrc/build_test_tools.cpp 是执行面。这里不承担“绝对隔离”的承诺,而承担“有限执行、及时收束、尽量不泄漏”的承诺。
  • src/agent_loop.cpp 是最后一道停机闸门。它用工具数、轮数、上下文和 fail-fast 规则防止错误继续扩散。

6. 作为贡献者,你通常怎么理解当前工具面? (What You Usually Do)

如果你是第一次想看清这套工具系统,最有效的顺序通常不是从工具名开始背,而是从“哪一层在回答哪个问题”开始:

  1. 先看 build_default_tool_registry()get_agent_tools_schema(),确认模型能看到哪些类别的工具。
  2. 再看 src/tool_registry.cpp,确认默认策略如何区分只读、变更和执行。
  3. 然后看具体 executor,理解每类工具的真实风险差异。
  4. 最后回到 src/agent_loop.cpp,看系统在工具失败后如何停下。

如果你正在改某一条边界,最相关的测试入口通常是:

  • approval 语义:tests/test_tool_registry.cpp
  • 执行类工具副作用与参数容错:tests/test_schema_and_args_tolerance.cpp
  • bash 边界:tests/test_bash_tool.cpp
  • loop 停机条件:tests/test_agent_loop_limits.cpp

7. 常见误解与失败模式 (Boundaries / Pitfalls)

误解一:只要工具 schema 暴露给模型,就说明这个工具能执行

不对。schema 暴露的是“模型知道它存在”,真正能否执行要看 ToolRegistry 和当前配置策略。

误解二:approval 是人工逐条审批

也不对。当前实现里的 approval 是运行前确定的策略开关,不是交互式确认流程。

误解三:bash_execute_safe() 相当于容器或系统级沙箱

这正是最需要纠正的过度安全表述。当前实现会锁定 workspace、清理环境、限制输出、设置超时并 kill 进程组,但它不是 chroot、不是 seccomp,也不是容器。

误解四:文件工具和 bash 工具有差不多的边界强度

不是。当前最强的路径边界主要在文件工具,而不是 bash。文件工具在 workspace 解析和安全打开上更严格;bash 更像“受限执行器”,不是“绝对隔离器”。

误解五:agent loop 会智能理解“这轮已经没意义了”

当前实现没有那种语义层面的智能裁决。它主要依靠固定阈值和结果状态字符串来 fail-fast。这种做法很朴素,但优点是边界明确。

最常见的失败模式也恰好对应这些误解:默认应阻止的工具被放行、shell 输出失控、后台进程泄漏、工具失败后 loop 继续跑、读写路径被越界利用。正因为这些问题具体而危险,这章才必须把“安全”拆回控制链,而不是抽象口号。

8. 继续深入 (Dive Deeper)

测试策略 (Testing)

这章讲的不是“仓库里有哪些测试文件”,而是 NanoCodeAgent 如何用测试证明自己真的守住了边界。

1. 为什么这一章重要? (Why)

NanoCodeAgent 最危险的失败方式,往往不是输出了一句不够聪明的话,而是把错误决策落成了真实副作用:读到了 workspace 外的文件、把半截 tool call 当成完整调用执行了、把默认应阻止的执行工具放过去了,或者在工具失败后还继续循环。

所以这个项目里的测试,核心目的不是展示“功能很全”,而是证明 runtime 的几条关键承诺是真的:

  • 路径边界不会轻易被绕过。
  • 流式响应不会把碎片化数据误当成完整指令。
  • approval policy 会在该阻止的时候阻止。
  • 执行工具会在超时、输出失控和环境污染风险下及时收束。
  • agent loop 在该停的时候真的会停。

如果把这些行为看成 runtime 的合同,tests/ 就是在持续检查合同是否还成立。

2. 整体图景 (Big Picture)

从运行方式看,测试并不是“单独的一套假系统”。CMakeLists.txt 会把生产源码直接编进测试目标,tests/CMakeLists.txt 再通过多个 gtest 可执行文件把这些边界拆开验证。因此它们测到的是和正式 runtime 同一批实现,而不是另一份专门为测试写的替身逻辑。

日常入口也很明确:./build.sh test 先做 Debug 构建,再进入 build/ 里跑 ctest --output-on-failure。Debug 构建默认开启 AddressSanitizer,因此这里检查的不只是业务行为,也包括更底层的内存与资源问题。

如果把这一套过程看成“信任链”,它更像下面这样:

flowchart LR
    BuildScript[build.sh test]
    DebugBuild[DebugBuildWithAsan]
    CTest[ctest]
    TestBins[GTestExecutables]
    ProdCode[ProductionSources]
    Contracts[RuntimeContracts]

    BuildScript --> DebugBuild
    DebugBuild --> CTest
    CTest --> TestBins
    TestBins --> ProdCode
    ProdCode --> Contracts

这条链说明的不是“测试很多”,而是“你运行测试时,实际上是在让一批直接链接生产源码的可执行文件重复验证 runtime 合同”。

从风险视角看,测试大致覆盖六条防线:

  • workspace 边界
  • streaming 与 tool-call 组装
  • approval 边界
  • 执行工具约束
  • agent loop 停机条件
  • 只读 repo 观察工具的硬化

风险覆盖图如下,按“风险面 -> 行为合同 -> 代表测试簇”的顺序阅读:

flowchart LR
    RiskA[Path Escape Risk]
    ContractA[Must Stay Inside Workspace]
    TestsA[workspace + file tool tests]

    RiskB[Fragmented Stream Risk]
    ContractB[Only Complete Calls Execute]
    TestsB[sse + llm + assembler tests]

    RiskC[Dangerous Tool Risk]
    ContractC[Mutating and Execution Blocked by Default]
    TestsC[registry + args tests]

    RiskD[Runaway Execution Risk]
    ContractD[Timeout and Fail-Fast Must Stop]
    TestsD[bash + loop tests]

    RiskE[Repo Probe Abuse Risk]
    ContractE[Read-Only Repo Tools Stay Hardened]
    TestsE[repo tool tests]

    RiskA --> ContractA --> TestsA
    RiskB --> ContractB --> TestsB
    RiskC --> ContractC --> TestsC
    RiskD --> ContractD --> TestsD
    RiskE --> ContractE --> TestsE

这张图展示的是测试为什么存在:每一列都从“最怕什么失控”走到“系统必须守住什么合同”,最后落到“哪组测试在替这条合同说话”。它没有列出所有测试文件名,也不打算替代正文中的细节清单;目的只是先让读者看懂测试覆盖的逻辑,而不是把 tests/ 当成索引表去背。

3. 主流程:这些测试是怎么运行到真实代码上的? (Main Flow)

./build.sh test 做的事情并不复杂,但很关键。它先执行 Debug 构建,再由 ctest 调度所有注册过的测试目标。由于 tests/CMakeLists.txtsrc/cli.cppsrc/config.cppsrc/workspace.cppsrc/http.cppsrc/llm.cppsrc/tool_registry.cppsrc/agent_loop.cpp 等源码直接编进测试可执行文件,所以每个测试都在碰真实实现。

这意味着测试的价值不只是“跑通接口”。它们真正验证的是:

  • 入口层的覆盖关系和 workspace 初始化是否按预期工作;
  • LLM 层对 SSE chunk、坏 JSON、碎片化 tool call 的处理是否足够稳;
  • ToolRegistry 的类别门禁是否会在默认情况下挡住危险工具;
  • bash/build/test 这类执行面是否真的能在超时、输出爆炸和环境泄漏下收束;
  • agent loop 是否会在上限、失败或污染状态出现后中止。

换句话说,测试不是在 runtime 外面看热闹,而是在 runtime 里面反复试探那些最容易出事故的地方。

如果要选一个最能体现“测试在证明边界”而不是“测试在炫数量”的例子,我会选文件边界。

设想这样一个坏场景:workspace 里有一个看似普通的相对路径 nested/link,但它其实是一个指向系统敏感文件的 symlink;或者有一个 linked_dir,看起来像 workspace 里的目录,实际上却指到别处。如果读写工具只是做字符串级检查,这种路径很容易骗过系统。

对应测试正是在验证 NanoCodeAgent 不会被这种“看起来在里面、实际上通向外面”的路径绕过去:

  • tests/test_workspace.cpp 先验证基础路径解析:绝对路径和 .. 逃逸都要被拒绝。
  • tests/test_read_file.cpp 验证读文件时,目标 symlink 和中间目录 symlink 都不能穿透,而且 FIFO、二进制内容也不能被当普通文本处理。
  • tests/test_write_file.cpp 验证写文件时,同样不能借 symlink 把写入落到预期之外的位置。

这条信任链可以概括成:

flowchart LR
    RelativePath[RelativePathInput]
    Resolve[workspace_resolve]
    SafeOpen[secureOpenNoFollow]
    FileType[fileTypeChecks]
    ReadWrite[read_or_write]
    Reject[RejectUnsafeAccess]

    RelativePath --> Resolve
    Resolve --> SafeOpen
    SafeOpen --> FileType
    FileType --> ReadWrite
    Resolve --> Reject
    SafeOpen --> Reject
    FileType --> Reject

这些测试真正建立的信任是:NanoCodeAgent 对文件边界的判断不是“看起来像在 workspace 里就算数”,而是经过路径解析、安全打开和文件类型检查之后,才允许真正读写。

Streaming 相关测试同样重要,但它们更适合在 HTTP 与 LLM 流式解析 那一章里展开;本章更想强调的是“测试如何把 runtime 承诺一条条钉死”。

5. 模块职责要和风险绑定来看 (Module Roles)

  • tests/test_workspace.cpptests/test_read_file.cpptests/test_write_file.cpp 证明路径不能绝对化、不能用 .. 逃逸、不能借 symlink 穿透,也不能把 FIFO 或二进制内容当普通文本处理。
  • tests/test_sse_parser.cpptests/test_llm_stream.cpptests/test_toolcall_assembler.cpptests/test_stream_robustness.cpp 证明 streaming 路径里的每一层只负责自己的那一段:事件切分、JSON 解析、参数拼装、最终交付。
  • tests/test_tool_registry.cpptests/test_schema_and_args_tolerance.cpp 证明 approval 是运行策略门,而不是写在文档里的礼貌约定。只读工具默认放行,变更类和执行类默认阻止。
  • tests/test_bash_tool.cpptests/test_build_test_tools.cpp 证明执行工具不是“随便起个子进程”。超时、输出上限、环境清理、后台进程清理和结构化失败结果都被钉住了。
  • tests/test_agent_loop_limits.cpptests/test_agent_mock_e2e.cpp 证明 agent loop 不是无限循环的 orchestrator,而是知道何时停、何时 fail-fast、何时丢弃旧工具输出以收缩上下文的 broker。
  • tests/test_repo_tools.cpp 证明“只读观察工具”也需要硬化,因为它们底层仍会调用 git 或 rg;如果不加约束,也可能变相绕成执行面。

6. 作为贡献者,你通常怎么用这些测试? (What You Usually Do)

最常见的入口就是:

./build.sh test

如果你刚改的是某条边界规则,读测试的顺序通常比盲跑全部测试更重要:

  1. 改 workspace / 文件工具时,先看 test_workspacetest_read_filetest_write_file
  2. 改 streaming / tool call 时,先看 test_sse_parsertest_llm_streamtest_toolcall_assemblertest_stream_robustness
  3. 改 approval / registry 时,先看 test_tool_registry
  4. 改执行工具时,先看 test_bash_tooltest_build_test_tools
  5. 改 loop 行为时,先看 test_agent_loop_limitstest_agent_mock_e2e

这种阅读方式的好处是:你不是在“找某个测试文件”,而是在先问“我碰的是哪条边界”,再去看哪组测试在替这条边界说话。

7. 常见误解与失败模式 (Boundaries / Pitfalls)

最常见的误解,是把测试理解成“证明功能可用”。在这个项目里,很多测试更重要的作用是证明某种坏事不会发生。例如:

  • 不会把 workspace 外的路径当成内部路径。
  • 不会把半截 tool-call 参数当成完整调用。
  • 不会因为工具默认可见就默认可执行。
  • 不会在工具失败后继续让 agent loop 带着污染状态往下跑。

另一个误解,是把“测试很多”当成“系统一定安全”。测试能证明的,是当前明确写下来的行为合同;它们不能替代对边界的克制描述,也不能把一个受限执行器吹成容器级隔离。对 NanoCodeAgent 来说,测试提升的是可信度,而不是神话色彩。

最后要注意,这一章里最值得读者带走的,不是某个测试名字,而是一个习惯:每当你想给 runtime 加一条新能力时,也应该同时问一句,“这条能力最怕哪种失控方式,我要用哪类测试把它钉住?”

8. 继续深入 (Dive Deeper)

文档自动化 (Documentation Automation)

这章讲的是 NanoCodeAgent 如何把“文档要不要改、该改哪里、改完怎么证明没写偏”拆成一条可验证的流程,而不是把整件事一次性交给模型自由发挥。

1. 为什么需要这条流程? (Why)

文档更新真正困难的部分,通常不是措辞,而是范围控制。一次代码变更出现后,系统必须先回答几个问题:这是不是用户可见变化、应该落到 README.md 还是 book/src/、只是补充说明还是需要调整章节结构。

如果这些判断没有先固定下来,模型就容易把“找证据、定范围、写内容、做校验”混成一步,最后写出一份看起来完整、但和仓库事实对不上的文档。文档自动化存在的意义,就是先把事实、职责和出口分开,再让写作发生。

2. 整体图景 (Big Picture)

现在这条能力更适合看成三层协作,而不是一条单脚本流水线。最前面是客观事实层scripts/docgen/change_facts.py 只提取 changed files、change type、public surface signals 之类的事实,不提前替 writer 猜目标文档。中间是范围与证据层:AI scope decision 负责回答 README / book 是否在范围内、批准哪些目标,而 scripts/docgen/reference_context.py 只补充 examples、tests、templates、source paths 与现有文档摘录,供写作阶段引用。最后才是起草与闭环层:writer 只改获批文档,verify / review 再判断这些修改是否真的站得住,而且会明确说明“哪些批准目标真的被检查了,哪些还没有”。

如果你只想知道这次代码变更是否值得更新文档,change-impact 路径仍然存在,但它现在更像前置筛查工具。它会从 scripts/docgen/changed_context.py 生成面向人的 change context,再由 scripts/docgen/generate_impact_report.py 写出 docs/generated/change_impact_report.md。这条路径停在“值不值得继续”。

如果你已经决定要让仓库里的真实文档落地更新,主入口则是 scripts/docgen/run_docgen_e2e_closed.sh。它会先清理旧产物,再产出 docs/generated/change_facts.jsondocs/generated/doc_scope_decision.jsondocs/generated/reference_context.json,随后调用 writer 更新 README.mdREADME_zh.mdbook/src/ 下的获批章节,以及在需要时更新 book/src/SUMMARY.md。真正的重点不是“又多了几个 JSON”,而是每一段 AI 写作之前都先有事实和边界,后面还有 verify、review 和 evidence summary 接手;verify 不再只是笼统地说“过了或没过”,而是要把覆盖范围和缺口一起暴露出来。

3. 主流程 (Main Flow)

正常情况下,流程从一次代码 diff 开始,但现在要先分清你是在做“影响判断”还是“真实文档更新”。

如果目标只是判断影响范围,入口是 scripts/docgen/run_change_impact.sh。它读取规则与 skill 后,调用 scripts/docgen/changed_context.py 生成 docs/generated/change_context_output.md,再由 scripts/docgen/generate_impact_report.py 写出 docs/generated/change_impact_report.md,最后对这份报告运行路径与链接校验。你得到的是一份供人或下游阶段参考的报告,而不是已修改的 README / book。

如果目标是真正改文档,入口是 scripts/docgen/run_docgen_e2e_closed.sh。这条闭环先做 Step 0 清理,避免旧的 docs/generated/change_facts.jsondocs/generated/doc_scope_decision.jsondocs/generated/reference_context.json、diagram artifacts 相关生成目录和旧状态文件污染当前 run。然后 scripts/docgen/change_facts.py 产出客观事实,AI scope decision 决定 README 和哪些书籍章节在范围内,scripts/docgen/reference_context.py 再收集参考证据。writer 只能编辑 scope 批准的真实目标文档,而不是去改 docs/generated/ 下的中间产物。

草稿写完后,流程不会直接宣布完成。scripts/docgen/missing_diagram_specs.py 会先检查在范围内的 Mermaid 文档是否缺少 diagram spec 产物;如果缺了,会先触发专门的 backfill 任务。随后 scripts/docgen/run_docgen_e2e_loop.py 进入 verify -> verify_repair -> review -> review_rework 的受限循环。验证阶段通过 scripts/docgen/run_verify_report.py 统一运行路径、链接、diagram spec、Mermaid render 与命令检查;其中路径、链接、diagram spec 与 Mermaid render 是 blocking,命令检查不是。新的重点是:verify 现在同时回答两件事,一是文档内容本身有没有坏路径、坏链接或坏 Mermaid,二是 scope decision 批准的目标里有没有文件根本没被 verify 覆盖。后者会在 docs/generated/verify_report.json 里通过 approved_targetstarget_docs_checkednot_checkedverify_coverage_note 显式留下痕迹,而不是让人误以为“批准了就一定检查到了”。若存在 Mermaid,scripts/docgen/verify_mermaid.py 还会写出 SVG、PNG 与 artifact report,供后续 visual review 使用。

只有当 blocking verify 通过、review 的 must-fix 也被清空,闭环才会通过 scripts/docgen/render_docgen_e2e_summary.pydocs/generated/e2e_run_evidence.json 给出这次 run 的最终摘要。如果修复次数达到上限,流程会明确写出 TerminalFail,而不是继续无限重试。

4. 模块职责 (Module Roles)

  • 客观事实层scripts/docgen/change_facts.py 负责从 diff 中抽取结构化事实,避免一开始就把“事实提取”和“写作判断”混在一起;scripts/docgen/changed_context.py 继续服务 change-impact 报告,它提供的是上下文提示,不是 scope 决策。
  • 影响判断层scripts/docgen/run_change_impact.shscripts/docgen/generate_impact_report.py 负责回答“这次变化是否值得更新文档”,并把答案落到 docs/generated/change_impact_report.md
  • 范围与证据层:AI scope decision 决定是否更新 README、是否更新 book、批准哪些目标,以及是否需要调整 book/src/SUMMARY.mdscripts/docgen/reference_context.py 只补充写作证据,不扩展编辑范围。
  • 真实文档写入层:writer 阶段只允许修改获批的 README.mdREADME_zh.mdbook/src/ 下文档;如果书籍结构要变,book/src/SUMMARY.md 也会跟着更新。
  • Diagram contract 层scripts/docgen/missing_diagram_specs.pyscripts/docgen/verify_diagram_specs.py 负责确保 Mermaid 图不只是“能画”,还要先有 diagram spec 说明合同。
  • 验证与审稿层scripts/docgen/run_verify_report.py 汇总 blocking / non-blocking 检查,并把批准范围与实际 verify 覆盖面一起写进 docs/generated/verify_report.jsonscripts/docgen/verify_paths.py 也支持对生成型证据目录做前缀级忽略,避免把 docs/generated/ 这类中间产物误判成必须存在的正式文档路径;scripts/docgen/verify_mermaid.py 负责真实渲染并产出截图证据;review 和 rework 则消费这些证据,而不是只看 Markdown 源码。
  • 闭环与审计层scripts/docgen/run_docgen_e2e_loop.py 管理 retry 上限、失败类型、状态文件,以及 docs/generated/e2e_run_evidence.json 这份 run 级别的证据清单。
  • 兼容与旧流程脚本scripts/docgen/run_docgen_e2e.shscripts/docgen/generate_candidates.pyscripts/docgen/run_validation_report.pyscripts/docgen/run_review_report.py 仍保留在仓库中,但更偏向旧版 candidate-based 流程或独立分析工具;E2E summary 现由 render_docgen_e2e_summary.py 在主路径中产出。

5. 你通常会怎么用? (What You Usually Do)

进入 AI 阶段之前,先把前置条件按责任拆开理解。scripts/docgen/setup.sh 会检查 python3gitnpm、Python venv 支持,以及当前目录是否位于 git worktree 中;它的职责是确认主机具备运行 docgen 与后续 Mermaid bootstrap 的基础条件,而不是替你检查闭环专属工具。

真正进入闭环时,scripts/docgen/run_docgen_e2e_closed.sh 才会继续要求 codexPATH 里。scripts/docgen/setup.sh 也不会提前安装 Mermaid 相关依赖;只有当 in-scope 文档里真的包含 Mermaid 时,scripts/docgen/verify_mermaid.py 才会按需准备 .venv/、安装 Python playwright 与 Playwright chromium,并通过 npmtmp/mermaid-tools/ 下安装 Mermaid bundle。

你只想判断影响范围时,通常走 scripts/docgen/run_change_impact.sh。这条路径适合在改动刚落地、你还不确定是否需要动 README 或书籍时使用;它的输出是 docs/generated/change_impact_report.md,重点是给出“值不值得继续”的结论。

你已经确定要更新真实文档时,通常走 scripts/docgen/run_docgen_e2e_closed.sh。这条路径默认以 HEAD~1 作为 diff 基准;如果仓库历史不足、是浅克隆,或你想拿别的基线比较,需要显式设置 REF=main 或其他可解析的 ref。它会按 change facts -> scope decision -> reference context -> doc update -> verify / review / summary 的顺序推进。对日常使用者来说,最重要的判断不是记住每个中间产物名字,而是理解这条闭环会先定边界,再写正文,最后验证“写得对不对、查得全不全”。

你只是想复用旧版 candidate-based 流程,或者暂时只想看 candidates、validation report 和 heuristic review,也还能运行 scripts/docgen/run_docgen_e2e.sh。但这条路径不会更新真实 README / book,也不会驱动新的闭环修复逻辑。

6. 边界与易错点 (Boundaries / Pitfalls)

真实编辑目标仍然只有 README.mdREADME_zh.mdbook/src/ 下的 Markdown 文档。docs/generated/ 下的 JSON、报告、状态文件、diagram specs 和 diagram artifacts 都是中间交接物或审计证据,不是最终用户文档。即使 book/src/SUMMARY.md 被纳入批准目标,它通常也只在章节结构真的变化时才需要修改。

验证阶段也不是所有检查都同等严格。按 scripts/docgen/run_verify_report.py 当前实现,路径检查、链接检查、diagram spec 检查与 Mermaid render 都是 blocking,命令检查是 non-blocking。这意味着坏路径、坏链接、缺 diagram spec 或 Mermaid render fail 都会挡住主流程,而命令问题更像额外信号。与此同时,路径检查不再是“见到 repo-relative 路径就一律必须存在”这么死板:verify_paths.py 现在支持 --ignore-repo-prefix,而 run_verify_report.py 会把这层能力以 --verify-paths-ignore-repo-prefix 继续向上暴露,允许调用方对 docs/generated/ 这类生成型证据目录做前缀级豁免。这个豁免只影响路径存在性校验,不会改变链接检查、diagram spec 或 Mermaid render 的 blocking 合同。

另一个常见误解,是把 verify 看成“只要对 in-scope 文档跑过一次脚本,就默认覆盖完整”。现在不该再这样理解了。scope decision 批准的是“理论上应该被检查的集合”,而 verify 报告负责告诉你“实际检查到了哪几个目标”。如果某个批准目标因为文件不存在或当前 verify 还不支持而没有进入检查集合,报告会把这个缺口单独列出来,提醒你这是 pipeline 覆盖问题,不是文案已经没问题的证据。

再一个常见误解,是把 scripts/docgen/run_change_impact.shscripts/docgen/run_docgen_e2e.shscripts/docgen/run_docgen_e2e_closed.sh 当成同一条线上的不同名字。实际上它们分属三种责任:change-impact 只做影响判断;scripts/docgen/run_docgen_e2e.sh 属于旧版 candidate-based 流程;scripts/docgen/run_docgen_e2e_closed.sh 才是当前把真实文档写入、verify、review、repair 和 evidence summary 串起来的闭环入口。

CI 里的责任也要分开看。.github/workflows/core-ci.yml 只在 workflow_dispatch 下运行完整闭环,而且前提是 build/test 先通过;.github/workflows/docs-validate.yml 仍然负责 PR 与主分支上的文档校验,并会在聚合 verify 前恢复 CI 用的 diagram specs,然后调用 scripts/docgen/run_verify_report.py 对固定 scope 做检查。这个校验入口也会传入 docs/generated/ 前缀忽略,避免把生成证据目录误当成必须随仓库提交的正式路径。

最后,闭环不是“修到过为止”。当 verify repair 达到 2 次、review rework 达到 2 次,或总修复动作达到 4 次时,scripts/docgen/run_docgen_e2e_loop.py 会停止并写出 TerminalFail 相关产物,要求人来接手。无论成功还是失败,本次 run 的 diagram spec / artifact / screenshot 证据都会集中记录到 docs/generated/e2e_run_evidence.json,避免和旧 run 的残留产物混在一起靠人猜。

7. 继续深入 (Dive Deeper)

更新日志 (Changelog)

此日志仅关注该库核心架构能力的演变,严禁出现未经合并的主观路线图推测。

  • **feat: integrate real LLM networking and unify mock streaming loops**
    • 核心突破:全量整合真实的 LLM 网络通讯到流式解析循环中。
  • **feat(agent): implement agent loop closed-state and multi-tier brake limits bounds**
    • 核心突破:加入了多轮拦截循环与超时/错误边界限制。
  • **feat: bootstrap teaching site with mdBook ...**
    • 架构变更:建立规范化的文档站点并引入基于 mdBook 的工程脚手架。
  • **feat: implement bash_execute_safe with DOD resource limits and deadlock-free piping**
    • 工具能力:引入完整的、防御死锁的终端沙箱 bash_tool
  • **feat: implement secure workspace physical write/read tool...**
    • 工具能力:实装严密的 read_filewrite_file 安全工具边界防线。
  • **feat: add streaming tool_call aggregation ...**
    • 数据解析:实现了增量的 JSON 函数调用拼接技术。
  • **feat: implement HTTP/LLM bridge ... & SSE streaming and incremental JSON parsing**
    • 通讯协议:创建基础的网络 C++ 套接字模块与 SSE 增量解码通道。
  • **feat: implement config precedence and workspace security sandbox**
    • 配置基建:构建四级加载的设定环境以及隔离非法路径的安全目录策略。
  • **commit (initial): CLI skeleton**
    • 工程初始:引入 CLI 选项解析及基础日志接入。