mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-28 21:33:05 +00:00
主线:工具调用并行化改造(阶段 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
71 lines
2.7 KiB
Go
71 lines
2.7 KiB
Go
package io
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
)
|
||
|
||
// 回归:驻留子的 IOManager 向父兜底时,父的**执行失败**不得被吞成「工具不存在」。
|
||
//
|
||
// 原实现(channel.go:655 附近):
|
||
//
|
||
// if ret, err := parent.ExecuteTool(name, args); err == nil { return ret, nil }
|
||
// // 父的 err 被丢弃 ⇒ 落到 return "tool X not found"
|
||
//
|
||
// 后果放大:设备离线、插件崩溃这类「本该 retry 的失败」被上报为「工具没了」,
|
||
// 于是 on_error 整组跳过 —— 与「插件真的没加载」无法区分。
|
||
func TestExecuteTool_DoesNotSwallowParentFailureAsNotFound(t *testing.T) {
|
||
parent := NewIOManager()
|
||
// 父持有一个会在执行时失败的设备:工具存在,但 Execute 报错。
|
||
failing := &failingDevice{name: "dev", toolName: "boom_tool", err: errors.New("device offline")}
|
||
if err := parent.RegisterDevice(failing); err != nil {
|
||
t.Fatalf("注册失败: %v", err)
|
||
}
|
||
|
||
child := NewIOManager()
|
||
child.SetParentIO(parent)
|
||
|
||
// 工具不在 child 上 → 向父兜底;父执行失败必须**如实上抛**。
|
||
_, err := child.ExecuteTool("boom_tool", map[string]interface{}{})
|
||
if err == nil {
|
||
t.Fatal("期望父的失败被上抛,实际 err=nil(被吞了)")
|
||
}
|
||
if IsToolNotFound(err) {
|
||
t.Fatalf("父的执行失败被误报为『工具不存在』: %v", err)
|
||
}
|
||
if err.Error() != "device offline" {
|
||
t.Errorf("应如实上抛父的错误文案,实际: %v", err)
|
||
}
|
||
}
|
||
|
||
// 真正的「不存在」仍须保持可判别(子与父都没有)。
|
||
func TestExecuteTool_NotFoundStillTypeable(t *testing.T) {
|
||
parent := NewIOManager()
|
||
child := NewIOManager()
|
||
child.SetParentIO(parent)
|
||
|
||
_, err := child.ExecuteTool("no_such_tool", map[string]interface{}{})
|
||
if !IsToolNotFound(err) {
|
||
t.Fatalf("两级都没有时应为 ErrToolNotFound,实际: %v", err)
|
||
}
|
||
}
|
||
|
||
// failingDevice 是一个 Execute 恒定报错的测试设备。
|
||
type failingDevice struct {
|
||
name string
|
||
toolName string
|
||
err error
|
||
}
|
||
|
||
func (d *failingDevice) Name() string { return d.name }
|
||
func (d *failingDevice) Type() DeviceType { return DeviceOutput }
|
||
func (d *failingDevice) Description() string { return "failing test device" }
|
||
func (d *failingDevice) Tools() []ToolDef { return []ToolDef{{Name: d.toolName}} }
|
||
func (d *failingDevice) Execute(string, map[string]interface{}) (interface{}, error) {
|
||
return nil, d.err
|
||
}
|
||
func (d *failingDevice) Start() error { return nil }
|
||
func (d *failingDevice) Stop() error { return nil }
|
||
func (d *failingDevice) OutputCapabilities() OutputCapability { return CapText }
|
||
func (d *failingDevice) ChannelDef() ChannelDef { return ChannelDef{} }
|