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

+134
View File
@@ -0,0 +1,134 @@
package agent
import (
"context"
"fmt"
"strings"
"sync"
"testing"
"git.gopher.ink/mirrors2/AutoZero/llm"
"git.gopher.ink/mirrors2/AutoZero/tools"
)
// 重复调用检测的测试。要护住的语义:"同名同参"才算重复,且成功空转也要能发现
// ([STUCK] 只数失败,那一半已有测试覆盖)。
func TestToolSignatureNormalizesJSON(t *testing.T) {
// 键序与空白不同、内容相同 → 同一个签名(模型两次生成同一调用经常这样)。
a := toolSignature("bash", `{"command":"ls -la","cwd":"/tmp"}`)
b := toolSignature("bash", "{ \"cwd\": \"/tmp\",\n \"command\": \"ls -la\" }")
if a != b {
t.Errorf("同参不同写法被判成两次调用:\n%q\n%q", a, b)
}
if a == toolSignature("bash", `{"command":"ls -l"}`) {
t.Error("换了参数却算同一次调用")
}
if a == toolSignature("read", `{"command":"ls -la","cwd":"/tmp"}`) {
t.Error("换了工具名却算同一次调用")
}
// 非法 JSON 退回原文比对:宁可漏判,不能把两次不同调用误判成重复。
if toolSignature("x", "not-json") == toolSignature("x", "other") {
t.Error("非 JSON 入参被归一成同一个签名")
}
}
func TestNoteToolRepeatFiresOncePerThresholdAndCaps(t *testing.T) {
a, _ := newTestAgent(t)
for i := 0; i < 2; i++ {
if n := a.noteToolRepeat("read", `{"path":"a.go"}`); n != 0 {
t.Fatalf("第 %d 次同名调用不该提醒", i+1)
}
}
if n := a.noteToolRepeat("read", `{"path":"a.go"}`); n != 3 {
t.Errorf("第 3 次应提醒(得到 %d)", n)
}
if n := a.noteToolRepeat("read", `{"path":"a.go"}`); n != 4 {
t.Errorf("第 4 次应继续提醒(得到 %d)", n)
}
// 每回合提醒总量封顶:模型无视两次就不会突然开始听,第三条起只是占 token。
// 上面 read 已经用掉 2 条(第 3、4 次各一条),所以 grep 的第 3 次还能提醒一次,
// 之后就再也发不出了。
if n := a.noteToolRepeat("grep", `{"pattern":"x"}`); n != 0 {
t.Errorf("grep 第 1 次就被提醒:%d", n)
}
a.noteToolRepeat("grep", `{"pattern":"x"}`)
if n := a.noteToolRepeat("grep", `{"pattern":"x"}`); n != 3 {
t.Errorf("第 3 条提醒(额度内)没发出:%d", n)
}
a.noteToolRepeat("ls", `{}`)
a.noteToolRepeat("ls", `{}`)
if n := a.noteToolRepeat("ls", `{}`); n != 0 {
t.Errorf("提醒数已达上限还在注入:%d", n)
}
a.resetAnomalyCounters()
if n := a.noteToolRepeat("read", `{"path":"a.go"}`); n != 0 {
t.Errorf("清零后第一次调用就被提醒:%d", n)
}
}
// TestRepeatNoteInToolResult 是端到端的一半:模型真的连着三次调同一个工具时,
// 第三次的**工具结果**里要带上那句话(提醒挂在结果上,不另塞一条假用户消息)。
func TestRepeatNoteInToolResult(t *testing.T) {
a, sess := newTestAgent(t)
calls := 0
reg := tools.NewRegistry()
if err := reg.Register(&tools.Tool{
Name: "peek",
Parameters: map[string]any{"type": "object"},
Run: func(context.Context, string) (tools.Result, error) {
calls++
return tools.TextResult("同一份内容"), nil
},
}); err != nil {
t.Fatal(err)
}
a.Tools = reg
prov := &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if call <= 3 {
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
{ID: fmt.Sprintf("c%d", call), Name: "peek", Input: `{"path":"a.go"}`},
}}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "收尾"}, nil
}}
a.Provider = prov
if err := a.Run(context.Background(), "看看"); err != nil {
t.Fatal(err)
}
var notes int
for _, m := range sess.Messages {
if m.Role == llm.RoleTool {
if strings.Contains(m.Content, "[REPEAT]") {
notes++
if !strings.Contains(m.Content, "同一份内容") {
t.Error("提醒把工具原本的输出挤掉了")
}
}
}
}
if notes != 1 {
t.Errorf("[REPEAT] 出现 %d 次,want 1(第 3 次调用时一次)", notes)
}
}
// TestRepeatNoteSurvivesConcurrency 是 -race 护栏:并发批次里多个 goroutine 同时
// 记签名,计数与提示不能被数据竞争吃掉(failStreak 当年就是为此加的锁)。
func TestRepeatNoteSurvivesConcurrency(t *testing.T) {
a, _ := newTestAgent(t)
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
a.noteToolRepeat("read", `{"path":"x"}`)
}()
}
wg.Wait()
if got := a.repeatCounts["read\x00{\"path\":\"x\"}"]; got != 20 {
t.Errorf("并发计数丢了:%d,want 20", got)
}
if a.anomalyNotes > anomalyNoteMax {
t.Errorf("提醒条数越过上限:%d > %d", a.anomalyNotes, anomalyNoteMax)
}
}