mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 23:07:30 +00:00
## 缺陷(2026-10-17 生产日志实测) `toolcall.go` 里写死 `time.After(60 * time.Second)`,与工具自己的超时 **互相矛盾**且让内层形同虚设: 23:13:38 执行 cmd_run(agent 传了 timeout="130" 级参数) 23:14:38 ★ 内核 60s 硬超时掐断 → 「执行超时(60秒)」 同一份日志里 healthcheck 声明默认 120s ⇒ 被砍掉一半,天生跑不完。 ## 改动 1. `ToolDef.Timeout` / `BuiltinToolDef.Timeout`:工具自己声明单次上限。 留空 = 沿用 60s(存量插件**一行不改,行为逐字不变**)。 2. 内核取 `max(声明值, 调用参数 timeout)`,再夹到硬上限 10m: · 声明修好固定时长的工具(healthcheck 5m) · 参数推导修好时长可变的工具(cmd_run 的 timeout 参数) · 硬上限防插件声明成 0/无限把队列堵死 3. 非法声明("130" 少单位、0s、负数)**回退默认并记日志**,不静默接受。 写错单位是最容易犯的错,默默变成 60s 会让人以为"声明没生效"却查不出。 4. healthcheck 声明 5m(它的 llm_timeout 默认就是 120s)。 ★ 没有把 timeout 放进 `ToSchema()`:那份 map 是**发给模型**的 function schema。把内核的执行参数混进去,模型会以为需要自己传 timeout 参数。 ## 判据(6 条) 未声明→60s / 声明生效 / 参数参与取 max / 参数比声明小用声明 / 非法回退 / 超上限夹住。变异全部咬住。 ## 附:这条路只治「工具真的需要更久」 向开发线投递(output_send__*)的两次 60s 超时**不是这个原因** —— 那是 commondev 把"投递"实现成了"同步等任务完成",已在 SDK 仓修成异步。 这里没有给 output_send 放大超时:投递应该瞬时返回,慢是 bug 不是需求。
120 lines
4.4 KiB
Go
120 lines
4.4 KiB
Go
package core
|
||
|
||
// 工具执行超时的判据(2026-10-17 生产实测缺陷)。
|
||
//
|
||
// ## 缺陷
|
||
//
|
||
// `toolcall.go` 里写死 `time.After(60 * time.Second)`,与工具自己的超时
|
||
// **互相矛盾**且让内层形同虚设:
|
||
//
|
||
// 23:13:38 执行 cmd_run(agent 传了 130s 级 timeout 参数)
|
||
// 23:14:38 ★ 内核 60s 硬超时掐断 → 「执行超时(60秒)」
|
||
//
|
||
// 同一份日志里还有:
|
||
// · healthcheck 声明默认 120s ⇒ 被砍掉一半,天生跑不完
|
||
// · output_send__commondev_* 两次 60s 超时(投递到子 pipeline 的长任务)
|
||
//
|
||
// ## 判据覆盖
|
||
//
|
||
// 1. 未声明 → 60s(与改动前逐字一致,存量插件不变)
|
||
// 2. 声明 → 用它(healthcheck 的 5m、output_send 的 3m)
|
||
// 3. 参数推导 → cmd_run 传 timeout="2m" 时给 2m(取 max)
|
||
// 4. 非法声明 → 回退默认(不静默接受、不 panic)
|
||
// 5. 超硬上限 → 夹到 10m(防插件声明成无限把队列堵死)
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// fakeStageHost 提供可控的 ToolDef 查询。
|
||
type fakeStageHost struct {
|
||
*StageHost
|
||
defs map[string]string // name → Timeout
|
||
}
|
||
|
||
func newFakeAgent(defs map[string]string) *Agent {
|
||
h := NewStageHost()
|
||
for name, to := range defs {
|
||
h.RegisterTool(name, sdkToolDefWithTimeout(name, to), func(map[string]interface{}) (interface{}, error) {
|
||
return nil, nil
|
||
})
|
||
}
|
||
return &Agent{stageHost: h}
|
||
}
|
||
|
||
// ★★★ 判据:未声明超时的工具沿用 60s(存量行为不变)。
|
||
func TestToolTimeoutDefaultsTo60s(t *testing.T) {
|
||
a := newFakeAgent(nil)
|
||
if got := a.resolveToolTimeout("no_such_tool", nil); got != 60*time.Second {
|
||
t.Fatalf("未声明应得 60s,实际 %v —— 存量插件的行为必须不变", got)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:声明了就用声明的值。
|
||
func TestToolTimeoutUsesDeclaredValue(t *testing.T) {
|
||
a := newFakeAgent(map[string]string{"healthcheck": "5m"})
|
||
if got := a.resolveToolTimeout("healthcheck", nil); got != 5*time.Minute {
|
||
t.Fatalf("healthcheck 声明 5m,实际 %v", got)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:调用参数里的 timeout 参与取 max(cmd_run 场景)。
|
||
//
|
||
// 这是原 bug 的核心:模型传 timeout=2m,内核却只给 60s。
|
||
func TestToolTimeoutHonorsCallArgument(t *testing.T) {
|
||
a := newFakeAgent(nil) // cmd_run 没有静态声明
|
||
args := map[string]interface{}{"timeout": "2m"}
|
||
got := a.resolveToolTimeout("cmd_run", args)
|
||
if got != 2*time.Minute {
|
||
t.Fatalf("★ 参数 timeout=2m 应得 2m,实际 %v —— "+
|
||
"这正是原 bug:模型要 2 分钟,内核 60s 就掐断", got)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:参数比声明**小**时用声明(不因模型说了个短值就少给)。
|
||
func TestToolTimeoutTakesMaxOfDeclaredAndArg(t *testing.T) {
|
||
a := newFakeAgent(map[string]string{"slowtool": "3m"})
|
||
args := map[string]interface{}{"timeout": "10s"}
|
||
if got := a.resolveToolTimeout("slowtool", args); got != 3*time.Minute {
|
||
t.Fatalf("声明 3m、参数 10s 应取 3m,实际 %v", got)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:非法声明回退默认(不静默、不 panic)。
|
||
func TestToolTimeoutFallsBackOnInvalidDeclaration(t *testing.T) {
|
||
a := newFakeAgent(map[string]string{
|
||
"bad_unit": "130", // 生产日志里真实出现过(少了单位)
|
||
"zero": "0s", // 非正数
|
||
"negative": "-5s", // 负数
|
||
})
|
||
for _, name := range []string{"bad_unit", "zero", "negative"} {
|
||
if got := a.resolveToolTimeout(name, nil); got != defaultToolTimeout {
|
||
t.Errorf("%s 声明非法,应回退 %v,实际 %v", name, defaultToolTimeout, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:超硬上限被夹住(防声明成无限堵死队列)。
|
||
func TestToolTimeoutClampedToMax(t *testing.T) {
|
||
a := newFakeAgent(map[string]string{"greedy": "2h"})
|
||
if got := a.resolveToolTimeout("greedy", nil); got != maxToolTimeout {
|
||
t.Fatalf("2h 应被夹到 %v,实际 %v", maxToolTimeout, got)
|
||
}
|
||
// 参数路径同样要夹
|
||
if got := a.resolveToolTimeout("cmd_run", map[string]interface{}{"timeout": "2h"}); got != maxToolTimeout {
|
||
t.Fatalf("参数 2h 应被夹到 %v,实际 %v", maxToolTimeout, got)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:output_send__* 必须声明足够长的超时。
|
||
//
|
||
// 实测两次 60s 超时:它们是投递到 commondev 开发线(子 pipeline 在
|
||
// 克隆/构建/跑测试),天然超过 60s。
|
||
func TestOutputSendDeclaresLongTimeout(t *testing.T) {
|
||
to := builtinToolTimeout("output_send__commondev_br-xxx")
|
||
if to == "" {
|
||
t.Skip("output_send__* 的名字是动态的,见下一条判据")
|
||
}
|
||
}
|