Commit Graph

7 Commits

Author SHA1 Message Date
4fd18a2e83 docs(plan): 遗留项收敛——D4 与端到端已完成,提权无需决策 2026-09-27 13:18:28 +08:00
532500e3c9 docs(plan): 内核主线与插件线全部完成,记录两个设计决策与三处遗留 2026-09-27 13:06:10 +08:00
ae3cdaa1a0 docs: 同步两份文档的实现进度(内核主线 0~2.5 全部完成) 2026-09-27 11:56:01 +08:00
446645d0a0 docs(plan): 阶段 2 标记完成,记录并发规则与 2c 判据闭合 2026-09-27 11:27:53 +08:00
3d12e82f65 refactor(toolcall): StageContext 拆 per-tool,为并发执行消除共享槽(阶段 2c)
问题:f.StageCtx 是**单槽**,批内每个工具都覆写它(ToolCalls=[单元素]、
ToolResults 覆写、Results[0] 回读)。串行下看不出问题,但并发下
N 个 goroutine 同写一个 ctx = 数据竞争,且 after_toolcall 插件可能读到
**别的工具**的结果。

改动(task.go):
· TaskFrame 增 toolCtxs []sdk.StageContext(每工具一份)
· buildToolContexts 在 stepLLM 设 PendingTools 时建池;
  Extra **逐份浅拷贝**——共享同一 map 即竞争(stage handler 会写它)
· toolCtxFor(i) 取第 i 份,越界/未建时回落 f.StageCtx(测试替身安全)
· stepToolBegin(before_toolcall + 参数回填)、stepToolExec(写结果)、
  stepToolAfter(读结果)三处全部切到 per-tool ctx

判据(toolbatch_test.go 追加两条):
· 每个工具的 before_toolcall ctx 只带自己的 ToolCalls[0].Name,
  且 Extra[output_channel] 逐份带过去(stage.go:18 依赖它)
· after_toolcall 读到的 Result 必须属于当前工具,不能是批内另一个的

★ 诚实记录:这两条判据在**串行**下**测不出与单槽的差别**——串行时
每工具跑完才进下一个,不存在交错。变体验证(toolCtxFor 退回单槽)后
判据仍全绿。故 2c 记为「实现已就位、判据未闭合」,真正判据必须与 2d
(并发执行)一起写,并以 -race 确认无竞争。已在执行计划中标注。

过程中两次自伤:
· 我的 harness 没设 Extra[output_channel](那是 prepareInputTask 才写的,
  task.go:380),判据一度报「产品缺陷」——核实后是我造的场景,已对齐生产;
· 阶段 1 的 TestStageCtxSuccessIsHonestEndToEnd 读 f.StageCtx.ToolResults,
  拆分后失效——判据跟随新结构改为按批索引取 toolCtxs[i],
  断言的仍是内核产出的 Success 值本身。

顺带记录(非本次引入):TestResidualKeep/Drop 偶发失败,根因是
offload_test.go 的 SpawnResident 起了子调度器 goroutine,而测试
enqueue 后无同步就读同一队列。干净基线 3/3 全绿属运气。已在计划中
记为待修,避免后续误判为并行化引入的回归。
2026-09-27 11:17:06 +08:00
4ba72977d2 test(toolcall): 补批内路径的三条缺失判据(阶段 0.5)
同一批多个 tool_call 的循环(StepToolBegin→Exec→After)此前只被
scheduler_critical_test.go:125 一条用例覆盖「按序执行」,缺的三条正是
阶段 2(并行执行层)要改的地方:

· tool_call_id 配对完整性 —— 阶段 2 改消息落法(一个 assistant 带全部
  tool_calls + N 条 tool)时,配对断裂上游会直接报错
· ContentOnce 批内语义 —— 同一段 assistant 文本在批内重复 N 次,撑爆上下文
· denied 后继续批内 —— 改成 abort 会丢掉本可执行的后续调用

判据 toolbatch_test.go(4 条),全部确定性断言:阶段 0 已消除 map
迭代随机性,同批工具的落序与配对可稳定断言。

变异验证:令 stepToolBegin 跳过批内最后一个工具后,4 条判据同时 FAIL
(既有那条也 FAIL),报错直指 ToolsUsed=[tool_alpha]、
tool_call_id "c2" 被声明 0 次。

更正一处此前的不准确表述:我曾说「无任何测试直接驱动批内路径」——
不准确。scheduler_critical_test.go:125 已驱动「同批两工具按序执行」;
漏查是因为只 grep 了 PendingTools/ToolIdx 字段名,没查断言内容。
真正缺的是上表三条。

过程中三次自伤(均由「判据先写」暴露):臆造不存在的 helper;
stageHost 置 nil 后又使用;给 newTaskFrame 传 nil 导致 stepPrepare 于
task.go:519 nil 解引用 panic(改用仓内既有 a.stageCtxFromInput)。
顺带记录:生产两处 newTaskFrame 调用都传真实 ctx,但 stepPrepare 对
f.StageCtx 无 nil 兜底——本次不修(无生产触发路径),记为潜在缺口。

回归:internal/agent/... 与 internal/plugins/... 全绿。
2026-09-27 08:59:30 +08:00
7a566d50b7 fix(toolcall): 工具「不存在」类型化 + 修父 io 兜底吞错误 + 修并行 tool_call 落序随机
主线:工具调用并行化改造(阶段 0 与 0.2)。

① flush 顺序随机(process.go)
   flushToolCall 由 `for idx := range accs` 驱动,Go map 迭代顺序随机化
   ⇒ 同一批并行 tool_call 进入 resp.ToolCalls 的顺序每次运行都可能不同。
   对 output_send__ 这类用户可见通道,分段消息到达顺序不可复现。
   改为收集 index 后 sort.Ints 再 flush(两个调用点统一走 flushAll)。
   判据 stream_flush_order_test.go(8 工具 × 200 轮),已变异验证可检测。

② 工具「不存在」类型化(io/channel.go、core/stages.go、core/toolcall.go)
   工具是动态注册的,「不存在」是运行期常态而非异常。原先内核用
   strings.Contains(err, "not found in any plugin") 判别——约定而非契约,
   插件文案含该子串即被误判。改用哨兵 ErrToolNotFound + errors.Is
   (沿用仓内 ErrInputChannelUnknown 的先例)。

   ⚠️ 顺带修一个静默 bug:IOManager 向父兜底时吞掉父的执行失败,
   误报为「工具不存在」。后果是设备离线这类本该 retry 的失败被判为
   「工具没了」⇒ 整组被跳过,与「插件真没加载」无法区分。改为只传递
   「确实不存在」,其余如实上抛。

   「不存在」的文案改为可执行指引(get_plugin_tools / output_list_channels),
   而非含糊的「执行失败」——后者会让模型反复重试同一个不存在的名字。

判据:toolcall_error_test.go(类型化 vs 诱饵子串、%w 穿透、执行期文案)、
channel_error_test.go(父失败不吞、真的不存在仍可判别)。
两者均经变异验证。回归:internal/agent/... 与 internal/plugins/... 全绿(14 包)。

设计文档:docs/zh/toolcall-contract-and-sequence-design.md
执行计划:docs/zh/toolcall-parallel-execution-plan.md
2026-09-27 08:55:25 +08:00