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

135 lines
4.6 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 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)
}
}