Files
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
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。
2026-10-26 00:54:46 +08:00

232 lines
7.7 KiB
Go
Raw Permalink 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 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
}