Files
HomeAgent/internal/agent/core/builtin_toolapi.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

138 lines
5.2 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 core
import (
"strings"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
// 本文件实现内核侧对"内置工具注册面"(方案 B)的注入。
//
// 背景与方案 B 的完整理由见 internal/sdk/tool.go 末尾的注释。
// 一句话:`memory_*` / `knowledge_*` / `doc_*` / `person_*` 这 20+ 个是
// **内核内置**的,在 executeToolCallInner 里按前缀分派,从不进 ToolAPI,
// 于是插件(seq)既查不到也调不了 —— 真机实跑实证:seq_run 报
// 「工具 knowledge_list 不存在或未注册」,而同一轮模型直接调它是成功的。
//
// 注入必须**晚绑定**且**per-agent**:内置工具的可见性由运行期状态门控
// (`if a.memory != nil` / `if a.knowledge != nil` …),而驻留子是轻量内核、
// memory 为 nil。<6C><E38082> ToolAPI 是全局单例,拿不到 agent,只能由 agent 自己
// 提供一份 provider。
// builtinProvider 是 *Agent 上的适配器:把 agent 的内置工具面
// 转成 sdk.BuiltinProvider。
type builtinProvider struct{ a *Agent }
// Defs 返回当前 agent 可见的内置工具声明。
//
// ⚠️ **必须复用 buildToolDefs 的同一批生成逻辑**,否则会出现"两套语义":
// 一套决定模型看得到什么(buildToolDefs),另一套决定插件看得到什么。
// 这里直接从 buildToolDefs 里筛出**不在** StageHost/IOManager 中的那些,
// 从而保证门控条件(memory/knowledge 是否就绪)完全一致。
func (p builtinProvider) Defs() []sdk.BuiltinToolDef {
if p.a == nil {
return nil
}
// 先算出"插件/设备侧已有的名字",剩下的才是内核内置的。
external := map[string]bool{}
if p.a.io != nil {
for _, d := range p.a.io.GetAllTools() {
external[d.Name] = true
}
}
if p.a.stageHost != nil {
for _, d := range p.a.stageHost.GetToolDefs() {
external[d.Name] = true
}
}
var out []sdk.BuiltinToolDef
for _, raw := range p.a.buildToolDefs() {
m, ok := raw.(map[string]interface{})
if !ok {
continue
}
fn, ok := m["function"].(map[string]interface{})
if !ok {
continue
}
name, _ := fn["name"].(string)
if name == "" || external[name] {
continue
}
desc, _ := fn["description"].(string)
params, _ := fn["parameters"].(map[string]interface{})
out = append(out, sdk.BuiltinToolDef{
Name: name, Description: desc, Parameters: params,
})
}
return out
}
// Exec 执行一个内置工具。
//
// 直接复用 executeToolCall 的完整路径(内置分支 + 授权闸 + 异常处理),
// **不复用** executeToolCallInner:后者要求经前缀 switch,而 ToolAPI 的
// 存在性判定已在 ToolDefByName/ExecuteTool 做过一次。
//
// ⚠️ 传入的 toolCall 不带 RawArguments(插件侧没有原始 JSON),
// 因此 __arg_error 的信息面在插件路径上天然缺失 —— 插件调用的是
// **已解析**的参数,不存在被截断的中间态。
func (p builtinProvider) Exec(name string, args map[string]interface{}) (string, error) {
if p.a == nil {
return "", errBuiltinNoAgent
}
tc := agentAPI.ToolCall{
ID: "builtin_" + name,
Name: name,
Arguments: args,
}
return p.a.executeToolCall(tc, p.a.defaultChannelForBuiltin()), nil
}
// defaultChannelForBuiltin 给出内置工具执行时的输出通道。
//
// 内置工具本身不产出"用户可见输出"(结果回给调用方),但 executeToolCall
// 的签名需要 channel(如记忆写入会记场景)。用 agent 的调度当前通道不可靠
// (它逐任务变化),故用一个稳定的内部标记。
func (a *Agent) defaultChannelForBuiltin() string { return "builtin" }
// errBuiltinNoAgent 表示 provider 未绑定 agent(装配顺序错误)。
var errBuiltinNoAgent = &builtinErr{"内置工具执行器未绑定 agent"}
type builtinErr struct{ msg string }
func (e *builtinErr) Error() string { return e.msg }
// InstallBuiltinToolProvider 把本 agent 的内置工具面注入 ToolAPI。
//
// 供内核在**创建 agent 之后**调用(bootstrap / resident 创建处)。
// 幂等:重复调用只是覆盖为同一个 agent。
func (a *Agent) InstallBuiltinToolProvider() { a.installBuiltinProvider() }
// installBuiltinProvider 把本 agent 的内置工具面注入 ToolAPI。
//
// 由内核在**创建 agent 之后**调用(bootstrat / resident 创建处)。
// 注入是**全局**的:最后一次注入生效。⚠️ 因此多 agent 场景下,
// ToolAPI 看到的是"最近一个注入者"的内置工具面 —— 这是当前架构的
// 已知局限(ToolAPI 是单例却需要 per-agent 数据)。
// 记入设计文档 §10 待定项,不在本次解决。
func (a *Agent) installBuiltinProvider() {
sdk.SetBuiltinProvider(builtinProvider{a: a})
}
// isBuiltinToolName 粗判某名字是否可能是内置工具(供提示词/文档用)。
// 真正的判定以 ToolAPI 查询为准(带门控)。
func isBuiltinToolName(name string) bool {
for _, p := range []string{
"memory_", "knowledge_", "doc_", "person_",
"output_", "input_", "resident_", "notify_parent", "persona_set",
} {
if strings.HasPrefix(name, p) {
return true
}
}
return false
}