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

+258
View File
@@ -0,0 +1,258 @@
package agent
import (
"context"
"errors"
"fmt"
"io"
"regexp"
"strings"
"testing"
"git.gopher.ink/mirrors2/AutoZero/llm"
"git.gopher.ink/mirrors2/AutoZero/session"
)
// TestCheckpointUpdatedInPlace:第二次压缩要把旧 checkpoint + 新消息交给更新
// prompt,而不是把整段历史(含旧摘要)重新摘要一遍;sidecar 同步更新。
func TestCheckpointUpdatedInPlace(t *testing.T) {
var inputs []string
var summaryCalls int
prov := &scriptedProvider{fn: func(_ int, system string, msgs []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if strings.HasPrefix(system, "You are a context compaction") {
summaryCalls++
inputs = append(inputs, msgs[0].Content)
return llm.Message{Role: llm.RoleAssistant, Content: fmt.Sprintf("CHECKPOINT-V%d", summaryCalls)}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "ok"}, nil
}}
a, sess := newTestAgent(t)
a.Provider = prov
a.MinCompactTokens = 1
a.Opt.Stderr = io.Discard
if err := sess.Append(llm.Message{Role: llm.RoleUser, Content: "first task"}); err != nil {
t.Fatal(err)
}
if err := sess.Append(llm.Message{Role: llm.RoleAssistant, Content: "working"}); err != nil {
t.Fatal(err)
}
if _, err := a.CompactNow(context.Background()); err != nil {
t.Fatal(err)
}
if a.checkpoint != "CHECKPOINT-V1" || a.checkpointCovered != 2 {
t.Fatalf("first checkpoint = %q covered=%d", a.checkpoint, a.checkpointCovered)
}
if len(a.compacted) != 1 || !strings.Contains(a.compacted[0].Content, "CHECKPOINT-V1") {
t.Fatalf("working set = %+v", a.compacted)
}
if err := sess.Append(llm.Message{Role: llm.RoleUser, Content: "more context"}); err != nil {
t.Fatal(err)
}
if err := sess.Append(llm.Message{Role: llm.RoleAssistant, Content: "answer"}); err != nil {
t.Fatal(err)
}
if _, err := a.CompactNow(context.Background()); err != nil {
t.Fatal(err)
}
if a.checkpoint != "CHECKPOINT-V2" {
t.Fatalf("second checkpoint = %q", a.checkpoint)
}
if a.checkpointCovered != 4 {
t.Fatalf("covered = %d, want 4", a.checkpointCovered)
}
if len(inputs) != 2 {
t.Fatalf("summary calls = %d", len(inputs))
}
second := inputs[1]
if !strings.Contains(second, "Existing checkpoint") || !strings.Contains(second, "CHECKPOINT-V1") {
t.Fatalf("update prompt did not carry the prior checkpoint: %q", second)
}
if !strings.Contains(second, "more context") {
t.Fatalf("update prompt missing newer history: %q", second)
}
if strings.Contains(second, "first task") {
t.Fatalf("update prompt re-summarized already-covered history: %q", second)
}
cp, ok := session.LoadCheckpoint(a.sessionID())
if !ok || cp.Text != "CHECKPOINT-V2" || cp.Covered != 4 {
t.Fatalf("sidecar = %+v ok=%v", cp, ok)
}
}
// TestLoadCheckpointRestoresAndValidates:新回合能从 sidecar 直接接上
// checkpoint + 未覆盖消息;transcript 被改写后指纹失效,退回完整历史。
func TestLoadCheckpointRestoresAndValidates(t *testing.T) {
prov := &scriptedProvider{fn: func(_ int, system string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if strings.HasPrefix(system, "You are a context compaction") {
return llm.Message{Role: llm.RoleAssistant, Content: "CHECKPOINT-X"}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "ok"}, nil
}}
a, sess := newTestAgent(t)
a.Provider = prov
a.MinCompactTokens = 1
a.Opt.Stderr = io.Discard
for _, content := range []string{"u1", "a1", "u2", "a2"} {
role := llm.RoleUser
if strings.HasPrefix(content, "a") {
role = llm.RoleAssistant
}
if err := sess.Append(llm.Message{Role: role, Content: content}); err != nil {
t.Fatal(err)
}
}
if _, err := a.CompactNow(context.Background()); err != nil {
t.Fatal(err)
}
b := &Agent{Provider: prov, Tools: a.Tools, Session: sess, System: "test",
Opt: Options{Stdout: io.Discard, Stderr: io.Discard}}
if !b.LoadCheckpoint() {
t.Fatal("valid checkpoint not restored")
}
if b.checkpoint != "CHECKPOINT-X" || b.compactFrom != 4 {
t.Fatalf("restored state: checkpoint=%q from=%d", b.checkpoint, b.compactFrom)
}
if err := sess.Append(llm.Message{Role: llm.RoleUser, Content: "next"}); err != nil {
t.Fatal(err)
}
b.ContextWindow = 1 << 30
b.CompactAt = 1
b.MinCompactTokens = 0
view, err := b.contextView(context.Background())
if err != nil {
t.Fatal(err)
}
if len(view) != 2 || !strings.Contains(view[0].Content, "CHECKPOINT-X") || view[1].Content != "next" {
t.Fatalf("restored view = %+v", view)
}
// 改写已覆盖的历史:指纹失效,不能继续用旧 checkpoint。
sess.Messages[3].Content = "tampered"
c := &Agent{Provider: prov, Tools: a.Tools, Session: sess, System: "test",
Opt: Options{Stdout: io.Discard, Stderr: io.Discard}}
if c.LoadCheckpoint() {
t.Fatal("stale checkpoint must be rejected after transcript changes")
}
}
// TestObservedTokensPrefersActualUsage:压缩判据用网关回报的真实 prompt tokens,
// 不再被 bytes/3 的估算单独带偏;请求之后新增的消息仍按估算补上。
func TestObservedTokensPrefersActualUsage(t *testing.T) {
a := &Agent{
System: strings.Repeat("s", 300),
ContextWindow: 10000,
CompactAt: 0.5,
MinCompactTokens: 1,
lastInputTokens: 0,
}
view := []llm.Message{
{Role: llm.RoleUser, Content: "short"},
{Role: llm.RoleAssistant, Content: "ok"},
}
a.noteRequestUsage(&llm.Usage{InputTokens: 6000}, view)
if !a.shouldCompact(view) {
t.Fatal("actual 6000 tokens should trigger compaction despite a low estimate")
}
big := []llm.Message{
{Role: llm.RoleUser, Content: strings.Repeat("a", 30000)},
{Role: llm.RoleAssistant, Content: "ok"},
}
a.noteRequestUsage(&llm.Usage{InputTokens: 1000}, big)
if a.shouldCompact(big) {
t.Fatal("actual 1000 tokens should not be overridden by the estimate")
}
grown := append(append([]llm.Message{}, big...),
llm.Message{Role: llm.RoleUser, Content: strings.Repeat("b", 15000)})
if !a.shouldCompact(grown) {
t.Fatal("messages added after the measured request must count toward the trigger")
}
}
// TestForceCompactTargetHint:收缩阶梯要把目标 token 数写进摘要提示词。
func TestForceCompactTargetHint(t *testing.T) {
var inputs []string
prov := &scriptedProvider{fn: func(_ int, system string, msgs []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if strings.HasPrefix(system, "You are a context compaction") {
inputs = append(inputs, msgs[0].Content)
return llm.Message{Role: llm.RoleAssistant, Content: "CP"}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "ok"}, nil
}}
a, sess := newTestAgent(t)
a.Provider = prov
a.MinCompactTokens = 1
a.Opt.Stderr = io.Discard
for _, content := range []string{"u1", "a1"} {
if err := sess.Append(llm.Message{Role: llm.RoleUser, Content: content}); err != nil {
t.Fatal(err)
}
}
view := append([]llm.Message(nil), sess.Messages...)
a.noteRequestUsage(&llm.Usage{InputTokens: 10000}, view)
if err := a.forceCompactForSize(context.Background(), view, 0.5); err != nil {
t.Fatal(err)
}
if len(inputs) != 1 || !strings.Contains(inputs[0], "under about 5000 tokens") {
t.Fatalf("target hint missing: %v", inputs)
}
}
// TestOversizedShrinkLadder:请求过大被拒后按 0.7 / 0.5 / 0.35 逐级收缩,
// 每次重发都基于刚压出来的新视图;压到网关肯收为止。
func TestOversizedShrinkLadder(t *testing.T) {
realCalls, summaryCalls := 0, 0
var targets []int
prov := &scriptedProvider{fn: func(_ int, system string, msgs []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if strings.HasPrefix(system, "You are a context compaction") {
summaryCalls++
m := regexp.MustCompile(`under about (\d+) tokens`).FindStringSubmatch(msgs[0].Content)
if m == nil {
t.Fatalf("summary input missing target: %q", msgs[0].Content)
}
target := atoiTest(m[1])
targets = append(targets, target)
return llm.Message{Role: llm.RoleAssistant, Content: "CP:" + strings.Repeat("x", target*3)}, nil
}
realCalls++
if realCalls <= 3 {
return llm.Message{}, errors.New("HTTP 400: context length too large")
}
return llm.Message{Role: llm.RoleAssistant, Content: "done"}, nil
}}
a, sess := newTestAgent(t)
a.Provider = prov
a.Opt.Stderr = io.Discard
a.ContextWindow = 1 << 30
a.CompactAt = 1
a.MinCompactTokens = 1
sess.Messages = []llm.Message{
{Role: llm.RoleUser, Content: strings.Repeat("a", 30000)},
{Role: llm.RoleAssistant, Content: strings.Repeat("b", 30000)},
}
if err := a.Run(context.Background(), ""); err != nil {
t.Fatalf("run: %v (real=%d summary=%d targets=%v)", err, realCalls, summaryCalls, targets)
}
if realCalls != 4 || summaryCalls != 3 {
t.Fatalf("real=%d summary=%d targets=%v", realCalls, summaryCalls, targets)
}
if !(targets[0] > targets[1] && targets[1] > targets[2]) {
t.Fatalf("targets must shrink: %v", targets)
}
}
func atoiTest(s string) int {
n := 0
for _, r := range s {
if r < '0' || r > '9' {
break
}
n = n*10 + int(r-'0')
}
return n
}