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,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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user