llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、 builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、 编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。 产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt) 留在 AutoGo,宿主接入方式见 MIGRATION.md。
232 lines
7.7 KiB
Go
232 lines
7.7 KiB
Go
// Package tools 实现工具注册表(借鉴 picoclaw 的注册表模式与 pigo 的
|
||
// agenttool 分层):每个工具自我描述 JSON schema,副作用工具标记 SideEffect
|
||
// 以便 agent 层做信任门控。
|
||
package tools
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
|
||
"git.gopher.ink/mirrors2/AutoZero/llm"
|
||
)
|
||
|
||
// Result 是工具的执行结果:文本回复 + 可选的图片附件(如截图),
|
||
// 图片随 tool result 一起发给模型(vision)。
|
||
type Result struct {
|
||
Text string
|
||
Images []llm.Image
|
||
// Err 非空表示本次调用失败,而 Text 仍可携带部分输出(典型如 bash:
|
||
// 命令失败但 stdout 有价值)。它与 Run 的第二个返回值等价,
|
||
// 由 Agent 统一归一后按结构化失败块回灌给模型(见 agent/toolfail.go)。
|
||
Err error
|
||
// Exit 是命令类工具的退出码;0 表示没取到或不适用(Agent 落盘时会跳过)。
|
||
Exit int
|
||
// Truncated 表示 Text 被本工具的长度上限削过——模型看到的不是全量。
|
||
// 由工具自己标:只有它知道上限是多少、有没有触发。
|
||
Truncated bool
|
||
}
|
||
|
||
// TextResult 是纯文本结果的便捷构造。
|
||
func TextResult(s string) Result { return Result{Text: s} }
|
||
|
||
// Change 是一次文件改动的留档记录,由宿主的 Snapshotter 实现消费
|
||
// (AutoGo 里对应 snapshot.Change,回退/补偿机制在宿主侧)。
|
||
type Change struct {
|
||
Path string // 解析后的真实路径(~ 与工作区相对都已展开)
|
||
Before []byte // 改动前的内容
|
||
Existed bool // false = 改动前不存在,是本次新建的(恢复时该删,不是该还原)
|
||
After []byte // 改动后的内容
|
||
// Reason 非空表示这次改动**没能**留档(例如旧内容读不出来)。据此记一条
|
||
// 断链记录,恢复时该文件会被拦下——绝不能把"读失败"当成"文件不存在",
|
||
// 那会让一次 rewind 删掉一个本来一直存在的文件。
|
||
Reason string
|
||
}
|
||
|
||
// Snapshotter 是写类工具的留档钩子;nil 时不留档,行为与从前一致。
|
||
//
|
||
// 为什么挂在工具层而不是 agent 层:只有工具自己知道解析后的真实路径,以及
|
||
// "改动前那一刻文件是什么"。write 本来不读旧内容,为回合级回退它得多读一次——
|
||
// 这笔开销写在工具里看得见,也可以随时整个关掉。
|
||
type Snapshotter interface {
|
||
Record(Change)
|
||
}
|
||
|
||
// BuiltinHooks 是内置工具向宿主回传的副作用钩子。
|
||
type BuiltinHooks struct {
|
||
// SessionID 用于后台作业完成推送(空 = 不推送)。
|
||
SessionID string
|
||
// OnBackgroundDone 在 bash_bg 结束时调用(可空)。
|
||
OnBackgroundDone func(BackgroundDone)
|
||
// OnInstructions 在 read 发现当前工作区内的嵌套 AGENTS.md 时调用(可空)。
|
||
// 路径与正文由工具层给出,宿主负责去重与注入。
|
||
OnInstructions func(path, text string)
|
||
}
|
||
|
||
// record 在钩子缺席时什么都不做。
|
||
func record(s Snapshotter, c Change) {
|
||
if s != nil {
|
||
s.Record(c)
|
||
}
|
||
}
|
||
|
||
// preImage 读出改动前的内容。第三种结果必须与"不存在"区分开:
|
||
// 存在但读不出(权限、被占用)时若按不存在处理,一次 rewind 就会删掉本来
|
||
// 存在的文件。这里返回原因码,留档层据此记一条断链记录。
|
||
func preImage(p string) (before []byte, existed bool, reason string) {
|
||
old, err := os.ReadFile(p)
|
||
switch {
|
||
case err == nil:
|
||
return old, true, ""
|
||
case errors.Is(err, os.ErrNotExist):
|
||
return nil, false, ""
|
||
default:
|
||
return nil, false, "unreadable"
|
||
}
|
||
}
|
||
|
||
// ToolKind 是工具的作用类别,供审批策略按类别放行(如「自动编辑」放行写入、
|
||
// 但执行类仍需确认)。三个 bool 标注无法区分 write/edit 与 bash——它们同为
|
||
// SideEffect,故需要单独的类别维度。
|
||
type ToolKind string
|
||
|
||
const (
|
||
KindRead ToolKind = "read" // 只读:读文件、搜索
|
||
KindWrite ToolKind = "write" // 改工作区文件
|
||
KindExec ToolKind = "exec" // 执行命令/控制本机
|
||
KindNetwork ToolKind = "network" // 访问外部网络
|
||
KindOther ToolKind = "other" // 未分类(含 MCP 动态工具),最保守
|
||
// KindAsk 是「向人提问」:它不改现实世界,也不该被任何审批门过问。
|
||
// 单列一个类别是因为非副作用工具在 ApprovalPolicy=always 下仍会被问一次
|
||
// 「要不要允许 ask_user」——为了问一个问题,先批准问这个问题。
|
||
KindAsk ToolKind = "ask"
|
||
)
|
||
|
||
// Tool 是一个可被模型调用的工具。语义标注借鉴 claude-code-best 的 Tool 接口
|
||
// (src/Tool.ts:373):Go 的零值即 fail-closed——未标注的工具默认视为有副作用、
|
||
// 不可并发。
|
||
type Tool struct {
|
||
Name string
|
||
Description string
|
||
Parameters map[string]any // JSON schema
|
||
SideEffect bool // 为 true 时需要信任门控(写文件、执行命令等)
|
||
// ReadOnly 表示工具不改变系统状态(读文件、搜索等)。
|
||
ReadOnly bool
|
||
// ConcurrencySafe 表示可与其他工具并发执行;只有同时 ReadOnly 的连续
|
||
// 段才会被编排器并发执行。
|
||
ConcurrencySafe bool
|
||
// Kind 是审批用的作用类别;零值按 EffectiveKind() 推断(fail-closed)。
|
||
Kind ToolKind
|
||
// Run 执行工具;input 是模型给的原始 JSON 参数字符串。
|
||
Run func(ctx context.Context, input string) (Result, error)
|
||
}
|
||
|
||
// EffectiveKind 返回工具类别:显式标注优先;否则只读工具算 read,
|
||
// 其余一律 other——**未知类别绝不被当作可自动放行的 write**(fail-closed)。
|
||
func (t *Tool) EffectiveKind() ToolKind {
|
||
if t.Kind != "" {
|
||
return t.Kind
|
||
}
|
||
if t.ReadOnly && !t.SideEffect {
|
||
return KindRead
|
||
}
|
||
return KindOther
|
||
}
|
||
|
||
// Registry 按名管理工具集合。
|
||
type Registry struct {
|
||
byName map[string]*Tool
|
||
order []string
|
||
}
|
||
|
||
func NewRegistry() *Registry {
|
||
return &Registry{byName: map[string]*Tool{}}
|
||
}
|
||
|
||
func (r *Registry) Register(t *Tool) error {
|
||
if _, dup := r.byName[t.Name]; dup {
|
||
return fmt.Errorf("tool %q already registered", t.Name)
|
||
}
|
||
r.byName[t.Name] = t
|
||
r.order = append(r.order, t.Name)
|
||
return nil
|
||
}
|
||
|
||
func (r *Registry) Get(name string) (*Tool, bool) {
|
||
t, ok := r.byName[name]
|
||
return t, ok
|
||
}
|
||
|
||
func (r *Registry) Names() []string { return r.order }
|
||
|
||
// CloneWithout 返回一个去除了指定工具的注册表副本(用于子 agent 防递归)。
|
||
func (r *Registry) CloneWithout(names ...string) *Registry {
|
||
drop := map[string]bool{}
|
||
for _, n := range names {
|
||
drop[n] = true
|
||
}
|
||
c := NewRegistry()
|
||
for _, n := range r.order {
|
||
if !drop[n] {
|
||
_ = c.Register(r.byName[n])
|
||
}
|
||
}
|
||
return c
|
||
}
|
||
|
||
// Defs 导出给 LLM 的工具声明列表。
|
||
func (r *Registry) Defs() []llm.ToolDef {
|
||
out := make([]llm.ToolDef, 0, len(r.order))
|
||
for _, n := range r.order {
|
||
t := r.byName[n]
|
||
out = append(out, llm.ToolDef{
|
||
Name: t.Name,
|
||
Description: t.Description,
|
||
Parameters: t.Parameters,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// arg 解析输入 JSON 到 map。
|
||
// 兼容模型可能生成的 {"arguments": {...}} 包装格式。
|
||
func arg(input string) (map[string]any, error) {
|
||
m := map[string]any{}
|
||
if input == "" {
|
||
return m, nil
|
||
}
|
||
if err := json.Unmarshal([]byte(input), &m); err != nil {
|
||
return nil, fmt.Errorf("invalid tool arguments: %w", err)
|
||
}
|
||
// 兼容 {"arguments": {...}} 包装:提取内层对象
|
||
if args, ok := m["arguments"]; ok {
|
||
if argsMap, ok := args.(map[string]any); ok {
|
||
return argsMap, nil
|
||
}
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func str(m map[string]any, key string) string {
|
||
if v, ok := m[key].(string); ok {
|
||
return v
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// expandHome 把 ~ 前缀替换为用户主目录。
|
||
func expandHome(p string) string {
|
||
if p == "~" || len(p) > 1 && p[0] == '~' && p[1] == '/' {
|
||
home, err := os.UserHomeDir()
|
||
if err == nil {
|
||
if p == "~" {
|
||
return home
|
||
}
|
||
return home + p[1:]
|
||
}
|
||
}
|
||
return p
|
||
}
|