提案:工具调用边界 + edit 工具族(对齐 pi-agent 的 agent-loop)
蓝本:reference/pi-agent/agent-loop.ts(earendil-works/pi,packages/agent/src/agent-loop.ts)。
本文只提出方案与分期,不含实现。
一句话
byteseek 缺的不是若干 edit 工具,而是工具调用边界 这一层。pi 把工具边界放在宿主循环里
(prepare → execute → finalize);byteseek 的循环是每轮现造的 kvlang 代码,所以这一层应落到
KV 树里 :lib/tool(契约、清单、结果记录)+ lib/edit(第一个工具族)。
现状(byteseek 已实现的部分)
已有
位置
说明
主循环
lib/byteseek/main.kv mainbrain
input → classify → ensure_category → recall → llm·call → byteseek·run
代码脑
lib/byteseek/llm.kv call
LLM 生成 → kvlang·vet → kvlang·layout → 返回入口
裸 LLM
lib/byteseek/llm.kv raw
供元能力复用
提示词即 KV
lib/byteseek/prompt.kv → /byteseek/prompt/system
可寻址、可自改
记忆
lib/byteseek/memory.kv
/byteseek/memory/* + cat/*;子串召回;classify 语言跟随
能力记忆
lib/byteseek/memgen.kv
每个 /lib/<name> 浓缩成 ·mem
工具
lib/byteseek/{shell,python}.kv
都只是 networld/proc·exec 的薄包
执行
main.kv run → vthread·call
同 vid 动态调用,跑完回主循环
自检
tests/selftest.kv
vet + session 执行 + shell/python 捕获
缺口(对照 pi 的六个机制)
pi 机制
位置
byteseek 现状
三段式工具调用 prepare → execute → finalize
prepareToolCall 607 / executePreparedToolCall 677 / finalizeExecutedToolCall 720
无 :shell·run 直接 exec,无参数校验、无前置闸门、无结果规范化
工具结果作为消息回灌 context
createToolResultMessage 784,isError
无 :结果只 println 到终端,模型下一轮看不见
截断即全批失败
231 行 stopReason === "length" → failToolCallsFromTruncatedMessage
无 :max_tokens 截断被当成 vet 语法错
工具声明串行
418 行 executionMode === "sequential"
无 :没有并发概念,但 edit 类必须显式串行
终止由工具决定
shouldTerminateToolBatch 589
无 :只有 exit 关键词
执行钩子
beforeToolCall / afterToolCall / getSteeringMessages / prepareNextTurn
无 (beforeToolCall 正是 G1 权限闸门的形状)
直接后果 :byteseek·run 在生成失败时只打印 跳过执行(生成失败) 就返回,模型无从得知自己错在
哪里;工具报错也停在终端。这是「agent 不能自我修复」的根因,edit 工具缺失只是其中一个症状。
方案总纲
不搬 pi 的双层 while (byteseek 的循环是生成物,不是宿主),只把 pi 的三条语义落到树里:
工具契约统一 :每个工具都是 kv rwfunc,遵守 校验 → 干跑 → 执行 → 回读 → 记账 五步。
工具结果落树并回灌 :结果写 /byteseek/tool/<vid>/<n>,下一轮由 mainbrain 注入 prompt——
这是 pi toolResult 消息的 KV 化,复用现有 memory 注入通道。
错误与截断是一等公民 :llm·call 区分「被截断 / vet 失败 / 运行时报错 / 工具报错」,
分类回灌触发有界 repair。
具体设计
1. lib/edit.kv —— 第一个工具族
工具
语义
关键约定
edit·read(path, from, lines)
读文本(带行号)
只接受 UTF-8;默认窗口 + 大小上限
edit·write(path, text)
覆盖写
仅新建或整文件替换
edit·replace(path, old, new, dry)
精确串替换(核心)
old 命中数必须 恰为 1 :0 → error: 未找到,>1 → error: 歧义
edit·multi(edits)
多处编辑
全部成功才落盘(事务)
edit·grep(pattern, path, glob) / edit·glob(pattern)
定位
避免整文读入,省 token
底层全部是现有能力:networld/fs·{size,read,write,exists} + xv·reinterpret([]uint8 ↔
[]char/utf8)+ string·{find,slice,len}。不新增 rwir ,唯一例外见 P2 的 fs·rename。
rwfunc replace(path, old, new, dry) -> (n) {
fs·read(path, 0, size) -> raw ; xv·reinterpret(raw, "[]char/utf8") -> text
string·find 计数 old ;!= 1 → error(不落盘,p0/p1:不容许尽力而为)
dry == 1 → 返回命中位置与 diff
overwrite → 回读比对 → 不一致则 error
}
2. 工具契约(lib/tool.kv)
工具实现只写「做什么」,tool·call 负责边界策略:
tool·call(name, args) -> (result, kind)
prepare : 工具在 /byteseek/tools/<name> 有声明(签名 + 是否串行 + 是否需确认)
参数按签名校验;不过 → kind = "arg"
gate : before_hook(权限/沙箱白名单)→ 不过 → kind = "denied"(对应 pi beforeToolCall)
execute : 调 rwfunc;异常 → kind = "runtime"
record : /byteseek/tool/<vid>/<n> = {name, args, kind, result, ts}
result : 回一段**给模型看的文本**(成功=摘要,失败=可行动的说明)
3. 工具清单是树数据
现在 prompt.kv 手写「执行 shell 用 shell·run、python 用 python·run」两条。改成:
/byteseek/tools/<name> 声明签名与用途,llm·call 拼 system prompt 时从树里生成工具清单。
新增工具 = 往树里写一条,提示词自动跟上 ——与「提示词即 KV 数据」同源,也让 agent 能自造工具。
4. 结果回灌与有界 repair(补 A3 / #7 前半)
mainbrain 每轮除了 [类别] 与 [相关记忆],再注入上一轮的工具结果摘要:
[上轮工具结果]
/byteseek/tool/1234/0 = edit·replace /tmp/a.py → error: old 命中 3 处,请带更多上下文
触发条件:上轮 kind != "ok"。上限 N=3(与 #41 迭代 2 的有界 repair 合并),失败历史落树可复盘。
5. 错误分类
kind
触发
回灌话术
truncated
响应被 max_tokens 截断(pi 231 行)
「输出被截断,请重发完整参数/代码」
vet
kvlang·vet 不过
原文诊断
runtime
执行期 NameError / 工具异常
错误原文 + PC
arg / denied
参数校验不过 / 闸门拦截
原因 + 正确的签名/边界
与既有设计的接合点
既有
接合方式
提示词即 KV 数据
工具清单同为树数据,自举注入
memory 注入通道
工具结果复用同一注入点,不新开通道
vthread·call / ‥pc
工具执行进度落在 /vthread/{vid}/‥pc,结果落在 /byteseek/tool/<vid>/<n>,与 #69 的 stackmem 同一族坐标
#69 函数执行脑
本提案的工具就是 thinknext 展开到的叶子动作 ;先有叶子,树才有意义
#41 JIT-Agent 总纲
本提案 ≈ 迭代 1(四模块)与迭代 2(repair)之间缺的「工具层」,可直接并入其依赖序
#66 生成代码格式检查
参数校验与 vet 是同一类闸门,共用 kind 分类
G1 沙箱 / G2 密钥
before_hook 就是它们的落点
分期与验收
期
内容
验收
P0
lib/edit.kv 的 read/write/replace:UTF-8 校验、唯一命中、dry-run、回读校验
2 处命中 → error 且文件未变;0 处 → error;1 处 → 成功且回读一致;tests/selftest.kv 增对应断言,make test 全绿
P1
lib/tool.kv 契约 + 结果落 /byteseek/tool/<vid>/<n> + system prompt 工具清单自举
kvspace list /byteseek/tools/ 可见工具;删掉 prompt.kv 里手写的两条后仍能跑通;每次调用有记录
P2
结果回灌 + 有界 repair + kind 分类(含 truncated)
造一个必失败的 edit,观察两轮内自动修复;截断用例回灌话术正确
P3
edit·multi 事务 + grep/glob + fs·rename(唯一新增 rwir,原子替换)+ usage 记账入树
事务中途失败文件不变;改名后无残留临时文件
非目标
不把 pi 的 session/turn 双层 while 搬进 kvlang(那是 01-anything/codex 启发:agent 核心循环分 session/task/turn/thread 四层 #8 的议题,且 byteseek 的循环是生成物)。
不做并行工具调用;edit 类一律串行(对齐 pi 的 executionMode,但用更简单的「全部串行」)。
不新增 Rust rwir,唯一例外是 P3 的 fs·rename(现有 fs·write 是截断式覆盖,崩在中间会毁文件)。
不用 shell·run 调 sed/patch 代替(转义地狱、不可审计),也不做 python 版编辑器(逻辑跑到树外)。
开放问题
工具清单注入方式:拼进 system prompt(简单)还是让模型用 kvspace·list("/byteseek/tools/") 自查(省 token、但多一轮)?
结果回灌粒度:全量回灌 vs 只回摘要 + 路径(推荐后者,与 memory 的薄注入一致)。
edit·replace 是否默认 dry-run(更安全但多一轮往返)?
工具执行记录是每 vid 一份(/byteseek/tool/<vid>/<n>)还是全局追加(/byteseek/tool/<n>)——前者随 vthread 生命周期,后者可跨会话复盘。
提案:工具调用边界 + edit 工具族(对齐 pi-agent 的 agent-loop)
一句话
byteseek 缺的不是若干 edit 工具,而是工具调用边界这一层。pi 把工具边界放在宿主循环里
(prepare → execute → finalize);byteseek 的循环是每轮现造的 kvlang 代码,所以这一层应落到
KV 树里:
lib/tool(契约、清单、结果记录)+lib/edit(第一个工具族)。现状(byteseek 已实现的部分)
lib/byteseek/main.kvmainbrainllm·call→byteseek·runlib/byteseek/llm.kvcallkvlang·vet→kvlang·layout→ 返回入口lib/byteseek/llm.kvrawlib/byteseek/prompt.kv→/byteseek/prompt/systemlib/byteseek/memory.kv/byteseek/memory/*+cat/*;子串召回;classify语言跟随lib/byteseek/memgen.kv/lib/<name>浓缩成·memlib/byteseek/{shell,python}.kvnetworld/proc·exec的薄包main.kvrun→vthread·calltests/selftest.kv缺口(对照 pi 的六个机制)
prepareToolCall607 /executePreparedToolCall677 /finalizeExecutedToolCall720shell·run直接 exec,无参数校验、无前置闸门、无结果规范化createToolResultMessage784,isErrorprintln到终端,模型下一轮看不见stopReason === "length"→failToolCallsFromTruncatedMessagemax_tokens截断被当成 vet 语法错executionMode === "sequential"shouldTerminateToolBatch589exit关键词beforeToolCall/afterToolCall/getSteeringMessages/prepareNextTurnbeforeToolCall正是 G1 权限闸门的形状)直接后果:
byteseek·run在生成失败时只打印跳过执行(生成失败)就返回,模型无从得知自己错在哪里;工具报错也停在终端。这是「agent 不能自我修复」的根因,edit 工具缺失只是其中一个症状。
方案总纲
不搬 pi 的双层 while(byteseek 的循环是生成物,不是宿主),只把 pi 的三条语义落到树里:
校验 → 干跑 → 执行 → 回读 → 记账五步。/byteseek/tool/<vid>/<n>,下一轮由mainbrain注入 prompt——这是 pi
toolResult消息的 KV 化,复用现有 memory 注入通道。llm·call区分「被截断 / vet 失败 / 运行时报错 / 工具报错」,分类回灌触发有界 repair。
具体设计
1.
lib/edit.kv—— 第一个工具族edit·read(path, from, lines)edit·write(path, text)edit·replace(path, old, new, dry)old命中数必须 恰为 1:0 →error: 未找到,>1 →error: 歧义edit·multi(edits)edit·grep(pattern, path, glob)/edit·glob(pattern)底层全部是现有能力:
networld/fs·{size,read,write,exists}+xv·reinterpret([]uint8↔[]char/utf8)+string·{find,slice,len}。不新增 rwir,唯一例外见 P2 的fs·rename。2. 工具契约(
lib/tool.kv)工具实现只写「做什么」,
tool·call负责边界策略:3. 工具清单是树数据
现在
prompt.kv手写「执行 shell 用 shell·run、python 用 python·run」两条。改成:/byteseek/tools/<name>声明签名与用途,llm·call拼 system prompt 时从树里生成工具清单。新增工具 = 往树里写一条,提示词自动跟上——与「提示词即 KV 数据」同源,也让 agent 能自造工具。
4. 结果回灌与有界 repair(补 A3 / #7 前半)
mainbrain每轮除了[类别]与[相关记忆],再注入上一轮的工具结果摘要:触发条件:上轮
kind != "ok"。上限 N=3(与 #41 迭代 2 的有界 repair 合并),失败历史落树可复盘。5. 错误分类
truncatedmax_tokens截断(pi 231 行)vetkvlang·vet不过runtimearg/denied与既有设计的接合点
vthread·call/‥pc/vthread/{vid}/‥pc,结果落在/byteseek/tool/<vid>/<n>,与 #69 的stackmem同一族坐标kind分类before_hook就是它们的落点分期与验收
lib/edit.kv的read/write/replace:UTF-8 校验、唯一命中、dry-run、回读校验error且文件未变;0 处 →error;1 处 → 成功且回读一致;tests/selftest.kv增对应断言,make test全绿lib/tool.kv契约 + 结果落/byteseek/tool/<vid>/<n>+ system prompt 工具清单自举kvspace list /byteseek/tools/可见工具;删掉prompt.kv里手写的两条后仍能跑通;每次调用有记录kind分类(含 truncated)edit·multi事务 +grep/glob+fs·rename(唯一新增 rwir,原子替换)+ usage 记账入树非目标
executionMode,但用更简单的「全部串行」)。fs·rename(现有fs·write是截断式覆盖,崩在中间会毁文件)。shell·run调sed/patch代替(转义地狱、不可审计),也不做 python 版编辑器(逻辑跑到树外)。开放问题
kvspace·list("/byteseek/tools/")自查(省 token、但多一轮)?edit·replace是否默认 dry-run(更安全但多一轮往返)?/byteseek/tool/<vid>/<n>)还是全局追加(/byteseek/tool/<n>)——前者随 vthread 生命周期,后者可跨会话复盘。