Files
HomeAgent/internal/sdk/tool_impl.go
JianFeeeee 7e1169bcda feat(kernel): 内置工具注册进 ToolAPI 面(方案 B,补真机实跑发现的架构缺口)
真机实跑实证(独立实例 /tmp/seqtest,未触碰生产):
  seq_run 报「工具 knowledge_list 不存在或未注册」,
  而**同一轮模型直接调 knowledge_list 是成功的**。

根因:`memory_*` / `knowledge_*` / `doc_*` / `person_*` 这 20+ 个是
**内核内置**工具,在 core.executeToolCallInner 里按**前缀分派**,
由 buildToolDefs 直接生成 schema,**从不进 StageHost / IOManager**
⇒ ToolAPI(只有插件工具 + IO 设备工具)既查不到也调不了。
后果:序列只能编排插件/设备工具,无法编排记忆/知识/文档/人物
——恰恰是最常用的能力。

方案 B 的实现:
· internal/sdk: 新增 BuiltinProvider(Defs/Exec)与 SetBuiltinProvider。
  用**晚绑定注入**而非让 toolImpl 依赖 core,理由:ToolAPI 是**全局单例**
  却需要 per-agent 数据(驻留子是轻量内核,memory 为 nil;内置工具可见性
  由 `if a.memory != nil` 等门控)。sdk 不能依赖 core(方向反了)。
· internal/sdk/tool_impl.go: ToolDefByName / ExecuteTool 补查内置工具。
  ⚠️ ExecuteTool 只在「io 确实没有该工具」时才转内置;io 的**执行失败**
  必须如实上抛 —— 否则会把「设备离线」误报成「工具不存在」,让调用方
  按 missing 策略跳过(与 P3 修过的父 io 吞错误同一族陷阱)。
  内置工具**默认不声明 ParallelSafe**(含 SQLite 写与召回)。
· internal/agent/core/builtin_toolapi.go: Agent 侧 provider。
  ★ Defs **复用 buildToolDefs 的同一批生成逻辑**(筛出不在
  StageHost/IOManager 中的那些),保证"模型看得到什么"与"插件看得到什么"
  门控完全一致 —— 避免两套语义。
  Exec 复用 executeToolCall 完整路径(授权闸 + 异常处理)。
· cmd/homed/bootstrap.go: agent 构造后注入。

★ 不会让模型看到重复工具(已核实):模型侧走 buildToolDefs
(a.io / a.stageHost **直调**),ToolAPI 只经 PluginSDK.Tool() 暴露给插件
—— 两条不重叠的路径。

判据(builtin_toolapi_test.go,6 条):
· 内置工具能从 ToolAPI 查到
· ★ 查到 ≠ 调得通:必须真的能执行
· 门控语义保持:未接 memory/knowledge 时不得声称有那些工具
· ★ 接了 knowledge 时必须可见(这正是要补的缺口)
· ToolAPI 上"不存在"必须是类型化 not-found(供 seq 的 missing 策略用)
· 内置工具默认不声明并发安全

真机复验(同一隔离实例,新二进制):
  序列 "smoke2" 执行完毕(1/1 组)— 工具 1 个
  变量槽: summary = cangjie/central-repo/agreement/...
⇒ knowledge_list 成功执行并回填具名槽。上一次的「不存在或未注册」已消除。

已知局限(记入待定):ToolAPI 单例而内置工具面是 per-agent,
多 agent 下看到的是"最近一个注入者"的面。本次不解决。
2026-09-27 13:40:22 +08:00

181 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
import (
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
)
// toolImpl 桥接 StageHost(插件工具)与 IOManager(设备/通道工具)。
type toolImpl struct {
stageHost ToolSource
iom *agentIO.IOManager
}
func NewTool(sh ToolSource, iom *agentIO.IOManager) ToolAPI {
return &toolImpl{stageHost: sh, iom: iom}
}
func (t *toolImpl) GetToolDefs() []ToolDef {
if t.stageHost == nil {
return nil
}
return t.stageHost.GetToolDefs()
}
func (t *toolImpl) GetAllTools() []ToolDef {
if t.iom == nil {
return nil
}
defs := t.iom.GetAllTools()
out := make([]ToolDef, 0, len(defs))
for _, d := range defs {
// ⚠️ ParallelSafe 必须一并带出:它决定该工具能否被并发执行。
// 此前这里漏了它 ⇒ 插件看到的设备工具一律"不可并发",
// 设备工具的并发声明等于对插件不可见。
out = append(out, ToolDef{
Name: d.Name,
Description: d.Description,
Parameters: d.Parameters,
ParallelSafe: d.ParallelSafe,
})
}
return out
}
func (t *toolImpl) ExecuteTool(name string, args map[string]interface{}) (interface{}, error) {
if t.stageHost != nil {
if def := t.stageHost.ToolDef(name); def != nil {
return t.stageHost.ExecuteTool(name, args)
}
}
if t.iom != nil {
ret, err := t.iom.ExecuteTool(name, args)
if err == nil {
return ret, nil
}
// io 没有该工具 ⇒ 试内置工具(方案 B)。
// ⚠️ 只在「确实不存在」时才继续;io 的**执行失败**必须如实上抛,
// 否则会把「设备离线」误报成「工具不存在」,让调用方按 missing
// 策略跳过——这与 P3 修过的父 io 吞错误是同一族陷阱。
if !agentIO.IsToolNotFound(err) {
return nil, err
}
}
// ② 内核内置工具
if hasBuiltin(name) {
return builtinExec(name, args)
}
return nil, agentIO.ToolNotFound(name)
}
var _ ToolAPI = (*toolImpl)(nil)
// ToolDefByName 按名字查任一来源的工具声明(StageHost 优先,再查 IO 设备)。
//
// 用途:插件在**运行前**判断目标工具是否存在、是否并发安全。工具是动态
// 注册的,"不存在"是常态(插件未加载/已卸载/崩溃),因此查不到一律返回
// nil 交由调用方按"不存在"处理——不得 panic。
func (t *toolImpl) ToolDefByName(name string) *ToolDef {
if t == nil || name == "" {
return nil
}
if t.stageHost != nil {
if def := t.stageHost.ToolDef(name); def != nil {
return def
}
}
if t.iom != nil {
if def, ok := t.iom.ToolDefOf(name); ok {
return &ToolDef{
Name: def.Name,
Description: def.Description,
Parameters: def.Parameters,
ParallelSafe: def.ParallelSafe,
}
}
}
// ③ 内核内置工具(方案 B):此前这里直接返回 nil ⇒ 插件既查不到也调不了
// memory_*/knowledge_*/doc_*/person_*。
for _, d := range builtinDefs() {
if d.Name == name {
return &ToolDef{
Name: d.Name,
Description: d.Description,
Parameters: d.Parameters,
// ⚠️ 内置工具**默认不声明并发安全**:它们含 SQLite 写与召回,
// 且部分依赖 per-agent 状态(驻留子是轻量内核,memory 为 nil)。
// 保守默认 = 不并发,与它们今天的行为一致。
ParallelSafe: false,
}
}
}
return nil
}
// CanUse 报告「执行该工具是否被授权」(D4)。
//
// 实现要点:**必须与 core.executeToolCallInner 里的那道闸同源同语义**,
// 否则两条路径判定不同,本身就是漏洞。判据 core.TestCanUseAgreesWithInnerPath
// 逐例比对两条路径的结论。
//
// 判据只有一条:设备类工具按 `device/<id>` 查当前 agent 的 allowedOutputs。
//
// ⚠️ device_id 缺失时**放行**(fail-open)——这是内核现状
// (core 的 TestDeviceToolAuth_* 依赖它)。两处行为已由
// TestCanUseMatchesInnerFailOpenOnMissingDeviceID 钉住;若将来要改成
// fail-closed,**必须两处同时改**,否则两条路径不一致。
func (t *toolImpl) CanUse(toolName string, args map[string]interface{}) bool {
if t == nil || t.iom == nil {
return true // 拿不到设备视图 ⇒ 不拦(与内核无 io 时的行为一致)
}
// 非设备工具不受此闸影响:闸的作用域必须窄,否则会把所有工具锁死。
if _, isDeviceTool := t.iom.DeviceOfTool(toolName); !isDeviceTool {
return true
}
id, _ := args["device_id"].(string)
if id == "" {
return true // fail-open,与内核一致
}
return t.canUseDevice(id)
}
// canUseDevice 是**可注入**的授权查询。
//
// 为何不直接在 toolImpl 里调 Agent:toolImpl 在 internal/sdk 包,
// 而 IsOutputAllowed 是 core.*Agent 的方法(core 反向依赖 sdk,
// sdk 不能依赖 core)。故内核在装配时把查询函数注入进来。
var deviceAuthQuery func(deviceID string) bool
// SetDeviceAuthQuery 注入"设备是否已授权"的查询(由内核在装配时调用)。
//
// 传 nil 表示尚未注入 ⇒ CanUse 对设备工具**放行**(保持存量行为不变)。
func SetDeviceAuthQuery(fn func(deviceID string) bool) { deviceAuthQuery = fn }
func (t *toolImpl) canUseDevice(deviceID string) bool {
if deviceAuthQuery == nil {
return true
}
return deviceAuthQuery(deviceID)
}
// builtinExec 执行内核内置工具。
//
// 放在本文件是因为要用 agentIO.ErrToolNotFound(sdk/tool.go 未导入 agentIO)。
// 未注入 provider 时返回**类型化**的 not-found,使调用方(如 seq 的
// missing 策略)能把它与「插件执行失败」区分开 —— 二者的处置完全不同。
func builtinExec(name string, args map[string]interface{}) (string, error) {
if builtinProv == nil {
return "", agentIO.ToolNotFound(name)
}
return builtinProv.Exec(name, args)
}
// hasBuiltin 报告该名字是否是可见的内置工具。
func hasBuiltin(name string) bool {
for _, d := range builtinDefs() {
if d.Name == name {
return true
}
}
return false
}