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:
commit
d5dac5a6fc
140 files changed
+26497
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user