Tools - Table-driven tool registry with permission classes and default/minimal profiles; new tools: edit_file, list_dir, glob, grep, delete_file, move_file, todo_write, web_fetch, ask_user (plus read_file paging). - Built-in Myers diff; write tools return unified diffs. - Persistent permission rules (.agent/permissions.json), per-turn undo snapshots (.agent/undo), AGENTS.md project notes, auto-compact at 80% of the model context, cancellable run_shell with process-group kill. TUI - Table-driven slash commands: /provider /key /effort /status /tools /permissions /yolo /undo /retry /diff /export /copy /rename /delete /init /max-steps /todo, with / and @ completion. - Permission modal with diff and prefix rules, ask_user modal, todo panel, header with model/effort/context, bracketed paste, line editing, wide-char cursor, NO_COLOR and ASCII fallback; Esc never quits. Startup and configuration - API keys are entered in the TUI (hidden) and saved per provider in <config>/credentials (0600); env variables are only a fallback. - Last provider, per-provider model and reasoning effort are remembered in <config>/settings.json; --effort/-e and /effort send reasoning_effort (low|medium|high|max) to models that accept it. - DeepSeek preset uses deepseek-flash / deepseek-v4-pro. - Session flags --new/--continue/--resume/--list-sessions, --yolo, --confirm. Compatibility and tests - ncurses detection, macOS/Homebrew support, make install, XDG config dir, POSIX cleanups; Gitea CI (gcc/clang/asan/musl/macOS). - Unit tests for diff, fs tools, registry, undo/perms and config; e2e fixtures multi_file_refactor and ask_or_todo; vendored cJSON.
128 lines
8.6 KiB
Markdown
128 lines
8.6 KiB
Markdown
# Tool-loop CST:确认前沿推广到工具调用
|
||
|
||
## 命题
|
||
|
||
在 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` 给出的行号附近查找,行号不准时退化为搜索,并容忍行尾空白差异。
|