Usability pass: tool registry, TUI polish, saved keys and settings

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.
This commit is contained in:
WHC
2026-10-05 01:47:01 +08:00
parent 9877ff48b6
commit 2fde898da0
105 changed files with 19348 additions and 3786 deletions
+67
View File
@@ -43,6 +43,29 @@ end\n
取 \(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 消息反馈给模型重发;已确认并执行的调用不受影响。
## 不可逆发射
实现是确认忠实的,当且仅当:
@@ -55,6 +78,50 @@ end\n
**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` 给出的行号附近查找,行号不准时退化为搜索,并容忍行尾空白差异。