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

+117
View File
@@ -0,0 +1,117 @@
package agent
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"git.gopher.ink/mirrors2/AutoZero/llm"
"git.gopher.ink/mirrors2/AutoZero/tools"
)
// TestEvictOldImages:只保留最近 K 张截图,驱逐发生在视图层、transcript 不动。
func TestEvictOldImages(t *testing.T) {
img := func(i int) llm.Image { return llm.Image{Path: string(rune('a'+i)) + ".png"} }
msgs := []llm.Message{
{Role: llm.RoleTool, ToolCallID: "1", Content: "shot1", Images: []llm.Image{img(0)}},
{Role: llm.RoleTool, ToolCallID: "2", Content: "shot2", Images: []llm.Image{img(1)}},
{Role: llm.RoleTool, ToolCallID: "3", Content: "shot3", Images: []llm.Image{img(2)}},
}
view := evictOldImages(msgs, 2, 0)
// transcript 不动
for i, m := range msgs {
if len(m.Images) != 1 {
t.Fatalf("transcript message %d mutated: %v", i, m.Images)
}
}
// 视图:最旧的被驱逐并留说明,其余保留
if len(view[0].Images) != 0 || !strings.Contains(view[0].Content, "evicted") {
t.Fatalf("oldest image not evicted: %+v", view[0])
}
if len(view[1].Images) != 1 || len(view[2].Images) != 1 {
t.Fatalf("recent images should be kept: %+v %+v", view[1].Images, view[2].Images)
}
// 数量充足时零拷贝
same := evictOldImages(msgs, 10, 0)
if &same[0] != &msgs[0] {
t.Fatal("expected original slice when under budget")
}
}
// TestUsageTotals:Usage 累计到 agent 汇总。
func TestUsageTotals(t *testing.T) {
prov := &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
u := &llm.Usage{InputTokens: 10, OutputTokens: 5}
if call == 2 {
u = &llm.Usage{InputTokens: 20, OutputTokens: 8}
}
return llm.Message{Role: llm.RoleAssistant, Content: "ok", Usage: u}, nil
}}
a, _ := newTestAgent(t)
a.Provider = prov
if err := a.Run(context.Background(), "hi"); err != nil {
t.Fatal(err)
}
// 第二次 Run 再走一轮,验证跨 turn 累计
if err := a.Run(context.Background(), "again"); err != nil {
t.Fatal(err)
}
if a.usedIn != 30 || a.usedOut != 13 {
t.Fatalf("usage totals wrong: in=%d out=%d", a.usedIn, a.usedOut)
}
}
// TestSafeRunLenSideEffectContract:错误标注(既 ConcurrencySafe 又
// SideEffect)的工具不得进入并发批次——运行时契约兜底。
func TestSafeRunLenSideEffectContract(t *testing.T) {
a, _ := newTestAgent(t)
if err := a.Tools.Register(&tools.Tool{
Name: "risky", ConcurrencySafe: true, SideEffect: true,
Parameters: map[string]any{"type": "object"},
Run: func(context.Context, string) (tools.Result, error) { return tools.TextResult("ok"), nil },
}); err != nil {
t.Fatal(err)
}
calls := []llm.ToolCall{{Name: "risky"}, {Name: "read"}}
if n := a.safeRunLen(calls, 0); n != 0 {
t.Fatalf("side-effect tool must not be concurrent-batched, got n=%d", n)
}
}
// TestEvictOldImagesByteBudget:字节预算触顶时只保留预算内最新的图片。
func TestEvictOldImagesByteBudget(t *testing.T) {
mk := func(dir, name string, size int) string {
p := filepath.Join(dir, name)
os.WriteFile(p, make([]byte, size), 0o644)
return p
}
dir := t.TempDir()
// 三张图:最旧 2MB、中间 1MB、最新 1MB;预算 2.5MB → 只留最新两张(估算 4/3 膨胀后 1.33+1.33=2.67 > 2.5 → 只留最新一张)
imgs := []llm.Image{
{Path: mk(dir, "old.png", 2_000_000)},
{Path: mk(dir, "mid.png", 1_000_000)},
{Path: mk(dir, "new.png", 1_000_000)},
}
msgs := []llm.Message{
{Role: llm.RoleTool, ToolCallID: "1", Content: "a", Images: imgs[:1]},
{Role: llm.RoleTool, ToolCallID: "2", Content: "b", Images: imgs[1:2]},
{Role: llm.RoleTool, ToolCallID: "3", Content: "c", Images: imgs[2:]},
}
view := evictOldImages(msgs, 10, 2_500_000)
if len(view[2].Images) != 1 {
t.Fatal("newest image must be kept")
}
if len(view[0].Images) != 0 || len(view[1].Images) != 0 {
t.Fatalf("older images must be evicted: %d %d", len(view[0].Images), len(view[1].Images))
}
if !strings.Contains(view[0].Content, "evicted") {
t.Fatal("eviction note missing")
}
// transcript 不动
if len(msgs[0].Images) != 1 {
t.Fatal("transcript mutated")
}
}