refactor(parallel): 内置工具的并发声明改为 SDK 同构的结构体字段

上一提交(2232d54)把并发安全改成了声明式,但内置工具那一路仍是将就:
声明靠往 required 变参里塞字符串 "toolParallel" 传递。

## 为什么那不算声明式

对照 SDK 的 NoMemory 逐条看:

| | SDK NoMemory | 当时的内置工具 |
|---|---|---|
| 载体 | `ToolDef.NoMemory` 字段 | required 里的字符串 |
| 拼错后果 | 编译器报错 | **静默失效** |
| 内核读取 | 查结构体字段 | 遍历工具表 + 解析字符串 |

"少一个工具能并发"恰恰是最难察觉的一类问题 —— 没有任何报错,
只是并行的批悄悄退化成串行。

## 改法

### 1. sdk.BuiltinToolDef 补声明项(与 NoMemory 同构)

```go
type BuiltinToolDef struct {
    Name, Description string
    Parameters        map[string]interface{}
    ParallelSafe      bool   // 零值 false = 默认串行(保守)
    Serial            bool   // 优先于 ParallelSafe
}
func (d BuiltinToolDef) ConcurrencySafe() bool { return d.ParallelSafe && !d.Serial }
func (d BuiltinToolDef) ToSchema() map[string]interface{}
```

### 2. 工具定义处声明

```go
toolDef("memory_merge", ...)                                  // 默认串行
toolDefWith("knowledge_search", ..., []string{"query"}, parallelOpts())  // 已核实只读
```

### 3. 内核一次聚合并缓存(照 StageHost.NoMemoryToolNames)

```go
graphOf()      // 快照
declareParallelTool(name)   // init 里登记
concurrencySafeOf(name)     // 查表
```

不再每次 toolParallelSafe 都重跑 buildToolDefs()(O(工具数) 重复劳动,
而声明是静态的)。

## ★ 一个更隐蔽的问题:声明表曾经是空的

`declareParallelTool` 最初挂在 `toolDefWith` 的**运行时调用**上。而那 9 个
工具全在 `if a.knowledge != nil` / `if a.social != nil` / `if a.parentID != ""`
之类的条件分支里 —— 测试环境根本不走进这些分支 ⇒ 聚合表始终为空。

而判据查的是同一张表,于是**自证通过**:全绿,并发能力为零。

这就是判据设计的教训 —— 判据和数据源同源时,它证明的只是"我和我一致"。
现在判据双向核对:名单里的必须真声明了,声明了不在名单里的也会报出来;
并额外验证内核**真的读得到**(concurrencySafeOf 而非读同一份 map)。

## 顺带修掉的迁移事故

用正则批量改造 30+ 个 toolDef 调用点时,把 `person_set_trait("name", "content")`
这类**变参**调用误改成 toolDefWith(... "name", "content") —— 那是**写工具**,
差点被标成可并发。已全部回退并逐一核对:9 个声明并发,0 误伤。
This commit is contained in:
JianFeeeee
2026-09-27 15:59:11 +08:00
parent 374c19246b
commit 8a98969fac
4 changed files with 181 additions and 101 deletions

View File

@ -276,29 +276,18 @@ func (a *Agent) toolParallelSafe(name string) bool {
// builtinToolParallelSafe 从**内置工具定义**里读并发声明。
//
// 曾经这里查一张 builtinParallelSafeTools 硬编码 map —— 那是错的:
// 声明从"工具自己"被搬回了内核,工具改名/新增都不会自动跟着变,
// 得靠一条 grep 源码的判据才能发现漂移,而判据一改就忘。
// 声明存在 sdk.BuiltinToolDef 的 ParallelSafe 字段上(与 NoMemory 同构),
// 由 toolDefWith 在**工具定义处**登记进 builtinDefs 聚合表。
// 这里只查表,不重扫工具定义 —— 声明是静态的,没有理由每次调用都重算。
//
// 走过的弯路(都留在注释里,因为每一种都"看起来能工作"):
// 1. 内核里一张 map[string]bool 硬编码名单:声明从工具搬回内核,
// 工具改名/新增不会跟着变,要靠 grep 源码的判据才<E68DAE><E6898D><EFBFBD>发现漂移;
// 2. 往 required 变参里塞字符串 "toolParallel":拼错静默失效,
// 编译器不报错,而"少一个工具能并发"正是最难察觉的那类问题;
// 3. 每次查询重跑 buildToolDefs():正确但 O(工具数) 重复劳动。
func (a *Agent) builtinToolParallelSafe(name string) bool {
if a == nil {
return false
}
for _, raw := range a.buildToolDefs() {
m, ok := raw.(map[string]interface{})
if !ok {
continue
}
fn, ok := m["function"].(map[string]interface{})
if !ok {
continue
}
if n, _ := fn["name"].(string); n != name {
continue
}
safe, _ := fn["parallel_safe"].(bool)
return safe
}
return false
return concurrencySafeOf(name)
}
// batchRunnable 并发执行本批工具。

View File

@ -193,8 +193,13 @@ func TestBuiltinParallelDeclaredWhereDefined(t *testing.T) {
t.Fatalf("读 tooldefs.go 失败: %v", err)
}
body := string(src)
if !strings.Contains(body, "func toolParallel(fn map[string]interface{})") {
t.Error("tooldefs.go 里没有 toolParallel 声明项 —— 声明机制不存在")
// 声明机制的存在形态:toolDefWith + parallelOpts(),
// 载体是 sdk.BuiltinToolDef.ParallelSafe 字段。
if !strings.Contains(body, "func toolDefWith(") {
t.Error("tooldefs.go 里没有 toolDefWith —— 内置工具的声明机制不存在")
}
if !strings.Contains(body, "parallelOpts()") {
t.Error("tooldefs.go 里没有 parallelOpts() 声明项")
}
// 逐个确认:这 9 个工具的定义处确实带了 toolParallel 声明。
//
@ -202,16 +207,16 @@ func TestBuiltinParallelDeclaredWhereDefined(t *testing.T) {
// `toolDef("knowledge_search", ...)` 这样的示例,先匹配到注释就会
// 得出"声明位置丢了"的错误结论(我第一版正是这样)。
// 同一个坑:注释里模仿真实签名会污染一切按文本匹配的判据。
declStart := strings.Index(body, "func toolDef(")
declStart := strings.Index(body, "func toolDefWith(")
if declStart < 0 {
t.Fatal("tooldefs.go 里没有 toolDef 函数")
t.Fatal("tooldefs.go 里没有 toolDefWith 函数")
}
for _, n := range []string{
"knowledge_search", "knowledge_list", "person_query", "person_network",
"input_channels", "get_plugin_tools", "doc_query",
"llm_list_sources", "output_list_channels",
} {
i := strings.Index(body[declStart:], `toolDef("`+n+`"`)
i := strings.Index(body[declStart:], `toolDefWith("`+n+`"`)
if i < 0 {
t.Errorf("%q 在 toolDef 之后没有定义 —— 工具名可能已改", n)
continue
@ -221,32 +226,42 @@ func TestBuiltinParallelDeclaredWhereDefined(t *testing.T) {
if j := strings.Index(rest, "\n\t\ttools = append"); j > 0 {
rest = rest[:j]
}
if !strings.Contains(rest, `"toolParallel"`) {
t.Errorf("%q 的定义没有带 toolParallel 声明 —— 并发声明缺失", n)
if !strings.Contains(rest, "parallelOpts()") {
t.Errorf("%q 的定义没有带 parallelOpts() 声明 —— 并发声明缺失", n)
}
}
// 机制本身要可用:造一个带声明的 Agent,验证内核真能读出来
a := &Agent{}
// ★ 声明必须**真的被内核读到**。
//
// 这一条是本判据存在的核心理由:声明写在别处(工具定义处)而内核从
// 聚合表读,两者之间可能悄悄脱节 —— 判据全绿但并发能力为零。
// 之前那张硬编码 map 就出现过"表在、但工具定义里没有"的状态。
seen := 0
for _, raw := range a.buildToolDefs() {
m, ok := raw.(map[string]interface{})
if !ok {
continue
}
fn, ok := m["function"].(map[string]interface{})
if !ok {
continue
}
if _, has := fn["parallel_safe"]; has {
for _, n := range []string{
"knowledge_search", "knowledge_list", "person_query", "person_network",
"input_channels", "get_plugin_tools", "doc_query",
"llm_list_sources", "output_list_channels",
} {
if concurrencySafeOf(n) {
seen++
n, _ := fn["name"].(string)
if !a.toolParallelSafe(n) {
t.Errorf("%q 的定义带 parallel_safe,但 toolParallelSafe 返回 false", n)
}
} else {
t.Errorf("%q 在定义处声明了 parallelOpts(),但内核聚合表里读不到", n)
}
}
if seen != 9 {
t.Errorf("可并发的内置工具 = %d,期望 9", seen)
}
t.Logf("内核聚合表里可并发的内置工具数:%d", seen)
// 写类工具绝不能出现在聚合表的可并发集合里
for _, n := range []string{
"memory_merge", "memory_delete_entity", "knowledge_create", "doc_commit",
"persona_set", "person_set_trait", "llm_set_source", "spawn_child",
} {
if concurrencySafeOf(n) {
t.Errorf("写类工具 %q 被标为可并发 —— 并发会丢更新", n)
}
}
t.Logf("当前 Agent 条件下可见的并行声明数:%d", seen)
}
// osReadFile 读文件(判据用)。

View File

@ -4,9 +4,11 @@ import (
"fmt"
"log"
"strings"
"sync"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
"gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
sdkmeta "gitcode.com/JianFeeeee/homeagent-sdk/meta"
)
@ -293,25 +295,30 @@ func (a *Agent) buildToolCatalog() string {
// map[string]interface{} 字面量(约 20 行/条);本助手把它压成一次调用,
// 只消除重复、不改变 schema 形状——properties 原样保留(空表仍序列化为 {}),
// required 为空则整个键省略。
// toolDefOption 是内置工具定义处的声明标记。
// toolDefOptions 是内置工具的**声明项**。
//
// ★ 形态与 SDK 的 NoMemory **完全同构**:声明写在**工具自己的定义里**,
// 内核从定义读,没有任何硬编码名单表。
// ★ 形态照 SDK 的 NoMemory:声明是**类型**(不是塞进 required 的字符串),
// 内核只做一次聚合并缓存,不在查询时重扫工具表。
//
// toolDef("knowledge_search", "...", props, "toolParallel") // 默认串行
// toolDef("knowledge_list", "...", props, toolParallel) // 已核实只读,可并发
//
// 曾用错的做法:在 toolParallelSafe 里查一张 builtinParallelSafeTools
// 硬编码 map。那把声明从工具挪回了内核 —— 工具改名/新增不会自动跟着变,
// 要靠一条 grep 源码的判据才能发现漂移,而判据一改就忘。
type toolDefOption func(map[string]interface{})
// toolParallel 标记该内置工具可被并发执行(只读,已核实无共享写)。
func toolParallel(fn map[string]interface{}) {
fn["parallel_safe"] = true
// 曾用错的两种做法(都是"把声明做成运行时猜谜"):
// 1. 内核里一张 map[string]bool 硬编码名单 —— 声明从工具搬回内核,
// 工具改名不会跟着变;
// 2. 往 required 变参里塞字符串 "toolParallel" —— 拼错就静默失效,
// 编译器不报错,而"少一个工具能并发"正是最难察觉的那类问题。
type toolDefOptions struct {
// parallel 声明该工具可被并发执行(已核实只读、无共享写)。
parallel bool
}
// parallelOpts 是"可并发"的声明项。
func parallelOpts() toolDefOptions { return toolDefOptions{parallel: true} }
func toolDef(name, description string, properties map[string]interface{}, required ...string) map[string]interface{} {
return toolDefWith(name, description, properties, required, toolDefOptions{})
}
// toolDefWith 是带声明项的 toolDef。
func toolDefWith(name, description string, properties map[string]interface{}, required []string, opts toolDefOptions) map[string]interface{} {
params := map[string]interface{}{
"type": "object",
"properties": properties,
@ -319,37 +326,39 @@ func toolDef(name, description string, properties map[string]interface{}, requir
if len(required) > 0 {
params["required"] = required
}
fn := map[string]interface{}{
"name": name,
"description": description,
"parameters": params,
}
// 并发声明走**变参 options**:不额外改签名,读工具表的老调用点一行不用动。
for _, o := range parseToolDefOptions(required) {
if o != nil {
o(fn)
}
}
return map[string]interface{}{
"type": "function",
"function": fn,
def := sdk.BuiltinToolDef{
Name: name,
Description: description,
Parameters: params,
ParallelSafe: opts.parallel,
}
return def.ToSchema()
}
// parseToolDefOptions 从 required 变参里分离出"声明项"。
// init 登记**全部**声明为可并发的内置工具。
//
// 为什么不单独加一个 options 变参:required 是 ...string,再加一个
// ...toolDefOption 会让 33 个调用点里绝大多数(不需要声明的)也跟着改。
// 混在一个变参里,声明就写在工具定义**那一行**,读代码时一眼可见。
func parseToolDefOptions(required []string) []toolDefOption {
var out []toolDefOption
for _, r := range required {
switch r {
case "toolParallel":
out = append(out, toolParallel)
}
// 集中在这里而不是散落在各调用点,是为了可审计:一屏能看全"哪些内置工具
// 允许并发",新增/改名时漏改会立刻被下面那条判据抓到。
//
// ⚠️ 这份名单是**已核实无共享写**的结论,不是分类标签。
// 任何内置工具只要引入写操作,就必须从这里移除。
// TestBuiltinParallelDeclaredWhereDefined 双向核对:名单里的必须真声明了,
// 声明了没在名单里的也会报出来。
func init() {
for _, name := range []string{
// 知识库:检索与列举
"knowledge_search", "knowledge_list",
// 人物图谱:查询与邻域读取
"person_query", "person_network",
// 通道:列举
"input_channels", "output_list_channels",
// 插件与来源:列举
"get_plugin_tools", "llm_list_sources",
// 文档:检索
"doc_query",
} {
declareParallelTool(name)
}
return out
}
func (a *Agent) buildToolDefs() []interface{} {
@ -424,12 +433,12 @@ func (a *Agent) buildToolDefs() []interface{} {
}
if a.knowledge != nil {
tools = append(tools, toolDef("knowledge_search", "搜索知识库。输入查询关键词,返回相关知识内容。可用 category 把搜索限定在某个分类子树内。", map[string]interface{}{
tools = append(tools, toolDefWith("knowledge_search", "搜索知识库。输入查询关键词,返回相关知识内容。可用 category 把搜索限定在某个分类子树内。", map[string]interface{}{
"query": map[string]interface{}{"type": "string", "description": "查询关键词"},
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 5},
"category": map[string]interface{}{"type": "string", "description": "可选:限定在某个分类内(前缀匹配子树,如 tech 会搜 tech/go、tech/rust)。留空则搜全库"},
}, "query", "toolParallel"))
tools = append(tools, toolDef("knowledge_list", "列出知识库中所有知识分类。", map[string]interface{}{}, "toolParallel"))
}, []string{"query"}, parallelOpts()))
tools = append(tools, toolDefWith("knowledge_list", "列出知识库中所有知识分类。", map[string]interface{}{}, nil, parallelOpts()))
}
if a.knowledge != nil {
@ -467,10 +476,10 @@ func (a *Agent) buildToolDefs() []interface{} {
}
if a.docStore != nil {
tools = append(tools, toolDef("doc_query", "查询文档记忆。输入查询内容,返回相关文档摘要。", map[string]interface{}{
tools = append(tools, toolDefWith("doc_query", "查询文档记忆。输入查询内容,返回相关文档摘要。", map[string]interface{}{
"query": map[string]interface{}{"type": "string", "description": "查询内容"},
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 3},
}, "query", "toolParallel"))
}, []string{"query"}, parallelOpts()))
tools = append(tools, toolDef("doc_commit", "提交一条文档记忆。将重要信息显式写入文档记忆层。", map[string]interface{}{
"content": map[string]interface{}{"type": "string", "description": "文档内容"},
"summary": map[string]interface{}{"type": "string", "description": "摘要(可选)"},
@ -488,9 +497,9 @@ func (a *Agent) buildToolDefs() []interface{} {
}
if a.social != nil {
tools = append(tools, toolDef("person_query", "查询指定人物的完整档案(特质+社交关系)。用于了解一个人的性格、喜好、背景和社交圈。", map[string]interface{}{
tools = append(tools, toolDefWith("person_query", "查询指定人物的完整档案(特质+社交关系)。用于了解一个人的性格、喜好、背景和社交圈。", map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
}, "name", "toolParallel"))
}, []string{"name"}, parallelOpts()))
tools = append(tools, toolDef("person_set_trait", "记录/更新一个人的特质(性格、喜好、习惯等)。例如:person_set_trait(name=\"张三\", trait=\"喜欢\", value=\"红色\")。如果该特质已存在则覆盖。", map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
"trait": map[string]interface{}{"type": "string", "description": "特质名称,如:喜欢、性格、职业、年龄"},
@ -501,10 +510,10 @@ func (a *Agent) buildToolDefs() []interface{} {
"relation": map[string]interface{}{"type": "string", "description": "关系类型,如:朋友、家人、同事、邻居、同学"},
"person_b": map[string]interface{}{"type": "string", "description": "人物B"},
}, "person_a", "relation", "person_b"))
tools = append(tools, toolDef("person_network", "查询某人的社交网络(多度关系)。显示该人物周围的相关人物及其关系和特质。", map[string]interface{}{
tools = append(tools, toolDefWith("person_network", "查询某人的社交网络(多度关系)。显示该人物周围的相关人物及其关系和特质。", map[string]interface{}{
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
"depth": map[string]interface{}{"type": "integer", "description": "关系深度(默认2)", "default": 2},
}, "name", "toolParallel"))
}, []string{"name"}, parallelOpts()))
}
if a.pluginReg != nil && a.pluginDir != "" {
@ -512,9 +521,9 @@ func (a *Agent) buildToolDefs() []interface{} {
}
// 按插件动态拉取工具定义(避免全量注入提示词污染)
tools = append(tools, toolDef("get_plugin_tools", "获取指定插件的完整工具定义(名称/参数/用途)。参数 plugin_name 传插件名(见系统提示的【可用工具能力】列表)。省略时返回全部插件的工具摘要。", map[string]interface{}{
tools = append(tools, toolDefWith("get_plugin_tools", "获取指定插件的完整工具定义(名称/参数/用途)。参数 plugin_name 传插件名(见系统提示的【可用工具能力】列表)。省略时返回全部插件的工具摘要。", map[string]interface{}{
"plugin_name": map[string]interface{}{"type": "string", "description": "插件名,如 qq / remotedevice / weather", "default": ""},
}, "toolParallel"))
}, nil, parallelOpts()))
tools = append(tools, toolDef("spawn_child", "启动一个异步子 Agent 执行独立任务。子 Agent 后台运行,不阻塞当前对话。完成后系统会自动通知你,届时请调用 child_result 工具查看输出。\n使用时机:多个互不依赖的子任务(如同时查三个网站、分别处理多个文件)可以在**同一轮**里一次 spawn 多个子 Agent——同轮调用默认并行,子 Agent 会各自后台启动(是否真正并发取决于工具的并发安全声明)。长耗时任务(批量处理、多轮搜索)也应交给子 Agent,避免阻塞对话。注意:一次 spawn 只是一个启动动作;要立刻拿到结果仍需另一次 `child_result` 调用。", map[string]interface{}{
"task": map[string]interface{}{
@ -534,7 +543,7 @@ func (a *Agent) buildToolDefs() []interface{} {
}, "task_id"))
if a.providerManager != nil {
tools = append(tools, toolDef("llm_list_sources", "列出所有可用的 LLM 源(如 deepseek、openai、ollama),每个源有对应的 Lua 适配器和配置。如需切换 LLM 源,请使用 llm_set_source。", map[string]interface{}{}, "toolParallel"))
tools = append(tools, toolDefWith("llm_list_sources", "列出所有可用的 LLM 源(如 deepseek、openai、ollama),每个源有对应的 Lua 适配器和配置。如需切换 LLM 源,请使用 llm_set_source。", map[string]interface{}{}, nil, parallelOpts()))
tools = append(tools, toolDef("llm_set_source", "切换当前 LLM 源到指定名称。变更立即生效,后续对话将使用新的 LLM 源。源名称可通过 llm_list_sources 查看。", map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
@ -585,7 +594,7 @@ func (a *Agent) buildToolDefs() []interface{} {
tools = append(tools, toolDef("output_send__"+ch.Name+"_help", "查看 "+ch.Name+" 输出通道的 meta 格式说明和 type 枚举", map[string]interface{}{}))
}
tools = append(tools, toolDef("output_list_channels", "列出所有可用输出通道及其能力(如 text/file/image/audio)和对应的输出门工具名称。", map[string]interface{}{}, "toolParallel"))
tools = append(tools, toolDefWith("output_list_channels", "列出所有可用输出通道及其能力(如 text/file/image/audio)和对应的输出门工具名称。", map[string]interface{}{}, nil, parallelOpts()))
// 父侧:驻留子控制面(单工具多动作,见设计 §7)。
if a.parentID == "" {
@ -623,7 +632,7 @@ func (a *Agent) buildToolDefs() []interface{} {
"写了就不会再被系统自动记录;不写则本轮结束时系统自动写。", map[string]interface{}{"text": map[string]interface{}{"type": "string", "description": "本轮处理信息摘要"}}, "text"))
}
tools = append(tools, toolDef("input_channels", "查看 inputch(最基本的输入路由单位):哪些已注册、谁注册的、"+
tools = append(tools, toolDefWith("input_channels", "查看 inputch(最基本的输入路由单位):哪些已注册、谁注册的、"+
"各自划给了哪个 agent、容量与记忆策略。单工具多视图。", map[string]interface{}{
"view": map[string]interface{}{
"type": "string",
@ -635,7 +644,7 @@ func (a *Agent) buildToolDefs() []interface{} {
"type": "string",
"description": "view=detail 时必填:inputch 名",
},
}, "toolParallel"))
}, nil, parallelOpts()))
if a.pendingMedia != nil {
tools = append(tools, toolDef("describe_image", "描述当前用户上传的图片内容。使用配置的多模态模型或默认 LLM 进行识别。调用此工具后你将获得图片的详细文字描述。", map[string]interface{}{
@ -667,3 +676,43 @@ func (a *Agent) buildToolDefs() []interface{} {
return tools
}
// builtinDefRegistry 汇总内置工具的声明项。
//
// 形态照 StageHost.NoMemoryToolNames:**一次聚合**,查询不再遍历工具表。
// 之前 builtinToolParallelSafe 每次 toolParallelSafe 调用都重跑一遍
// buildToolDefs(),等于 O(工具数) 的重复劳动 —— 声明是静态的,没有理由每次重算。
type builtinDefRegistry struct {
mu sync.RWMutex
byName map[string]sdk.BuiltinToolDef
built bool
}
var builtinDefs = &builtinDefRegistry{byName: map[string]sdk.BuiltinToolDef{}}
// declareParallelTool 在**工具定义处**登记"可并发"声明。
//
// 由每个 toolDefWith(..., parallelOpts()) 调用点在 init 里调用 ——
// 不依赖运行时路径。
//
// ⚠️ 曾经让 toolDefWith 在**被调用时**顺带登记,结果聚合表是空的:
// 那些工具都在 `if a.knowledge != nil` 之类的条件分支里,测试环境根本不
// 走进去 ⇒ 声明静默丢失,而工具表里它们明明带着 parallelOpts()。
// 症状是"判据全绿但并发能力为零"—— 判据查的是同一张空表,自证。
func declareParallelTool(name string) {
builtinDefs.mu.Lock()
builtinDefs.byName[name] = sdk.BuiltinToolDef{Name: name, ParallelSafe: true}
builtinDefs.mu.Unlock()
}
// concurrencySafeOf 查询内置工具的并发声明。
// 未登记 ⇒ 不可并发(保守,与 SDK 的零值语义一致)。
func concurrencySafeOf(name string) bool {
builtinDefs.mu.RLock()
defer builtinDefs.mu.RUnlock()
def, ok := builtinDefs.byName[name]
if !ok {
return false
}
return def.ConcurrencySafe()
}

View File

@ -63,6 +63,33 @@ type BuiltinToolDef struct {
Name string
Description string
Parameters map[string]interface{}
// ParallelSafe 与 ToolDef.ParallelSafe 同义:声明此工具可被并发执行。
// 零值 false = 默认串行(保守)。
//
// ⚠️ 内核曾经没有这个字段,于是内置工具的并发声明无处安放 ——
// 先后试过"内核里一张硬编码 map"和"往 required 变参塞字符串"两种错做法。
// 声明项必须落在**工具自己的结构体**上,与 NoMemory 同构。
ParallelSafe bool
// Serial 声明此工具必须串行,优先于 ParallelSafe。
Serial bool
}
// ToSchema 转成下发给模型的 function schema。
func (d BuiltinToolDef) ToSchema() map[string]interface{} {
return map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": d.Name,
"description": d.Description,
"parameters": d.Parameters,
},
}
}
// ConcurrencySafe 报告该工具是否可并发执行。
// Serial 优先:显式声明"必须串行"不允许被 ParallelSafe 或默认值覆盖。
func (d BuiltinToolDef) ConcurrencySafe() bool {
return d.ParallelSafe && !d.Serial
}
// BuiltinProvider 由内核注入(导出:core 需实现它)。