Files
CST-agent/docs/archive/theory_tool_cst.md
T
WHCandCursor 2602806aea 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

130 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tool-loop CST:确认前沿推广到工具调用
> **归档。** 早期设计笔记,已停止维护;请勿当作当前产品说明。见 [README.md](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}}\);参数体用长度前缀,避免转义歧义。
```text
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>` | 流结束后执行 |
文本协议的结果帧:
```text
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` 给出的行号附近查找,行号不准时退化为搜索,并容忍行尾空白差异。