mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
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
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
@ -10,6 +11,24 @@ import (
|
||||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
// ErrToolNotFound 表示「工具不存在」(未注册 / 所属插件已卸载或崩溃)。
|
||||
//
|
||||
// 存在的理由:工具是**动态注册**的(buildToolDefs 每轮重建、plgreload 即时生效、
|
||||
// 插件崩溃后被摘除),因此「不存在」是运行期常态而非异常。
|
||||
// 判别它必须**类型化**:此前 core 靠 strings.Contains(err, "not found in any plugin")
|
||||
// 匹配错误文案,而插件的错误文案只要恰好含该子串就会被误判为「工具不存在」
|
||||
// 并错误 fallback。errors.Is 才能精确区分「不存在」与「执行失败」——
|
||||
// 二者对 on_error 的处置完全不同(前者工具没了,后者可 retry)。
|
||||
var ErrToolNotFound = errors.New("工具不存在或未注册")
|
||||
|
||||
// ToolNotFound 返回一个包裹 ErrToolNotFound 的错误,带上工具名。
|
||||
func ToolNotFound(name string) error {
|
||||
return fmt.Errorf("tool %s: %w", name, ErrToolNotFound)
|
||||
}
|
||||
|
||||
// IsToolNotFound 报告 err 是否为「工具不存在」。
|
||||
func IsToolNotFound(err error) bool { return errors.Is(err, ErrToolNotFound) }
|
||||
|
||||
// ChannelDef 描述通道在记忆计算层的行为,与 ToolDef.NoMemory/Cleaner 语义一致。
|
||||
type ChannelDef = pubsdk.ChannelDef
|
||||
|
||||
@ -651,15 +670,23 @@ func (m *IOManager) ExecuteTool(name string, args map[string]interface{}) (ret i
|
||||
|
||||
if len(candidates) == 0 {
|
||||
// 自己没这个设备工具 → 看上级(驻留子的设备工具都在父的 io 上)。
|
||||
//
|
||||
// ⚠️ 父的**执行失败**不得被吞成「工具不存在」:那会让 on_error 的
|
||||
// retry 失效(本该重试的失败被判为工具没了,整组被跳过)。
|
||||
// 因此只把父的「确实不存在」继续向上传递,其余错误如实上抛。
|
||||
m.mu.RLock()
|
||||
parent := m.parent
|
||||
m.mu.RUnlock()
|
||||
if parent != nil {
|
||||
if ret, err := parent.ExecuteTool(name, args); err == nil {
|
||||
ret, err := parent.ExecuteTool(name, args)
|
||||
if err == nil {
|
||||
return ret, nil
|
||||
}
|
||||
if !IsToolNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("tool %s not found", name)
|
||||
return nil, ToolNotFound(name)
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
||||
Reference in New Issue
Block a user