Files
HomeAgent/internal/plugins/seq/tools.go
JianFeeeee 116dc413f0 feat(seq): 新增 seq_help —— 格式说明 + 可照抄示例
动机来自真机实跑:模型写序列时踩了三个坑,各试 1~3 次才改对
  ① tools 漏末尾的 ';'       → 「末尾缺少 ';'」
  ② group 的 in 传成字符串     → 重试 3 次
  ③ as 指向未声明的 out 槽      → 静态校验拦下
这三处都是**格式细节**,塞不进工具描述(有长度限制),却恰是模型最易错处。
散落在七个描述里等于没有集中入口。

实现(help.go + tools.go + plugin.go):
· seq_help 无参数、纯文本返回(与仓内 output_send__*_help 同范式)
· 「格式要点」逐条写明:in/out 必须是**对象**、tools 必须是**字符串**、
  每个 tool 后(含最后一个)都要 ';'、as 必须在 out 声明、
  groups 与 file 二选一、组内并行组间串行
· 「条件 when」列出支持的表达式形态
· 「可照抄的完整示例」给一行**单行紧凑**的合法序列

★ 判据(plugin_test.go,3 条):
· seq_help 已注册、有 description、不声明并发安全
· 内容覆盖真机踩过的**每一个**坑(判据从"坑"出发而非从"打算写什么")
· ★ 示例**自己能被本包解析器接受**:validateHelpExample 从帮助文本里
  抽出示例喂给 Parse —— 模型是照抄的,示例自己解析不过就是给模型挖坑。
  而"从文本里有没有某个词"是看不出这类 bug 的。

过程中判据自己错了两次(都被这条示例判据照出来):
1. 抽取用 strings.Index(help, `{"name"`) ⇒ 先命中「格式要点」里**有意写的**
   示意片段,截到非示例的内容,报出莫名其妙的 invalid character '…'。
2. 修完又混用两套偏移基准(base 的下标拿去切 help)⇒ invalid character '\xaf'。
   ⇒ 重写为全程在同一 base 上定位。
★ 两次都说明:**判据的抽取逻辑本身就是需要验证的代码**,
它出错时报出的信息极具误导性(看起来像实现有 bug)。

示例形态也改过一次:原为多行缩进 JSON,改为**单行紧凑** —— 模型照抄时
免去缩进/换行带来的额外风险。

变异验证:去掉示例里的末尾 ';' ⇒ 示例判据 FAIL。

另一处:加 seq_help 后「恰好注册 6 个工具」判据 FAIL(实际 7)——
这正是那条判据的用意(防止悄悄多加工具稀释工具面),已更新并注明原因。

回归:go build ./... 通过;internal/plugins/... core sdk 全绿。
2026-09-27 13:48:15 +08:00

154 lines
5.6 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 seq
import "strings"
// seqToolDefs 返回本插件导出的六个工具定义。
//
// ⚠️ 全部**不声明** ParallelSafe:seq_run / seq_call 会执行**一串**工具,
// 其中可能含写操作。标成并发安全会让内核把两条 seq_run 并发跑起来,
// 两个序列的执行顺序交错、变量表互相污染。
//
// 为独立真相源:注册、判据、文档都从这里取,避免三处各写一份。
func seqToolDefs() []toolDefInfo {
return []toolDefInfo{
{
Name: "seq_create",
Description: "创建/更新一条工具序列。**groups 与 file 二选一**:传 groups 直接给结构," +
"或用 file 加载你已写好的序列文件(适合长序列——长参数会被 max_tokens 截断,写文件更稳)。\n" +
"序列由若干 group 组成:**组内并行、组间串行**;group 拥有独立签名(in 入参 / out 出参)," +
"可被 seq_call 按名调用。\n" +
"保存时会做静态校验:as 必须已在 out 声明、$args.x 必须已在 in 声明、组名不重复、" +
"组内 ; 分隔符必须完整。任一项不满足都会报错并说明原因。\n" +
"格式:JSON;每个 group 的 tools 是一个字符串,内含若干以 ' ; ' 分隔的 JSON 对象," +
"形如 {\"tool\":\"cmd_run\",\"args\":{...},\"as\":\"槽名\"}。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "序列名(唯一)",
},
"description": map[string]interface{}{
"type": "string", "description": "一句话说明这条序列做什么",
},
"groups": map[string]interface{}{
"type": "array",
"description": "组数组,按数组顺序执行;与 file 二选一",
"items": map[string]interface{}{"type": "object"},
},
"file": map[string]interface{}{
"type": "string",
"description": "序列文件路径(与 groups 二选一)。目录内文件优先用 files 工具查看",
},
},
"required": []string{"name"},
},
},
{
Name: "seq_help",
Description: "查看工具序列的完整格式说明与可照抄的示例。**写序列前先查这个** —— " +
"tools 字段的分隔符、in/out 的写法、as 与 out 的关系都在这里。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
{
Name: "seq_list",
Description: "列出全部可用序列:名称、描述、组数与各组的签名(in/out)。" +
"按名调用前先用它确认名称与签名。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
{
Name: "seq_delete",
Description: "删除一条序列。序列不存在时会报错(不会静默成功)。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "要删除的序列名",
},
},
"required": []string{"name"},
},
},
{
Name: "seq_run",
Description: "执行一条序列:按 groups 数组的顺序逐组执行,组内并行、组间串行。" +
"每组先求值 when 条件,为真才执行。返回逐组摘要与最终变量槽快照。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "要执行的序列名",
},
"args": map[string]interface{}{
"type": "object",
"description": "传给各组的入参(组内用 $args.<键> 读取)",
},
},
"required": []string{"name"},
},
},
{
Name: "seq_call",
Description: "按名调用一条序列里的 group:target 写组名(限本序列内)或 " +
"#序列名(跨序列)。返回该组的 out 槽。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "所属序列名(组名调用时必填)",
},
"target": map[string]interface{}{
"type": "string",
"description": "组名,或 #序列名 表示跨序列调用",
},
"args": map[string]interface{}{
"type": "object",
"description": "传给该组的入参",
},
},
"required": []string{"target"},
},
},
{
Name: "seq_when_call",
Description: "条件按名调用:when 表达式为真才执行,否则整次调用跳过(不产出任何槽)。" +
"when 只可读传入的 args(如 $args.flag == true)。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "所属序列名(组名调用时必填)",
},
"target": map[string]interface{}{
"type": "string",
"description": "组名,或 #序列名 表示跨序列调用",
},
"args": map[string]interface{}{
"type": "object",
"description": "传给该组的入参",
},
"when": map[string]interface{}{
"type": "string",
"description": "条件表达式,如 $args.flag == true",
},
},
"required": []string{"target", "when"},
},
},
}
}
// 内部小工具:把 args 里的字符串字段取出来并 trim。
func argString(args map[string]interface{}, key string) string {
if args == nil {
return ""
}
s, _ := args[key].(string)
return strings.TrimSpace(s)
}