From ea6cae956cb53baeeef543bbde67ddbd52e2e681 Mon Sep 17 00:00:00 2001 From: "peng.li24" <734991033@qq.com> Date: Wed, 16 Sep 2026 10:08:00 +0800 Subject: [PATCH] =?UTF-8?q?byteseek:=20=E5=81=9C=E7=82=B9=E8=A7=84?= =?UTF-8?q?=E5=88=92=E2=80=94=E2=80=94ask=C2=B7pause=20=E8=B5=B7=E5=81=9C?= =?UTF-8?q?=E7=82=B9=EF=BC=8Csolve=20=E6=8C=89=E5=81=9C=E7=82=B9=E5=86=B3?= =?UTF-8?q?=E7=AD=96=E7=BB=AD=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生成程序在需要「先看结果再决定下一步」时调用 byteseek/ask·pause(reason):本 vthread 就地置 paused,控制权交回 harness。solve 见 kind=paused 就用 kvlang·printstack 取 活动栈问 LLM,把 LLM 生成的一段程序写进 /byteseek/next/fn,再把该 vthread 置回 running——ask·pause 恢复后就地 vthread·call 那段程序,跑完回到原函数继续。 停点是显式的、栈是交给 LLM 的事实:这比让模型内联调 LLM 更可复盘——决策与当时 的栈都留在轨迹里。 - main·run 改开子 vthread,返回 (kind, detail, vid);终态判定抽成 classify (error→runtime / paused→paused / 其余看 /byteseek/last/result) - 新增 main·resume(vid, fn);重试上限 3 → 4 轮,paused 不吃重试 - llm 生成函数名唯一化:同名会覆盖已入库的函数树,而暂停中的帧还指着它 - prompt 增「停点由你自己规划」硬规则 + 示例3/4(该停 vs 程序自己能判断就别停) - 工具清单登记 byteseek.ask.pause;doc/substrate.md 同步(dump → printlib) - selftest 增停点/取栈/恢复/标记断言;tests/stopplan.sh:真实 LLM 停点回归 (确定性任务期望 0 停点,需语义判断的期望 ≥1) Co-Authored-By: Claude Code --- Makefile | 4 ++ doc/substrate.md | 2 +- lib/byteseek/ask.kv | 26 ++++++++++ lib/byteseek/llm.kv | 9 ++++ lib/byteseek/main.kv | 105 +++++++++++++++++++++++++++++------------ lib/byteseek/prompt.kv | 23 +++++++++ lib/byteseek/tools.kv | 1 + tests/selftest.kv | 22 +++++++++ tests/stopplan.sh | 40 ++++++++++++++++ 9 files changed, 201 insertions(+), 31 deletions(-) create mode 100644 lib/byteseek/ask.kv create mode 100755 tests/stopplan.sh diff --git a/Makefile b/Makefile index 36faac1..960c490 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,10 @@ test: boot echo "$$out" | grep -q "ALLOWED: 0" && \ echo "$$out" | grep -q "ARG: 9" && \ echo "$$out" | grep -q "TOOLCALL: true" && \ + echo "$$out" | grep -q "PAUSED: paused" && \ + echo "$$out" | grep -q "STACK: true" && \ + echo "$$out" | grep -q "RESUMED: ok" && \ + echo "$$out" | grep -q "MARKERS: 1 1" && \ echo "$$out" | grep -q "MEM: theme" && \ echo "$$out" | grep -q "EDIT: 1 0 a c" && echo "✅ selftest 通过" || { echo "❌ selftest 失败"; exit 1; } @dsn=$${REPL_SMOKE_DSN:-shm:///tmp/byteseek_repl_smoke}; \ diff --git a/doc/substrate.md b/doc/substrate.md index b0a3021..6b2f7c4 100644 --- a/doc/substrate.md +++ b/doc/substrate.md @@ -111,7 +111,7 @@ byteseek 不再注册任何自有 Rust rwir。所需能力全部是 kvlang 标 | `print` / `println` / `cerr` / `input` | 标准 term rwir | | `json·to` / `json·from` | 标准 json rwir | | `http·call(method,header,url,body) -> resp` | 标准 http rwir | -| `kvlang·vet / ·format / ·layout / ·dump` | 标准 layout rwir | +| `kvlang·vet / ·format / ·layout / ·printlib` | 标准 layout rwir(printlib = 看 layout 结果,不读 .src) | | `networld/proc·exec(args,envs) -> code, out, err` | 标准 networld rwir(子进程 + 捕获 @ 句柄) | | `vthread·call(funckey)` | native builtin(同 vid 动态调用) | | `string·* / kv·* / xv·*` | native builtin | diff --git a/lib/byteseek/ask.kv b/lib/byteseek/ask.kv new file mode 100644 index 0000000..c2f8c34 --- /dev/null +++ b/lib/byteseek/ask.kv @@ -0,0 +1,26 @@ +// byteseek 决策点:程序跑到需要「外部决策」(下一步该做什么)时,就地暂停本 vthread, +// 把控制权交回 harness;harness 用 kvlang·printstack 取活动栈问 LLM,把 LLM 生成的 +// 下一步程序写进 /byteseek/next/fn,再把本 vthread 置回 running 续跑—— +// ask·pause 恢复后就地 vthread·call 那段新程序,跑完回到原函数继续。 +// 这是 #69「函数执行脑」的最小落点:停点是显式的,栈是交给 LLM 的事实。 + +lib byteseek { + lib ask { + // pause(reason):写暂停原因 → 暂停本 vthread → 恢复后读取 harness 注入的下一步并就地调用。 + rwfunc pause(reason:[]char/utf32) -> () { + kvspace·set("/byteseek/next/reason", reason) + kvspace·set("/byteseek/next/fn", "") + vthread·setstatus("paused") + kvspace·has("/byteseek/next/fn") -> has + fn = "" + if (has) { + kvspace·get("/byteseek/next/fn") -> raw + char/utf32(raw) -> fn + } + if (fn != "") { + kvspace·set("/byteseek/next/fn", "") + vthread·call(fn) + } + } + } +} diff --git a/lib/byteseek/llm.kv b/lib/byteseek/llm.kv index 4393767..8cf64ec 100644 --- a/lib/byteseek/llm.kv +++ b/lib/byteseek/llm.kv @@ -64,6 +64,15 @@ lib llm { println("[llm] 生成代码:") println(kv) } + // 名字唯一化:同名会覆盖已入库的函数树——暂停中的帧还指着它,续跑/重跑都会踩。 + kvspace·has("/lib/byteseek/session/" + name) -> taken + suffix = 1 + while (taken) { + suffix + 1 -> suffix + string·formatint(suffix, 10) -> sn + name + "_" + sn -> name + kvspace·has("/lib/byteseek/session/" + name) -> taken + } "lib byteseek {\nlib session {\nlib " + name + " {\n" + kv + "\nbyteseek/session/" + name + "·main()\n}\n}\n}\n" -> wrapped kvlang·vet(wrapped) -> v if (v != "ok") { diff --git a/lib/byteseek/main.kv b/lib/byteseek/main.kv index 6affc96..f1705ef 100644 --- a/lib/byteseek/main.kv +++ b/lib/byteseek/main.kv @@ -38,30 +38,48 @@ lib byteseek { println("bye.") } - // byteseek·solve(prompt):生成 → 执行 → 失败回灌 → 有界重试(≤3 轮)。 - // 失败回灌是 pi agent-loop 的 toolResult(isError) 的 KV 化:模型下一轮看得见自己错在哪。 + // byteseek·solve(prompt):生成 → 执行 → (失败回灌 | 停点决策)→ 有界重试(≤4 轮)。 + // 失败回灌 = pi 的 toolResult(isError) 的 KV 化;停点决策 = 把活动栈交给 LLM 定下一步(#69)。 rwfunc solve(prompt:[]char/utf32) -> () { llm·call(prompt) -> entry - byteseek·run(entry) -> (kind, detail) - attempt = 1 - while (kind != "ok" && attempt <= 3) { + byteseek·run(entry) -> (kind, detail, vid) + round = 1 + while (kind != "ok" && round <= 4) { byteseek·brief(detail) -> bd - prompt + "\n\n[上轮执行未成功] " + kind + ":" + bd + "\n请修正后重新生成(只输出 /)。" -> retry - llm·call(retry) -> entry - byteseek·run(entry) -> (kind, detail) - attempt + 1 -> attempt + if (kind == "paused") { + // 停点:把活动栈交给 LLM,让它只补「下一步」,由暂停中的 vthread 就地接上 + kvlang·printstack(vid) -> stack + prompt + "\n\n[任务执行到停点,需要你决定下一步]\n" + stack + "\n[暂停原因] " + bd + "\n请只输出 /:一段补充的 kvlang 程序,它会就地接在停点继续执行。" -> askprompt + llm·call(askprompt) -> next + string·find(next, "error") -> ne + string·find(next, "truncated") -> nt + if (ne == 0 || nt == 0) { + "vet" -> kind + next -> detail + } else { + byteseek·resume(vid, next) + byteseek·classify(vid) -> (kind, detail) + } + } else { + prompt + "\n\n[上轮执行未成功] " + kind + ":" + bd + "\n请修正后重新生成(只输出 /)。" -> retry + llm·call(retry) -> entry + byteseek·run(entry) -> (kind, detail, vid) + } + round + 1 -> round } if (kind != "ok") { - println("[byteseek] 重试 3 轮仍未成功:", kind, "——", detail) + println("[byteseek] 4 轮内仍未成功:", kind, "——", detail) } } - // byteseek·run(entry) -> (kind, detail):在当前 vthread(同 vid)执行生成程序并记账。 - // kind empty / truncated / vet / fail / ok + // byteseek·run(entry) -> (kind, detail, vid):开子 vthread 执行生成程序并记账。 + // kind empty / truncated / vet / fail / runtime / paused / ok // 工具级失败由程序按协议写 /byteseek/last/result("fail: 原因");没写 = ok。 - rwfunc run(entry:[]char/utf32) -> (kind:[]char/utf32, detail:[]char/utf32) { + rwfunc run(entry:[]char/utf32) -> (kind:[]char/utf32, detail:[]char/utf32, + vid:[]char/utf32) { "" -> kind "" -> detail + "" -> vid if (entry == "") { "empty" -> kind } else { @@ -83,23 +101,7 @@ lib byteseek { "入口不存在: " + entry -> detail } else { vthread·run(vid) - "/vthread/" + vid + "/‥status" -> sp - kvspace·get(sp) -> st - if (st == "error") { - "runtime" -> kind - "/vthread/" + vid + "/‥error/msg" -> ep - kvspace·get(ep) -> em - char/utf32(em) -> em - em -> detail - } else { - kvspace·get("/byteseek/last/result") -> rep - if (rep == "" || rep == "ok") { - "ok" -> kind - } else { - "fail" -> kind - rep -> detail - } - } + byteseek·classify(vid) -> (kind, detail) } } } @@ -107,6 +109,49 @@ lib byteseek { byteseek·record(kind, detail, entry) } + // byteseek·classify(vid) -> (kind, detail):读子 vthread 的终态/停点与程序自报。 + // error→runtime(带 /vthread//‥error/msg);paused→paused(带暂停原因); + // 其余看 /byteseek/last/result:""/"ok"→ok,其他→fail。 + rwfunc classify(vid:[]char/utf32) -> (kind:[]char/utf32, detail:[]char/utf32) { + "" -> kind + "" -> detail + "/vthread/" + vid + "/‥status" -> sp + kvspace·get(sp) -> st + if (st == "error") { + "runtime" -> kind + "/vthread/" + vid + "/‥error/msg" -> ep + kvspace·get(ep) -> em + char/utf32(em) -> em + em -> detail + } else { + if (st == "paused") { + "paused" -> kind + kvspace·has("/byteseek/next/reason") -> hr + if (hr) { + kvspace·get("/byteseek/next/reason") -> rr + char/utf32(rr) -> detail + } + } else { + kvspace·get("/byteseek/last/result") -> rep + if (rep == "" || rep == "ok") { + "ok" -> kind + } else { + "fail" -> kind + rep -> detail + } + } + } + } + + // byteseek·resume(vid, fn):把 LLM 生成的下一步入口写进 /byteseek/next/fn, + // 把暂停中的 vthread 置回 running 再驱动——暂停点里的 ask·pause 恢复后会就地调用它。 + rwfunc resume(vid:[]char/utf32, fn:[]char/utf32) -> () { + kvspace·set("/byteseek/next/fn", fn) + "/vthread/" + vid + "/‥status" -> sp + kvspace·set(sp, "running") + vthread·run(vid) + } + // byteseek·record(kind, detail, entry):一次执行的记录落 /byteseek/tool/(可寻址、可复盘)。 rwfunc record(kind:[]char/utf32, detail:[]char/utf32, entry:[]char/utf32) -> () { kvspace·listlen("/byteseek/tool/") -> n diff --git a/lib/byteseek/prompt.kv b/lib/byteseek/prompt.kv index 595febe..70751b3 100644 --- a/lib/byteseek/prompt.kv +++ b/lib/byteseek/prompt.kv @@ -31,10 +31,33 @@ rwfunc main() -> () { } +示例3——用户说"读 /tmp/a.txt 判断它属于哪类内容,把结论写进 /tmp/kind"(判断得由你来做,写代码时你看不到文件内容): +judge_file + +rwfunc main() -> () { + networld/edit·read("/tmp/a.txt", 1, 0) -> (text, total, sz) + println(text) + byteseek/ask·pause("已读出内容,需要判断它属于哪类并决定写什么") +} + +注意:暂停之后**不用写后续步骤**——harness 会把当前活动栈交给 LLM,把你生成的"下一步"接回同一栈继续执行。 + +示例4——用户说"读 /tmp/b.txt,如果含 b 就替换成 c"(这是程序自己就能判断的事,不要停): +replace_b + +rwfunc main() -> () { + byteseek/tool·edit_replace("/tmp/b.txt", "b", "c", 0) -> (done, hit, code) + println(code) +} + + 硬规则: - 只写顶层 rwfunc main 定义,不要写 main() 调用。禁止 lib 块,禁止把代码包进别的结构,禁止 markdown 围栏。 - 工具清单见 system prompt 的《可用工具》段——那一段由 /byteseek/tools/ 树生成,只列真实存在的工具;不要调用清单以外的名字。 - 改文件必须走清单里的 byteseek/tool·edit_*(闸门 + 记账;replace 只在原文唯一命中时落盘);不要用 string·replace + networld/fs·writetext,也不要直调 networld/edit·* 绕过闸门。 +- **停点由你自己规划**:写代码时你只能看到需求、看不到运行结果。凡是"下一步做什么"需要你先看过结果再判断的(读懂文件/报错/网页内容,再决定策略或写法),就用 byteseek/ask·pause("为什么需要决策") 停在那里——harness 会把活动栈交给 LLM,并把下一步接回同一栈继续。 +- **程序自己能判断的就不要停**:条件分支、循环、字符串处理、工具返回码判断,直接写进代码(见示例4);每次停点都是一次 LLM 往返,只在该停的地方停。 +- 需要语义判断时也不要自己内联调 LLM(llm·raw 之类):停点交给 harness,决策与当时的栈才留在可复盘的轨迹里。 - 若你判断任务没完成(例如工具返回非 0 的 code),把原因写进 `/byteseek/last/result`(格式 `fail: 原因`);byteseek 会把它回灌给你重试。成功不用写。 - 最终结果用 println(...) 打印。 - 如果需求后面附有 `[相关记忆]`(若干 `key = value` 行),这些是已沉淀在 kvspace 树里的记忆;优先利用其中与任务相关的事实或结论来生成代码。 diff --git a/lib/byteseek/tools.kv b/lib/byteseek/tools.kv index fe9561d..10e3d2d 100644 --- a/lib/byteseek/tools.kv +++ b/lib/byteseek/tools.kv @@ -11,6 +11,7 @@ lib byteseek { kvspace·set("/byteseek/tools/byteseek.tool.edit_write", "byteseek/tool·edit_write(path, text) -> (n, code) —— 原子覆盖写(受闸门与记账;code 8 = 被策略拒绝)") kvspace·set("/byteseek/tools/byteseek.tool.edit_replace", "byteseek/tool·edit_replace(path, old, new, dry) -> (done, hit, code) —— 精确替换:old 必须唯一命中,否则一个字节都不动;四个参数都要给:dry=0 落盘 / dry=1 只预演;受闸门与记账") kvspace·set("/byteseek/tools/byteseek.tool.edit_multi", "byteseek/tool·edit_multi(path, olds, news, dry) -> (done, bad, code) —— 事务式多处替换,全成功才落盘;四个参数都要给:dry=0 落盘 / dry=1 只预演;受闸门与记账") + kvspace·set("/byteseek/tools/byteseek.ask.pause", "byteseek/ask·pause(reason) —— 停点:需要先看到这一步的结果才能决定下一步时调用;本 vthread 就地暂停,harness 拿活动栈问 LLM,再把下一步接回同一栈续跑") kvspace·set("/byteseek/tools/networld.fs.readtext", "networld/fs·readtext(path) -> (text, size) —— 读整个文件为文本") kvspace·set("/byteseek/tools/networld.fs.writetext", "networld/fs·writetext(path, text) -> n —— 把文本写入文件") kvspace·set("/byteseek/tools/networld.fs.glob", "networld/fs·glob(pattern) -> paths —— 按通配找文件(\\n 连接)") diff --git a/tests/selftest.kv b/tests/selftest.kv index a89988c..994b9a9 100644 --- a/tests/selftest.kv +++ b/tests/selftest.kv @@ -31,6 +31,18 @@ lib byteseek { } byteseek/session/boom·main() } + lib waiter { + rwfunc main() -> () { + kvspace·set("/byteseek/selftest/before", "1") + byteseek/ask·pause("测试停点") + kvspace·set("/byteseek/selftest/after", "1") + } + } + lib step2 { + rwfunc main() -> () { + kvspace·set("/byteseek/selftest/injected", "1") + } + } } } @@ -62,6 +74,16 @@ lib selftest { println("ARG:", tc3) kvspace·listlen("/byteseek/toolcall/") -> tcn println("TOOLCALL:", tcn > 0) + byteseek·run("byteseek/session/waiter·main") -> (pk, pd, pvid) + println("PAUSED:", pk) + kvlang·printstack(pvid) -> pst + println("STACK:", string·contains(pst, "status=paused")) + byteseek·resume(pvid, "byteseek/session/step2·main") + byteseek·classify(pvid) -> (rk, rd) + println("RESUMED:", rk) + kvspace·get("/byteseek/selftest/injected") -> inj + kvspace·get("/byteseek/selftest/after") -> aft + println("MARKERS:", inj, aft) byteseek/memory·remember("theme", "byteseek 使用 kvlang 构建") byteseek/memory·remember("unrelated", "今天天气不错") byteseek/memory·recall("kvlang") -> m diff --git a/tests/stopplan.sh b/tests/stopplan.sh new file mode 100755 index 0000000..721c799 --- /dev/null +++ b/tests/stopplan.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# 停点规划回归:模型是否**自己**判断该不该停(需要真实 LLM)。 +# 用例1 确定性任务(程序自己能判断)→ 期望 0 次停点 +# 用例2 需要语义判断的任务(写代码时看不到内容)→ 期望 ≥1 次停点 +# 用法:tests/stopplan.sh [任务名...](默认两个用例都跑) +set -uo pipefail +cd "$(dirname "$0")/.." +export KVSPACE=${KVSPACE:-redis://127.0.0.1:6379} +export KVLANG_LIB=lib + +D=/tmp/stopplan +mkdir -p "$D" +printf 'x\nb\ny\n' > "$D/b.txt" +printf 'def f(x):\n return x + 1\n' > "$D/a.txt" + +KVLANG_LIB=lib kvlang >/dev/null 2>&1 || { echo "byteseek 引导失败"; exit 2; } + +run_case() { # $1=用例名 $2=期望(0|1) $3=任务 + local name=$1 want=$2 task=$3 log="/tmp/stopplan-$1.log" + printf '%s\n' "$task" | timeout 600 kvlang byteseek·main >"$log" 2>&1 + local got + got=$(grep -c "任务执行到停点" "$log") + local ok="FAIL" + if [ "$want" = "0" ] && [ "$got" = "0" ]; then ok="PASS"; fi + if [ "$want" = "1" ] && [ "$got" -ge 1 ]; then ok="PASS"; fi + printf '%s %-14s 停点=%s(期望%s) log=%s\n' "$ok" "$name" "$got" "$want" "$log" +} + +if [ $# -gt 0 ]; then + for n in "$@"; do + case "$n" in + deterministic) run_case deterministic 0 "读 $D/b.txt,如果含 b 就替换成 c" ;; + judgement) run_case judgement 1 "读 $D/a.txt,判断它属于哪类内容(代码/散文/数据),把结论写进 $D/kind" ;; + *) echo "未知用例:$n" ;; + esac + done +else + run_case deterministic 0 "读 $D/b.txt,如果含 b 就替换成 c" + run_case judgement 1 "读 $D/a.txt,判断它属于哪类内容(代码/散文/数据),把结论写进 $D/kind" +fi