Files
Hermit/docs/archive/theory_tool_cst.md
WHCandCursor 2602806aea
ci / tcc-check (push) Failing after 43s
ci / linux (aarch64, 0) (push) Skipped
ci / linux (x86, 1) (push) Skipped
ci / linux (x86_64, 1) (push) Skipped
ci / windows (push) Skipped
docs: product-facing README; archive CST theory notes (0.9.34)
Rewrite the homepage for day-to-day use, move paper/eval drafts under
docs/archive, and add a docs index so theory no longer leads the project.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 20:00:22 +08:00

8.7 KiB
Raw Permalink Blame History

Tool-loop CST:确认前沿推广到工具调用

归档。 早期设计笔记,已停止维护;请勿当作当前产品说明。见 README.md。

命题

在 agent 工具环中,对模型输出做确认流式转导:只在 (O_{\mathrm{conf}}) 内不可逆地发射「可执行工具调用 / 可应用补丁 / 可提交会话状态」。相对「拼到合法 JSON 再执行」的缓冲基线,这能降低注意力饥渴导致的残缺工具帧(未闭合括号、半截 arguments)被过早执行或整轮失败的问题。

与论文 CST 的对应

论文(Markdown→HTML) 工具环推广
输入字母表 (\Sigma) 模型文本 / CTL 字节流
指称 (f,g) → HTML (f_{\mathrm{tool}}) → 已确认 ToolCall / PatchHunk
(O_{\mathrm{conf}}) 不可撤回 HTML 不可撤回:开始执行工具、应用 hunk、写入 transcript
(\mathcal{L}_0) 排除引用式链接 (\mathcal{L}_0^{\mathrm{tool}}) 排除裸 JSON 对象
(K=2L_{\mathrm{line}}) 工具帧同样要求有界 pending

为何排除裸 JSON

JSON 对象依赖括号匹配与字符串转义。在未闭合 " / { 时,任意后缀都可能改变已见结构的指称,故 (O_{\mathrm{conf}}) 往往停在空前缀,pending 随流长增长——这与论文中引用式链接的无界后向绑定同构。「等整个 JSON Parse 成功」只是 EOF 缓冲,不是有界 CST。

(\mathcal{L}_0^{\mathrm{tool}}):CTL(CST Tool Language)

行界 (L_{\mathrm{line}});参数体用长度前缀,避免转义歧义。

call <id> <name>\n
arg <key> <nbytes>\n
<raw bytes exactly nbytes>
end\n

确认规则

  1. call 行以 \n 结束:确认 (id, name)(此后不可撤回工具身份)。
  2. arg 头以 \n 结束:确认 (key, nbytes);随后精确消费 nbytes 字节,每字节到达即可确认(无 JSON 转义)。
  3. end\n:确认整次调用可执行;转导器发射一个 ToolCall 事件。
  4. 行长 (> L_{\mathrm{line}})、非法 token、nbytes 超上限、非 CTL 噪声 → 离开契约,标记 tool_parse_error。

有界 pending (K)

  • 解析行头时 pending (\le L_{\mathrm{line}})。
  • 读参数体时 pending (\le) 剩余未读字节数,但已读体字节已确认,工作区只需当前行缓冲 + 当前 arg 剩余计数((O(L_{\mathrm{line}})) 寄存器),不随会话总长增长。

取 (K = L_{\mathrm{line}})(行头)与实现常量 CTL_MAX_ARG(单参上限,契约拒绝更大值)。

参数的三种写法

写法 语法 何时确认 代价
长度前缀 arg <key> <nbytes> + 恰好 nbytes 字节 每个体字节到达即确认;end\n 确认整次调用 模型必须精确数字节(含换行、UTF-8 多字节)
行内 arg <key> <value…>(值为行尾剩余部分) 头行 \n 即确认 只能单行;值不能全是数字(否则视为长度前缀)
定界(heredoc) arg <key> <<TAG … 单独一行 TAG 每个完整体行(\n 到达且 ≠ TAG)即确认 体不能含独立成行的 TAG;值总以换行结尾;单行 ≤ (L_{\mathrm{line}})

定界写法的确认规则:

  1. 头行 arg <key> <<TAG\n 确认 (key, TAG);TAG 须匹配 [A-Za-z0-9_]{1,32},可加单/双引号。
  2. 体内每读到一个 \n:若该行逐字节等于 TAG,则参数结束;否则该行(连同换行)确认为体的一部分。行首空白、end、call、代码围栏在体内都只是普通文本。
  3. 未闭合行的 pending (\le L_{\mathrm{line}}):它可能成为 TAG,也可能是体行,在 \n 前不能确认——与行头同界,所以 (K = L_{\mathrm{line}}) 不变。
  4. 流在 TAG 前结束 → heredoc_eof 契约错误,不执行。最后一行恰为 TAG 且无换行时,EOF 视为该行结束(EOF 充分性)。

定界写法与长度前缀的确认前沿完全相同(都是“参数完整后 end\n”),差别只在谁承担边界信息:长度前缀要求模型事先给出精确计数(真实模型常数错,见 eval 实验 D),定界写法把边界放到流里,由运行时逐行检测。

契约错误的分类

解析器在离开 (\mathcal{L}_0^{\mathrm{tool}}) 时给出 ctl_error_kind_t,供实验统计: bad_call、bad_arg、unexpected_line(长度前缀写小了,体后面还有内容)、body_eof(写大了,流先结束)、heredoc_eof、line_too_long、limit。 错误信息会带上声明值与实际到达字节数(例如 declared 495 bytes but only 438 arrived),运行时把它作为下一条 user 消息反馈给模型重发;已确认并执行的调用不受影响。

不可逆发射

实现是确认忠实的,当且仅当:

  • 拼接发射的工具事件序列是 (O_{\mathrm{conf}}(u)) 的前缀;
  • 一旦 ToolCall 被发射,runtime 必须执行(或显式记录拒绝),不得因后续字节撤回;
  • session transcript 只追加:用户消息、已确认助手文本、工具调用与结果。

对照基线

JSON-buffer:将模型输出拼接到可 cJSON_Parse 的 {"name","arguments"}(或 OpenAI tool_calls 形)才执行;流中途截断则失败。与 CTL 使用同一截断点集合,只换帧语法。

细分两档(实验 C 都测):

  • json-whole:整条消息的 tool_calls 数组可解析才执行(= 原生接口流结束后执行,native-eos)。
  • json-object:每个调用对象一旦可解析就执行(= native-inc)。它也是前缀单调的,但 pending 是“当前调用的全部参数”,不受 (L_{\mathrm{line}}) 约束:一个 5KB 的 content 字段在最后一个 "} 到达前一个字节都不能确认。

运行时:五种协议模式

同一个 agent 循环支持五种承载方式(--protocol),只换“调用从哪里来、何时执行”,工具、会话、权限完全相同,便于做对照实验。

模式 请求 调用来源 执行时刻(不可逆点)
native-eos 发 tools[] SSE tool_calls 流结束后统一执行
native-inc 发 tools[] SSE tool_calls 某 index 的参数完整(出现下一个 index / finish_reason / [DONE]),且 JSON 可解析
ctl 不发 tools[] 正文 CTL 帧(长度前缀) CALL_READY(end\n 确认)即执行,流继续
ctl-heredoc 同上 正文 CTL 帧(定界) 同上
xml 同上 MiniMax <minimax:tool_call> 流结束后执行

文本协议的结果帧:

result <id> exit=<code> <nbytes>\n
<nbytes 字节输出>\n
end\n

作为下一条 user 消息发送(模型不得自己写 result 帧)。超过 CST_TOOL_OUT_CAP 的输出先按“头 60% + 尾 40%”截断再计算 nbytes,保证帧内字节数精确。

eager 执行的不可逆语义

  • 调用在确认时刻立刻执行(同步,在 SSE 回调内)。执行期间客户端暂停读流,但服务端仍在继续生成,字节堆在 socket 缓冲里,工具返回后再被消费——模型生成与工具执行因此重叠。多个调用严格按确认顺序执行。
  • CTL 模式下,已确认调用之后若模型继续写普通文本(例如“臆想”工具结果),运行时立即停止读流(客户端 stop,而非错误),只把截至最后一个确认调用的原始文本写入会话。这正是“确认前沿之外的字节不产生副作用”。
  • 若同一条消息里,已执行的调用之后出现契约错误:已执行的副作用保留并记录,错误帧之后的内容丢弃,并请模型重发。
  • 用户取消(Esc)在执行中的调用上表现为“进程组被终止(exit=130)”,仍然记录,不撤回。

实测检验的三条性质(实验 C)

对真实录制流的每一个字节前缀:

  • S1:未完整的调用永远不可执行;
  • S2:前缀上已执行的调用与完整流中的同一调用逐字节相同(执行永不被撤回或改写);
  • S3:可执行调用数随前缀长度单调不减。

补丁确认

apply_patch 的 patch 文本作为 CTL 的一个 arg;应用侧再要求 unified diff 的每个 hunk 完整(@@ 到下一块或 EOF)才写盘——整 hunk 确认,禁止半截应用。

实现上更进一步:一个 patch 可含多个文件(---/+++ 头,/dev/null 表示新建/删除)。所有文件的所有 hunk 先在内存中定位并计算结果,全部成功才写盘;任一 hunk 定位失败或有歧义(同一上下文匹配多处且无行号),整个 patch 不产生任何磁盘副作用。hunk 优先在 @@ -a,b 给出的行号附近查找,行号不准时退化为搜索,并容忍行尾空白差异。