mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
feat(toolcall): 结果契约诚实化 —— Success 不再恒真 + 结构化失败可回填(阶段 1b/1d)
问题(实测,三条互相印证):
· ToolResult.Success 硬编码 true(task.go 唯一赋值点)⇒ 该字段在结构上
不可能为 false,是**谎报字段**;
· 工具失败以 nil error + 错误**值**返回(files 的 errorResult、
pluginmgr 的 {"error":…}),上游无从判别;
· stepToolAfter 用 `Result.(string)` 断言,而插件返回的多是 map ⇒
断言几乎恒失败,after_toolcall 阶段对结构化结果的改写**静默失效**。
改动:
· third_party/homeagent-sdk: 新增 ToolError{Field,Reason,Detail,Hint}
与 Error()。纯新增、无签名变更,存量插件不必改动(零值语义:
内核的失败识别同时兼容既有三种约定,新类型是可选项而非迁移要求)。
· internal/sdk: 补 ToolError 别名。
· core/toolerror.go: isToolError / toolErrorText / renderToolResult。
⚠️ 判据必须同时兼容仓内**三种**既有失败约定,且**不得**把成功误判:
① {"error": msg} ② {"isError":true,content:…} ③ *ToolError
明确不判失败的:exit_code != 0(业务结果,带真实 stdout/stderr)、
stderr 非空(cmd 成功常带 warn)、字符串/数字/bool/数组/nil
(自由文本按成功处理:宁可少报失败,也不把正常结果报成失败)。
· core/toolcall.go: executeToolCallOutcome 返回 toolOutcome{Text,Raw},
**Raw 必须在成功分支也带上**——否则结构化失败在 fmt.Sprintf("%v")
那一步被抹平,Success 又退回恒真。panic 与 60s 超时统一以 ToolError
表达(可执行 Hint,避免模型原样重试工具故障)。
· core/task.go: Success=!isToolError(Raw);修恒失败的类型断言;
TaskFrame 增 CurRaw(未降级的原值)。
判据:
· toolerror_test.go 单元级:三种失败约定识别 / 成功形态不误判 /
非零退出不算工具失败 / ToolError 识别。
· TestStageCtxSuccessIsHonestEndToEnd 端到端读 f.StageCtx.ToolResults,
验证**内核产出的值本身**,而非辅助函数。
变异验证(两轮):
· Success 退回硬编码 true ⇒ 端到端判据 3 个子用例 FAIL;
· 把字符串判据改成 strings.Contains(x,"error") ⇒ 「成功文本含 error 字样」
用例 FAIL。**第二轮暴露了判据缺口**(最初没有该用例),已补。
过程中三次自伤(均由判据/编译暴露):用正则批量包装 return 时把
多行 fmt.Sprintf 截断;包装范围溢出到返回 string 的辅助函数;
测试里重复注册同名工具导致 IOManager 取到错误的 device。
遗留:全量 go test ./internal/... 在本机无法完整跑完——/tmp 是 9.8G
tmpfs 且已 98% 占用,link 阶段报 "no space left on device";
/var/tmp 另有约 29G 陈旧 release worktree。与本次改动无关(未触碰
memory/* 等失败包),已在干净基线(7a566d5)对比确认。
This commit is contained in:
35
third_party/homeagent-sdk/sdk/plugin.go
vendored
35
third_party/homeagent-sdk/sdk/plugin.go
vendored
@ -240,6 +240,41 @@ type ToolResult struct {
|
||||
Result interface{} `json:"result"`
|
||||
}
|
||||
|
||||
// ToolError 描述一次工具调用的失败原因。
|
||||
//
|
||||
// 存在的理由:失败若只表达为文本,模型无法定位到字段,只能原样重试
|
||||
// (实测 cmd_run 失败率 34%~48%,全部源于同一个成因:参数被截断或
|
||||
// JSON 写坏,工具却只回报 "command is required" 这类与真因无关的错)。
|
||||
//
|
||||
// ⚠️ 零值语义:插件**不必**改用本类型。内核的失败识别同时兼容既有三种约定
|
||||
// ({"error":…}、{"isError":true,…}、显式 error 返回),见 core.isToolError。
|
||||
// 本类型是给**新写**的工具用的可选项,不是迁移要求。
|
||||
type ToolError struct {
|
||||
// Field 是出错的参数字段名(参数校验失败时填)。
|
||||
Field string `json:"field,omitempty"`
|
||||
// Reason 是机器可读的原因码:required / type / unauthorized / timeout / not_found。
|
||||
Reason string `json:"reason"`
|
||||
// Detail 是人类可读的补充说明。
|
||||
Detail string `json:"detail,omitempty"`
|
||||
// Hint 是给模型的可执行指引(该改什么、不要重试什么)。
|
||||
Hint string `json:"hint,omitempty"`
|
||||
}
|
||||
|
||||
// Error 实现 error,便于工具同时走 (ToolError, error) 通道。
|
||||
func (e *ToolError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
s := e.Reason
|
||||
if e.Field != "" {
|
||||
s = e.Field + ": " + s
|
||||
}
|
||||
if e.Detail != "" {
|
||||
s += " (" + e.Detail + ")"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ToolDef describes a tool that the plugin exposes.
|
||||
type ToolDef struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
Reference in New Issue
Block a user