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。
This commit is contained in:
Sakurasan committed 2026-10-26 00:54:46 +08:00
commit d5dac5a6fc
140 files changed
+26497

No files matched your search

+231
View File
@@ -0,0 +1,231 @@
// 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
}